Блог про програмування, в основному PHP, і, можливо, про інші речі

цьому випадку

Проект, який підтримує franiglesias Розміщено на сторінках GitHub - Тема від mattgraham

Френ Іглесіас

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

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

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

У цій статті ми збираємось застосувати такі обмеження:

  • Єдиний рівень відступу
  • Не використовуйте інше
  • Тримайте одиниці невеликими

Обмеження

Єдиний рівень відступу

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

Відступ типовий для структур управління, наприклад if/then:

І він може мати кілька рівнів:

Цього разу проблемою буде зменшення один рівні відступу у кожному методі класу.

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

Не використовуйте інше

Структура if/then/else може заплутати з кількох причин. Одним з них є саме непотрібне використання else, особливо коли тодішня ніжка передбачає вихід із петлі або основний метод.

Я ввів це обмеження, оскільки воно цілком пов’язане з попереднім.

Тримайте одиниці невеликими

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

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

Навчання

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

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

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

Тест, до речі, такий, і виконується за допомогою phpspec:

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

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

  • Параметр $ length непотрібний, оскільки оскільки ми можемо легко отримати його з $ source, то ми його опустимо.
  • Ми могли використовувати структуру foreach замість for .
  • Можна передавати елемент масиву замість його індексу.

Це цікавий момент: факт вилучення методу викликає у нас кілька роздумів про наші попередні рішення та можливі вдосконалення коду. Тому перед тим, як продовжити, ми збираємось застосувати деякі.

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

Тепер маємо два рівні відступу усередині методу compareEveryElementWithCurrent, тож давайте розглянемо їх однаково: витяг у метод.

Ну, тут є кілька цікавих речей:

  • У нас є лише один рівень відступу на кожному рівні.
  • В назві методу ми робимо посилання на поточний елемент, тому було б добре висловити його в коді, змінивши ім'я змінної $ i на $ currentIndex або подібне, щоб посилання було явним.
  • Ми могли б застосувати таку ж процедуру перетворення for у foreach та зберегти змінну $ length .

Давайте внесемо деякі з цих змін:

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

Є кілька речей, на які також варто звернути увагу:

  • Ми передаємо масив $ source від методу до методу для обробки та повертаємо його. Виникає запитання, чи можна його передати за посиланням, щоб уникнути повернення та зберегти зміни або навіть зберегти його у класі як властивість та оперувати ним. Щодо цього останнього варіанту, я б сказав, що ні, оскільки алгоритм, інкапсульований у класі, не повинен мати стан, і збереження масиву означало б надання йому такого. Що стосується передачі масиву за посиланням, це варіант, який би зробив код трохи більш стислим, але, можливо, у нас є інші способи зробити це, тому наразі ми не збираємося застосовувати його.
  • З іншого боку, у методі swapElementsIfCurrentIsLower ми маємо блок із трьох рядків, який цілком міг би заслужити вилучення у власний метод, щоб чітко визначити його намір.

До речі, ми поширюємо зміну імені $ i на всі його використання, щоб вона була зрозумілішою в усі часи:

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

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

Ми зосередимося на swapElements і застосуємо трохи радикальної обробки, відмовляючись від передачі $ source та тимчасової змінної. Ми передамо елементи для обміну, посилаючись на метод, і перепризначимо їх через дрібку синтаксичного цукру, що пропонує PHP:

Зараз можна сказати, що кожен метод має мінімально можливі рівні відступу, а також мінімально можливі лінії.

Зараз ми передамо $ source за посиланням, заощаджуючи нам кілька повернень, за винятком основного загальнодоступного методу.

Видалення іншого

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

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

Тест, який захистить нас у цьому процесі, є наступним:

Структура дерева бінарного пошуку характеризується тим, що кожен вузол має двох дітей. Коли вузол вставляється, його порівнюють з кореневим вузлом. Якщо він не існує, оскільки до дерева ще не додано жодних елементів, новий вузол стає кореневим. Якщо кореневий вузол існує, новий вузол вставляється під нього за допомогою методу insertNew. Це те, що робить метод вставки, що ми додаємо елементи до дерева.

Пішли туди. Ми починаємо з методу вставки, який містить інше:

У цьому випадку нам просто потрібно повернутися до гілки if:

Як варіант, ми могли б змінити умовне на позитивне, що легше прочитати:

Метод insertNew додає вузли під даним вузлом. У такому двійковому дереві, як це, кожен вузол може мати двох дітей (і так далі рекурсивно), так що лівий дочірній вузол містить менші значення, ніж батьківський вузол, а інший вузол містить більші значення. Якщо який-небудь з дочірніх вузлів уже існує, він намагається додати новий вузол рекурсивно, поки не буде знайдено вільну “гілку” для його розміщення.

Якщо говорити про insertNew, він досягає обох рівнів відступу і має два інших, що ми можемо з цим зробити?

Перш за все, я збираюся змінити негативні умови:

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

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

Для того, щоб згладити метод, нам потрібно виділити два основні шляхи виконання до власних методів, чітко вказавши їх намір:

Два способи виконання insertNew тепер явні, і ваш код практично однаковий. Це цікаво, оскільки висвітлює одне з хибних тлумачень принципу Не повторюйся. Принцип СУХОГО стосується знань, а не коду, хоча іноді вони збігаються. У цьому випадку ми маємо однакову структуру, але це означає різні речі: як обробляти більшу величину і як обробляти меншу величину.

Метод findParent має кілька проблем, два рівні відступу, вкладені умовні умови та п’ять інших, окрім деяких дефектів, які ми можемо виправити попутно.

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

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

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

Оскільки всі гілки повертаються, думаю, було б непогано видалити решту 3:

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

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

Внесення цієї зміни показує, що метод має два можливі потоки, тому ми можемо зробити його явним, перемістивши кожен блок до свого методу:

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

Останній метод, який ми можемо пропрасувати, - це findNode, який я показую тут із необхідними механізмами для його оновлення. Видалити інше повинно бути просто:

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

Наше BinarySearchTree зараз набагато менш залякує: