Se le dichiarazioni dovessero essere nel metodo interno o esterno?


17

Quale di questi disegni è migliore? Quali sono i pro ed i contro di ognuno? Quale useresti? Ogni altro suggerimento su come gestire metodi come è apprezzato.

È ragionevole supporre che Draw () sia l'unico posto da cui vengono chiamati gli altri metodi draw. Questo deve espandersi in molti altri metodi Draw * e Show *, non solo i tre mostrati qui.

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.
}

O

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.
}

Per ulteriori riferimento, ho chiesto questo su SO - stackoverflow.com/questions/2966216/...
Tesserex

1
Ogni volta che vedo una frase del tipo "Questo deve espandersi a molti altri ...", penso immediatamente "Questo dovrebbe essere un ciclo".
Paul Butcher,

Risposte:


28

Non penso che tu possa avere una regola generale per questo genere di cose, dipende dalla situazione.

In questo caso, suggerirei di avere le clausole if al di fuori dei metodi perché i nomi dei metodi Draw implicano che disegnano semplicemente le cose senza alcuna condizione speciale.

Se scopri che devi effettuare i controlli prima di chiamare i metodi in molti luoghi, potresti voler inserire i controlli all'interno dei metodi e rinominarli per chiarire che ciò sta accadendo


7

Dico il secondo.

I metodi dovrebbero avere lo stesso livello di astrazione.

Nel secondo esempio, la responsabilità del Draw()metodo è quella di agire come un controller, chiamando ciascuno dei singoli metodi di disegno. Tutto il codice in questo Draw()metodo è allo stesso livello di astrazione. Questa linea guida entra in gioco negli scenari di riutilizzo. Ad esempio, se sei stato tentato di riutilizzare il DrawPoints()metodo in un altro metodo pubblico (chiamiamoloSketch() ), non sarebbe necessario ripetere la clausola di guardia (l'istruzione if che decide se disegnare i punti).

Nel primo esempio, tuttavia, il Draw()metodo è responsabile per determinare se chiamare ciascuno dei singoli metodi e quindi chiamare quei metodi. Draw()ha alcuni metodi di basso livello che funzionano, ma delega altri lavori di basso livello ad altri metodi e quindi Draw()ha codice a diversi livelli di astrazione. Al fine di riutilizzo DrawPoints()in Sketch(), si avrebbe bisogno di replicare la clausola di guardiaSketch() pure.

Questa idea è discussa nel libro di Robert C. Martin "Codice pulito", che consiglio vivamente.


1
Roba buona. Per affrontare un punto sollevato in un'altra risposta, suggerirei di cambiare i nomi di DrawAxis()et. al. per indicare che la loro esecuzione è condizionale, forse TryDrawAxis(). In caso contrario, è necessario spostarsi dal Draw()metodo a ciascun singolo metodo secondario per confermarne il comportamento.
Josh Earl,

6

La mia opinione sull'argomento è piuttosto controversa, ma abbiate pazienza con me, poiché credo che quasi tutti siano d'accordo sul risultato finale. Ho solo un approccio diverso per arrivarci.

Nel mio articolo Funzione Inferno , spiego perché non mi piace dividere i metodi solo per il gusto di creare metodi più piccoli. Li ho divisi solo quando lo so che saranno riutilizzati, o ovviamente, quando posso riutilizzarli.

L'OP ha dichiarato:

È ragionevole supporre che Draw () sia l'unico posto da cui vengono chiamati gli altri metodi draw.

Questo mi porta a una terza opzione (intermedia) non menzionata. Creo "blocchi di codice" o "paragrafi di codice" per cui altri creerebbero funzioni.

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

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

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

L'OP ha inoltre dichiarato:

Questo deve espandersi in molti altri metodi Draw * e Show *, non solo i tre mostrati qui.

... quindi questo metodo crescerà molto rapidamente. Quasi tutti concordano sul fatto che ciò riduce la leggibilità. A mio avviso, la soluzione corretta non consiste solo nella suddivisione in diversi metodi, ma nella suddivisione del codice in classi riutilizzabili. La mia soluzione sarebbe probabilmente simile a questa.

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();
    }
}

Argomenti ovviamente dovrebbero ancora essere passati e così via.


Sebbene non sia d'accordo con te sull'inferno di Function, la tua soluzione è (IMHO) quella corretta e quella che deriverebbe dalla corretta applicazione di Clean Code & SRP.
Paul Butcher,

4

Per i casi in cui stai controllando un campo che controlla se disegnare o meno, ha senso che sia dentro Draw(). Quando quella decisione diventa più complicata, tendo a preferire quest'ultima (o dividerla). Se hai bisogno di maggiore flessibilità, puoi sempre espanderlo.

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

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

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

Si noti che i metodi aggiuntivi sono protetti, il che consente alle sottoclassi di estendere ciò di cui hanno bisogno. Ciò consente anche di forzare il disegno per i test o qualsiasi altra ragione si possa avere.


È bello: tutto ciò che viene ripetuto è nel suo metodo. Ora puoi anche aggiungere un DrawPointsIfItShould()metodo che abbrevia il if(should){do}:)
Konerak

4

Di queste due alternative, preferisco la prima versione. La mia ragione è che voglio un metodo per fare ciò che implica il nome, senza alcuna dipendenza nascosta. DrawLegend dovrebbe disegnare una legenda, forse non disegnare una legenda.

Tuttavia, la risposta di Steven Jeuris è migliore delle due versioni della domanda.


3

Invece di avere Show___proprietà individuali per ogni parte, probabilmente definirei un campo bit. Ciò semplificherebbe un po ':

[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; }
}

A parte questo, mi spingerei a controllare la visibilità nel metodo esterno, non in quello interno. Dopo tutto, DrawLegendlo è DrawLegendIfShown. Ma dipende dal resto della classe. Se ci sono altri posti che chiamano quel DrawLegendmetodo e devono anche controllare ShowLegend, probabilmente sposterò il controllo in DrawLegend.


2

Vorrei andare con la prima opzione - con il "se" al di fuori del metodo. Descrive meglio la logica seguita, inoltre ti dà la possibilità di disegnare effettivamente un asse, ad esempio, nei casi in cui vuoi disegnarne uno indipendentemente da un'impostazione. Inoltre rimuove il sovraccarico di una chiamata di funzione aggiuntiva (supponendo che non sia allineata), che può accumularsi se stai andando per la velocità (il tuo esempio sembra che stia semplicemente disegnando un grafico in cui potrebbe non essere un fattore, ma in un'animazione o gioco potrebbe essere).


1

In generale, preferisco che i livelli più bassi di codice assumano che siano soddisfatte le condizioni preliminari e riescano a fare qualunque lavoro si supponga facciano, con i controlli che vengono effettuati più in alto nello stack di chiamate. Questo ha il vantaggio secondario di salvare i cicli non eseguendo controlli ridondanti, ma significa anche che puoi scrivere un bel codice come questo:

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

invece di:

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 ovviamente questo diventa ancora più cattivo se imponi l'uscita singola, hai memoria che devi liberare, ecc.


0

Tutto dipende dal contesto:

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();
    }

} 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.