Коли парадигма «Зробити одну річ» стає шкідливою?


21

Для аргументу ось зразок функції, яка друкує вміст даного файлу по черзі.

Версія 1:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  string line;
  while (std::getline(file, line)) {
    cout << line << endl;
  }
}

Я знаю, що рекомендується, щоб функції виконували одне на одному рівні абстракції. Для мене, хоча код вище, це дуже одне і є досить атомним.

Деякі книги (наприклад, Чистий код Роберта К. Мартіна), здається, пропонують розбити вищезазначений код на окремі функції.

Версія 2:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  printLines(file);
}

void printLines(fstream & file) {
  string line;
  while (std::getline(file, line)) {
    printLine(line);
  }
}

void printLine(const string & line) {
  cout << line << endl;
}

Я розумію, чого вони хочуть досягти (відкритий файл / рядки читання / рядок друку), але хіба це не надто багато?

Оригінальна версія проста і в деякому сенсі вже робить одне - друкує файл.

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

Чи не було б у цьому випадку код мати в одному місці?

У який момент парадигма «Зробити одну річ» стає шкідливою?


13
Цей вид кодування завжди базується на кожному конкретному випадку. Ніколи не існує єдиного підходу.
iammilind

1
@Alex - прийнята відповідь буквально не має нічого спільного з питанням. Я вважаю це дійсно дивним.
ChaosPandion

2
Зауважу, що ваша реконструйована версія перевернута, що сприяє недостатній читабельності. Читання вниз файлу, можна було б очікувати , щоб побачити printFile, printLinesі , нарешті printLine.
Ентоні Пеграм

1
@Kev, я ще раз можу погодитися лише, особливо з цією категоризацією. Це не педантизм, це справа! Саме ОП конкретно говорить, що друга версія може бути не начитаною. Саме ОП спеціально цитує Чистий код як натхнення для другої версії. Мій коментар по суті полягає в тому, що Clean Code не дав би йому написати код таким чином. Порядок насправді важливий для читабельності, ви читаєте файл так, як читаєте статтю в газеті, отримуючи все більше і більше деталей, поки ви в основному не зацікавитеся.
Ентоні Пеграм

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

Відповіді:


15

Звичайно, це просто ставить питання "Що таке одне ?" Чи читає рядок одне, а пишеть рядок - інше? Або копіювання рядка з одного потоку в інший вважається одним? Або копіювати файл?

На це немає важкої, об'єктивної відповіді. Тобі вирішувати. Ви можете вирішити. Ви повинні вирішити. Основна мета парадигми "Зробити одну справу" - це, мабуть, створити код, який максимально простий для розуміння, і ти можеш використовувати це як орієнтир. На жаль, це теж об'єктивно не піддається вимірюванню, тому доведеться покладатися на почуття кишечника та на "WTF?" підрахунок в огляді коду .

Функція IMO, що складається лише з одного рядка коду, рідко коли-небудь коштує проблем. У printLine()вас немає переваги перед використанням std::cout << line << '\n'1 безпосередньо. Якщо я бачу printLine(), я мушу або припускати, що він робить те, що говорить його назва, або переглянути його і перевірити. Якщо я бачу std::cout << line << '\n', я відразу знаю, що це робить, тому що це канонічний спосіб виведення вмісту рядка як рядка std::cout.

Однак ще одна важлива мета парадигми - дозволити повторне використання коду, і це набагато більш об'єктивний захід. Наприклад, у вашій 2 - й версії printLines() може легко бути написана так , що вона є універсально корисною алгоритм , який копіює рядки з одного потоку в інший:

void copyLines(std::istream& is, std::ostream& os)
{
  std::string line;
  while( std::getline(is, line) );
    os << line << '\n';
  }
}

Такий алгоритм може бути використаний і в інших контекстах.

Потім ви можете помістити все, що стосується цього випадку використання, у функцію, яка викликає цей загальний алгоритм:

void printFile(const std::string& filePath) {
  std::ifstream file(filePath.c_str());
  printLines(file, std::cout);
}

1 Зауважте, що я '\n'скоріше використовував std::endl. '\n'має бути вибором за замовчуванням для виведення нового рядка , std::endlце непарний випадок .


2
+1 - Я переважно згоден, але я думаю, що в цьому є більше, ніж "відчуття кишки". Проблема полягає в тому, коли люди судять про "одне", підраховуючи деталі реалізації. Для мене функція повинна реалізовувати (і її назва описує) єдину чітку абстракцію. Ніколи не слід називати функцію "do_x_and_y". Впровадження може і повинно робити декілька (простіших) речей - і кожна з цих більш простих речей може бути розкладена на кілька ще простіших речей тощо. Це просто функціональне розкладання з додатковим правилом - що функції (та їх назви) повинні описувати єдине чітке поняття / завдання / що завгодно.
Steve314

@ Steve314: Я не вказав деталі реалізації як можливості. Копіювання рядків з одного потоку в інший , безумовно , є однією річчю абстракції. Або це? І цього легко уникнути do_x_and_y(), назвавши функцію do_everything()замість цього. Так, це нерозумний приклад, але це показує, що це правило не дозволяє навіть запобігти найбільш крайні приклади поганого дизайну. IMO це є рішенням почуття кишки стільки , скільки один диктуються конвенціями. В іншому випадку, якби це було об'єктивно, ви можете придумати для нього метрику - чого не можете.
sbi

1
Я не мав наміру суперечити - лише запропонувати доповнення. Я здогадуюсь, що я забув сказати - це те, що, починаючи від запитання, розкладання на printLineінше є дійсним - кожне з них є однією абстракцією - але це не означає, що це необхідно. printFileце вже "одне". Хоча ви можете розкласти це на три окремі абстракції нижнього рівня, вам не доведеться розкладати на кожному можливому рівні абстракції. Кожна функція повинна виконувати "одну річ", але не кожна можлива "одна річ" повинна бути функцією. Переміщення занадто великої складності в графіку викликів може само бути проблемою.
Steve314

7

Функція виконувати лише "одне" - це засіб до двох бажаних цілей, а не заповідь від Бога:

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

  2. Функції, які виконують лише "одне", можуть зробити код читабельнішим. Це залежить від того, чи отримуєте ви більше ясності та простоти міркувань, розлучаючи речі, ніж втрачаєте на багатослів’я, непрямості та концептуальні накладні конструкції, які дозволяють розв’язувати речі.

Тому "одне" неминуче суб'єктивне і залежить від того, який рівень абстракції відповідає вашій програмі. Якщо printLinesрозглядати як єдину, фундаментальну операцію та єдиний спосіб друкування ліній, про які ви дбаєте або передбачаєте догляд, то для ваших цілей printLinesробиться лише одне. Якщо ви не знайдете другу версію більш читаною (я не знаю), перша версія чудово.

