Додатковий рядок у блоці та додатковий параметр у чистому коді


33

Контекст

У Чистому коді на сторінці 35 написано

Це означає, що блоки, якщо оператори, оператори else, в той час як оператори тощо, повинні бути довгими на один рядок. Можливо, ця лінія повинна бути викликом функції. Це не тільки робить функцію, що вкладається, невеликою, але й додає документальну цінність, оскільки функція, викликана всередині блоку, може мати добре описову назву.

Я повністю згоден, це має багато сенсу.

Пізніше, на сторінці 40, йдеться про аргументи функції

Ідеальна кількість аргументів для функції дорівнює нулю (niladic). Далі йде один (монадичний), за ним близько два (діадічний). По можливості слід уникати трьох аргументів (тріадичних). Більше трьох (поліадичних) вимагає дуже спеціального обґрунтування - і тоді їх не можна використовувати. Аргументи важкі. Вони забирають багато концептуальної сили.

Я повністю згоден, це має багато сенсу.

Проблема

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

Або я використовую два рядки в блоці , один для створення речі, один для додавання його до результату:

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

Або я додаю аргумент до функції для списку, куди буде додана річ, зробивши її "на один аргумент гірше".

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

Питання

Чи є (не) переваги, яких я не бачу, які роблять одне з них переважнішим взагалі? Або є такі переваги в певних ситуаціях; у такому випадку, на що слід звернути увагу, приймаючи рішення?


58
Що не так flurps.Add(CreateFlurp(badaBoom));?
cmaster

47
Ні, це лише одне твердження. Це просто тривіально вкладений вираз (єдиний вкладений рівень). І якщо простий f(g(x))суперечить вашому довіднику стилів, я не можу виправити ваш посібник зі стилів. Я маю на увазі, ви також не розділилися sqrt(x*x + y*y)на чотири рядки, чи не так? І це три (!) Вкладених підвираження на двох (!) Внутрішніх рівнях гніздування (ах!). Ваша мета повинна бути читабельністю , а не окремими заявами операторів. Якщо ви хочете пізніше, я маю для вас ідеальну мову: Асемблер.
cmaster

6
@cmaster Навіть у складі x86 строго немає операторів одного оператора. Режими адресації в пам'яті містять багато складних операцій і можуть бути використані для арифметики - насправді ви можете зробити комп'ютер із повним Тюрінгом, використовуючи лише movінструкції x86 та єдиний jmp toStartв кінці. Хтось насправді зробив компілятор, який робить саме так: D
Луаан

5
@Luaan Не говорити про сумнозвісну rlwimiінструкцію щодо КПП. (Це означає, що негайне вставлення маски Rotate Left Word.) Ця команда займала не менше п'яти операндів (два регістри та три безпосередніх значення), і вона виконувала такі операції: Один вміст реєстру повертався негайним зсувом, маска - створений з одним прогоном в 1 біт, який контролювався двома іншими безпосередніми операндами, і біти, які відповідали 1 біту в цій масці в іншому операнді реєстру, були замінені на відповідні біти повернутого регістра. Дуже класна інструкція :-)
cmaster

