Linha adicional no bloco vs parâmetro adicional no Código Limpo

33

Contexto

No Código Limpo , página 35, diz

Isso implica que os blocos dentro de instruções if, else, while e etc. devem ter uma linha. Provavelmente essa linha deve ser uma chamada de função. Isso não apenas mantém a função anexa pequena, mas também agrega valor documental, porque a função chamada dentro do bloco pode ter um nome bem descritivo.

Concordo completamente, isso faz muito sentido.

Mais tarde, na página 40, ele diz sobre argumentos de função

O número ideal de argumentos para uma função é zero (niládico). A seguir vem um (monádico), seguido de perto por dois (diádico). Três argumentos (triádicos) devem ser evitados sempre que possível. Mais de três (poládicos) exigem justificativas muito especiais - e, portanto, não devem ser usadas de qualquer maneira. Argumentos são difíceis. Eles tomam muito poder conceitual.

Concordo completamente, isso faz muito sentido.

Questão

No entanto, muitas vezes me pego criando uma lista de outra lista e terei que viver com um dos dois males.

Ou eu uso duas linhas no bloco , uma para criar a coisa e outra para adicioná-la ao resultado:

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

Ou adiciono um argumento à função da lista à qual a coisa será adicionada, tornando-a "um argumento pior".

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

Questão

Existem (des) vantagens que não estou vendo, o que torna um deles preferível em geral? Ou existem tais vantagens em determinadas situações; Nesse caso, o que devo procurar ao tomar uma decisão?

