Como sei como meus métodos devem ser reutilizáveis? [fechadas]

133

Estou cuidando do meu próprio negócio em casa e minha esposa vem até mim e diz

Querida .. Você pode imprimir todas as Economias do dia em todo o mundo para 2018 no console? Eu preciso verificar uma coisa.

E eu estou super feliz porque era isso que eu estava esperando a vida toda com minha experiência em Java e pensei:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(2018, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (2018 == now.getYear()) {
                    int hour = now.getHour();
                    now = now.plusHours(1);
                    if (now.getHour() == hour) {
                        System.out.println(now);
                    }
                }
            }
        );
    }
}

Mas então ela diz que estava apenas me testando se eu era um engenheiro de software treinado com ética e me diz que parece que não sou mais (tirado daqui ).

Deve-se notar que nenhum engenheiro de software treinado com ética aceitaria escrever um procedimento para DestroyBaghdad. A ética profissional básica exigiria que ele escrevesse um procedimento DestroyCity, ao qual Bagdá poderia ser dada como parâmetro.

E eu sou como, tudo bem, ok, você me pegou .. Passe o ano que quiser, aqui vai você:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings(int year) {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(year, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (year == now.getYear()) {
                    // rest is same..

Mas como eu sei quanto (e o que) parametrizar? Afinal, ela pode dizer ..

  • ela deseja passar um formatador de string personalizado, talvez ela não goste do formato em que já estou imprimindo: 2018-10-28T02:00+01:00[Arctic/Longyearbyen]

void dayLightSavings(int year, DateTimeFormatter dtf)

  • ela está interessada apenas em determinados períodos do mês

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)

  • ela está interessada em certos períodos de horas

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)

Se você está procurando uma pergunta concreta:

Se destroyCity(City city)é melhor que destroyBaghdad(), é takeActionOnCity(Action action, City city)ainda melhor? Porque porque não?

Afinal de contas, eu posso primeiro chamá-lo com Action.DESTROYentão Action.REBUILD, não é?

Mas tomar ações nas cidades não é suficiente para mim, que tal takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)? Afinal, não quero ligar para:

takeActionOnCity(Action.DESTORY, City.BAGHDAD);

então

takeActionOnCity(Action.DESTORY, City.ERBIL);

e assim por diante quando eu posso fazer:

takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);

ps Eu apenas construí minha pergunta em torno da citação que mencionei, não tenho nada contra nenhum país, religião, raça ou qualquer outra coisa no mundo. Eu só estou tentando fazer um ponto.

Koray Tugay
fonte
71
O que você está dizendo aqui é o que tentei expressar muitas vezes: a generalidade é cara e, portanto, deve ser justificada por benefícios claros e específicos . Mas vai muito além disso; linguagens de programação são criadas por seus designers para facilitar alguns tipos de generalidade que outros, e isso influencia nossas escolhas como desenvolvedores. É fácil parametrizar um método por um valor e, quando essa é a ferramenta mais fácil que você possui na sua caixa de ferramentas, a tentação é usá-lo, independentemente de fazer sentido para o usuário.
precisa
30
Reutilizar não é algo que você deseja por si só. Priorizamos a reutilização porque acreditamos que os artefatos de código são caros de construir e, portanto, devem ser utilizáveis ​​no maior número de cenários possível, para amortizar esse custo nesses cenários. Essa crença freqüentemente não é justificada por observações, e o conselho para projetar a reutilização é, portanto, frequentemente mal aplicado . Crie seu código para reduzir o custo total do aplicativo .
precisa
7
Sua esposa é antiética por desperdiçar seu tempo mentindo para você. Ela pediu uma resposta e deu um meio sugerido; Por esse contrato, como você obtém essa saída é apenas entre você e você. Além disso, destroyCity(target)é muito mais antiético do que destroyBagdad()! Que tipo de monstro escreve um programa para acabar com uma cidade, sem falar em qualquer cidade do mundo? E se o sistema estivesse comprometido ?! Além disso, o que o gerenciamento de tempo / recursos (esforço investido) tem a ver com ética? Desde que o contrato verbal / escrito seja concluído conforme acordado.
Tezra 27/11/2018
25
Eu acho que você pode estar lendo muito sobre essa piada. É uma piada sobre como os programadores de computador tomam más decisões éticas, porque priorizam considerações técnicas sobre os efeitos de seu trabalho em seres humanos. Não pretende ser um bom conselho sobre o design do programa.
Eric Lippert

Respostas:

114

São tartarugas até o fim.

Ou abstrações neste caso.

A codificação de boas práticas é algo que pode ser aplicado infinitamente e, em algum momento, você está abstraindo por abstração, o que significa que você foi longe demais. Encontrar essa linha não é algo fácil de colocar em uma regra de ouro, pois depende muito do seu ambiente.

Por exemplo, tivemos clientes conhecidos por solicitar aplicativos simples primeiro, mas depois solicitar expansões. Também tivemos clientes que perguntam o que querem e geralmente nunca nos retornam para uma expansão.
Sua abordagem varia de acordo com o cliente. Para o primeiro cliente, pagará para abstrair preventivamente o código, porque você está razoavelmente certo de que precisará revisar esse código no futuro. Para o segundo cliente, talvez você não queira investir esse esforço extra se não desejar expandir o aplicativo a qualquer momento (nota: isso não significa que você não segue nenhuma boa prática, mas simplesmente que você evite fazer mais do que o necessário atualmente .

Como sei quais recursos implementar?

A razão de eu mencionar o acima é porque você já caiu nessa armadilha:

Mas como eu sei quanto (e o que) parametrizar? Afinal, ela poderia dizer .

"Ela pode dizer" não é um requisito comercial atual. É um palpite sobre um requisito comercial futuro. Como regra geral, não se baseie em suposições, apenas desenvolva o que é atualmente necessário.

No entanto, o contexto se aplica aqui. Eu não conheço sua esposa. Talvez você tenha calculado com precisão que ela realmente quer isso. Mas você ainda deve confirmar com o cliente que isso é realmente o que ele quer, porque, caso contrário, você gastará tempo desenvolvendo um recurso que nunca vai usar.

Como sei qual arquitetura implementar?

Isso é mais complicado. O cliente não se importa com o código interno; portanto, você não pode perguntar se eles precisam. A opinião deles sobre o assunto é principalmente irrelevante.

No entanto, você ainda pode confirmar a necessidade de fazer isso fazendo as perguntas certas ao cliente. Em vez de perguntar sobre a arquitetura, pergunte a eles sobre suas expectativas de desenvolvimento futuro ou expansões para a base de código. Você também pode perguntar se o objetivo atual tem um prazo, porque talvez você não consiga implementar sua arquitetura sofisticada no prazo necessário.

Como sei quando abstrair ainda mais meu código?

Não sei onde leio (se alguém souber, me avise e eu darei crédito), mas uma boa regra é que os desenvolvedores devem contar como um homem das cavernas: um, dois muitos .

insira a descrição da imagem aqui XKCD # 764

Em outras palavras, quando um determinado algoritmo / padrão está sendo usado pela terceira vez, ele deve ser abstraído para ser reutilizável (= utilizável várias vezes).

Só para esclarecer, não estou sugerindo que você não deve escrever código reutilizável quando houver apenas duas instâncias do algoritmo em uso. É claro que você também pode abstrair isso, mas a regra deve ser a de que, por três instâncias, você deve abstrair.

Novamente, isso leva em consideração suas expectativas. Se você já sabe que precisa de três ou mais instâncias, é claro que pode abstrair imediatamente. Mas se você apenas adivinhar que pode implementá-lo mais vezes, a exatidão da implementação da abstração depende totalmente da exatidão da sua adivinhação.
Se você adivinhou corretamente, economizou algum tempo. Se você adivinhou errado, desperdiçou parte de seu tempo e esforço e, possivelmente, comprometeu sua arquitetura para implementar algo que acaba não necessitando.

Se destroyCity(City city)é melhor que destroyBaghdad(), é takeActionOnCity(Action action, City city)ainda melhor? Porque porque não?

Isso depende muito de várias coisas:

  • Existem várias ações que podem ser executadas em qualquer cidade?
  • Essas ações podem ser usadas de forma intercambiável? Porque se as ações "destruir" e "reconstruir" têm execuções completamente diferentes, não faz sentido mesclar as duas em um único takeActionOnCitymétodo.

Lembre-se também de que, se você recursivamente abstrair isso, terá um método tão abstrato que não será mais que um contêiner para executar outro método, o que significa que você tornou seu método irrelevante e sem sentido.
Se todo o takeActionOnCity(Action action, City city)corpo do método acabar sendo nada além de action.TakeOn(city);, você deve se perguntar se o takeActionOnCitymétodo realmente tem um propósito ou não é apenas uma camada extra que não agrega nada de valor.

Mas tomar ações nas cidades não é suficiente para mim, que tal takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)?

A mesma pergunta aparece aqui:

  • Você tem um caso de uso para regiões geográficas?
  • A execução de uma ação em uma cidade e uma região é a mesma?
  • É possível executar alguma ação em qualquer região / cidade?

Se você puder responder definitivamente "sim" a todos os três, uma abstração será garantida.

Flater
fonte
16
Não posso enfatizar a regra "um, dois, muitos" o suficiente. Existem infinitas possibilidades de abstrair / parametrizar algo, mas o subconjunto útil é pequeno, geralmente zero. Saber exatamente qual variante tem valor na maioria das vezes só pode ser determinado em retrospecto. Portanto, atenha-se aos requisitos imediatos * e adicione complexidade conforme necessário por novos requisitos ou retrospectivas. * Às vezes, você conhece bem o espaço do problema, então pode ser bom adicionar algo porque você sabe que precisa dele amanhã. Mas use esse poder com sabedoria, também pode levar à ruína.
Christian Sauer
2
> "Não sei onde li [..]". Você pode estar lendo Coding Horror: The Rule of Three .
Rune
10
A "regra de um, dois, muitos" está realmente lá para impedir que você crie a abstração errada aplicando o DRY às cegas. O problema é que duas partes do código podem começar parecendo quase exatamente iguais, por isso é tentador abstrair as diferenças; mas, desde o início, você não sabe quais partes do código são estáveis ​​e quais não são; além disso, pode acontecer que eles realmente precisem evoluir independentemente (diferentes padrões de mudança, diferentes conjuntos de responsabilidades). Em ambos os casos, uma abstração errada funciona contra você e atrapalha.
Filip Milovanović 28/11
4
Esperar por mais de dois exemplos de "a mesma lógica" permite que você julgue melhor o que deve ser abstraído e como (e realmente, trata-se de gerenciar dependências / acoplamento entre código com padrões de mudança diferentes).
Filip Milovanović 28/11
1
@kukis: A linha realista deve ser desenhada em 2 (conforme comentário de Baldrickk): zero-um-muitos (como é o caso das relações com o banco de dados). No entanto, isso abre a porta para um comportamento desnecessário na busca de padrões. Duas coisas podem parecer vagamente parecidas, mas isso não significa que elas sejam realmente iguais. No entanto, quando uma terceira instância entra na briga que se assemelha à primeira e à segunda instância, você pode julgar com mais precisão que suas semelhanças são de fato um padrão reutilizável. Portanto, a linha do senso comum é traçada em 3, o que leva a um erro humano ao detectar "padrões" entre apenas duas instâncias.
Flater
44

Prática

Esta é a Software Engineering SE, mas criar software é muito mais arte do que engenharia. Não há algoritmo universal a ser seguido ou medidas a serem tomadas para descobrir quanta reutilização é suficiente. Como em qualquer coisa, quanto mais prática você desenvolver projetos, melhor terá. Você sentirá melhor o que é "suficiente" porque verá o que está errado e o que está errado quando você parametriza muito ou pouco.

Isso não é muito útil agora , então, que tal algumas diretrizes?

Olhe para sua pergunta. Há muito "ela poderia dizer" e "eu poderia". Muitas declarações teorizando sobre alguma necessidade futura. Os seres humanos são péssimos em prever o futuro. E você (provavelmente) é humano. O grande problema do design de software está tentando dar conta de um futuro que você não conhece.

Diretriz 1: Você não precisará dela

A sério. Simplesmente pare. Na maioria das vezes, esse problema futuro imaginado não aparece - e certamente não aparecerá exatamente como você imaginou.

Diretriz 2: Custo / Benefício

Legal, esse pequeno programa levou algumas horas para você escrever, talvez? Então, o que se sua esposa não voltar e pedir para essas coisas? Na pior das hipóteses, você passa mais algumas horas lançando outro programa para fazer isso. Nesse caso, não é muito tempo para tornar esse programa mais flexível. E isso não adicionará muito à velocidade do tempo de execução ou ao uso da memória. Mas programas não triviais têm respostas diferentes. Cenários diferentes têm respostas diferentes. Em algum momento, os custos claramente não valem o benefício, mesmo com as habilidades imperfeitas de dizer o futuro.

Diretriz 3: Foco nas constantes

Olhe para a pergunta. No seu código original, há muitas entradas constantes. 2018, 1. Ints constantes, strings constantes ... São as coisas mais prováveis ​​que precisam ser não constantes . Melhor ainda, eles levam apenas um pouco de tempo para parametrizar (ou pelo menos definir como constantes reais). Mas outra coisa a ser cautelosa é o comportamento constante . oSystem.out.printlnpor exemplo. Esse tipo de suposição sobre o uso tende a ser algo que muda no futuro e tende a ser muito caro de corrigir. Não apenas isso, mas o IO como esse torna a função impura (junto com o fuso horário que busca um pouco). A parametrização desse comportamento pode tornar a função mais pura, levando a maior flexibilidade e testabilidade. Grandes benefícios com custo mínimo (especialmente se você fizer uma sobrecarga que use System.outpor padrão).

Telastyn
fonte
1
É apenas uma orientação, os 1s são bons, mas você olha para eles e diz "isso nunca vai mudar?" E o println pode ser parametrizado com uma função de ordem superior - embora o Java não seja bom nisso.
Telastyn
5
@KorayTugay: se o programa fosse realmente para a sua esposa chegando em casa, a YAGNI diria que sua versão inicial é perfeita e que você não deve investir mais tempo para introduzir constantes ou parâmetros. O YAGNI precisa de contexto - o seu programa é uma solução descartável, ou um programa de migração é executado apenas por alguns meses ou faz parte de um enorme sistema ERP, destinado a ser usado e mantido por várias décadas?
Doc Brown
8
@KorayTugay: Separar E / S da computação é uma técnica fundamental de estruturação de programas. Separe a geração de dados da filtragem de dados da transformação de dados do consumo de dados da apresentação de dados. Você deve estudar alguns programas funcionais e verá isso mais claramente. Na programação funcional, é bastante comum gerar uma quantidade infinita de dados, filtrar apenas os dados de seu interesse, transformar os dados no formato necessário, construir uma sequência a partir dela e imprimir essa sequência em 5 funções diferentes, um para cada etapa.
Jörg W Mittag
3
Como nota de rodapé, seguir fortemente o YAGNI leva à necessidade de refatorar continuamente: "Usado sem refatoração contínua, poderia levar a código desorganizado e retrabalho maciço, conhecido como dívida técnica". Portanto, embora o YAGNI seja uma coisa boa em geral, ele tem uma grande responsabilidade de revisitar e reavaliar o código, o que não é algo que todo desenvolvedor / empresa esteja disposto a fazer.
Flater
4
@Telastyn: Sugiro expandir a pergunta para “isso nunca mudará e a intenção do código é trivialmente legível sem nomear a constante ?” Mesmo para valores que nunca mudam, pode ser relevante nomeá-los apenas para manter as coisas legíveis.
Flater
27

Primeiro: nenhum desenvolvedor de software preocupado com a segurança escreveria um método DestroyCity sem passar um token de autorização por qualquer motivo.

Eu também posso escrever qualquer coisa como um imperativo que tenha sabedoria evidente sem que seja aplicável em outro contexto. Por que é necessário autorizar uma concatenação de cadeias?

Segundo: Todo o código quando executado deve ser totalmente especificado .

Não importa se a decisão foi codificada ou adiada para outra camada. Em algum momento, há um pedaço de código em alguma linguagem que sabe o que deve ser destruído e como instruí-lo.

Isso pode estar no mesmo arquivo de objeto destroyCity(xyz)e em um arquivo de configuração:, destroy {"city": "XYZ"}"ou pode haver uma série de cliques e pressionamentos de tecla em uma interface do usuário.

Em terceiro lugar:

Querida .. Você pode imprimir todas as Economias do dia em todo o mundo para 2018 no console? Eu preciso verificar uma coisa.

é um conjunto muito diferente de requisitos para:

ela quer passar por um formatador de string personalizado, interessado apenas em determinados períodos do mês, interessado em determinados períodos de hora.

Agora, o segundo conjunto de requisitos obviamente cria uma ferramenta mais flexível. Ele tem um público-alvo mais amplo e um campo de aplicação mais amplo. O perigo aqui é que a aplicação mais flexível do mundo é de fato um compilador para código de máquina. É literalmente um programa tão genérico que pode criar qualquer coisa para tornar o computador o que você precisa (dentro das restrições de seu hardware).

De um modo geral, as pessoas que precisam de software não querem algo genérico; eles querem algo específico. Ao dar mais opções, você está de fato tornando a vida deles mais complicada. Se eles quisessem essa complexidade, estariam usando um compilador, sem perguntar.

Sua esposa estava pedindo funcionalidade e sub-especificou os requisitos dela para você. Nesse caso, foi aparentemente de propósito e, em geral, é porque eles não sabem melhor. Caso contrário, eles teriam usado o compilador. Portanto, o primeiro problema é que você não pediu mais detalhes sobre exatamente o que ela queria fazer. Ela queria fazer isso por vários anos diferentes? Ela queria isso em um arquivo CSV? Você não descobriu quais decisões ela queria tomar e o que estava pedindo para você descobrir e decidir por ela. Depois de descobrir quais decisões precisam ser adiadas, você pode descobrir como comunicá-las por meio de parâmetros (e outros meios configuráveis).

Dito isto, a maioria dos clientes se comunica erroneamente, presume ou desconhece certos detalhes (também conhecidos como decisões) que eles realmente gostariam de se fazer, ou que realmente não queriam fazer (mas parece incrível). É por isso que métodos de trabalho como PDSA (planejar, desenvolver, estudar e atuar) são importantes. Você planejou o trabalho de acordo com os requisitos e desenvolveu um conjunto de decisões (código). Agora é hora de estudá-lo, sozinho ou com seu cliente, e aprender coisas novas, e isso informa o seu pensamento daqui para frente. Por fim, aja de acordo com suas novas idéias - atualize os requisitos, refine o processo, obtenha novas ferramentas, etc ... Então comece a planejar novamente. Isso revelaria quaisquer requisitos ocultos ao longo do tempo e comprova o progresso de muitos clientes.

Finalmente. Seu tempo é importante ; é muito real e muito finito. Toda decisão que você toma envolve muitas outras decisões ocultas, e é disso que se trata o desenvolvimento de software. Atrasar uma decisão como argumento pode tornar a função atual mais simples, mas torna outro lugar mais complexo. Essa decisão é relevante nesse outro local? É mais relevante aqui? De quem é a decisão a tomar? Você está decidindo isso; isso é codificação. Se você repetir conjuntos de decisões com frequência, há um benefício muito real em codificá-los dentro de alguma abstração. O XKCD tem uma perspectiva útil aqui. E isso é relevante no nível de um sistema, seja uma função, módulo, programa, etc.

O conselho no início implica que as decisões que sua função não tem o direito de tomar devem ser passadas como argumento. O problema é que uma DestroyBaghdadfunção pode realmente ser a função que tem esse direito.

Kain0_0
fonte
+1 adoro a parte do compilador!
Lee
4

Há muitas respostas longas aqui, mas sinceramente acho super simples

Qualquer informação codificada que você tenha em sua função que não faça parte do nome da função deve ser um parâmetro.

então na sua função

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(zoneId -> {
            LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
            ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
            while (2018 == now.getYear()) {
                int hour = now.getHour();
                now = now.plusHours(1);
                if (now.getHour() == hour) {
                    System.out.println(now);
                }
            }
        });
    }
}

Você tem:

The zoneIds
2018, 1, 1
System.out

Então, eu moveria tudo isso para parâmetros de uma forma ou de outra. Você pode argumentar que o zoneIds está implícito no nome da função, talvez você queira torná-lo ainda mais alterando-o para "DaylightSavingsAroundTheWorld" ou algo assim

Você não possui uma sequência de formato, portanto, adicionar uma é uma solicitação de recurso e deve encaminhar sua esposa à instância Jira da sua família. Ele pode ser colocado na lista de pendências e priorizado na reunião apropriada do comitê de gerenciamento de projetos.

Ewan
fonte
1
Você (endereçando-me ao OP) certamente não deve adicionar uma string de formato, pois não deve imprimir nada. A única coisa sobre esse código que absolutamente impede sua reutilização é que ele é impresso. Ele deve retornar as zonas ou um mapa das zonas para quando elas saem do horário de verão. (Embora por que ele só identifica quando o ir off DST, e não sobre DST, eu não entendo Não parece para coincidir com a declaração do problema..)
David Conrad
o requisito é imprimir no console. você pode mitgate o couplung apertado passando o fluxo de saída como um parâmetro como sugiro
Ewan
1
Mesmo assim, se você deseja que o código seja reutilizável, não imprima no console. Escreva um método que retorne os resultados e, em seguida, escreva um chamador que os obtenha e imprima. Isso também o torna testável. Se você quer que ele produza saída, eu não passaria em um fluxo de saída, passaria em um Consumidor.
David Conrad
um fluxo ourput é um consumidor
Ewan
Não, um OutputStream não é um consumidor .
David Conrad
4

Em resumo, não projete seu software para reutilização, porque nenhum usuário final se importa se suas funções podem ser reutilizadas. Em vez disso, engenheiro para a compreensão do design - é fácil meu código para outra pessoa ou meu futuro esquecido? - e flexibilidade de design- quando inevitavelmente precisar corrigir bugs, adicionar recursos ou modificar funcionalidades, quanto meu código resistirá às alterações? A única coisa com a qual seu cliente se importa é a rapidez com que você pode responder quando ele relata um erro ou pede uma alteração. Fazer essas perguntas sobre o seu design, incidentalmente, tende a resultar em um código reutilizável, mas essa abordagem mantém você focado em evitar os problemas reais que você enfrentará ao longo da vida útil desse código, para que você possa servir melhor o usuário final em vez de buscar noções elevadas e impraticáveis. ideais de "engenharia" para agradar as barbas do pescoço.

Para algo tão simples quanto o exemplo que você forneceu, sua implementação inicial é boa por causa de quão pequena é, mas esse design simples se tornará difícil de entender e quebradiço se você tentar colocar muita flexibilidade funcional (em oposição à flexibilidade de design) em um procedimento. Abaixo está minha explicação de minha abordagem preferida para projetar sistemas complexos para compreensibilidade e flexibilidade, que espero demonstrem o que quero dizer com eles. Eu não empregaria essa estratégia para algo que pudesse ser escrito em menos de 20 linhas em um único procedimento, porque algo tão pequeno já atende aos meus critérios de compreensibilidade e flexibilidade.


Objetos, não procedimentos

Em vez de usar classes como módulos da velha escola com várias rotinas que você chama para executar as coisas que seu software deve fazer, considere modelar o domínio como objetos que interagem e cooperam para realizar a tarefa em questão. Os métodos em um paradigma orientado a objetos foram originalmente criados para serem sinais entre objetos, de modo que eles Object1pudessem dizer Object2o que quer que fosse, e receber um sinal de retorno. Isso ocorre porque o paradigma Orientado a Objeto é inerentemente sobre modelagem dos objetos de domínio e suas interações, em vez de uma maneira sofisticada de organizar as mesmas funções e procedimentos antigos do paradigma Imperativo. No caso dovoid destroyBaghdadPor exemplo, em vez de tentar escrever um método genérico sem contexto para lidar com a destruição de Bagdá ou qualquer outra coisa (que pode se tornar rapidamente complexa, difícil de entender e quebradiça), tudo que puder ser destruído deve ser responsável por entender como destruir a si mesmo. Por exemplo, você tem uma interface que descreve o comportamento de coisas que podem ser destruídas:

interface Destroyable {
    void destroy();
}

Então você tem uma cidade que implementa essa interface:

class City implements Destroyable {
    @Override
    public void destroy() {
        ...code that destroys the city
    }
}

Nada que exija a destruição de uma instância de Cityalguma vez se importará com isso, portanto, não há razão para que esse código exista em qualquer lugar fora de City::destroy, e de fato, um conhecimento íntimo do funcionamento interno de Cityfora de si seria um acoplamento rígido que reduz felxibility, já que você precisa considerar esses elementos externos, caso precise modificar o comportamento de City. Esse é o verdadeiro objetivo por trás do encapsulamento. Pense nisso como se cada objeto tivesse sua própria API, o que lhe permitirá fazer o que for necessário para que você possa se preocupar em atender às suas solicitações.

