SOLID Principles

https://www.udemy.com/course/solid-software-design-fundamentals/

Single Responsibility Principle

  1. A class should have only one reason to change.
  2. A class should have only one responsibility.
  3. The user of the class should use the entire class as a whole, not just part of it.
  4. The class must be as „atomic“ as possible.

Разсъждавай за класовете и техните обекти като за една променлива, която прави само своите си неща, не прави т.н. „god classes“. Обектите на практика са променливи, просто по-комплексни, в смисъл такъв, че съдържат и неатомарни данни и си вървят с методите за работа с тях.

Да няма един или повече методи, които биха се счупили ако някой/някои други методи се променят. Следователно, методите трябва да са максимално независими един от друг и максимално „фокусирани“ върху конкретната функционалност, която класът пресъздава. Тук идва и понятието кохезия, между отделните методи и пропъртита, която трябва да е максимално висока.

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

Този принцип повелява класът да не се превръща в „швейцарско ножче“.

Rigid (дървен) software – промяната на едно води задължително до промяната на друго, и затова не може просто така да се правят промени.

Fragile software – това понятие е свързано с rigid software в този смисъл, че тръгнеш ли да променяш или да фиксваш бъг, започват да се чупят други неща, често даже несвързани с това, което променяме.
Следователно, fragile e по-скоро следствието, а rigid – причината.

Open-Close Principle

Open for extension, closed for modification.

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

Добавяй, не променяй! Вместо да имаш логика коя точно функционалност да използваш (демек методът на кой обект), замести тази логика с това кой клас инджектваш.

Освен това ако постоянно променяш, правиш кода „fragile“, тоест :
„The Open Close Principle states that the design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code. The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible existing code unchanged.

Liskov Substitution Principle

Състои се от 3 type rules и 4 behavior rules.

Type rules

Likov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

Или още: „Крушата не трябва да пада по-далеч от дървото“. Нарушаването на този принцип е добър сигнал, че неправилно използваме самата идея на наследяването.

Целта на този принцип е да даде нещо като взаимозаменяемост на типовете, които са йерархично организирани. Ако имаме клас Животно с няколко подкласа Котка, Куче…. да можем където се иска обект от клас Животно, да можем да подадем обект от Котка, Куче, Динозавър… Нали искаш животно, ето ти Куче, то не е ли животно? И не наследява ли всички методи и пропъртита на Животно? Не носи ли всички свойста на животните?

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

Това се нарича още Subtype Polymorphism – to substitute subtypes for their supertypes.

Има ли нещо, което родителят прави, трябва да могат и наследниците да го правят. Което ни отговаря на въпроса, кога трябва и кога не трябва да използваме inheritance в ООП по принцип.

This principle is just an extension of the Open Close Principle and it means that we must make sure that new derived classes are extending, not changing the behavior of the base classes.

Ако си представим два класа – Parent да е родителят, а Child да е наследникът, трябва спокойно да може обект от клас Child да бъде използван вместо обект от клас Parent, дори и без да имаме оверрайдване на методи на Parent в Child.

Оверрайдващите методи трябва да имат същият сигнъчър като родителя си (в PHP поне, те нямат и друг избор, иначе ще гръмне с… „Declaration of … must be compatible with …“, защото иначе би било overload-ване).

Behavior rules

Също, ако в родителският метод има входни валидации (pre-conditions), в наследяващият трябва да ги има същите, може и още по-либерални, но не и по-консервативни.
За изходните валидации (post-conditions) е обратното – те трябва да са или същите, или по-консервативни.
Нарича се Robustness principle, Arrow rule, а също и Postel’s lawbe liberal in what you accept and conservative in what you return.

Liskov Substitution Principle води и до една добра практика, наречено „Design by contract“. Или още, за да си взаимодействат безпроблемно, методите на отделните класове трябва да спазват следните условия:

a) Preconditions – a set of conditions that must be true before the method body gets executed. And they must be same or weaker in Child than those in Parent.

b) Postconditions – a set of conditions that must be true before the return. And they must be same or stronger in Child than those in Parent.

c) Same or more liberal input types and same or more conservative return types.

d) Трябва да се внимава дали „детето“ хвърля ексепшъни при едни условия, а „родителят“ – при други.

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

Interface Segregation Principle

Много подобен на Single Responsibility Principle но не за класове, а за интерфейси.

С този принцип се избягва т.н. „fat interface“.

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

Проблемът се решава с йерархия от наследяващи се интерфейси.

Dependency Inversion Principle

High level modules should not depend on low level modules.

Или още, този който върши работата (high level module), на него не трябва да му се подава конкретна функционалност (обект от даден клас – low level module), a обект, който обаче имплементира даден интерфейс (функционалност по принцип).

Или още „инджекнатият не трябва да зависи от това, какво конкретно му инджекнатваш“, а това което ще му свърши работа в конкретният случай.

Как? Като не инджектваш обект от конкретен клас, а от даден интерфейс.

Така един вид, генерализираш нещата. Казваш на инджектваният клас не „ето ти нещо конкретно“, а „ето ти нещо, което по принцип прави това, което ти трябва“. Така късаш конкретната зависимост между двата класа. Демек, не ми давай обект от конкертен този и този клас, дай ми обект, от клас който по принцип прави това, което ми трябва.

Все едно си майстор и казваш на помощника си „дай ми нещо с което да отвия тази гайка“, като може да е гаечен ключ, френски ключ, клещи…
А не „дай ми изрично гаечен ключ марка USAG, номер 12, и да е червен…“.

Или още: не инджектвай конкретна функционалност, а функционалност по принцип – какво трябва да се направи, а не как точно да се направи.

Kои са low level и кои hight level модулите?

High-and low-level regarding the level of abstraction, това се има предвид, демек нивото на абстракция.

Ако имаме клас, който използва в себе си обект от друг клас, използващият може да се разглежда като high level, а използваният (бидейки инджектнат отвън или създаден вътре) е low level.

И използващият трябва да не е стриктно свързан и зависим с използваният. Трябва да му се подава (или да му се създава вътре) функционалност по принцип, която ще му свърши работа, а не конкретна такава, като например конкретен клас.

Друг добър пример би бил ако трябва даден клас да работи с База Данни, да не и подаваме обект за конкретно MySQL или PostrgeSQL…, а да може да приема интерфейс/абсклас, който да указва, че приема обект за работа с БД по принцип, не конкретно за даден вид БД.

Друг добър пример би бил модулите за бизнес логиката да не зависят от модулите от по-ниско ниво като такива за дискови операции, HTTP рекуести, БД… За да бъде избегнато това, един добър начин на мислене при проектиране би бил да се мисли „отгоре надолу“ и от „общото към частното“, тоест – първо самата бизнес логика и после мислим как low level модулите да вършат low level нещата. А не да напишем например клас за работа с БД и после да кажем „дай да видим сега как да ги надградим с бизнес логиката“. Какво ако се наложи после MySQL да го заменим с PostgreSQL?

Спомни си Avoid looking at Medusa.

Литература:

https://www.oodesign.com/liskov-s-substitution-principle.html

https://www.oodesign.com/open-close-principle.html

https://www.oodesign.com/dependency-inversion-principle.html

https://www.oodesign.com/interface-segregation-principle.html

Вашият коментар

Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *