Separação de preocupações: Quando é a separação "demais"?

9

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?

TheOnionMaster
fonte
1
Possível duplicata Quando devo criar função separada (ou classe)
mosquito
2
Além: você pretende retornar uma string ou um número? line_number = 0é um padrão numérico, e line_number = lineatribui um valor de string (que é a linha conteúdos não-lo de posição )
Caleth
3
No seu último exemplo, você reimplementa duas funções existentes: opene in. Reimplementar as funções existentes não aumenta a separação de preocupações, a preocupação já é tratada na função existente!
MikeFHay

Respostas:

10

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:

def get_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if keyword in line:
            line_number = line
    return line_number

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.

David Arno
fonte
2
Pense em pesquisa que não diferencia maiúsculas de minúsculas. Pense em pular as linhas de comentário. A separação de preocupações pode se tornar diferente. Além disso, line_number = lineé claramente um erro.
9000
2
também o último exemplo praticamente faz isso
Ewan
1

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.

Juan Carlos Eduardo Romaina AC
fonte
2
Não há absolutamente nada de errado com as funções de uma linha. De fato, algumas das funções mais úteis são apenas uma única linha de código.
Joshua Jones
2
@ JoshuaJones Não há nada inerentemente errado nas funções de uma linha, mas elas podem ser um obstáculo se não abstraírem algo útil. Uma função de uma linha para retornar a distância cartesiana entre dois pontos é muito útil, mas se você tiver uma linha para return keyword in textisso, basta adicionar uma camada desnecessária em cima de uma construção de linguagem interna.
precisa saber é
@cariehl Por que return keyword in textseria 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?
Joshua Jones
1
@ JoshuaJones Nesse contexto, você está abstraindo algo útil. No contexto do exemplo original, não há uma boa razão para essa função existir. 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.
precisa saber é
0

Minha opinião: depende :-)

Na minha opinião, o código deve atender a esses objetivos, ordenados por prioridade:

  1. Cumprir todos os requisitos (ou seja, ele faz corretamente o que deveria)
  2. Seja legível e fácil de seguir / entender
  3. Seja fácil de refatorar
  4. Siga as boas práticas / princípios de codificação

Para mim, seu exemplo original passa por todos esses objetivos (exceto talvez a correção por causa do line_number = lineque 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.

def get_last_match(file, predicate):
    with open(file, 'r') as file:
        line_number = 0
        for line in file:
            if predicate matches line:
                line_number = line
        return line_number

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 ? ;-)

siegi
fonte
0

É 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 Arnoaponta 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.

Dragon Energy
fonte
-1

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)

def lines_from_file(file):
    with open(file, 'r') as text:
        line_number = 1
        lines = []
        for line in text:
            lines.append((line_number, line.strip()))
            line_number += 1
    return lines

def filter(l, func):
    new_l = []
    for x in l:
        if func(x):
            new_l.append(x)
    return new_l

def contains(needle):
    return lambda haystack: needle in haystack

def last(l):
    length = len(l)
    if length > 0:
        return l[length - 1]
    else:
        return None

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.

lines = lines_from_file('./test_file')
filtered = filter(lines, lambda x : contains('some value')(x[1]))
line = last(filtered)
if line is not None:
    print(line[0])

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.

Joshua Jones
fonte
-2

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_keywordpode 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_filepode ser uma boa ideia se você quiser lidar com vários casos especiais imediatamente. Pode não ser se você confiar em um IOErrormanipulador 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:

def get_last_appearance_of_keyword(filename, keyword):
    with open(filename) as f:  # File-opening concern.
        numbered_lines = enumerate(f, start=1)  # Line-numbering concern.
        last_line = None  # Also a concern! Some e.g. prefer -1.
        for line_number, line in numbered_lines:  # The searching concern.
            if keyword in line: # The matching concern, applied.
                last_line = line_number
    # Here the file closes; an I/O concern again.
    return last_line

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.

9000
fonte
-4

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.

Ewan
fonte
5
"Você não pode ter muita separação." Eu não acho que isso seja verdade. O terceiro exemplo do OP é apenas a reescrita de construções python comuns em funções separadas. Eu realmente preciso de uma função totalmente nova apenas para executar 'se x em y'?
cariehl 24/09/18
@ cariehl você deve adicionar uma resposta argumentando esse caso. Eu acho que você teria que encontrar para que realmente funcione, você precisaria de um pouco mais lógica nessas funções
Ewan