Delegação, não "Controle"

Agora, se sua classe de implementação é CityouBaghdad depende de quão genérico é o processo de destruição da cidade. Com toda a probabilidade, a Cityserá composta de peças menores que precisarão ser destruídas individualmente para realizar a destruição total da cidade; portanto, nesse caso, cada uma dessas peças também será implementada Destroyablee elas serão instruídas pelo Citydestruidor. da mesma maneira que alguém de fora pediu Cityque se destruísse.

interface Part extends Destroyable {
    ...part-specific methods
}

class Building implements Part {
    ...part-specific methods
    @Override
    public void destroy() {
       ...code to destroy a building
    }
}

class Street implements Part {
    ...part-specific methods
    @Override
    public void destroy() {
        ...code to destroy a building
    }
}

class City implements Destroyable {
    public List<Part> parts() {...}

    @Override
    public void destroy() {
        parts().forEach(Destroyable::destroy);            
    }
}

Se você quiser ficar realmente doido e implementar a ideia de uma Bombque é descartada em um local e destrói tudo dentro de um determinado raio, pode ser algo como isto:

class Bomb {
    private final Integer radius;

    public Bomb(final Integer radius) {
        this.radius = radius;
    }

    public void drop(final Grid grid, final Coordinate target) {
        new ObjectsByRadius(
            grid,
            target,
            this.radius
        ).forEach(Destroyable::destroy);
    }
}

ObjectsByRadius representa um conjunto de objetos que é calculado para o Bomb partir das entradas, porque Bombnão se importa como esse cálculo é feito desde que possa trabalhar com os objetos. Isso é reutilizável incidentalmente, mas o objetivo principal é isolar o cálculo dos processos de descartar Bombe destruir os objetos, para que você possa compreender cada peça e como eles se encaixam e mudar o comportamento de uma peça individual sem precisar remodelar o algoritmo inteiro. .

Interações, não algoritmos

Em vez de tentar adivinhar o número certo de parâmetros para um algoritmo complexo, faz mais sentido modelar o processo como um conjunto de objetos em interação, cada um com funções extremamente estreitas, pois permitirá modelar a complexidade do seu processo através das interações entre esses objetos bem definidos, fáceis de compreender e quase imutáveis. Quando feito corretamente, isso faz com que algumas das modificações mais complexas sejam tão triviais quanto implementar uma interface ou duas e refazer quais objetos são instanciados no seu main()método.

Eu daria algo para o seu exemplo original, mas sinceramente não consigo entender o que significa "imprimir ... o horário de verão". O que posso dizer sobre essa categoria de problema é que, sempre que você estiver executando um cálculo, cujo resultado pode ser formatado de várias maneiras, minha maneira preferida de decompor isso é a seguinte:

interface Result {
    String print();
}

class Caclulation {
    private final Parameter paramater1;

    private final Parameter parameter2;

    public Calculation(final Parameter parameter1, final Parameter parameter2) {
        this.parameter1 = parameter1;
        this.parameter2 = parameter2;
    }

    public Result calculate() {
        ...calculate the result
    }
}

class FormattedResult {
    private final Result result;

    public FormattedResult(final Result result) {
        this.result = result;
    }

    @Override
    public String print() {
        ...interact with this.result to format it and return the formatted String
    }
}

Como seu exemplo usa classes da biblioteca Java que não suportam esse design, você pode simplesmente usar a API ZonedDateTimediretamente. A idéia aqui é que cada cálculo seja encapsulado dentro de seu próprio objeto. Não faz suposições sobre quantas vezes deve ser executado ou como deve formatar o resultado. Trata-se exclusivamente de executar a forma mais simples do cálculo. Isso facilita o entendimento e é flexível para alterar. Da mesma forma, o Resultitem se preocupa exclusivamente em encapsular o resultado do cálculo, e o FormattedResultitem se preocupa exclusivamente em interagir com o Resultpara formatá-lo de acordo com as regras que definimos. Nesse caminho,podemos encontrar o número perfeito de argumentos para cada um dos nossos métodos, pois cada um deles possui uma tarefa bem definida . Também é muito mais simples modificar o avanço, desde que as interfaces não sejam alteradas (o que provavelmente não ocorrerão se você tiver minimizado adequadamente as responsabilidades de seus objetos). Nossomain()método pode ficar assim:

class App {
    public static void main(String[] args) {
        final List<Set<Paramater>> parameters = ...instantiated from args
        parameters.forEach(set -> {
            System.out.println(
                new FormattedResult(
                    new Calculation(
                        set.get(0),
                        set.get(1)
                    ).calculate()
                ).print()
            );
        });
    }
}

De fato, a Programação Orientada a Objetos foi inventada especificamente como uma solução para o problema de complexidade / flexibilidade do paradigma Imperativo, porque simplesmente não há uma boa resposta (com a qual todos possam concordar ou chegar de forma independente) de como otimizar. especifique funções e procedimentos imperativos dentro do idioma.

Stuporman
fonte
Essa é uma resposta muito detalhada e bem pensada, mas, infelizmente, acho que não percebemos o que o OP estava realmente pedindo. Ele não estava pedindo uma lição sobre boas práticas de POO para resolver seu exemplo ilusório, ele estava perguntando sobre os critérios pelos quais decidimos investir tempo em uma solução versus generalização.
maple_shaft
@ maple_shaft Talvez eu tenha perdido o alvo, mas acho que você também. O OP não pergunta sobre o investimento de tempo versus generalização. Ele pergunta "Como sei como meus métodos devem ser reutilizáveis?" Ele continua perguntando no corpo de sua pergunta: "Se destroyCity (cidade) é melhor que destroyBaghdad (), takeActionOnCity (ação / ação, cidade) é ainda melhor? Por que / por que não?" Defendi uma abordagem alternativa para soluções de engenharia que, acredito, resolve o problema de descobrir quão genérico é a criação de métodos e forneci exemplos para apoiar minha afirmação. Me desculpe, você não gostou.
Stuporman
@maple_shaft Francamente, apenas o OP pode determinar se minha resposta foi relevante para sua pergunta, já que o resto de nós poderia travar guerras defendendo nossas interpretações de suas intenções, e tudo isso poderia estar igualmente errado.
Stuporman
@maple_shaft Adicionei uma introdução para tentar esclarecer como ela se relaciona com a pergunta e forneci um delineamento claro entre a resposta e a implementação do exemplo. Isto é melhor?
Stuporman
1
Honestamente, se você aplicar todos esses princípios, a resposta virá naturalmente, será suave e legível em massa. Além disso, mutável sem muito barulho. Não sei quem você é, mas gostaria que houvesse mais de você! Eu continuo copiando o código Reativo para obter OO decente, e SEMPRE é metade do tamanho, mais legível, mais controlável e ainda possui segmentação / divisão / mapeamento. Acho que o React é para pessoas que não entendem os conceitos "básicos" que você acabou de listar.
Stephen J
3

Experiência , conhecimento de domínio e revisões de código.

E, independentemente de quanta ou pouca experiência , conhecimento de domínio ou equipe você tenha, não será possível evitar a necessidade de refatorar conforme necessário.


Com o Experience , você começará a reconhecer padrões nos métodos (e classes) não específicos do domínio que você escreve. E, se você estiver interessado no código DRY, sentirá maus sentimentos ao escrever um método que, instintivamente, sabe que escreverá variações no futuro. Então, você intuitivamente escreverá um denominador menos comum parametrizado.

(Essa experiência também pode ser transferida instintivamente para alguns objetos e métodos do seu domínio.)

Com o Conhecimento do domínio , você terá uma noção de quais conceitos de negócios estão intimamente relacionados, quais conceitos têm variáveis, quais são razoavelmente estáticos etc.

Com as Revisões de código, é mais provável que a sub e a parametrização sejam detectadas antes de se tornar código de produção, porque seus pares (esperançosamente) terão experiências e perspectivas únicas, tanto no domínio quanto na codificação em geral.


Dito isto, os novos desenvolvedores geralmente não têm esses Spidey Senses ou um grupo experiente de colegas para se apoiar imediatamente. E, mesmo desenvolvedores experientes se beneficiam de uma disciplina básica para guiá-los através de novos requisitos - ou em dias enevoados. Então, aqui está o que eu sugeriria para começar :

  • Comece com a implementação ingênua, com parametrização mínima.
    (Inclua todos os parâmetros que você já sabe que precisará, obviamente ...)
  • Remova números mágicos e seqüências de caracteres, movendo-os para configurações e / ou parâmetros
  • Fatore métodos "grandes" em métodos menores e bem nomeados
  • Refatorar métodos altamente redundantes (se conveniente) em um denominador comum, parametrizando as diferenças.

