Блог за програмиране, предимно PHP, и може би други неща

говорещият

Проект, поддържан от franiglesias, Хостван на GitHub Pages - Тема от mattgraham

от Фран Иглесиас

В тази статия представяме упражнение, което може да се използва за получаване на плавност при писането на по-компактни класове с по-изразителни и лесни за разбиране методи.

Обектните упражнения по калистика могат да ни помогнат да автоматизираме практиките, които ни позволяват да се доближим до най-добрите принципи на проектиране. По някакъв начин тя се състои в това да се научим да откриваме определени грешни модели в кода и да го трансформираме, така че да напредваме в целта да подобрим качеството на кода.

Идеята на тези упражнения е да наложат изкуствени ограничения за налагане на реакции, които ни принуждават да мислим отвъд конвенционалните решения.

В тази статия ще приложим следните ограничения:

  • Единично ниво на отстъп
  • Не използвайте друго
  • Дръжте единиците малки

Ограниченията

Единично ниво на отстъп

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

Вдлъбнатината е типична за контролните структури, като if/then:

И може да има няколко нива:

Предизвикателството, този път, ще бъде да намалим един нивата на отстъп във всеки метод на клас.

По същество това ни принуждава да обмислим какво прави всеки блок код и да го извлечем в свой собствен метод, обяснявайки какво прави от тяхно име.

Не използвайте друго

Структурата if/then/else може да обърка поради редица причини. Един от тях е именно ненужното използване на else, особено когато тогавашният крак предполага излизане от цикъла или основния метод.

Въведох това ограничение, защото то е доста свързано с предишното.

Дръжте единиците малки

В статията, цитирана в началото, говорим за обекти, но като се замисля, предпочетох да сложа единици, за да се отнася както за класове, така и за методи. В крайна сметка става въпрос за всяка единица, независимо от разделителната способност, която обработваме, да бъде възможно най-малка и управляема.

Това води до известно противоречие. Например, един клас може да заеме по-малко редове с един голям метод, отколкото ако го разделим на няколко по-малки въз основа на принципи като намаляване на нивата на отстъп. Очевидно това са решения, които включват компромиси. Балансът е в поддържането на четливостта и разбираемостта на класа и неговите методи.

Обучение

Преди време практикувах писането на класически алгоритми и структури от данни, използвайки TDD, и въпреки че те са относително малки класове, те имат някои структури, които биха могли да бъдат подобрени чрез прилагане на предложените ограничения, така че нека да видим някои примери и как можем да ги развием.

Като бележка от интерес да кажа, че ще направя повечето рефактори с инструментите, предоставени от IDE (PHPStorm).

Нека започнем с BubbleSort, най-простият и интуитивен алгоритъм за сортиране, макар и не много ефективен за много елементи:

Между другото, тестът е такъв и се прави с phpspec:

В този случай имаме три нива на вдлъбнатина и ограничението е, че всеки метод може да има само един най-много. За да направим това, ще извлечем вложеното за неговия собствен метод с помощта на IDE, като се уверим, че тестовете продължават да преминават:

Тестът преминава, така че не сме счупили нищо. Можем да видим някои подробности, които биха могли да бъдат подобрени при това извличане:

  • Параметърът $ length е ненужен, тъй като тъй като можем лесно да го вземем от $ source, така че ще го пропуснем.
  • Можехме да използваме структура на foreach вместо for .
  • Възможно е да се предаде елементът на масива вместо неговия индекс.

Това е интересна точка: фактът, че извършваме извличането на метода, ни кара да разсъждаваме върху предишните ни решения и възможни подобрения в кода. Така че, преди да продължим, ще приложим някои.

В момента предаването на елемента, а не на индекса не изглежда жизнеспособно, но постигнахме някои подобрения, тъй като сега не е нужно изрично да изчисляваме дължината на $ source, което означава една по-малка линия и сме по-изрични в представянето на идея, че пресичаме масива. Продължаваме да поддържаме едно ниво на отстъп, което също има само един ред.

Сега имаме две нива на вдлъбнатина вътре в метода compareEveryElementWithCurrent, така че нека се отнасяме към тях по същия начин: извличане към метод.

Е, тук има няколко страхотни неща:

  • Имаме само едно ниво на отстъп на всяко ниво.
  • В името на метода правим препратка към текущ елемент, така че би било добре да го изразим в код, като променим името на променливата $ i на $ currentIndex или подобно, така че препратката да е изрична.
  • Можем да приложим същото третиране на преобразуването на for във foreach и да запазим променливата $ length .

Нека направим някои от тези промени:

С това сме изравнили нивата на отстъпите във всички методи. Отрицателната част е, че класът е нараснал в броя на редовете, но това се компенсира от факта, че всеки метод обяснява по-добре какво прави и можем да задълбочим обяснението, колкото ни е необходимо.

Има няколко неща, които също си струва да се отбележат:

  • Предаваме масива $ source от метод на метод за обработка и го връщаме. Възниква въпросът дали би могъл да бъде предаден чрез препратка, за да се избегнат връщанията и да се запазят промените или дори да се запише в класа като свойство и да се работи върху него. По отношение на тази последна опция бих казал „не“, тъй като алгоритъмът, капсулиран в класа, не трябва да има състояние и запазването на масива би му дало такова. Що се отнася до предаването на масива чрез препратка, това е опция, която би направила кода малко по-кратък, но може би имаме други начини да го направим, така че засега няма да го прилагаме.
  • От друга страна, в метода swapElementsIfCurrentIsLower имаме блок от три реда, който може да заслужава да бъде извлечен в собствения си метод, за да изясни намерението си.