7
@ R.Schmitz "Я займаюся програмуванням загального призначення" - насправді ні, ти ні, ти займаєшся програмуванням для певної мети (я не знаю, з якою метою, але я припускаю, що ти це робиш ;-). Цілей буквально тисячі цілей програмування, і оптимальні стилі кодування для них різняться - тому те, що підходить для вас, може бути не підходящим для інших, і навпаки: часто поради тут абсолютні (" завжди роби X; Y погано" "тощо) ігноруючи, що в деяких областях дотримуватися вкрай недоцільно. Ось чому поради в таких книгах як «Чистий код» завжди слід сприймати з дрібкою (практичної) солі :)
psmears

Відповіді:


104

Ці вказівки - це компас, а не карта. Вони вказують на вас у розумному напрямку . Але вони насправді не можуть вам точно сказати, яке рішення є «найкращим». У якийсь момент вам потрібно перестати йти у напрямку, на який вказує ваш компас, адже ви прибули до місця призначення.

Clean Code рекомендує розділити код на дуже малі, очевидні блоки. Це загалом хороший напрямок. Але якщо ви поставитеся до крайності (як це підказує буквальне тлумачення цитованих порад), то ви поділили свій код на марно невеликі шматочки. Ніщо насправді нічого не робить, все лише делегує. Це, по суті, інший вид затухання коду.

Ваша робота врівноважувати "менший, тим краще" проти "занадто малий - марний". Запитайте себе, яке рішення простіше. Для мене це, очевидно, перше рішення, оскільки воно, очевидно , складає список. Це добре зрозуміла ідіома. Можна зрозуміти цей код, не дивлячись на ще одну функцію.

Якщо це можливо зробити краще, це зауваживши, що «перетворення всіх елементів зі списку в інший список» є загальною схемою, яку часто можна абстрагувати за допомогою функціональної map()операції. В C #, я думаю, це називається Select. Щось на зразок цього:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}

7
Код все ще помиляється, і він безглуздо відновлює колесо. Навіщо дзвонити, CreateFlurps(someList)коли BCL вже надає someList.ConvertAll(CreateFlurp)?
Ben Voigt

44
@BenVoigt Це питання на рівні дизайну. Мене не турбує точний синтаксис, тим більше, що на дошці немає компілятора (і я востаннє писав C # '09). Моя думка не в тому, що "я показав найкращий можливий код", але "як осторонь, це загальна модель, яка вже вирішена". Linq - це один із способів зробити це, ConvertAll ви згадуєте інший . Дякую, що запропонували цю альтернативу.
амон

1
Ваша відповідь слушна, але той факт, що LINQ абстрагує логіку і зводить висловлювання до одного рядка, зрештою, здається, суперечить вашим порадам. Як бічна примітка, BadaBoom => CreateFlurp(badaBoom)є надмірною; ви можете передати CreateFlurpяк функцію безпосередньо ( Select(CreateFlurp)). (Наскільки я знаю, це було завжди.)
jpmc26

2
Зауважте, що це повністю усуває потребу в методі. Назва CreateFlurpsнасправді є більш оманливою і важче зрозуміти, ніж просто бачити badaBooms.Select(CreateFlurp). Останнє є повністю декларативним - розкладатись не існує проблеми, а отже, немає необхідності в методі.
Карл Лет

1
@ R.Schmitz Це не важко зрозуміти, але це менш просто, ніж зрозуміти badaBooms.Select(CreateFlurp). Ви створюєте метод так, щоб його ім'я (високий рівень) відповідало його реалізації (низький рівень). У цьому випадку вони знаходяться на одному рівні, тому для того, щоб дізнатися, що саме відбувається, треба просто переглянути метод (замість того, щоб бачити його вбудованим). CreateFlurps(badaBooms)може проводити сюрпризи, але badaBooms.Select(CreateFlurp)не може. Це також вводить в оману, оскільки помилково просить Listзамість нього IEnumerable.
Карл Лет

61

Ідеальна кількість аргументів для функції дорівнює нулю (niladic)

Ні! Ідеальна кількість аргументів для функції - це один. Якщо він дорівнює нулю, то ви гарантуєте, що функція має доступ до зовнішньої інформації, щоб мати змогу виконувати дію. "Дядько" Боб зрозумів це дуже неправильно.

Що стосується вашого коду, ваш перший приклад містить лише два рядки в блоці, оскільки ви створюєте локальну змінну в першому рядку. Видаліть це завдання, і ви дотримуєтесь цих чітких правил коду:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

Але це дуже довго звитий (C #) код. Просто зробіть це так:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);

14
Функція з нульовими аргументами має на увазі, що об'єкт інкапсулює необхідні дані, а не те, що речі існують у глобальному стані поза об'єктом.
Рятхал

19
@Ryathal, два моменти: (1) якщо ви говорите методами, то для більшості (усіх?) Мов OO цей об'єкт робиться (або явно зазначено у випадку Python) як перший параметр. У Java, C # тощо всі методи - це функції, що мають принаймні один параметр. Компілятор просто приховує від вас цю деталь. (2) Я ніколи не згадував про "глобальний". Стан об'єкта, наприклад, зовнішній для методу.
Девід Арно

17
Я впевнений, що дядько Боб написав "нуль", він мав на увазі "нуль (не рахуючи цього)".
Док Браун

26
@DocBrown, напевно, оскільки він великий шанувальник змішування стану та функціональності в об'єктах, тому під "функцією" він, ймовірно, відноситься саме до методів. І я досі з ним не згоден. Набагато краще давати методу лише те, що йому потрібно, а не залишати його копатися по об’єкту, щоб отримати те, що він хоче (тобто, це класичне в дії «скажи, не питай»).
Девід Арно

8
@AlessandroTeruzzi, Ідеал - один параметр. Нуль занадто мало. Ось чому, наприклад, функціональні мови приймають один як кількість параметрів для цілей каррінгу (адже в деяких функціональних мовах усі функції мають точно один параметр: не більше; не менше). Каррінг із нульовими параметрами був би безглуздим. Заявивши, що "ідеал якнайменше, нуль ergo - найкращий", є прикладом reductio ad absurdum .
Девід Арно

19

Порада "Чистий код" абсолютно неправильна.

Використовуйте два або більше рядків у циклі. Приховувати однакові два рядки у функції має сенс, коли вони є деякими випадковими математиками, для яких потрібен опис, але він нічого не робить, коли рядки вже є описовими. "Створити" та "Додати"

Другий спосіб, про який ви згадуєте, насправді не має сенсу, оскільки ви не змушені додавати другий аргумент, щоб уникнути двох рядків.

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

або

foreach(var flurp in ConvertToFlurps(badaBooms))...

Як зазначають інші, порада про те, що найкраща функція - це те, що не має аргументів, перекошується в ООП у кращому випадку, а в гіршому - погана порада.


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

вибачте, що я розтлумачив ваше запитання як таке, що передбачає, що перший був "нормальним" шляхом, але вас підштовхували до другого. Я взагалі не анти-чистий код, але ця цитата, очевидно, помилкова
Еван

19
@ R.Schmitz Я сам прочитав "Чистий код", і я дотримуюся більшості того, що говорить ця книга. Однак, щодо того, що ідеальний розмір функції є майже одним твердженням, це просто неправильно. Єдиний ефект полягає в тому, що він перетворює код спагетті в код рису. Читач втрачається у безлічі тривіальних функцій, які лише при розумному погляді отримують розумний сенс. Люди мають обмежену ємність робочої пам'яті, і ви можете або перевантажувати це операторами, або функціями. Ви повинні встановити баланс між двома, якщо хочете бути читабельними. Уникайте крайнощів!
cmaster

@cmaster Відповідь отримала лише перші два абзаци, коли я написав цей коментар. Це вже краще відповідь.
Р. Шмітц

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

15

Друге, безумовно, гірше, оскільки CreateFlurpInListприймає список і модифікує цей список, роблячи цю функцію не чистою і складніше міркувати. Ніщо в назві методу не підказує, що метод лише додає до списку.

І я пропоную третій, найкращий варіант:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

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


Я б не скаржився на те, що цей метод "не є чистим і важче міркувати про це" (хоча це правда), але про те, що він є абсолютно непотрібним методом розгляду особливої ​​справи. Що робити, якщо я хочу створити окремий Flurp, Flurp, доданий до масиву, до словника, Flurp, який потім шукається у словнику та видалений відповідний Flurp тощо? З тим самим аргументом, код Flurp також знадобиться для всіх цих методів.
gnasher729

10

Версія з одним аргументом краща, але не в першу чергу через кількість аргументів.

Найважливіша причина, що це краще - це те, що він має менший зв'язок , що робить його більш корисним, простіше міркувати, простіше провести тестування і менше шансів перетворитися на копії + вставлені клони.

Якщо ви надасте мені з CreateFlurp(BadaBoom), я можу використовувати це з будь-яким типом контейнера для збору: Простий Flurp[], List<Flurp>, LinkedList<Flurp>, Dictionary<Key, Flurp>, і так далі. Але CreateFlurpInList(BadaBoom, List<Flurp>)завтра я повернуся до вас завтра з проханням, CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)щоб мій перегляд моделей міг отримати повідомлення про те, що список змінився. Гидота!

В якості додаткової переваги більш простий підпис скоріше відповідатиме існуючим API. Ви кажете, що у вас є повторювані проблеми

досить часто я опиняюсь, що створюю список з іншого списку

Це лише питання використання наявних інструментів. Найкоротша, найефективніша та найкраща версія:

var Flurps = badaBooms.ConvertAll(CreateFlurp);

Це не лише менший код для запису та тестування, він також швидший, тому що List<T>.ConvertAll()досить розумний, щоб знати, що в результаті буде така ж кількість елементів, як і вхід, і попередньо розподілити список результатів до потрібного розміру. У той час як ваш код (обидві версії) потребував розширення списку.


Не використовуйте List.ConvertAll. Ідіоматичним способом відображення безлічі об'єктів на різні об'єкти в C # називається Select. Єдина причина, ConvertAllяка доступна навіть тут, полягає в тому, що ОП помилково запитує Listв методі - це повинно бути IEnumerable.
Карл Лет

6

Пам’ятайте про загальну мету: полегшення коду для читання та обслуговування.

Часто вдається згрупувати кілька рядків в одну, змістовну функцію. Зробіть так у цих випадках. Інколи потрібно буде переглянути свій загальний підхід.

Наприклад, у вашому випадку, замінивши всю реалізацію на var

flups = badaBooms.Select(bb => new Flurp(bb));

може бути можливість. Або ви можете зробити щось на кшталт

flups.Add(new Flurp(badaBoom))

Іноді найчистіше і найчитабельніше рішення просто не впишеться в одну лінію. Так у вас буде два рядки. Не ускладнюйте розуміння коду, просто виконайте якесь довільне правило.

Ваш другий приклад (на мою думку) зрозуміти значно важче, ніж перший. Це не тільки те, що у вас є другий параметр, це те, що параметр модифікується функцією. Подивіться, що Чистий код повинен сказати з цього приводу. (Немайте книги під рукою прямо зараз, але я впевнений, що в основному це "не робити цього, якщо ви зможете цього уникнути").

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.