Essas etapas não ocorrem necessariamente na ordem declarada. Se você se sentar para escrever um método que você já sabe ser altamente redundante com um método existente , vá direto para a refatoração, se for conveniente. (Se não levar muito mais tempo para refatorar do que seria apenas escrever, testar e manter dois métodos).

Mas, além de ter muita experiência, aconselho o código bastante minimalista de DRY-ing. Não é difícil refatorar violações óbvias. E, se você é muito zeloso , pode acabar com um código "over-DRY" que é ainda mais difícil de ler, entender e manter do que o equivalente a "WET".

svidgen
fonte
2
Portanto, não há resposta certa para If destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?? É uma pergunta sim / não, mas não tem resposta, certo? Ou a suposição inicial está errada, destroyCity(City)pode não ser necessariamente melhor e depende realmente ? Portanto, isso não significa que eu não sou um engenheiro de software com formação ética, porque eu implementei diretamente sem parâmetros em primeiro lugar? Quero dizer, qual é a resposta para a pergunta concreta que estou fazendo?
Koray Tugay
Sua pergunta meio que faz algumas perguntas. A resposta para a pergunta do título é "experiência, conhecimento de domínio, revisão de código ... e ... não tenha medo de refatorar". A resposta para qualquer pergunta concreta "são estes os parâmetros corretos para a pergunta do método ABC" é ... "Eu não sei. Por que você está perguntando? Há algo errado com o número de parâmetros que ela possui atualmente? Em caso afirmativo, articule conserte. " ... Eu poderia encaminhá-lo para "o POAP" para obter mais orientações: Você precisa entender por que está fazendo o que está fazendo!
Svidgen
Quero dizer ... vamos dar um passo atrás no destroyBaghdad()método. Qual é o contexto? É um videogame em que o final do jogo resulta em Bagdá sendo destruída ??? Se assim for ... destroyBaghdad()pode ser um perfeitamente razoável nome do método / assinatura ...
svidgen
1
Então você não concorda com a citação citada na minha pergunta, certo? It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.Se você estivesse na sala com Nathaniel Borenstein, você argumentaria que isso realmente depende e sua afirmação não está correta? Quero dizer, é bonito que muitas pessoas estejam respondendo, gastando seu tempo e energia, mas não vejo uma única resposta concreta em lugar algum. Spidey-senses, code reviews .. Mas qual é a resposta is takeActionOnCity(Action action, City city) better? null?
Koray Tugay
1
@svidgen Obviamente, outra maneira de abstrair isso sem nenhum esforço extra é inverter a dependência - faça com que a função retorne uma lista de cidades, em vez de executar qualquer ação nelas (como o "println" no código original). Isso pode ser resumido ainda mais, se necessário, mas apenas essa alteração por si só atende a metade dos requisitos adicionados na pergunta original - e, em vez de uma função impura que pode fazer todo tipo de coisa ruim, você apenas tem uma função que retorna uma lista e o chamador faz as coisas ruins.
11268 Luaan 29/11
2

A mesma resposta que com qualidade, usabilidade, dívida técnica, etc:

Como reutilizável como você, o usuário, 1 precisa deles para ser

É basicamente uma decisão judicial - se o custo de projetar e manter a abstração será reembolsado pelo custo (= tempo e esforço) que o economizará na linha.

  • Observe a frase "abaixo da linha": há um mecânico de pagamento aqui, portanto, isso dependerá de quanto você trabalhará mais com esse código. Por exemplo:
    • Esse é um projeto pontual ou será aprimorado progressivamente por um longo tempo?
    • Você está confiante no seu design ou provavelmente terá que descartá-lo ou alterá-lo drasticamente para o próximo projeto / marco (por exemplo, tente outra estrutura)?
  • O benefício projetado também depende da sua capacidade de prever o futuro (alterações no aplicativo). Às vezes, você pode ver razoavelmente o local do seu aplicativo. Mais vezes, você pensa que pode, mas na verdade não pode. As regras básicas aqui são o princípio YAGNI e a regra dos três - ambos enfatizam o trabalho com o que você sabe agora.

1 Esta é uma construção de código, então você é o "usuário" neste caso - o usuário do código-fonte

ivan_pozdeev
fonte
1

Há um processo claro que você pode seguir:

  • Escreva um teste com falha para um único recurso que por si só é uma "coisa" (ou seja, não é uma divisão arbitrária de um recurso em que nem a metade realmente faz sentido).
  • Escreva o código mínimo absoluto para que ele passe verde, não mais uma linha.
  • Enxague e repita.
  • (Refatorie incansavelmente, se necessário, o que deve ser fácil devido à excelente cobertura do teste.)

Isso acontece com - pelo menos na opinião de algumas pessoas - o código ideal, já que é o menor possível, cada recurso concluído leva o mínimo de tempo possível (o que pode ou não ser verdadeiro se você olhar para o finalizado produto após a refatoração) e possui uma cobertura de teste muito boa. Também evita visivelmente métodos ou classes demasiado genéricos com excesso de engenharia.

Isso também fornece instruções muito claras sobre quando tornar as coisas genéricas e quando se especializar.

Acho o exemplo da sua cidade estranho; Eu provavelmente nunca codificaria o nome de uma cidade. É tão óbvio que cidades adicionais serão incluídas mais tarde, seja lá o que você estiver fazendo. Mas outro exemplo seria cores. Em algumas circunstâncias, codificar "vermelho" ou "verde" seria uma possibilidade. Por exemplo, os semáforos são de uma cor tão onipresente que você pode simplesmente se safar (e sempre pode refatorar). A diferença é que "vermelho" e "verde" têm um significado universal e "codificado" em nosso mundo, é incrivelmente improvável que isso mude, e também não há realmente uma alternativa.

Seu primeiro método de horário de verão é simplesmente quebrado. Embora esteja em conformidade com as especificações, o codificado em 2018 é particularmente ruim porque a) não é mencionado no "contrato" técnico (no nome do método, neste caso) eb) estará desatualizado em breve, portanto, a quebra está incluído desde o início. Para coisas relacionadas à hora / data, raramente faria sentido codificar um valor específico, já que, bem, o tempo continua. Além disso, tudo o mais está em discussão. Se você der um ano simples e sempre calcular o ano completo, vá em frente. A maioria das coisas que você listou (formatação, escolha de um intervalo menor etc.) grita que seu método está fazendo muito e, provavelmente, deve retornar uma lista / matriz de valores para que o chamador possa fazer a formatação / filtragem por conta própria.

Mas no final do dia, a maior parte disso é opinião, gosto, experiência e preconceito pessoal; portanto, não se preocupe muito com isso.

AnoE
fonte
Com relação ao seu penúltimo parágrafo, observe os "requisitos" dados inicialmente, ou seja, aqueles nos quais o primeiro método foi baseado. Ele especifica 2018, para que o código esteja tecnicamente correto (e provavelmente corresponda à sua abordagem orientada a recursos).
dwizum
@ dwizum, está correto quanto aos requisitos, mas o nome do método é enganoso. Em 2019, qualquer programador que apenas olhar para o nome do método assumiria que está fazendo o que quer (talvez retorne os valores para o ano atual), não para 2018 ... adicionarei uma frase à resposta para deixar mais claro o que eu quis dizer.
AnoE
1

Cheguei à opinião de que existem dois tipos de código reutilizável:

  • Código reutilizável porque é uma coisa fundamental e básica.
  • Código reutilizável porque possui parâmetros, substituições e ganchos para todos os lugares.

O primeiro tipo de reutilização geralmente é uma boa idéia. Aplica-se a coisas como listas, hashmaps, armazenamentos de chave / valor, correspondentes de sequência de caracteres (por exemplo, regex, glob, ...), tuplas, unificação, árvores de pesquisa (profundidade primeiro, largura primeiro, aprofundamento iterativo, ...) , combinadores de analisador, caches / memoisers, leitores / gravadores de formato de dados (expressões-s, XML, JSON, protobuf, ...), filas de tarefas etc.

Essas coisas são tão gerais, de uma maneira muito abstrata, que são reutilizadas em todo o lugar na programação do dia a dia. Se você escrever um código para fins especiais que seria mais simples se fosse mais abstrato / geral (por exemplo, se tivermos "uma lista de pedidos de clientes", poderíamos jogar fora o material "pedido de clientes" para obter "uma lista" ), pode ser uma boa ideia extrair isso. Mesmo que não seja reutilizado, permite dissociar a funcionalidade não relacionada.