R. Schmitz
fonte
58
O que há de errado flurps.Add(CreateFlurp(badaBoom));?
cmaster
47
Não, é apenas uma declaração. É apenas uma expressão aninhada trivialmente (um único nível aninhado). E se um simples f(g(x))é contra o seu guia de estilo, bem, não posso consertá-lo. Quero dizer, você também não se divide sqrt(x*x + y*y)em quatro linhas, não é? E são três (!) Subexpressões aninhadas em dois (!) Níveis de aninhamento interno (suspiro!). Seu objetivo deve ser legibilidade , não declarações de operador único. Se você quiser mais tarde, bem, eu tenho a linguagem perfeita para você: Assembler.
cmaster
6
@cmaster Mesmo a montagem x86 não possui estritamente instruções de operador único. Os modos de endereçamento de memória incluem muitas operações complicadas e podem ser usados ​​para aritmética - na verdade, você pode criar um computador completo com Turing usando apenas movinstruções x86 e uma única jmp toStartno final. Alguém realmente fez um compilador que faz exatamente isso: D
Luaan
5
@Lanan Para não falar das rlwimiinstruções infames sobre o PPC. (Significa Rotate Insert Left Immediate Mask Insert.) Esse comando levou pelo menos cinco operandos (dois registros e três valores imediatos) e executou as seguintes operações: Um conteúdo de um registro foi rotacionado por uma mudança imediata, uma máscara foi criado com uma única execução de 1 bits que foi controlada pelos outros dois operandos imediatos, e os bits que correspondiam a 1 bits nessa máscara no outro operando de registro foram substituídos pelos bits correspondentes do registro girado. Instrução muito bacana :-)
cmaster
7
@ R.Schmitz "Estou fazendo programação de propósito geral" - na verdade não, você não está fazendo programação para um propósito específico (não sei qual propósito, mas suponho que sim ;-). Existem literalmente milhares de propósitos para a programação, e os estilos de codificação ideais para eles variam - portanto, o que é apropriado para você pode não ser adequado para outros e vice-versa: Geralmente, o conselho aqui é absoluto (" sempre faça X; Y é ruim" "etc) ignorando que em alguns domínios é absolutamente impraticável manter-se. É por isso que o conselho em livros como Código Limpo deve sempre ser tomadas com uma pitada de sal (prático) :)
psmears

Respostas:

104

Essas diretrizes são uma bússola, não um mapa. Eles apontam você em uma direção sensata . Mas eles não podem realmente dizer em termos absolutos qual é a melhor solução. Em algum momento, você precisa parar de caminhar na direção em que a bússola está apontando, porque chegou ao seu destino.

O Clean Code incentiva você a dividir seu código em blocos muito pequenos e óbvios. Essa é uma direção geralmente boa. Mas quando levado ao extremo (como sugere uma interpretação literal dos conselhos citados), você subdividirá seu código em pedaços inúteis. Nada realmente faz nada, tudo apenas delega. Este é essencialmente outro tipo de ofuscação de código.

É seu trabalho equilibrar “menor é melhor” e “pequeno demais é inútil”. Pergunte a si mesmo qual solução é mais simples. Para mim, essa é claramente a primeira solução, pois obviamente monta uma lista. Este é um idioma bem compreendido. É possível entender esse código sem precisar olhar para outra função.

Se é possível fazer melhor, é notando que “transformar todos os elementos de uma lista para outra lista” é um padrão comum que geralmente pode ser abstraído, usando uma map()operação funcional . Em C #, acho que é chamado Select. Algo assim:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}
amon
fonte
7
O código ainda está errado e inutilmente reinventa a roda. Por que ligar CreateFlurps(someList)quando o BCL já fornece someList.ConvertAll(CreateFlurp)?
Ben Voigt
44
@BenVoigt Esta é uma pergunta em nível de design. Não estou preocupado com a sintaxe exata, principalmente porque um quadro branco não possui um compilador (e eu escrevi C # pela última vez em '09). Meu argumento não é "mostrei o melhor código possível", mas "como um aparte, esse é um padrão comum que já foi resolvido". O Linq é uma maneira de fazer isso, o ConvertAll que você menciona outra . Obrigado por sugerir essa alternativa.
amon
1
Sua resposta é sensata, mas o fato de o LINQ abstrair a lógica e reduzir a declaração para uma linha depois de tudo parece contradizer seu conselho. Como uma observação lateral, BadaBoom => CreateFlurp(badaBoom)é redundante; você pode passar CreateFlurpdiretamente como a função ( Select(CreateFlurp)). (Tanto quanto eu sei, este tem sido sempre o caso.)
jpmc26
2
Observe que isso remove completamente a necessidade do método. O nome CreateFlurpsé realmente mais enganoso e mais difícil de entender do que apenas ver badaBooms.Select(CreateFlurp). O último é completamente declarativo - não há problema em se decompor e, portanto, não há necessidade de um método.
Carl Leth
1
@ R.Schmitz Não é difícil de entender, mas é menos fácil de entender do que badaBooms.Select(CreateFlurp). Você cria um método para que seu nome (alto nível) represente sua implementação (baixo nível). Nesse caso, eles estão no mesmo nível; portanto, para descobrir exatamente o que está acontecendo, preciso apenas olhar o método (em vez de vê-lo na linha). CreateFlurps(badaBooms)pode conter surpresas, mas badaBooms.Select(CreateFlurp)não pode. Também é enganoso, porque está pedindo erroneamente um em Listvez de um IEnumerable.
Carl Leth
61

O número ideal de argumentos para uma função é zero (niládico)

Não! O número ideal de argumentos para uma função é um. Se for zero, você está garantindo que a função precise acessar informações externas para poder executar uma ação. "Tio" Bob entendeu isso muito errado.

Em relação ao seu código, seu primeiro exemplo possui apenas duas linhas no bloco porque você está criando uma variável local na primeira linha. Remova essa atribuição e cumpra estas diretrizes de código limpo:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

Mas esse código é muito longo (C #). Apenas faça como:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);
David Arno
fonte
14
Uma função com zero argumentos pretende implicar que o objeto encapsula os dados necessários, não que as coisas existam em um estado global fora de um objeto.
Ryathal 16/02
19
@Ryathal, dois pontos: (1) se você está falando de métodos, então para a maioria das linguagens OO (todas?), Esse objeto é inferido (ou declarado explicitamente, no caso de Python) como o primeiro parâmetro. Em Java, C # etc, todos os métodos são funções com pelo menos um parâmetro. O compilador apenas oculta esses detalhes de você. (2) Eu nunca mencionei "global". O estado do objeto é externo a um método, por exemplo.
David Arno
17
Tenho certeza de que, quando o tio Bob escreveu "zero", ele quis dizer "zero (sem contar isso)".
Doc Brown
26
@DocBrown, provavelmente como ele é um grande fã de misturar estado e funcionalidade em objetos, então por "função" ele provavelmente se refere especificamente a métodos. E ainda discordo dele. É muito melhor fornecer apenas a um método o que ele precisa, em vez de vasculhar o objeto para obter o que deseja (ou seja, é clássico "diga, não pergunte" em ação).
David Arno
8
@AlessandroTeruzzi, o ideal é um parâmetro. Zero é muito pouco. É por isso que, por exemplo, as linguagens funcionais adotam um como o número de parâmetros para fins de currying (de fato, em algumas linguagens funcionais, todas as funções têm exatamente um parâmetro: nem mais; nem menos). Currying com zero parâmetros seria absurdo. Afirmar que "o ideal é o mínimo possível, logo o zero é o melhor" é um exemplo de reductio ad absurdum .
David Arno
19

O conselho do 'Código Limpo' está completamente errado.

Use duas ou mais linhas no seu loop. Ocultar as mesmas duas linhas em uma função faz sentido quando são algumas matemáticas aleatórias que precisam de uma descrição, mas não fazem nada quando as linhas já são descritivas. 'Criar' e 'Adicionar'

O segundo método mencionado não faz muito sentido, pois você não é obrigado a adicionar um segundo argumento para evitar as duas linhas.

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

ou

foreach(var flurp in ConvertToFlurps(badaBooms))...

Conforme observado por outros, o conselho de que a melhor função é aquela sem argumentos é inclinado para OOP na melhor das hipóteses e, na pior das hipóteses, conselhos ruins na pior das hipóteses.

Ewan
fonte
Talvez você queira editar esta resposta para torná-la mais clara? Minha pergunta era se uma coisa supera a outra no Código Limpo. Você diz que tudo está errado e depois descreve uma das opções que dei. No momento, esta resposta parece que você está seguindo uma agenda do Código Anti-Limpo, em vez de realmente tentar responder à pergunta.
R. Schmitz
desculpe, eu interpretei sua pergunta como sugerindo que o primeiro era o caminho 'normal', mas você estava sendo empurrado para o segundo. Eu não sou anti-código limpo em geral, mas esta citação é obviamente errado
Ewan
19
@ R.Schmitz Eu já li "Código Limpo" e sigo a maior parte do que esse livro diz. No entanto, no que diz respeito ao tamanho perfeito da função ser praticamente uma única declaração, é simplesmente errado. O único efeito é que ele transforma o código do espaguete em código do arroz. O leitor se perde na multiplicidade de funções triviais que só produzem sentido sensível quando vistas juntas. Os seres humanos têm uma capacidade limitada de memória de trabalho, e você pode sobrecarregá-lo com instruções ou funções. Você deve encontrar um equilíbrio entre os dois se quiser ser legível. Evite os extremos!
cmaster
@master A resposta foi apenas os dois primeiros parágrafos quando escrevi esse comentário. É uma resposta muito melhor agora.
R. Schmitz
7
sinceramente, preferi minha resposta mais curta. Há muita conversa diplomática na maioria dessas respostas. O conselho citado está completamente errado, não há necessidade de especular sobre "o que realmente significa" ou girar para tentar encontrar uma boa interpretação.
Ewan
15

O segundo é definitivamente pior, pois CreateFlurpInListaceita a lista e modifica essa lista, tornando a função não pura e difícil de raciocinar. Nada no nome do método sugere que o método seja adicionado apenas à lista.

E ofereço a terceira, melhor opção:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

E, diabos, você pode incorporar esse método imediatamente se houver apenas um lugar onde ele é usado, pois o one-liner é claro por si só, portanto não precisa ser encapsulado pelo método para dar sentido.

Eufórico
fonte
Eu não reclamaria tanto desse método "não ser puro e difícil de raciocinar" (embora verdadeiro), mas de ser um método completamente desnecessário para lidar com um caso especial. E se eu quiser criar um Flurp autônomo, um Flurp adicionado a uma matriz, a um dicionário, um Flurp que, em seguida, seja consultado em um dicionário e o Flurp correspondente seja removido etc.? Com o mesmo argumento, o código Flurp também precisaria de todos esses métodos.
gnasher729
10

A versão de um argumento é melhor, mas não principalmente devido ao número de argumentos.

O motivo mais importante é que ele tem um acoplamento mais baixo , o que o torna mais útil, mais fácil de raciocinar, mais fácil de testar e menos provável de se transformar em clones copiados + copiados.

Se você me fornecer uma CreateFlurp(BadaBoom), eu posso usar isso com qualquer tipo de recipiente de coleta: Simple Flurp[], List<Flurp>, LinkedList<Flurp>, Dictionary<Key, Flurp>, e assim por diante. Mas com a CreateFlurpInList(BadaBoom, List<Flurp>), eu voltarei para você amanhã pedindo para CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)que meu viewmodel possa receber a notificação de que a lista foi alterada. Que nojo!

Como um benefício adicional, é mais provável que a assinatura mais simples se ajuste às APIs existentes. Você diz que tem um problema recorrente

muitas vezes me pego criando uma lista de outra lista

É apenas uma questão de usar as ferramentas disponíveis. A versão mais curta, mais eficiente e melhor é:

var Flurps = badaBooms.ConvertAll(CreateFlurp);

Esse código não é apenas para você escrever e testar, mas também é mais rápido, porque List<T>.ConvertAll()é inteligente o suficiente para saber que o resultado terá o mesmo número de itens que a entrada e pré-aloca a lista de resultados para o tamanho correto. Enquanto seu código (ambas as versões) exigia o aumento da lista.

Ben Voigt
fonte
Não use List.ConvertAll. A maneira idiomática de mapear um enumerável de objetos para diferentes objetos em C # é chamada Select. O único motivo que ConvertAllestá disponível aqui é porque o OP está pedindo erroneamente um Listno método - deve ser um IEnumerable.
Carl Leth
6

Lembre-se do objetivo geral: facilitar a leitura e a manutenção do código.

Freqüentemente, será possível agrupar várias linhas em uma única função significativa. Faça isso nesses casos. Ocasionalmente, você precisará reconsiderar sua abordagem geral.

Por exemplo, no seu caso, substituindo toda a implementação por var

flups = badaBooms.Select(bb => new Flurp(bb));

pode ser uma possibilidade. Ou você pode fazer algo como

flups.Add(new Flurp(badaBoom))

Às vezes, a solução mais limpa e legível simplesmente não se encaixa em uma linha. Então você terá duas linhas. Não torne o código mais difícil de entender, apenas para cumprir alguma regra arbitrária.

Seu segundo exemplo é (na minha opinião) consideravelmente mais difícil de entender do que o primeiro. Não é apenas que você tenha um segundo parâmetro, é que o parâmetro é modificado pela função. Veja o que o Clean Code tem a dizer sobre isso. (Não tenha o livro em mãos agora, mas tenho certeza de que é basicamente "não faça isso se puder evitá-lo").

doubleYou
fonte