¿Debería si las declaraciones están en método interno o externo?

17

¿Cuál de estos diseños es mejor? ¿Cuáles son los pros y los contras de cada uno? ¿Cuál usarías? Se agradece cualquier otra sugerencia sobre cómo tratar con métodos como.

Es razonable suponer que Draw () es el único lugar desde el que se llama a los otros métodos de dibujo. Esto necesita expandirse a muchos más métodos Draw * y Show *, no solo a los tres que se muestran aquí.

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.
}
mjcopple
fuente
Para más referencia, pregunté esto en SO - stackoverflow.com/questions/2966216/…
Tesserex
1
Cada vez que veo una frase como "Esto necesita expandirse a muchos más ...", inmediatamente pienso "Esto debería ser un bucle".
Paul Butcher

Respuestas:

28

No creo que pueda tener una regla general para este tipo de cosas, depende de la situación.

En este caso, sugeriría tener las cláusulas if fuera de los métodos porque los nombres de los métodos Draw implican que solo dibujan las cosas sin condiciones especiales.

Si encuentra que tiene que hacer las verificaciones antes de llamar a los métodos en muchos lugares, entonces puede poner la verificación dentro de los métodos y cambiarles el nombre para aclarar que esto está sucediendo.

Brian Reichle
fuente
7

Yo digo el segundo.

Los métodos deben tener el mismo nivel de abstracción.

En el segundo ejemplo, la responsabilidad del Draw()método es actuar como un controlador, llamando a cada uno de los métodos de dibujo individuales. Todo el código en este Draw()método está en el mismo nivel de abstracción. Esta guía entra en juego en escenarios de reutilización. Por ejemplo, si estuvieras tentado a reutilizar el DrawPoints()método en otro método público (llamémoslo Sketch()), no necesitarías repetir la cláusula de guardia (la declaración if para decidir si dibujar los puntos).

Sin embargo, en el primer ejemplo, el Draw()método es responsable de determinar si se debe llamar a cada uno de los métodos individuales y luego llamar a esos métodos. Draw()tiene algunos métodos de bajo nivel que funcionan, pero delega otros trabajos de bajo nivel a otros métodos y, por lo tanto, Draw()tiene código en diferentes niveles de abstracción. Con el fin de reutilización DrawPoints()en Sketch(), lo que se necesita para replicar la cláusula de guardia Sketch()también.

Esta idea se discute en el libro de Robert C. Martin "Código limpio", que recomiendo encarecidamente.

neontapir
fuente
1
Buen material. Para abordar un punto hecho en otra respuesta, sugeriría cambiar los nombres de DrawAxis()et. Alabama. para indicar que su ejecución es condicional, tal vez TryDrawAxis(). De lo contrario, debe navegar desde el Draw()método a cada submétodo individual para confirmar su comportamiento.
Josh Earl
6

Mi opinión sobre el tema es bastante controvertida, pero tengan paciencia conmigo, ya que creo que casi todos están de acuerdo con el resultado final. Simplemente tengo un enfoque diferente para llegar allí.

En mi artículo Function Hell , explico por qué no me gusta dividir los métodos solo por crear métodos más pequeños. Solo los separo cuando que serán reutilizados, o por supuesto, cuando pueda reutilizarlos.

El OP declaró:

Es razonable suponer que Draw () es el único lugar desde el que se llama a los otros métodos de dibujo.

Esto me lleva a una tercera opción (intermedia) no mencionada. Creo 'bloques de código' o 'párrafos de código' para los que otros crearían funciones.

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

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

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

El OP también declaró:

Esto necesita expandirse a muchos más métodos Draw * y Show *, no solo a los tres que se muestran aquí.

... entonces este método crecerá muy grande rápidamente. Casi todos están de acuerdo en que esto disminuye la legibilidad. En mi opinión, la solución adecuada no es solo dividir en varios métodos, sino dividir el código en clases reutilizables. Mi solución probablemente se vería así.

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

Por supuesto, los argumentos aún tendrían que ser aprobados y tal.

Steven Jeuris
fuente
Aunque no estoy de acuerdo con usted acerca de Function hell, su solución es (en mi humilde opinión) la correcta y la que surgiría de la aplicación adecuada de Clean Code & SRP.
Paul Butcher
4

Para los casos en los que está marcando un campo que controla si dibujar o no, tiene sentido que esté dentro Draw(). Cuando esa decisión se vuelve más complicada, tiendo a preferir la última (o dividirla). Si termina necesitando más flexibilidad, siempre puede expandirla.

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

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

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

Observe que los métodos adicionales están protegidos, lo que permite que las subclases amplíen lo que necesitan. Esto también le permite forzar el dibujo para la prueba o cualquier otra razón que pueda tener.

David Harkness
fuente
Esto es bueno: todo lo que se repite, está en su propio método. Ahora puede incluso añadir un DrawPointsIfItShould()método que los atajos del if(should){do}:)
Konerak
4

De esas dos alternativas, prefiero la primera versión. Mi razón es que quiero un método para hacer lo que el nombre implica, sin dependencias ocultas. DrawLegend debería dibujar una leyenda, no quizás dibujar una leyenda.

Sin embargo, la respuesta de Steven Jeuris es mejor que las dos versiones de la pregunta.

usuario281377
fuente
3

En lugar de tener Show___propiedades individuales para cada parte, probablemente definiría un campo de bits. Eso lo simplificaría un poco a:

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

Aparte de eso, me inclinaría por verificar la visibilidad en el método externo, no en el interno. Es, después de todo, DrawLegendy no DrawLegendIfShown. Pero depende del resto de la clase. Si hay otros lugares que llaman a ese DrawLegendmétodo y también necesitan verificar ShowLegend, probablemente solo movería el cheque DrawLegend.

munificente
fuente
2

Iría con la primera opción, con el "si" fuera del método. Describe mejor la lógica que se sigue, además le da la opción de dibujar un eje, por ejemplo, en los casos en que desea dibujar uno independientemente de la configuración. Además, elimina la sobrecarga de una llamada de función adicional (suponiendo que no esté en línea), que puede acumularse si se busca velocidad (su ejemplo parece que puede estar dibujando un gráfico donde puede que no sea un factor, sino en una animación o juego podría ser).

Gran maestro B
fuente
1

En general, prefiero que los niveles más bajos de código asuman que se cumplen las condiciones previas y empiecen a hacer cualquier trabajo que se supone que deben hacer, con las comprobaciones realizadas más arriba en la pila de llamadas. Esto tiene el beneficio adicional de guardar ciclos al no hacer verificaciones redundantes, pero también significa que puede escribir un código agradable como este:

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

en lugar 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;
}

Y, por supuesto, esto se vuelve aún más desagradable si aplica una salida única, tiene memoria que necesita liberar, etc.

Cercerilla
fuente
0

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

} 
Martin York
fuente