Все, що ви хотіли знати про принципи SOLID. Частина перша: SRP

Привіт! Мене звати Сергій Немчинський, і я — програміст з понад 20-річним досвідом, а також засновник та директор навчального центру FoxmindEd.

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

Ця стаття — частина циклу, в якому я детально поясню кожен із принципів SOLID: розглянемо їхню логіку, практичну цінність і як вони допомагають створювати чистий, надійний код.

Що таке SOLID

Коли говоримо про SOLID, варто згадати Роберта Мартіна — людину, яку багато програмістів, і я серед них, вважають своїм наставником. Саме він написав культову книгу «Чистий код» («Clean Code») та багато інших чудових праць. Раджу читати їх в оригіналі: переклади на інші мови часто втрачають частину сенсу та атмосфери.

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

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

5 принципів SOLID

  1. S — SRP (Single Responsibility Principle). Кожен клас повинен мати лише одну причину для змін.
  2. O — OCP (Open Closed Principle). Код має бути відкритим до розширення (тобто до додавання нового функціоналу) та закритим до змін (все, що вже написано, не повинно змінюватися).
  3. L — LSP (Liskov Substitution Principle). Об’єкти в програмі повинні замінюватися екземплярами їхніх підтипів без порушення коректності виконання програми.
  4. I — ISP (Interface Segregation Principle). Багато інтерфейсів, спеціально призначених для клієнтів, краще за один універсальний для всіх.
  5. D — DIP (Dependency Inversion Principle). Залежність від абстракцій, а не від конкретних деталей. Абстракція не повинна залежати від конкретних речей, а конкретні речі повинні залежати від абстракції.

Навіщо потрібні принципи SOLID

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

Також використання принципів SOLID зменшує так званий «сморід коду». Що мають на увазі, коли кажуть, що «Code smells»? Ми називаємо це «лайнокод». Це, власне, не помилки та баги, але ознаки порушення тих самих основних принципів гарного програмування: надто великі класи, занадто довгі методи, дублювання коду та інше. Що менше «смердить» ваш код, то менше в ньому слабких місць, які можуть призвести до серйозних проблем в проєкті.

Детальніше про принцип SRP (Single Responsibility Principle)

Сам Роберт Мартін дає таке визначення принципу SRP: «Кожен об’єкт повинен мати одну відповідальність, і ця відповідальність має бути повністю інкапсульована в класі».

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

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

Приклад принципу SRP. Уявіть, що ви розробляєте Embedded-систему, в якій є датчик, що вимірює час і температуру. Ви створюєте клас DateTemperature, де є поля для роботи з часом та температурою, а також методи, які частково працюють з часом, а частково з температурою. Поки що все йде добре.

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

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

Якщо в вашому класі зібрано надто багато параметрів, це може породити антипатерн God Object.

Антипатерн God Object

God Object — це антипатерн і одна з найчастіших причин порушення принципу SRP. Це великий клас, у якому зібрано все, що тільки можливо. Це порушує патерн Information Expert з GRASP і також порушує SRP, адже через такий клас проходять усі осі змін, і будь-яке нове замовлення на зміну потребує внесення правок саме до цього класу. Це значно ускладнює підтримку й розширення коду.

Що відбувається на проєкті, якщо ваш клас занадто великий, щоб його перевикористати? Василь і Петро починають копіпастити ті фрагменти вашого коду, які потрібні їм. Це призводить до того, що код розростається через копіювання, і виникає плутанина. Але головна проблема попереду.

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

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

Для окремої роботи з кожним із параметрів можна використовувати та перевикористовувати відповідний клас. Тобто ви зможете віддати Василеві клас DateTime, Петру — клас Temperature, вони їх успішно перевикористають, всі молодці, код красивий.

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

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

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

Критичне мислення в SOLID

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

На обмірковування рішення слід витратити вдесятеро більше часу, ніж на написання. Перед розробкою детально плануйте та малюйте схеми. Спробуйте це робити олівцем на папері, щоб за комп’ютером не відволікатись на інші таски та на котиків. Вималюйте причинно-наслідкові зв’язки в вашому коді: якщо я зроблю так, що відбудеться? А ось ця дія до чого призведе? І тільки після того, як схема зійшлася, беріться до коду.

Патерни, що порушують SRP

Існують деякі патерни Enterprise, які порушують принцип SRP, наприклад, ActiveRecord. ActiveRecord — це клас, у якому є як поля для бізнес-логіки, так і методи для збереження та завантаження з бази даних. У цьому випадку клас має дві відповідальності: бізнес-логіка та робота з базою даних. Тому при зміні бізнесу змінюються бізнес-поля, а при зміні бази даних — методи збереження, що порушує SRP.

Чи означає це, що ми не маємо використовувати ActiveRecord? Звісно, ні. ActiveRecord широко використовується, наприклад, у Ruby on Rails та PHP (Laravel, Yii). У C# також використовували ActiveRecord, хоча зараз перейшли до ORM. Тобто це популярний патерн, який розв’язує певні задачі.

Чи означає це, що ми маємо відмовитися від принципу SRP? Теж ні. Принципи SOLID довели свою корисність. Але знов-таки, потрібно підходити до будь-якого принципу критично, і використовувати його розумно.

👍ПодобаєтьсяСподобалось29
До обраногоВ обраному12
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Нагадало одну історію) Якось, коли ще був джуном, працював у медичному стартапі. У нас був об’єкт Пацієнт з 400+ полями де купа різної інформації зберігалася про стан здоров’я, хвороби, реакції на різні медичні препарати і т.д. Так от один програміст, великий фанат SOLID, подивився на це і каже: «Слухайте, це можна покращити. У вас тут є інформація і про голову, і про торс, і про руки, ноги, внутрішні органи, шкіру і т.д. Давайте все це розіб’ємо на різні об’єкти і будемо складати об’єкт Пацієнт як конструктор, об’єднуючи різні об’єкти частин тіла та органів. Щоб окремо у нас зберігалася інформація про голову, окремо про ліву руку, окремо про праву і т.д.» Проект поставили на паузу раніше, ніж ця ідея була імплементована)

І в котрий раз: SOLID — поганий вибір тому, що 1) всі принципи туманно сформульовані і формулювання від самого Мартина дають що завгодно, а не чітке розуміння, 2) вибір саме цих 5 принципів суто механічний заради красивого акроніму (і то не від Мартина, це винахід Фізерса).

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

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

Я можу зрозуміти, чому Сергій пропагує все це, але від того не краще.

Дайте ссилки почитати критику, заради спортивного інтересу

У цьому випадку клас має дві відповідальності: бізнес-логіка та робота з базою даних.

Помоему он в книге четко пишет, под «отвественным» понимается конкретный юзер, Машка из Бухгалтерии.

Як відомо, патерн Singleton порушує принцип єдиної відповідальності. Питання читачеві мого коменту: на твою думку, Одинак — це патерн чи антипатерн? І чому?
Пане Сергію, як завжди на висоті, респект за стабільно якісний контент =)

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

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

дивлячись як використовувати

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

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

Підписатись на коментарі