Между другото, ние разширяваме промяната на името на $ i за всички негови приложения, така че да е по-ясно по всяко време:

Благодарение на този рефактор не е нужно да навлизаме в червата на обменния механизъм, за да разберем какво се случва с елементите.

След това ще се опитаме да направим някои подобрения, които ни помагат да изчистим кода малко и да намалим броя на редовете, и ще го направим, като се възползваме от факта, че всяка стъпка от алгоритъма е представена в свой собствен метод.

Ще се съсредоточим върху swapElements и ще приложим малко радикално третиране, освобождавайки се от преминаване на $ source и временната променлива. Ще предадем елементите за обмен, като се позоваваме на метода и ще ги преназначим чрез щипка малка синтактична захар, която PHP предлага:

Вече може да се каже, че всеки метод има минимално възможните нива на отстъп, както и минимално възможните линии.

Сега ще преминем $ source чрез препратка, спестявайки ни няколко връщания, освен в основния публичен метод.

Изтриване на друго

Този път ще прегледаме бинарно дърво за търсене, което е наполовина фиксирано. Тоест: има някои първи опити за поставяне на кода в по-добро състояние, но все още имаше много място за подобрение.

Както виждаме, има някои зони с две нива на вдлъбнатина и доста приложения на else .

Тестът, който ще ни защити в този процес, е следният:

Структурата на двоичното дърво за търсене се характеризира с това, че всеки възел има две деца. Когато въведете възел, той се сравнява с основния възел. Ако не съществува, тъй като в дървото все още не са добавени елементи, новият възел се прави корен. Ако коренният възел съществува, новият възел се вмъква под него с помощта на метода insertNew. Това прави методът на вмъкване, което добавяме елементи към дървото.

Хайде да отидем там. Започваме с метода на вмъкване, който съдържа друго:

В този случай просто трябва да се върнем към клон if:

Като алтернатива, бихме могли да обърнем условното на положително, което е по-лесно за четене:

Методът insertNew добавя възли под даден възел. В двоично дърво като това всеки възел може да има две деца (и така нататък рекурсивно), така че левият дъщерен възел съдържа по-малки стойности от родителския възел, а другият възел съдържа по-големи стойности. Ако някой от дъщерните възли вече съществува, той се опитва да добави новия възел рекурсивно, докато се намери свободен „клон“, който да го постави.

Говорейки за insertNew, той достига и двете нива на отстъп и има още две, какво можем да направим по въпроса?

Първо, ще обърна отрицателните условия:

Първото нещо, което можем да забележим, е, че всички крака на условните не водят до изхода и няма обработка преди или след това. С други думи, можем да поставим възвръщаемост на всеки крак. Това ще ни улесни да премахнем останалите, защото ги прави ненужни.

Тестът показва, че тази промяна не засяга функционалността, елиминирахме останалите и един от случаите на две нива на отстъп.

За да изравним метода, трябва да извлечем двата основни пътя на изпълнение към техните собствени методи, като направим изрично намерението им:

Двата начина за изпълнение на insertNew вече са явни и вашият код е практически един и същ. Това е интересно, защото подчертава една от погрешните интерпретации на принципа „Не се повтаряй“. Принципът на СУХО се отнася до знания, а не до код, въпреки че понякога те съвпадат. В този случай имаме една и съща структура, но това означава различни неща: как да се третира по-голяма стойност и как да се обработва по-малка стойност.

Методът findParent има няколко проблема, две нива на отстъп, вложени условности и пет други, освен някои дефекти, които можем да отстраним мимоходом.

Първо, общо почистване. Когато имаме няколко връщания, трябва да посочим типа на връщане, за да гарантираме, че всички те са последователни. В този случай методът може да върне BinarySearchNode или null, ако не бъде намерен.

Имаме отрицателни условия. В този случай е малко по-деликатно да ги обърнете, тъй като те имат три клона и това може да промени поведението. Но тъй като имаме тестове, нека видим какво ще стане, ако го направим:

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

Тъй като всички клонове се връщат, мисля, че ще бъде добра идея да премахнете останалите 3:

Тестовете продължават да преминават и методът е малко по-плосък. В този момент изглежда добра идея да отмените някои от усъвършенстваните по-рано, тъй като ако обърна някои условни условия, мога да намаля нивата на отстъп, без да извличам методи:

Възможно е да сме стигнали до същата точка, ако не сме обърнали условните условия, когато за първи път се сблъскаме с метода. Най-малкото е, най-важното е да се движите безопасно през кода.

Извършването на тази промяна показва, че методът има два възможни потока, така че можем да го направим изрично, като преместим всеки блок в свой собствен метод:

Новият findParent вече е много по-лесен за разбиране, както и неговите клонове. Дори отрицателните условници сега действат като предпазни клаузи, което прави функциите им много по-очевидни (не правете нищо, ако няма с какво да работите). И двата извлечени клона основно ни казват, че ако търсената стойност съвпада с тази на детето, съответно ляво или дясно, на възела, това е неговият родителски възел. И ако не съвпада, продължете да търсите.

Последният метод, който можем да изгладим, е findNode, който показвам тук с необходимите мерки, за да го актуализира. Премахването на другото трябва да е лесно:

Методът намира възела, който съответства на стойност. Ако текущата съвпада, тя я връща и ако не, търси от лявата страна, ако е по-малка, и от дясната, ако е по-голяма. Изравненият метод изглежда така:

Нашето BinarySearchTree сега е много по-малко плашещо: