Якщо слід застосовувати статеві внутрішні чи зовнішні методи?


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()), вам не потрібно буде повторювати захисне застереження (заява if вирішує, чи потрібно малювати точки).

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

Ця ідея обговорюється в книзі Роберта К. Мартіна "Чистий код", яку я дуже рекомендую.


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

6

Моя думка з цього приводу є доволі суперечливою, але майте на увазі мене, оскільки я вважаю, що майже всі згодні з кінцевим результатом. У мене просто інший підхід до туди.

У своїй статті Function Hell я пояснюю, чому мені не подобаються методи розщеплення лише для створення менших методів. Я розділяю їх лише тоді, коли знаю , вони будуть повторно використані, або, звичайно, коли я можу їх повторно використати.

ОП заявила:

Доцільно припустити, що 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();
    }
}

Звичайно, аргументи все-таки потрібно передавати і таке.


Хоча я не погоджуюсь з вами з приводу функцій пекла, ваше рішення (IMHO) є правильним і тим, яке виникне при правильному застосуванні Clean Code & 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}:)
Konerak

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.