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.DESTROY
entã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.
fonte
destroyCity(target)
é muito mais antiético do quedestroyBagdad()
! 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.Respostas:
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:
"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 .
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.
Isso depende muito de várias coisas:
takeActionOnCity
mé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 deaction.TakeOn(city);
, você deve se perguntar se otakeActionOnCity
método realmente tem um propósito ou não é apenas uma camada extra que não agrega nada de valor.A mesma pergunta aparece aqui:
Se você puder responder definitivamente "sim" a todos os três, uma abstração será garantida.
fonte
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.println
por 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 useSystem.out
por padrão).fonte
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:
é um conjunto muito diferente de requisitos para:
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
DestroyBaghdad
função pode realmente ser a função que tem esse direito.fonte
Há muitas respostas longas aqui, mas sinceramente acho super simples
então na sua função
Você tem:
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.
fonte
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
Object1
pudessem dizerObject2
o 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 destroyBaghdad
Por 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:Então você tem uma cidade que implementa essa interface:
Nada que exija a destruição de uma instância de
City
alguma vez se importará com isso, portanto, não há razão para que esse código exista em qualquer lugar fora deCity::destroy
, e de fato, um conhecimento íntimo do funcionamento interno deCity
fora de si seria um acoplamento rígido que reduz felxibility, já que você precisa considerar esses elementos externos, caso precise modificar o comportamento deCity
. 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 é
City
ouBaghdad
depende de quão genérico é o processo de destruição da cidade. Com toda a probabilidade, aCity
será 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á implementadaDestroyable
e elas serão instruídas peloCity
destruidor. da mesma maneira que alguém de fora pediuCity
que se destruísse.Se você quiser ficar realmente doido e implementar a ideia de uma
Bomb
que é descartada em um local e destrói tudo dentro de um determinado raio, pode ser algo como isto:ObjectsByRadius
representa um conjunto de objetos que é calculado para oBomb
partir das entradas, porqueBomb
nã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 descartarBomb
e 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:
Como seu exemplo usa classes da biblioteca Java que não suportam esse design, você pode simplesmente usar a API
ZonedDateTime
diretamente. 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, oResult
item se preocupa exclusivamente em encapsular o resultado do cálculo, e oFormattedResult
item se preocupa exclusivamente em interagir com oResult
para 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: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.
fonte
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 :
(Inclua todos os parâmetros que você já sabe que precisará, obviamente ...)
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".
fonte
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?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 ...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 respostais takeActionOnCity(Action action, City city) better?
null
?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.
1 Esta é uma construção de código, então você é o "usuário" neste caso - o usuário do código-fonte
fonte
Há um processo claro que você pode seguir:
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.
fonte
Cheguei à opinião de que existem dois tipos de código reutilizável:
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.
fonte
destroyBaghdad
é um script único (ou pelo menos é idempotente). Talvez destruir qualquer cidade seria uma melhoria, mas e sedestroyBaghdad
funcionar inundando o Tigre? Isso pode ser reutilizável para Mosul e Basra, mas não para Meca ou Atlanta.static
mé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.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
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
public
void 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
public
void dayLightSavings(int year)
e altere a implementação original para ser apenas
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:
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.
fonte
dayLightSavings()
telefonardayLightSavings(2018)
não me parece uma boa ideia.dayLightSavings(computeCurrentYear());
. ..."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.
fonte
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
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.
fonte
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 .)
fonte
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.
fonte
Your customer will tell you how much flexibility and how many layers of generalization you should seek.
que mundo é esse?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?
fonte
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 já 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.
fonte
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.
fonte