Должно ли быть, чтобы отчеты были во внутреннем или внешнем методе?


17

Какой из этих дизайнов лучше? Каковы плюсы и минусы каждого? Какой из них вы бы использовали? Любые другие предложения о том, как иметь дело с методами, как это приветствуется.

Разумно предположить, что Draw () - единственное место, из которого вызываются другие методы рисования. Это необходимо расширить до многих других методов Draw * и свойств Show *, а не только до трех, показанных здесь.

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

Или

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

Для большей справки я спросил об этом на SO - stackoverflow.com/questions/2966216/…
Tesserex

1
Всякий раз, когда я вижу фразу типа «Это должно распространиться на многие другие ...», я сразу же думаю: «Это должно быть циклом».
Пол Батчер

Ответы:


28

Я не думаю, что вы можете иметь общее правило для такого рода вещей, это зависит от ситуации.

В этом случае я бы предложил иметь предложения if вне методов, потому что имена методов Draw подразумевают, что они просто рисуют вещи без каких-либо особых условий.

Если вы обнаружите, что вам нужно выполнить проверки перед вызовом методов во многих местах, вы можете поместить проверку внутри методов и переименовать их, чтобы уточнить, что это происходит.


7

Я говорю второе.

Методы должны иметь одинаковый уровень абстракции.

Во втором примере ответственность Draw()метода заключается в том, чтобы действовать как контроллер, вызывая каждый из отдельных методов рисования. Весь код в этом Draw()методе находится на одном уровне абстракции. Это руководство вступает в силу в сценариях повторного использования. Например, если у вас возникнет соблазн повторно использовать DrawPoints()метод в другом открытом методе (давайте его назовем Sketch()), вам не нужно будет повторять предложение guard (оператор if решает, следует ли рисовать точки).

Однако в первом примере Draw()метод отвечает за определение необходимости вызова каждого из отдельных методов, а затем за вызов этих методов. Draw()имеет некоторые низкоуровневые методы, которые работают, но он делегирует другую низкоуровневую работу другим методам и, таким образом, Draw()имеет код на разных уровнях абстракции. Для повторного использования DrawPoints()в Sketch(), вам нужно будет повторить пункт охраны в Sketch()а.

Эта идея обсуждается в книге Роберта С. Мартина «Чистый код», которую я очень рекомендую.


1
Хорошая вещь. Чтобы ответить на вопрос, высказанный в другом ответе, я бы предложил изменить названия DrawAxis()et. и др. чтобы указать, что их исполнение является условным, может быть TryDrawAxis(). В противном случае вам нужно перейти от Draw()метода к каждому отдельному субметоду, чтобы подтвердить его поведение.
Джош Эрл

6

Мое мнение по этому вопросу довольно противоречиво, но терпите меня, так как я считаю, что почти все согласны с конечным результатом. У меня просто другой подход к этому.

В моей статье « Функциональный ад» я объясняю, почему мне не нравятся методы расщепления только ради создания меньших методов. Я разделяю их только тогда, когда знаю , что они будут использованы повторно или, конечно, когда я смогу их повторно использовать.

ФП заявил:

Разумно предположить, что Draw () - единственное место, из которого вызываются другие методы рисования.

Это приводит меня к (промежуточному) третьему варианту, не упомянутому. Я создаю «блоки кода» или «параграфы кода», для которых другие будут создавать функции.

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

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

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

ОП также заявил:

Это необходимо расширить до многих других методов Draw * и свойств Show *, а не только до трех, показанных здесь.

... так что этот метод очень быстро вырастет. Почти все согласны с тем, что это снижает читабельность. На мой взгляд, правильное решение не просто разделить на несколько методов, но разделить код на повторно используемые классы. Мое решение, вероятно, будет выглядеть примерно так.

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

Конечно, аргументы все равно должны быть переданы и тому подобное.


Хотя я не согласен с вами по поводу функционального ада, ваше решение (ИМХО) правильное, и оно может возникнуть в результате правильного применения Чистого кода и SRP.
Пол Мясник,

4

Для случаев, когда вы проверяете поле, которое контролирует, рисовать или нет, имеет смысл иметь это внутри Draw(). Когда это решение становится более сложным, я предпочитаю последнее (или делю его). Если вам понадобится больше гибкости, вы всегда можете расширить ее.

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

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

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

Обратите внимание, что дополнительные методы защищены, что позволяет подклассам расширять то, что им нужно. Это также позволяет вам принудительно рисовать для тестирования или по любой другой причине, которая у вас может быть.


Это хорошо: все, что повторяется, по-своему. Теперь вы можете даже добавить DrawPointsIfItShould()метод, который if(should){do}
сокращает

4

Из этих двух альтернатив я предпочитаю первую версию. Моя причина в том, что я хочу, чтобы метод делал то, что подразумевает имя, без каких-либо скрытых зависимостей. DrawLegend должен нарисовать легенду, не возможно нарисовать легенду.

Ответ Стивена Джеуриса лучше, чем две версии вопроса.


3

Вместо того, чтобы иметь отдельные Show___свойства для каждой части, я бы определил битовое поле. Это немного упростит это:

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

Кроме того, я бы склонялся к проверке видимости во внешнем методе, а не во внутреннем. Это ведь DrawLegendи нет DrawLegendIfShown. Но это отчасти зависит от остальной части класса. Если есть другие места, которые вызывают этот DrawLegendметод, и они ShowLegendтоже должны проверить , я, вероятно, просто перенесу проверку DrawLegend.


2

Я бы пошел с первым вариантом - с «если» вне метода. Он лучше описывает логику, которой следуют, плюс дает вам возможность фактически нарисовать ось, например, в тех случаях, когда вы хотите нарисовать одну ось независимо от настройки. Кроме того, он устраняет накладные расходы при дополнительном вызове функции (при условии, что он не встроен), который может накапливаться, если вы стремитесь к скорости (ваш пример выглядит так, как будто он просто рисует график, где это может быть не фактором, а анимацией). или игра это может быть).


1

В целом, я предпочитаю, чтобы нижние уровни кода предполагали выполнение предварительных условий и выполняли любую работу, которую они должны выполнять, с проверками, выполняемыми выше в стеке вызовов. Это дает дополнительное преимущество, заключающееся в сохранении циклов за счет отсутствия лишних проверок, но это также означает, что вы можете написать хороший код, например так:

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

вместо того:

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

И, конечно, это становится еще более неприятным, если вы применяете единый выход, имеете память, которую нужно освободить, и т. Д.


0

Все зависит от контекста:

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

} 
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.