Eu realmente amo códigos limpos e sempre quero codificá-los da melhor maneira possível. Mas sempre houve uma coisa que eu realmente não entendi:
Quando é muito "separação de preocupações" em relação a métodos?
Digamos que temos o seguinte método:
def get_last_appearance_of_keyword(file, keyword):
with open(file, 'r') as file:
line_number = 0
for line in file:
if keyword in line:
line_number = line
return line_number
Eu acho que esse método está bem como está. É simples, fácil de ler e claramente mostra o que o nome diz. Mas: não está realmente fazendo "apenas uma coisa". Na verdade, ele abre o arquivo e o encontra. Isso significa que eu poderia dividir ainda mais (considerando também o "Princípio da responsabilidade única"):
Variação B (Bem, isso faz algum sentido. Dessa maneira, podemos facilmente reutilizar o algoritmo de encontrar a última aparência de uma palavra-chave em um texto, mas parece "demais". Não posso explicar o porquê, mas apenas "sinto" "dessa maneira):
def get_last_appearance_of_keyword(file, keyword):
with open(file, 'r') as text_from_file:
line_number = find_last_appearance_of_keyword(text_from_file, keyword)
return line_number
def find_last_appearance_of_keyword(text, keyword):
line_number = 0
for line in text:
if keyword in line:
line_number = line
return line_number
Variação C (na minha opinião, isso é um absurdo. Basicamente, estamos encapsulando uma linha em outro método com apenas uma linha duas vezes. Mas alguém poderia argumentar que a maneira de abrir algo pode mudar no futuro, devido a alguns pedidos de recursos , e como não queremos alterá-lo muitas vezes, mas apenas uma vez, apenas o encapsulamos e separamos ainda mais nossa função principal):
def get_last_appearance_of_keyword(file, keyword):
text_from_file = get_text_from_file(file)
line_number = find_keyword_in_text(text_from_file, keyword)
return line_number
def get_text_from_file(file):
with open(file, 'r') as text:
return text
def find_last_appearance_of_keyword(text, keyword):
line_number = 0
for line in text:
if check_if_keyword_in_string(line, keyword):
line_number = line
return line_number
def check_if_keyword_in_string(text, keyword):
if keyword in string:
return true
return false
Então, minha pergunta agora: qual é a maneira correta de escrever esse código e por que as outras abordagens estão certas ou erradas? Eu sempre aprendi: separação, mas nunca quando é simplesmente demais. E como posso ter certeza no futuro, de que está "correto" e de que não precisa de mais separação quando estou codificando novamente?
fonte
line_number = 0
é um padrão numérico, eline_number = line
atribui um valor de string (que é a linha conteúdos não-lo de posição )open
ein
. Reimplementar as funções existentes não aumenta a separação de preocupações, a preocupação já é tratada na função existente!Respostas:
Seus vários exemplos de divisão de preocupações em funções separadas sofrem com o mesmo problema: você ainda está codificando a dependência do arquivo
get_last_appearance_of_keyword
. Isso dificulta o teste dessa função, pois agora ela precisa responder em um arquivo existente no sistema de arquivos quando o teste é executado. Isso leva a testes frágeis.Então, eu simplesmente mudaria sua função original para:
Agora você tem uma função que tem apenas uma responsabilidade: encontre a última ocorrência de uma palavra-chave em algum texto. Se esse texto vier de um arquivo, será responsabilidade do chamador lidar com isso. Ao testar, você pode apenas passar um bloco de texto. Ao usá-lo com código de tempo de execução, primeiro o arquivo é lido e, em seguida, essa função é chamada. Essa é a verdadeira separação de preocupações.
fonte
line_number = line
é claramente um erro.O princípio da responsabilidade única afirma que uma classe deve cuidar de uma única peça de funcionalidade, e essa funcionalidade deve ser adequadamente encapsulada dentro.
O que exatamente o seu método faz? Obtém a última aparência de uma palavra-chave. Cada linha dentro do método trabalha para isso e não está relacionada a mais nada, e o resultado final é apenas um e um sozinho. Em outras palavras: você não precisa dividir esse método em mais nada.
A idéia principal por trás do princípio é que você não deve fazer mais do que uma coisa no final. Talvez você abra o arquivo e também o deixe assim para que outros métodos possam usá-lo, você estará fazendo duas coisas. Ou, se você persistir com os dados relacionados a esse método, novamente, duas coisas.
Agora, você pode extrair a linha "abrir arquivo" e fazer com que o método receba um objeto de arquivo para trabalhar, mas isso é mais uma refatoração técnica do que tentar obedecer ao SRP.
Este é um bom exemplo de excesso de engenharia. Não pense muito ou você vai acabar com vários métodos de uma linha.
fonte
return keyword in text
isso, basta adicionar uma camada desnecessária em cima de uma construção de linguagem interna.return keyword in text
seria uma camada desnecessária? Se você se encontra consistentemente usando esse código em um lambda como parâmetro em funções de ordem superior, por que não envolvê-lo em uma função?in
é uma palavra-chave comum do Python, cumpre o objetivo e é expressiva por si só. Escrever uma função de invólucro em torno dele apenas para ter uma função de invólucro oculta o código, tornando-o menos imediatamente intuitivo.Minha opinião: depende :-)
Na minha opinião, o código deve atender a esses objetivos, ordenados por prioridade:
Para mim, seu exemplo original passa por todos esses objetivos (exceto talvez a correção por causa do
line_number = line
que já foi mencionado nos comentários , mas esse não é o ponto aqui).A questão é que o SRP não é o único princípio a seguir. Há também Você não vai precisar (YAGNI) (entre muitos outros). Quando os princípios colidem, você precisa equilibrá-los.
Seu primeiro exemplo é perfeitamente legível, fácil de refatorar quando necessário, mas pode não seguir o SRP o máximo possível.
Cada método no seu terceiro exemplo também é perfeitamente legível, mas a coisa toda não é mais tão fácil de entender, porque você precisa juntar todas as peças em sua mente. Ele segue o SRP.
Como você não está conseguindo nada agora de dividir seu método, não faça isso, porque você tem uma alternativa mais fácil de entender.
À medida que seus requisitos mudam, você pode refatorar o método de acordo. De fato, pode ser mais fácil refatorar o "tudo em uma coisa" : imagine que você deseja encontrar a última linha que corresponde a algum critério arbitrário. Agora você só precisa passar alguma função lambda de predicado para avaliar se uma linha corresponde ao critério ou não.
No seu último exemplo, você precisa passar o predicado em 3 níveis, ou seja, modificar 3 métodos apenas para modificar o comportamento do último.
Observe que até dividir a leitura do arquivo (uma refatoração que geralmente parece útil, inclusive eu) pode ter consequências inesperadas: Você precisa ler o arquivo inteiro na memória para passar como uma string para o seu método. Se os arquivos forem grandes, pode não ser o que você deseja.
Conclusão: os princípios nunca devem ser seguidos ao extremo, sem dar um passo atrás e levar em consideração todos os outros fatores.
Talvez a "divisão prematura de métodos" possa ser considerada um caso especial de otimização prematura ? ;-)
fonte
É como uma pergunta equilibrada em minha mente, sem resposta fácil, certa e errada. Vou seguir com uma abordagem de compartilhar minhas experiências pessoais aqui, incluindo minhas próprias tendências e erros ao longo da minha carreira. YMMV consideravelmente.
Como advertência, trabalho em áreas que envolvem algumas bases de código em larga escala (milhões de LOC, às vezes décadas de legado). Também trabalho em uma área particularmente peculiar, onde nenhuma quantidade de comentários ou clareza de código pode necessariamente se traduzir para qualquer desenvolvedor competente, capaz de entender o que a implementação está fazendo (não podemos necessariamente pegar um desenvolvedor decente e fazê-lo entender a implementação de um estado implementação de dinâmica de fluidos de última geração, com base em um artigo publicado há 6 meses, sem que ele gaste um bom tempo longe do código, especializando-se nessa área). Isso geralmente significa que apenas alguns desenvolvedores podem entender e manter efetivamente qualquer parte específica da base de código.
Dadas minhas experiências particulares e talvez combinadas com a natureza peculiar dessa indústria, eu não achei mais produtivo usar SoC, DRY, tornando as implementações de funções o mais legíveis possível, até reutilizá-las até seus limites máximos em favor de YAGNI, desacoplamento, testabilidade, escrevendo testes, documentação da interface (pelo menos sabemos como usar uma interface, mesmo que a implementação exija muito conhecimento especializado) e, finalmente, enviando o software.
Blocos de Lego
Na verdade, eu estava propenso a seguir a direção totalmente oposta, originalmente em algum momento da minha carreira. Fiquei muito empolgado com a programação funcional e os designs das classes de políticas no Modern C ++ Design, na metaprogramação de modelos e assim por diante. Em particular, fiquei empolgado com os designs mais compactos e ortogonais, nos quais você tem todas essas pequenas funcionalidades (como "átomos") que podem ser combinadas (para formar "moléculas") de maneiras aparentemente infinitas para obter os resultados desejados. Isso me fez querer escrever quase tudo como funções que consistiam em algumas linhas de código, e não há necessariamente algo inerentemente errado com uma função tão curta (ela ainda pode ser muito ampla em termos de aplicabilidade e esclarecimento de código), exceto que eu estava começando a ir na direção dogmática de pensar que meu código tinha algo errado se alguma função tivesse mais de algumas linhas. E eu peguei alguns brinquedos realmente legais e até mesmo algum código de produção desse tipo de código, mas estava ignorando o relógio: as horas, os dias e as semanas passando.
Em particular, enquanto eu admirava a simplicidade de cada pequeno "bloco de lego" que eu criei para poder combinar de infinitas maneiras, eu ignorei a quantidade de tempo e o poder intelectual que eu estava investindo para juntar todos esses blocos para formar uma "engenhoca" elaborada. Além disso, nos raros mas dolorosos casos em que algo deu errado com a engenhoca elaborada, ignorei intencionalmente o tempo que passava tentando descobrir o que estava errado, rastreando uma variedade aparentemente interminável de chamadas de função, analisando cada peça lego descentralizada e subconjuntos de suas combinações quando a coisa toda poderia ter sido muito mais simples se não fosse feita com esses "legos", se você quiser, e apenas escrita como um punhado de funções mais ousadas ou uma classe de peso médio.
Ainda assim, dei uma volta completa e, como os prazos me forçaram a ficar mais consciente do tempo, comecei a perceber que meus esforços estavam me ensinando mais sobre o que eu estava fazendo de errado do que o que estava fazendo de certo . Comecei a apreciar novamente a função e o objeto / componente mais detalhistas aqui e ali, que existem maneiras mais pragmáticas de obter um nível razoável de SoC, como
David Arno
aponta a separação da entrada do arquivo do processamento de strings, sem necessariamente decompor o processamento de strings até o máximo possível. nível granular imaginável.Funções Meatier
E ainda mais, comecei a ficar bem com alguma duplicação de código, até mesmo duplicação lógica (não estou dizendo copiar e colar codificação, tudo o que estou falando é encontrar "equilíbrio"), desde que a função não seja propensa para incorrer em alterações repetidas e documentadas em termos de uso e, acima de tudo, bem testadas para garantir que sua funcionalidade corresponda corretamente ao que está documentado e que continue assim. Comecei a perceber que a reutilização está muito ligada à confiabilidade .
Eu percebi que mesmo a função mais complexa, que ainda é singular o suficiente para não ser muito restrita e muito complicada de usar e testar, mesmo que duplique alguma lógica em algumas funções distantes em outras partes da base de código, e desde que seja bem testados e confiáveis, e os testes razoavelmente garantem que continue assim, ainda é preferível à combinação de funções mais decomposta e flexível que não possui essa qualidade. Então, eu gosto bastante de algumas coisas mais carnudas hoje em dia, se é confiável .
Também me parece que na maioria das vezes, é mais barato para perceber que você está indo precisar algo em retrospectiva e adicioná-lo, desde que o seu código é pelo menos receptivo a novas adições sem cascata fogo do inferno, do que código todos os tipos de coisas quando você aren 't vai precisar dele e, em seguida, enfrentar a tentação de remover tudo quando se está começando a se tornar uma verdadeira PITA de manter.
Então foi isso que eu aprendi, essas são as lições que julguei mais necessárias para que eu aprendesse pessoalmente em retrospectiva neste contexto, e como uma advertência, ela deve ser tomada com um grão de sal. YMMV. Mas espero que isso tenha algum valor para ajudá-lo a encontrar o tipo certo de equilíbrio para enviar produtos que deixem seus usuários felizes dentro de um período de tempo razoável e os mantenham efetivamente.
fonte
O problema que você está enfrentando é que você não está fatorando suas funções na forma mais reduzida. Dê uma olhada no seguinte: (Eu não sou um programador de python, então me dê uma folga)
Cada uma das funções acima faz algo completamente diferente, e acredito que você teria dificuldade em fatorá-las ainda mais. Podemos combinar essas funções para realizar a tarefa em questão.
As linhas de código acima podem ser facilmente reunidas em uma única função para executar exatamente o que você deseja fazer. A maneira de realmente separar preocupações é dividir operações complexas em sua forma mais fatorada. Depois de ter um grupo de funções bem fatoradas, você pode começar a reuni-las para resolver o problema mais complexo. Uma coisa boa sobre funções bem fatoradas é que elas geralmente são reutilizáveis fora do contexto do problema atual em questão.
fonte
Eu diria que, de fato, nunca há muita separação de preocupações. Mas pode haver funções que você usa apenas uma vez e nem mesmo testam separadamente. Eles podem ser alinhados com segurança , impedindo que a separação penetre no espaço de nomes externo.
Seu exemplo literalmente não precisa
check_if_keyword_in_string
, porque a classe string já fornece uma implementação:keyword in line
é suficiente. Mas você pode planejar trocar implementações, por exemplo, uma usando uma pesquisa Boyer-Moore ou permitindo uma pesquisa lenta em um gerador; então faria sentido.Você
find_last_appearance_of_keyword
pode ser mais geral e encontrar a última aparência de um item em uma sequência. Para isso, você pode usar uma implementação existente ou fazer uma implementação reutilizável. Além disso, pode ser necessário um filtro diferente , para que você possa procurar uma regex ou correspondências que não diferenciam maiúsculas de minúsculas, etc.Normalmente, qualquer coisa que lida com E / S merece uma função separada, portanto,
get_text_from_file
pode ser uma boa ideia se você quiser lidar com vários casos especiais imediatamente. Pode não ser se você confiar em umIOError
manipulador externo para isso.Mesmo a contagem de linhas pode ser uma preocupação separada se, no futuro, você precisar suportar, por exemplo, linhas de continuação (por exemplo, com
\
) e precisar do número da linha lógica. Ou talvez seja necessário ignorar as linhas de comentário, sem interromper a numeração das linhas.Considerar:
Veja como você pode querer dividir o seu código quando você considerar algumas preocupações que podem sofrer alterações no futuro, ou apenas porque você observe como o mesmo código pode ser reutilizado em outro lugar.
Isso é algo a ter em atenção quando você escreve a função curta e doce original. Mesmo que você ainda não precise das preocupações separadas como funções, mantenha-as separadas tanto quanto possível. Não só ajuda a evoluir o código mais tarde, mas ajuda a entender melhor o código imediatamente e a cometer menos erros.
fonte
Quando é a separação "demais"? Nunca. Você não pode ter muita separação.
Seu último exemplo é muito bom, mas talvez você possa simplificar o loop for com um
text.GetLines(i=>i.containsKeyword)
ou algo assim.* Versão prática: Pare quando funcionar. Separe mais quando quebrar.
fonte