O segundo tipo é onde temos um código concreto, que resolve um problema real, mas o faz tomando várias decisões. Podemos torná-lo mais geral / reutilizável "codificando com facilidade" essas decisões, por exemplo, transformando-as em parâmetros, complicando a implementação e fornecendo detalhes ainda mais concretos (ou seja, conhecimento de quais ganchos podemos desejar para substituições). Seu exemplo parece ser desse tipo. O problema com esse tipo de reutilização é que podemos acabar tentando adivinhar os casos de uso de outras pessoas ou de nosso futuro. Eventualmente, podemos acabar tendo tantos parâmetros que nosso código não é utilizável, muito menos reutilizável! Em outras palavras, ao ligar, é preciso mais esforço do que apenas escrever nossa própria versão. É aqui que YAGNI (você não vai precisar) é importante. Muitas vezes, essas tentativas de código "reutilizável" acabam não sendo reutilizadas, uma vez que podem ser incompatíveis com esses casos de uso mais fundamentalmente do que os parâmetros podem explicar, ou esses usuários em potencial preferem fazer o seu próprio (heck, veja todas as padrões e bibliotecas por aí cujos autores prefixaram a palavra "Simples", para distingui-los dos antecessores!).

Essa segunda forma de "reutilização" deve ser basicamente feita conforme a necessidade. Claro, você pode colocar alguns parâmetros "óbvios", mas não comece a tentar prever o futuro. YAGNI.

Warbo
fonte
Podemos dizer que você concorda que minha primeira tomada foi boa, onde até o ano foi codificado? Ou se você estivesse implementando inicialmente esse requisito, tornaria o ano um parâmetro em sua primeira captura?
precisa
Sua primeira tentativa foi boa, pois o requisito era um script único para 'verificar alguma coisa'. Ele falha no teste "ético", mas ela falha no teste "sem dogma". "Ela pode dizer ..." está inventando requisitos dos quais você não precisa.
Warbo 30/11/19
Não podemos dizer qual 'cidade destruída' é "melhor" sem mais informações: destroyBaghdadé um script único (ou pelo menos é idempotente). Talvez destruir qualquer cidade seria uma melhoria, mas e se destroyBaghdadfuncionar inundando o Tigre? Isso pode ser reutilizável para Mosul e Basra, mas não para Meca ou Atlanta.
Warbo 30/11/19
Entendo que você não concorde com Nathaniel Borenstein, o proprietário da citação. Estou tentando entender lentamente, lendo todas essas respostas e discussões.
Koray Tugay
1
Eu gosto dessa diferenciação. Nem sempre é claro, e sempre existem "casos de fronteira". Mas, em geral, também sou fã de "blocos de construção" (geralmente na forma de staticmétodos) que são puramente funcionais e de baixo nível, e, em contraste com isso, decidir sobre os "parâmetros e ganchos de configuração" geralmente é onde você tem que construir algumas estruturas que pedem justificação.
precisa saber é o seguinte
1

Já existem muitas respostas excelentes e elaboradas. Alguns deles se aprofundam em detalhes específicos, expõem certos pontos de vista sobre metodologias de desenvolvimento de software em geral, e alguns deles certamente têm elementos polêmicos ou "opiniões" espalhadas.

A resposta de Warbo já apontava diferentes tipos de reutilização. Ou seja, se algo é reutilizável por ser um componente fundamental ou se algo é reutilizável por ser "genérico" de alguma maneira. Referindo-se a este último, há algo que eu consideraria como algum tipo de medida de reutilização:

Se um método pode emular outro.

Com relação ao exemplo da pergunta: Imagine que o método

void dayLightSavings()

foi a implementação de uma funcionalidade solicitada por um cliente. Portanto, será algo que outros programadores devem usar e, portanto, ser um método público , como em

publicvoid dayLightSavings()

Isso pode ser implementado como você mostrou na sua resposta. Agora, alguém quer parametrizar isso com o ano. Então você pode adicionar um método

publicvoid dayLightSavings(int year)

e altere a implementação original para ser apenas

public void dayLightSavings() {
    dayLightSavings(2018);
}

Os próximos "pedidos de recursos" e generalizações seguem o mesmo padrão. Portanto, se e somente se houver demanda para a forma mais genérica, você poderá implementá-la, sabendo que essa forma mais genérica permite implementações triviais das mais específicas:

public void dayLightSavings() {
    dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}

Se você tivesse antecipado extensões futuras e solicitações de recursos, e tivesse algum tempo à sua disposição e desejasse passar um fim de semana entediante com generalizações (potencialmente inúteis), poderia começar com a mais genérica desde o início. Mas apenas como um método privado . Contanto que você apenas exponha o método simples solicitado pelo cliente como o método público , você estará seguro.

tl; dr :

A questão não é tanto "quão reutilizável deve ser um método". A questão é quanto dessa reutilização está exposta e como é a API. Criar uma API confiável que possa resistir ao teste do tempo (mesmo quando outros requisitos surgirem mais tarde) é uma arte e um ofício, e o tópico é muito complexo para abordá-lo aqui. Dê uma olhada nesta apresentação de Joshua Bloch ou no wiki do livro de design da API para começar.

Marco13
fonte
dayLightSavings()telefonar dayLightSavings(2018)não me parece uma boa ideia.
Koray Tugay
@KorayTugay Quando o pedido inicial é que ele imprima "o horário de verão de 2018", tudo bem. De fato, esse é exatamente o método que você implementou originalmente. Se deveria imprimir o "horário de verão para o ano atual , é claro que você ligaria dayLightSavings(computeCurrentYear());. ..."
Marco13
0

Uma boa regra geral é: seu método deve ser tão reutilizável quanto ... reutilizável.

Se você espera que você chamará seu método apenas em um local, ele deverá ter apenas parâmetros conhecidos pelo site de chamada e que não estejam disponíveis para esse método.

Se você tiver mais chamadores, poderá introduzir novos parâmetros, desde que outros chamadores passem por esses parâmetros; caso contrário, você precisará de um novo método.

Como o número de chamadas pode aumentar com o tempo, você precisa estar preparado para refatorar ou sobrecarregar. Em muitos casos, isso significa que você deve se sentir seguro para selecionar a expressão e executar a ação "extrair parâmetro" do seu IDE.

Karol
fonte
0

Resposta ultra curta: quanto menos acoplamento ou dependência de outro código o seu módulo genérico tiver, mais reutilizável poderá ser.

Seu exemplo depende apenas de

import java.time.*;
import java.util.Set;

portanto, em teoria, pode ser altamente reutilizável.

Na prática, acho que você nunca terá um segundo caso de uso que precise desse código, portanto, seguindo o princípio yagni , eu não o tornaria reutilizável se não houvesse mais de três projetos diferentes que precisassem desse código.

Outros aspectos da reutilização são a facilidade de uso e a documentação que se correlacionam com o Desenvolvimento Orientado a Testes : É útil se você tiver um teste unitário simples que demonstre / documente o uso fácil do seu módulo genérico como um exemplo de codificação para os usuários da sua biblioteca.

k3b
fonte
0

Esta é uma boa oportunidade para declarar uma regra que cunhei recentemente:

Ser um bom programador significa ser capaz de prever o futuro.

Claro, isso é estritamente impossível! Afinal, você nunca sabe ao certo quais generalizações serão úteis mais tarde, quais tarefas relacionadas você deseja executar, quais novos recursos seus usuários desejarão, etc. Mas a experiência às vezes fornece uma idéia aproximada do que pode ser útil.

Os outros fatores que você deve comparar são quanto tempo e esforço extras estariam envolvidos e quão mais complexo isso tornaria seu código. Às vezes você tem sorte e resolver o problema mais geral é mais simples! (Pelo menos conceitualmente, se não na quantidade de código.) Mas, mais frequentemente, há um custo de complexidade e também de tempo e esforço.

Portanto, se você acha que uma generalização provavelmente é necessária, geralmente vale a pena fazer (a menos que adicione muito trabalho ou complexidade); mas se parecer muito menos provável, provavelmente não será (a menos que seja muito fácil e / ou simplifique o código).