Якщо ви почнете потребувати більшого контролю над нижчими рівнями абстрагування і закінчуєтесь тонким дублюванням та комбінаторним вибухом (тобто, printLinesдля імен файлів і повністю розділених printLinesдля fstreamоб'єктів, printLinesдля консолі та printLinesдля файлів), тоді ви printLinesробите більше ніж одну річ на рівні абстракції, яке вас хвилює.


Я б додав третю, і це менші функції легше перевіряються. Оскільки, мабуть, потрібно менше входів, якщо функція виконує лише одне, це полегшує тестування її самостійно.
PersonalNexus

@PersonalNexus: Я дещо погоджуюся з питанням тестування, але IMHO нерозумно перевірити деталі реалізації. Для мене одиничний тест повинен перевірити "одну річ", як визначено в моїй відповіді. Що-небудь дрібніше, це робить ваші тести крихкими (оскільки зміна деталей впровадження вимагатиме зміни ваших тестів), а ваш код дратівливо багатослівний, непрямий і т. Д. (Тому що ви додасте непрямість лише для підтримки тестування).
dimimcha

6

У такому масштабі це не має значення. Однофункціональна реалізація є абсолютно очевидною і зрозумілою. Однак додавання лише трохи більшої складності робить дуже привабливим розділення ітерації від дії. Наприклад, припустимо, що вам потрібно було надрукувати рядки з набору файлів, визначених шаблоном типу "* .txt". Тоді я б відокремив ітерацію від дії:

printLines(FileSet files) {
   files.each({ 
       file -> file.eachLine({ 
           line -> printLine(line); 
       })
   })
}

Тепер ітерацію файлів можна перевірити окремо.

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


4
Я думаю, ти це прибив. Якщо нам потрібен коментар для пояснення рядка, то завжди є час витягти метод.
Roger CS Wernersson

5

Витягуйте методи, коли відчуваєте потребу в коментарі для пояснення речей.

Напишіть методи, які або лише роблять те, що говорить їх ім’я очевидним чином, або розповідають історію, називаючи розумно названі методи.


3

Навіть у вашому простому випадку вам не вистачає деталей, що Принцип єдиної відповідальності допоможе вам краще керувати. Наприклад, що відбувається, коли щось відкривається не так, як відкрити файл. Додавання в обробку винятків для затвердіння проти випадкових випадків доступу до файлів додасть до вашої функції 7-10 рядків коду.

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

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

Мета СРП - дозволити задуматися над одним завданням одночасно. Це як би розбити великий блок тексту на кілька абзаців, щоб читач міг зрозуміти точку, яку ти намагаєшся перетнути. На написання коду, який дотримується цих принципів, потрібно трохи більше часу. Але цим ми полегшуємо читання цього коду. Подумайте, наскільки щасливим буде ваше майбутнє «я», коли йому доведеться відшукати помилку в коді та визначити його чітко розподіленим.


2
Я відповів на цю відповідь, тому що мені подобається логіка, хоча я не згоден з нею! Забезпечити структуру на основі складних роздумів про те, що може статися в майбутньому, є протипродуктивним. Факторний код, коли потрібно. Не абстрагуйте речі, поки не потрібно. Сучасний код загрожує людьми, які намагаються по-рабськи дотримуватися правил, а не просто писати код, який працює і неохоче адаптується . Хороші програмісти ліниві .
Yttrill

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

2

Особисто я віддаю перевагу останньому підходу, оскільки це заощаджує вашу роботу в майбутньому і змушує розуму "як це зробити загальним способом". Незважаючи на це, у вашому випадку Версія 1 краща, ніж Версія 2 - тільки тому, що проблеми, вирішені Версією 2, занадто тривіальні та характерні для fstream. Я думаю, що це слід зробити наступним чином (включаючи виправлення помилок, запропоноване Nawaz):

Загальні функції корисності:

void printLine(ostream& output, const string & line) { 
    output << line << endl; 
} 

void printLines(istream& input, ostream& output) { 
    string line; 
    while (getline(input, line)) {
        printLine(output, line); 
    } 
} 

Функція, що залежить від домену:

void printFile(const string & filePath, ostream& output = std::cout) { 
    fstream file(filePath, ios::in); 
    printLines(file, output); 
} 

Тепер printLinesі printLineможе працювати не тільки з fstream, але і з будь-яким потоком.


2
Я не погоджуюсь. Ця printLine()функція не має значення. Дивіться мою відповідь .
sbi

1
Ну, якщо ми збережемо printLine (), тоді ми можемо додати декоратор, який додає номери рядків або синтаксичне забарвлення. Сказавши це, я не витягав би ці методи, поки не знайшов причину.
Roger CS Wernersson

2

Кожна парадигма (не обов'язково та, яку ви цитували) вимагає певної дисципліни, і, таким чином, - зменшення "свободи слова" - призводить до початкових витрат (принаймні, тільки тому, що ви повинні це навчитися!). У цьому сенсі кожна парадигма може стати шкідливою, коли вартість цієї накладних витрат не буде компенсована перевагою, яку парадигма покликана тримати в собі.

Таким чином, справжня відповідь на питання вимагає хорошої здатності "передбачити" майбутнє, наприклад:

  • Мені зараз потрібно це зробити AіB
  • Яка ймовірність, що найближчим часом мені потрібно буде зробити A-і B+(тобто щось, схоже на A і B, але трохи інше)?
  • Яка ймовірність у більш далекому майбутньому, що A + стане A*або A*-?

Якщо ця ймовірність буде відносно високою, це буде хороший шанс, якщо, поміркувавши про A і B-, я також подумаю про їх можливі варіанти, таким чином, щоб виділити загальні частини, щоб я міг їх повторно використовувати.

Якщо ця ймовірність дуже мала (будь-який варіант навколо Aнасправді є нічим іншим як Aсамим собою), вивчіть, як розкласти А далі, швидше за все, це призведе до втрати часу.

Як приклад, дозвольте розповісти вам цю справжню історію:

Під час мого минулого життя , як учитель, я виявив , що -він великої частини роботи- студента практично всі з них надають свої власні функції для обчислення довжини рядка C .

Після деякого розслідування я виявив, що, будучи частою проблемою, всі учні приходять до ідеї використовувати для цього функцію. Після того, як вони сказали їм, що існує функція бібліотеки для цього ( strlen), багато з них відповіли, що оскільки проблема була настільки простою і тривіальною, їм було ефективніше написати власну функцію (2 рядки коду), ніж шукати посібник з бібліотеки С (це був 1984 рік, забули веб-сайт і google!) в суворому алфавітному порядку, щоб побачити, чи є готова функція для цього.

Це приклад, коли парадигма "не винаходити колесо" може стати шкідливою без ефективного каталогу коліс!


2

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

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

Ваша приємна і досить лінійна маленька функція раптом закінчується складним безладом, який просить розділитись на окремі функції.


2

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

З оригінальної публікації

void printLine(const string & line) {
  cout << line << endl;
}

Якщо ви достатньо педантичні, ви можете помітити, що printLine все ще робить дві речі: написання рядка в cout та додавання символу "кінцевої лінії". Деякі люди можуть захотіти вирішити це, створивши нові функції:

void printLine(const string & line) {
  reallyPrintLine(line);
  addEndLine();
}

void reallyPrintLine(const string & line) {
  cout << line;
}

void addEndLine() {
  cout << endl;
}

О ні, зараз ми зробили проблему ще гіршою! Тепер навіть НЕБЕЗПЕЧНО, що printLine робить ДВІ речі !!! 1! Створювати найнеобхідніші "робочі обходи" не має великої дурості, можна собі уявити лише для того, щоб позбутися тієї неминучої проблеми, що друк рядка складається з друку самого рядка та додавання символу кінця рядка.

void printLine(const string & line) {
  for (int i=0; i<2; i++)
    reallyPrintLine(line, i);
}

void reallyPrintLine(const string & line, int action) {
  cout << (action==0?line:endl);
}

1

Коротка відповідь ... це залежить.

Подумайте над цим: що робити, якщо в майбутньому ви не хочете друкувати тільки на стандартний вихід, а на файл.

Я знаю, що таке YAGNI, але я просто кажу, що можуть бути випадки, коли відомо, що деякі реалізації потрібні, але відкладені. Тож, можливо, архітектор або що інше знає, що функція повинна бути здатна також друкувати у файл, але не хоче робити реалізацію прямо зараз. Тож він створює цю додаткову функцію, тож у майбутньому вам потрібно буде лише змінити вихід в одному місці. Має сенс?

Якщо ви впевнені, що вам потрібен лише вихід на консолі, це насправді не має великого сенсу. Написання «обгортки» cout <<здається марним.


1
Але строго кажучи, чи не функція printLine відрізняється рівнем абстракції від ітерації по рядках?

@Petr Я думаю, що так, саме тому вони пропонують вам розділити функціонал. Я думаю, що концепція є правильною, але її потрібно застосовувати в кожному конкретному випадку.

1

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


0

Як зауважили інші афіші, робити одне - це питання масштабу.

Я б також припустив, що ідея Єдиної речі - зупинити кодування людей за побічним ефектом. Це пояснюється послідовним з'єднанням, коли методи потрібно викликати в певному порядку, щоб отримати «правильний» результат.

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