Deveria se as declarações estivessem no método interno ou externo?


17

Qual desses designs é melhor? Quais são os prós e os contras de cada um? Qual você usaria? Quaisquer outras sugestões de como lidar com métodos como esse são apreciadas.

É razoável supor que Draw () é o único local de onde os outros métodos de desenho são chamados. Isso precisa ser expandido para muitos outros métodos Draw * e propriedades Show *, não apenas os três mostrados aqui.

public void Draw()
{
    if (ShowAxis)
    {
        DrawAxis();
    }

    if (ShowLegend)
    {
        DrawLegend();
    }

    if (ShowPoints && Points.Count > 0)
    {
        DrawPoints();
    }
}

private void DrawAxis()
{
    // Draw things.
}

private void DrawLegend()
{
    // Draw things.
}

private void DrawPoints()
{
    // Draw things.
}

Ou

public void Draw()
{
    DrawAxis();
    DrawLegend();
    DrawPoints();
}

private void DrawAxis()
{
    if (!ShowAxis)
    {
        return;
    }

    // Draw things.
}

private void DrawLegend()
{
    if (!ShowLegend)
    {
        return;
    }

    // Draw things.
}

private void DrawPoints()
{
    if (!ShowPoints ||  Points.Count <= 0))
    {
        return;
    }

    // Draw things.
}

Para obter mais referências, perguntei isso no SO - stackoverflow.com/questions/2966216/…
Tesserex

1
Sempre que vejo uma frase como "Isso precisa se expandir para muito mais ...", imediatamente penso "Isso deve ser um loop".
Paul Butcher

Respostas:


28

Eu não acho que você possa ter uma regra geral para esse tipo de coisa, depende da situação.

Nesse caso, eu sugeriria ter as cláusulas if fora dos métodos, porque os nomes dos métodos Draw sugerem que eles apenas desenham as coisas sem nenhuma condição especial.

Se você achar que precisa fazer as verificações antes de chamar os métodos em muitos lugares, convém colocar a verificação dentro dos métodos e renomeá-los para esclarecer que isso está acontecendo


7

Eu digo o segundo.

Os métodos devem ter o mesmo nível de abstração.

No segundo exemplo, a responsabilidade do Draw()método é agir como um controlador, chamando cada um dos métodos de desenho individuais. Todo o código neste Draw()método está no mesmo nível de abstração. Essa diretriz entra em jogo nos cenários de reutilização. Por exemplo, se você estava tentado a reutilizar o DrawPoints()método em outro método público (vamos chamá-lo Sketch()), não seria necessário repetir a cláusula guard (a instrução if que decide se os pontos devem ser destacados).

No primeiro exemplo, porém, o Draw()método é responsável por determinar se deve chamar cada um dos métodos individuais e, em seguida, chamar esses métodos. Draw()possui alguns métodos de baixo nível que funcionam, mas delega outro trabalho de baixo nível para outros métodos e, portanto, Draw()possui código em diferentes níveis de abstração. A fim de re-uso DrawPoints()em Sketch(), você precisaria para replicar a cláusula guarda em Sketch()bem.

Essa idéia é discutida no livro de Robert C. Martin, "Clean Code", que eu recomendo.


1
Coisa boa. Para abordar uma questão apresentada em outra resposta, sugiro alterar os nomes de DrawAxis()et. al. para indicar que a execução deles é condicional, talvez TryDrawAxis(). Caso contrário, você precisará navegar do Draw()método para cada submodo individual para confirmar seu comportamento.
Josh Earl

6

Minha opinião sobre o assunto é bastante controversa, mas tenha paciência comigo, pois acredito que quase todo mundo concorda com o resultado final. Eu só tenho uma abordagem diferente de chegar lá.

No meu artigo Function Hell , explico por que não gosto de dividir métodos apenas para criar métodos menores. Só os separo quando sei que eles serão reutilizados ou, é claro, quando eu puder reutilizá-los.

O PO declarou:

É razoável supor que Draw () é o único local de onde os outros métodos de desenho são chamados.

Isso me leva a uma terceira opção (intermediária) não mencionada. Eu crio 'blocos de código' ou 'parágrafos de código' para os quais outros criariam funções.

public void Draw()
{
    // Draw axis.
    if (ShowAxis)
    {
        // Drawing code ...
    }

    // Draw legend.
    if (ShowLegend)
    {
        // Drawing code ...
    }

    // Draw points.
    if (ShowPoints && Points.Count > 0)
    {
        // Drawing code ...
    }
}

O PO também declarou:

Isso precisa ser expandido para muitos outros métodos Draw * e propriedades Show *, não apenas os três mostrados aqui.

... para que esse método cresça muito rapidamente. Quase todo mundo concorda que isso diminui a legibilidade. Na minha opinião, a solução adequada não é apenas dividir em vários métodos, mas dividir o código em classes reutilizáveis. Minha solução provavelmente seria algo parecido com isto.

private void Initialize()
{               
    // Create axis.
    Axe xAxe = new Axe();
    Axe yAxe = new Axe();

    _drawObjects = new List<Drawable>
    {
        xAxe,
        yAxe,
        new Legend(),
        ...
    }
}

public void Draw()
{
    foreach ( Drawable d in _drawObjects )
    {
        d.Draw();
    }
}

Claro que argumentos ainda precisariam ser passados ​​e tal.


Embora eu não concorde com você sobre a função inferno, sua solução é (IMHO) a correta e a que surgiria da aplicação adequada do Clean Code & SRP.
Paul Butcher

4

Nos casos em que você está verificando um campo que controla se deve ou não desenhar, faz sentido que ele esteja dentro Draw(). Quando essa decisão se torna mais complicada, tenho a tendência de preferir a última (ou dividi-la). Se você precisar de mais flexibilidade, sempre poderá expandi-la.

private void DrawPoints()
{
    if (ShouldDrawPoints())
    {
        DoDrawPoints();
    }
}

protected void ShouldDrawPoints()
{
    return ShowPoints && Points.Count > 0;
}

protected void DoDrawPoints()
{
    // Draw things.
}

Observe que os métodos adicionais estão protegidos, o que permite que as subclasses estendam o que precisam. Isso também permite forçar o desenho para teste ou qualquer outro motivo que você possa ter.


Isso é legal: tudo o que é repetido está em seu próprio método. Você agora pode até adicionar um DrawPointsIfItShould()método que os atalhos if(should){do}:)
Konerak

4

Dessas duas alternativas, eu prefiro a primeira versão. Minha razão é que eu quero um método para fazer o que o nome implica, sem nenhuma dependência oculta. DrawLegend deve desenhar uma legenda, talvez não uma legenda.

A resposta de Steven Jeuris é melhor do que as duas versões da pergunta, no entanto.


3

Em vez de ter individual Show___ propriedades para cada parte, provavelmente definiria um campo de bits. Isso simplificaria um pouco para:

[Flags]
public enum DrawParts
{
    Axis = 1,
    Legend = 2,
    Points = 4,
    // More...
}

public class MyClass
{
    public void Draw() {
        if (VisibleParts.HasFlag(DrawPart.Axis))   DrawAxis();
        if (VisibleParts.HasFlag(DrawPart.Legend)) DrawLegend();
        if (VisibleParts.HasFlag(DrawPart.Points)) DrawPoints();
        // More...
    }

    public DrawParts VisibleParts { get; set; }
}

Além disso, eu me inclinaria para verificar a visibilidade no método externo, não no interno. Afinal, é DrawLegende não DrawLegendIfShown. Mas isso depende do resto da classe. Se houver outros lugares que chamam esse DrawLegendmétodo e eles precisarem ShowLegendtambém verificar , eu provavelmente apenas moverei a verificação DrawLegend.


2

Eu iria com a primeira opção - com o "se" fora do método. Descreve melhor a lógica que está sendo seguida, além de oferecer a opção de desenhar um eixo, por exemplo, nos casos em que você deseja desenhar um, independentemente de uma configuração. Além disso, remove a sobrecarga de uma chamada de função adicional (supondo que não esteja embutida), que pode se acumular se você estiver buscando velocidade (seu exemplo parece que pode estar apenas desenhando um gráfico onde pode não ser um fator, mas em uma animação ou jogo poderia ser).


1

Em geral, eu prefiro que os níveis mais baixos de código assumam que as pré-condições sejam atendidas e façam o trabalho que deveriam estar fazendo, com as verificações sendo realizadas mais acima na pilha de chamadas. Isso tem o benefício de salvar ciclos, não fazendo verificações redundantes, mas também significa que você pode escrever um código interessante como este:

int do_something()
{
    sometype* x;
    if(!(x = sometype_new())) return -1;
    foo(x);
    bar(x);
    baz(x);
    return 0;
}

ao invés de:

int do_something()
{
    sometype x* = sometype_new();
    if(ERROR == foo(x)) return -1;
    if(ERROR == bar(x)) return -1;
    if(ERROR == baz(x)) return -1;
    return 0;
}

E é claro que isso fica ainda mais desagradável se você aplicar uma saída única, tiver memória necessária para liberar etc.


0

Tudo depende do contexto:

void someThing(var1, var2)
{
    // If input fails validation then return quickly.
    if (!isValid(var1) || !isValid(var2))
    {   return;
    }


    // Otherwise do what is logical and makes the code easy to read.
    if (doTaskConditionOK())
    {   doTask();
    }

    // Return early if it is logical
    // This is OK in C++ but not C like languages.
    // You have to be careful of cleanup in C like languages while RIAA will do
    // that auto-magically in C++. 
    if (allFinished)
    {   return;
    }

    doAlternativeIfNotFinished();

    // -- Alternative to the above for C
    if (!allFinished)
    {   
        doAlternativeIfNotFinished();
    }

} 
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.