(Para um exemplo recente: na semana passada, recebi uma especificação para ações que um sistema deveria levar exatamente 2 dias após a expiração de algo. Então, é claro, tornei o período de 2 dias um parâmetro. Nesta semana, os empresários ficaram encantados, pois Eu estava com sorte: foi uma mudança fácil e imaginei que era provável que fosse desejada. Muitas vezes é mais difícil julgar. Mas ainda vale a pena tentar prever, e a experiência geralmente é um bom guia .)

gidds
fonte
0

Em primeiro lugar, a melhor resposta para 'Como sei como meus métodos devem ser reutilizáveis?' É 'experiência'. Faça isso alguns milhares de vezes e normalmente você obtém a resposta certa. Mas, como provocação, posso lhe dar a última linha desta resposta: Seu cliente lhe dirá quanta flexibilidade e quantas camadas de generalização você deve procurar.

Muitas dessas respostas têm conselhos específicos. Eu queria dar algo mais genérico ... porque a ironia é muito divertida para deixar passar!

Como algumas das respostas observaram, a generalidade é cara. No entanto, realmente não é. Nem sempre. Entender a despesa é essencial para jogar o jogo de reutilização.

Eu me concentro em colocar as coisas em uma escala de "irreversível" a "reversível". É uma escala suave. A única coisa verdadeiramente irreversível é "o tempo gasto no projeto". Você nunca receberá esses recursos de volta. Um pouco menos reversível pode ser a situação das "algemas douradas", como a API do Windows. Os recursos descontinuados permanecem nessa API há décadas porque o modelo de negócios da Microsoft exige isso. Se você tiver clientes cujo relacionamento seria permanentemente danificado ao desfazer algum recurso da API, isso deverá ser tratado como irreversível. Olhando para o outro extremo da escala, você tem coisas como código de protótipo. Se você não gosta de onde ele vai, você pode simplesmente jogá-lo fora. Um pouco menos reversível pode ser APIs de uso interno. Eles podem ser refatorados sem incomodar um cliente,time (o recurso mais irreversível de todos!)

Então, coloque-os em uma escala. Agora você pode aplicar uma heurística: quanto mais reversível algo, mais você pode usá-lo para atividades futuras. Se algo for irreversível, use-o apenas para tarefas concretas conduzidas pelo cliente. É por isso que você vê princípios como os da programação extrema que sugerem apenas fazer o que o cliente pede e nada mais. Esses princípios são bons para garantir que você não faça algo de que se arrependa.

Coisas como o princípio DRY sugerem uma maneira de mover esse equilíbrio. Se você se repetir, é uma oportunidade de criar o que é basicamente uma API interna. Nenhum cliente vê isso, então você sempre pode alterá-lo. Depois de ter essa API interna, agora você pode começar a jogar com coisas voltadas para o futuro. Quantas tarefas diferentes baseadas no fuso horário você acha que sua esposa vai lhe dar? Você tem outros clientes que desejam tarefas baseadas no fuso horário? Sua flexibilidade aqui é adquirida pelas demandas concretas de seus clientes atuais e suporta as demandas futuras em potencial de futuros clientes.

Essa abordagem de pensamento em camadas, que vem naturalmente da DRY, fornece a generalização desejada sem desperdício. Mas existe um limite? Claro que existe. Mas para vê-lo, você precisa ver a floresta para as árvores.

Se você tem muitas camadas de flexibilidade, elas geralmente levam à falta de controle direto das camadas que seus clientes enfrentam. Eu tive um software em que tive a brutal tarefa de explicar a um cliente por que ele não pode ter o que deseja devido à flexibilidade construída em 10 camadas abaixo da qual nunca deveria ver. Nós nos escrevemos em um canto. Nós nos amarramos com toda a flexibilidade que pensávamos que precisávamos.

Portanto, quando você estiver fazendo esse truque de generalização / DRY, sempre mantenha um pulso no seu cliente . O que você acha que sua esposa vai pedir em seguida? Você está se colocando em uma posição para atender a essas necessidades? Se você tiver o dom, o cliente efetivamente informará suas necessidades futuras. Se você não tem talento, bem, a maioria de nós depende apenas de suposições! (especialmente com os cônjuges!) Alguns clientes desejam uma grande flexibilidade e estão dispostos a aceitar o custo extra do seu desenvolvimento com todas essas camadas porque se beneficiam diretamente da flexibilidade dessas camadas. Outros clientes fixaram requisitos inabaláveis ​​e preferem que o desenvolvimento seja mais direto. Seu cliente lhe dirá quanta flexibilidade e quantas camadas de generalização você deve procurar.

Cort Ammon
fonte
Bem, deve haver outras pessoas que fizeram isso 10000 vezes; então, por que devo fazer isso 10000 vezes e ganhar experiência quando posso aprender com os outros? Como a resposta será diferente para cada indivíduo, as respostas de pessoas experientes não são aplicáveis ​​a mim? Também Your customer will tell you how much flexibility and how many layers of generalization you should seek.que mundo é esse?
precisa
@KorayTugay É um mundo de negócios. Se seus clientes não estão dizendo o que fazer, você não está ouvindo o suficiente. Obviamente, eles nem sempre dizem em palavras, mas de outras maneiras. A experiência ajuda você a ouvir as mensagens mais sutis. Se você ainda não possui essa habilidade, encontre alguém em sua empresa que tenha a habilidade de ouvir essas dicas sutis do cliente e peça orientação. Alguém terá essa habilidade, mesmo que seja o CEO ou o marketing.
Cort Ammon
No seu caso específico, se você não conseguiu remover o lixo porque estava muito ocupado codificando uma versão generalizada desse problema de fuso horário em vez de criar uma solução específica, como o seu cliente se sentiria?
Cort Ammon
Então você concorda que minha primeira abordagem foi a correta, codificando 2018 na primeira tomada em vez de parametrizar o ano? (btw, isso não está realmente ouvindo meu cliente, eu acho, o exemplo do lixo. Isso é conhecer seu cliente. Mesmo que obtenha suporte da Oracle, não há mensagens sutis para ouvir quando ela diz que preciso de uma lista de luz do dia alterações para 2018.) Obrigado pelo seu tempo e responda btw.
precisa
@KorayTugay Sem conhecer detalhes adicionais, eu diria que a codificação é a abordagem correta. Você não tinha como saber se iria precisar do futuro código DLS, nem tinha idéia do tipo de solicitação que ela poderia dar a seguir. E se o seu cliente está tentando testá-lo, ele recebe o que recebe = D
Cort Ammon
0

nenhum engenheiro de software treinado com ética aceitaria escrever um procedimento para DestroyBaghdad. A ética profissional básica exigiria que ele escrevesse um procedimento DestroyCity, ao qual Bagdá poderia ser dada como parâmetro.

Isso é algo conhecido nos círculos avançados de engenharia de software como uma "piada". As piadas não precisam ser o que chamamos de "verdade", embora, para serem engraçadas, geralmente devam sugerir algo verdadeiro.

Nesse caso em particular, a "piada" não é "verdadeira". O trabalho envolvido na elaboração de um procedimento geral para destruir qualquer cidade é, podemos assumir com segurança, ordens de magnitude além da necessária para destruir uma cidade. específica.cidade. Caso contrário, qualquer pessoa que destrua uma ou várias cidades (o Josué bíblico, digamos, ou o Presidente Truman) poderia generalizar trivialmente o que fez e ser capaz de destruir absolutamente qualquer cidade à vontade. Na verdade, esse não é o caso. Os métodos que essas duas pessoas famosamente usaram para destruir um pequeno número de cidades específicas não necessariamente funcionariam em qualquer cidade a qualquer momento. Outra cidade cujas muralhas tinham frequências ressonantes diferentes ou cujas defesas aéreas de altitude eram melhores, precisaria de mudanças menores ou fundamentais de abordagem (uma trombeta de tom diferente ou um foguete).

Isso também leva à manutenção do código contra mudanças ao longo do tempo: agora existem muitas cidades que não se enquadram em nenhuma dessas abordagens, graças aos métodos modernos de construção e ao radar onipresente.

Desenvolver e testar um meio completamente geral que destruirá qualquer cidade, antes que você concorde em destruir apenas uma cidade, é uma abordagem desesperadamente ineficiente. Nenhum engenheiro de software treinado eticamente tentaria generalizar um problema a ponto de exigir ordens de magnitude mais trabalho do que seu empregador / cliente realmente precisa pagar, sem necessidade demonstrada.

Então o que é verdade? Às vezes, adicionar generalidade é trivial. Devemos sempre adicionar generalidade quando é trivial fazê-lo? Eu ainda argumentaria "não, nem sempre", devido ao problema de manutenção a longo prazo. Supondo que, no momento em que escrevo, todas as cidades sejam basicamente as mesmas, então eu continuo com o DestroyCity. Depois de escrever isso, junto com os testes de integração que (devido ao espaço finito e inumerável de entradas) iteram sobre todas as cidades conhecidas e garantem que a função funcione em cada uma (não sei como isso funciona. Provavelmente chama City.clone () e destrói o clone? Enfim).

Na prática, a função é usada apenas para destruir Bagdá, suponha que alguém construa uma nova cidade resistente às minhas técnicas (é subterrânea ou algo assim). Agora, eu tenho uma falha no teste de integração para um caso de uso que nem sequer existe , e antes que eu possa continuar minha campanha de terror contra os civis inocentes do Iraque, tenho que descobrir como destruir a Subterrania. Não importa se isso é ético ou não, é estúpido e um desperdício do meu tempo .

Então, você realmente deseja uma função que possa gerar horário de verão para qualquer ano, apenas para gerar dados para 2018? Talvez, mas certamente exigirá uma pequena quantidade de esforço extra para reunir os casos de teste. Pode ser necessário um grande esforço para obter um banco de dados de fuso horário melhor do que o que você realmente possui. Por exemplo, em 1908, a cidade de Port Arthur, Ontário, teve um período de horário de verão começando em 1º de julho. Isso está no banco de dados de fuso horário do seu sistema operacional? Pensou que não, então sua função generalizada está errada . Não há nada especialmente ético em escrever código que faça promessas que não pode cumprir.

Tudo bem, então, com advertências apropriadas, é fácil escrever uma função que faz fusos horários por vários anos, digamos, nos dias atuais de 1970. Mas é tão fácil assumir a função que você realmente escreveu e generalizá-la para parametrizar o ano. Portanto, não é realmente mais ético / sensato generalizar agora, que é fazer o que você fez e generalizar se e quando você precisar.

No entanto, se você soubesse por que sua esposa queria verificar esta lista de horário de verão, teria uma opinião informada sobre se ela provavelmente fará a mesma pergunta em 2019 e, se for, se você pode sair desse ciclo dando ela é uma função que ela pode chamar, sem precisar recompilar. Depois de fazer essa análise, a resposta para a pergunta "isso deve generalizar para os anos recentes" pode ser "sim". Mas você cria outro problema para si mesmo: os dados do fuso horário no futuro são apenas provisórios e, portanto, se ela os executar para 2019 hoje, ela pode ou não perceber que isso está lhe dando um palpite. Portanto, você ainda precisa escrever um monte de documentação que não seria necessária para a função menos geral ("os dados vêm do banco de dados de fuso horário blá blá, eis o link para ver suas políticas sobre atualizações de blá blá"). Se você recusar o caso especial, o tempo todo, ela não poderá continuar com a tarefa pela qual precisava dos dados de 2018, por causa de algumas bobagens sobre 2019 com as quais ainda nem se importa.

Não faça coisas difíceis sem pensar adequadamente, apenas porque uma piada lhe disse. É útil? É barato o suficiente para esse grau de utilidade?

Steve Jessop
fonte
0

Essa é uma linha fácil de desenhar, porque a reutilização, conforme definida pelos astronautas da arquitetura, é uma besteira.

Quase todo o código criado pelos desenvolvedores de aplicativos é extremamente específico do domínio. Isso não é 1980. Quase tudo o que vale a pena está em uma estrutura.

Abstrações e convenções requerem documentação e esforço de aprendizado. Por favor, pare de criar novos apenas por isso. (Eu estou olhando para você , pessoal JavaScript!)

Vamos ceder à fantasia implausível de que você encontrou algo que realmente deveria estar na sua estrutura de escolha. Você não pode simplesmente digitar o código da maneira que costuma fazer. Ah, não, você precisa de cobertura de teste não apenas para o uso pretendido, mas também para desvios do uso pretendido, para todos casos extremos conhecidos, para todos os modos de falha imagináveis, casos de teste para diagnóstico, dados de teste, documentação técnica, documentação do usuário, gerenciamento de liberação, suporte scripts, testes de regressão, gerenciamento de alterações ...

Seu empregador está feliz em pagar por tudo isso? Eu vou dizer não.

Abstração é o preço que pagamos pela flexibilidade. Isso torna nosso código mais complexo e difícil de entender. A menos que a flexibilidade atenda a uma necessidade real e presente, apenas não o faça, porque a YAGNI.

Vamos dar uma olhada em um exemplo do mundo real com o qual tive que lidar: HTMLRenderer. Não estava dimensionando corretamente quando tentei renderizar no contexto do dispositivo de uma impressora. Levei o dia inteiro para descobrir que, por padrão, estava usando GDI (que não é dimensionável) em vez de GDI + (que faz, mas não antialias) porque eu tive que percorrer seis níveis de indireção em duas montagens antes de encontrar código que fez qualquer coisa .

Nesse caso, perdoarei o autor. A abstração é realmente necessária porque isso é código da estrutura que visa cinco destinos de renderização muito diferentes: WinForms, WPF, dotnet Core, Mono e PdfSharp.

Mas isso apenas ressalta o meu argumento: você certamente não está fazendo algo extremamente complexo (renderização HTML com folhas de estilo) visando várias plataformas com um objetivo declarado de alto desempenho em todas as plataformas.

Seu código é quase certamente mais uma grade de banco de dados com regras comerciais que se aplicam apenas ao seu empregador e regras fiscais que se aplicam apenas ao seu estado, em um aplicativo que não está à venda.

Toda essa indireção resolve um problema que você não tem e torna seu código muito mais difícil de ler, o que aumenta enormemente o custo de manutenção e é um enorme desserviço para o seu empregador. Felizmente, as pessoas que deveriam reclamar disso são incapazes de compreender o que você está fazendo com elas.

Um contra-argumento é que esse tipo de abstração suporta o desenvolvimento orientado a testes, mas acho que o TDD também é um problema, porque pressupõe que a empresa tenha uma compreensão clara, completa e correta de seus requisitos. O TDD é ótimo para a NASA e software de controle para equipamentos médicos e carros autônomos, mas muito caro para todos os outros.


Aliás, não é possível prever todas as horas de verão do mundo. Israel, em particular, tem cerca de 40 transições por ano que acontecem por todo o lado, porque não podemos ter pessoas orando na hora errada e Deus não faz o horário de verão.

Peter Wone
fonte
Embora eu concorde com o que você disse sobre abstrações e esforço de aprendizado, e particularmente quando ele visa a abominação chamada "JavaScript", eu tenho que discordar totalmente da primeira frase. A reutilização pode ocorrer em vários níveis, pode ir muito longe e o código de descarte pode ser escrito por programadores de descarte. Mas não são as pessoas que estão idealista o suficiente para, pelo menos, objetivo pelo código reutilizável. Uma pena se você não vê os benefícios que isso pode trazer.
Marco13
@ Marco13 - Como a sua objeção é razoável, vou expandir o assunto.
Peter Wone
-3

Se você estiver usando pelo menos o java 8, escreveria a classe WorldTimeZones para fornecer o que, em essência, parece ser uma coleção de fusos horários.

Em seguida, adicione um método de filtro (filtro de predicado) à classe WorldTimeZones. Isso permite que o chamador filtre o que quiser, passando uma expressão lambda como parâmetro.

Em essência, o método de filtro único suporta a filtragem de qualquer coisa contida no valor passado ao predicado.

Como alternativa, adicione um método stream () à sua classe WorldTimeZones que produz um fluxo de fusos horários quando chamado. Em seguida, o chamador pode filtrar, mapear e reduzir conforme desejado, sem a necessidade de nenhuma especialização.

Rodney P. Barbati
fonte
3
Essas são boas idéias de generalização, no entanto, essa resposta falha completamente a marca do que está sendo perguntado na pergunta. A questão não é sobre como melhor generalizar a solução, mas onde traçamos o limite nas generalizações e como pesamos essas considerações eticamente.
Maple_shaft
Portanto, estou dizendo que você deve ponderá-los pelo número de casos de uso que eles suportam, modificados pela complexidade da criação. Um método que suporta um caso de uso não é tão valioso quanto um método que suporta 20 casos de uso. Por outro lado, se tudo o que você conhece é um caso de uso e leva 5 minutos para codificá-lo - vá em frente. Geralmente, os métodos de codificação que oferecem suporte a usos específicos informam sobre a maneira de generalizar.
Rodney P. Barbati 29/11