Казки про SOLID код для шестирічного малюка

Ця моя перша стаття — це як нова звичка, яку я завів. Коли я йду в магазин, я завжди беру щось нове, щось, що раніше не пробував, просто щоб побачити, що з цього вийде. Чи пробував ти коли-небудь смачний зміїний фрукт чи броколіні?

Коротка історія на довгу. Це не коротка стаття. Мушу тебе розчарувати.

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

pic

Щиро вдячний і позичено звідси

Далі я звернувся до нашого нового друга, ChatGPT, використовуючи модель 4o, навіть ці приклади не мають, скажімо так, для мене хороших прикладів, які б були майже на 100% безпомилковими. Потім я спробував модель o1, яка є моделлю з розширеним мисленням. І мушу визнати, що це досить добра основа для цієї статті, хоча й не ідеальна. Вдаримо кулаками, друже 😁

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

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

“Якщо ти можеш пояснити те, що робиш, шестирічному, значить, ти справді розумієш, про що йде мова.”

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

У легендарних словах Дедпула і Росомахи — давайте, чорт забирай, зробимо це.

Принцип єдиної відповідальності (SRP)

pic

Щиро вдячний і позичено звідси

Визначення: Клас повинен мати лише одну причину для змін.

Серйозно? Як може зрозуміти новачок, як застосувати це взагалі? Ще краще — це приклад для дітей з моделі 4o.

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

Боюсь, Тіммі б сильно сміявся і сказав: "Ось у чому вся суть і розвага сендвіча з арахісовим маслом і джемом, тату." Добре, давайте почнемо спочатку.

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

Приклад порушення принципу: Дуже поширеною темою в програмуванні є логін та створення нового користувача.
Я пам’ятаю початок моєї кар'єри, і я міг би легко змішати ці два поняття — думаючи собі "А який шкода?"

class UserManager {  
 save(user) {  
 // Понад 15 рівнів безпеки, збереження та шифрування, користувач зберігається  
 console.log(`Нарешті зберігаємо ${user} в базу даних...`);  
 }  

 login(usernName, password) {  
 // Робимо якусь магію, хешуємо обидва параметри для перевірки в БД, щоб ніхто не знав пароль у відкритому вигляді  
 console.log(`Користувач ${usernName} виконує вхід...`);  
 }  
}

Спосіб перевірити "хорошу" архітектуру, як мене навчив один дорогий колега, — це зробити її гіршою тисячу разів. У цьому випадку ми могли б додати функції видалення, отримання/зміни користувача, а також додати вихід із системи. Потім додамо трохи безпеки за допомогою токена і, між іншим, використаємо шифрування та додатковий рівень бази даних. Тепер попросіть молодшого розробника змінити шифрування входу, не зламавши все.

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

class UserDBFactory {  
 save(user) {  
 // Понад 15 рівнів безпеки, збереження та шифрування, користувач зберігається  
 console.log(`Нарешті зберігаємо ${user} в базу даних...`);  
 }  
}  


class UserService {  
 login(usernName, password) {  
 // Робимо якусь магію, хешуємо обидва параметри для перевірки в БД, щоб ніхто не знав пароль у відкритому вигляді  
 console.log(`Користувач ${usernName} виконує вхід...`);  
 }  
}

Принцип відкритості та закритості (OCP)

pic

Щиро вдячний і запозичено з цього джерела

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

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

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

Приклад порушення принципу: Уявіть, що у нас є різні методи оплати, такі як CreditCard, PayPal, і ми хочемо додати новий метод (наприклад, Cryptocurrency), не змінюючи основну функціональність оформлення замовлення. Кожного разу, коли ми додаємо новий тип оплати, ми змінюємо наш клас PaymentProcessor.
Це змушує нас щоразу відкривати код PaymentProcessor і змінювати його, коли ми додаємо новий тип оплати.

class PaymentProcessor {  
 processPayment(paymentType) {  
 if (paymentType === "creditCard") {  
 // Робимо банки багатими  
 } else if (paymentType === "paypal") {  
 // Робимо Ілона Маска багатим  
 } else if (paymentType === "crypto") {  
 // Робимо якогось анонімного розробника на прихованому острові багатим  
 }  
 // ... і так далі  
 }  
}

Приклад правильного коду: Створіть інтерфейс (в JavaScript це просто умовність), який імплементує кожен метод оплати.
Тепер PaymentProcessor приймає будь-який метод оплати, який відповідає необхідному інтерфейсу.

class CreditCardPayment {  
 pay(amount) {  
 console.log(`Оплата $${amount} за допомогою кредитної картки`);  
 }  
}  

class PayPalPayment {  
 pay(amount) {  
 console.log(`Оплата $${amount} через PayPal`);  
 }  
}  

// Тепер ми можемо додавати нові методи оплати без зміни PaymentProcessor  
class PaymentProcessor {  
 constructor(paymentMethod) {  
 this.paymentMethod = paymentMethod;  
 }  

 process(amount) {  
 this.paymentMethod.pay(amount);  
 }  
}  

// Використання  
const creditCard = new CreditCardPayment();  
const paymentProcessor = new PaymentProcessor(creditCard);  
paymentProcessor.process(100);  

const paypal = new PayPalPayment();  
const paymentProcessor2 = new PaymentProcessor(paypal);  
paymentProcessor2.process(200);

Принцип заміщення Ліскова (LSP)

pic

Щиро вдячний і позичено з тут

Визначення: Похідні класи повинні бути взаємозамінними з їхніми базовими класами.

Історія: У вас є іграшкова машина і іграшковий вантажівка, обидва — це «іграшкові транспортні засоби». Якщо ваша іграшкова машина може з'їжджати з рампи, ваша іграшкова вантажівка також повинна мати таку можливість — тому що обидва це іграшкові транспортні засоби.
Якщо ваша іграшкова вантажівка не може котитися, це може зламати гру, де ви очікуєте, що всі іграшкові транспортні засоби будуть котитися.

Приклад порушення коду ([джерело](https://stackoverflow.com/questions/56860/what-is-an-example-of-the-liskov-substitution-principle)):** Чудовий приклад, що ілюструє принцип LSP (наданий дядьком Бобом у подкасті, який він нещодавно слухав), показує, як інколи те, що звучить правильно у природній мові, не зовсім працює в коді.

У математиці, Квадрат — це Прямокутник. Дійсно, це спеціалізація прямокутника. Термін "є" змушує вас моделювати це через успадкування. Однак якщо в коді ви зробите так, що Квадрат буде спадкувати від Прямокутника, тоді Квадрат повинен бути використаний скрізь, де ви очікуєте Прямокутник.
Це викликає деяку дивну поведінку.

class Rectangle {  
 constructor(width, height) {  
 this.width = width;  
 this.height = height;  
 }  

 setWidth(width) {  
 this.width = width;  
 }  

 setHeight(height) {  
 this.height = height;  
 }  
}  

// Почекайте? Хто я? Прямокутник чи квадрат?  
class Square extends Rectangle {  
 setWidth(width) {  
 this.width = width;  
 this.height = width;  
 }  

 setHeight(height) {  
 this.width = height;  
 this.height = height;  
 }  
}

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

Дійсний приклад коду: Ігноруючи той факт, що квадрат є прямокутником, можливе рішення може виглядати ось так. Заміна GeometricShape на Rectangle чи Square пройде тест на заміщення Liskov для GeometricShape.
Але передача GeometricShape для Square не пройде, через можливу різницю в width і height, що не було явно зазначено в наведеному вище визначенні.

class GeometricShape {  
 constructor(width, height) {  
 this.width = width;  
 this.height = height;  
 }  

 setWidth(width) {  
 this.width = width;  
 }  

 setHeight(height) {  
 this.height = height;  
 }  
}  

class Rectangle extends GeometricShape {  
 // Не потребує якихось складних функцій, як його брат Square  
}  

class Square extends GeometricShape {  
 // Перевизначення  
 setWidth(width) {  
 this.width = width;  
 this.height = width;  
 }  

 // Перевизначення  
 setHeight(height) {  
 this.width = height;  
 this.height = height;  
 }  
}

Підсумок: Моделюйте свої класи на основі поведінки, а не властивостей; моделюйте свої дані на основі властивостей, а не поведінки.
Якщо це поводиться як качка, то це точно птах.

Принцип сегрегації інтерфейсів (ISP)

pic

Щиро вдячний і запозичено з тут

Визначення: Створюйте дрібнозернисті інтерфейси, які є специфічними для клієнта.

Я би не погодився, коли бачу, скільки різних типів кабелів існує сьогодні, і вони врешті-решт погодилися на USB-C як стандарт… оскільки це програмування, а не реальний світ, давайте зосередимося.

Історія: Швейцарський армійський ніж з зубочисткою, ножем, відкривачкою для пляшок, пилкою був дуже корисним інструментом для армії колись. Особливо в умовах дикої природи. Проте він зруйнував багато кишень на штанах через свою масу і об'єм для звичайних людей. Натомість зараз молоді люди мають відкривачку для пляшок на своїх брелоках або беруть ніж на похід.
Вони використовують спеціалізовані інструменти для конкретних цілей у відповідних контекстах.

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

class MusicPlayerInterface {  
 playMusic(song) {}  
 createPlaylist(list) {}  
 shareMusic(song) {}  
 startJamSession(playlist, participant) {}  
 streamMusicToOtherDevice(song, deviceName) {}  
}  

class Mp3Player extends MusicPlayerInterface {  
 playMusic(song) {  
 console.log(`Playing ${song} yeahh!`);  
 }  
 createPlaylist(list) {  
 console.log(`Remembering the song ${list} I want to play - can do.`);  
 }  
 shareMusic(song) {  
 throw new Error("Toooo old, can't do it");  
 }  
 startJamSession(playlist, participant) {  
 throw new Error("What is a Jam session?");  
 }  
 streamMusicToOtherDevice(song, deviceName) {  
 throw new Error("I'm tooo old for this crap.");  
 }  
}

Коректний приклад коду: Це не точна копія того, як вони побудували сервіси.
Однак це ідея, масштабована до простого прикладу.

class MusicService {  
 play(song) {  
 console.log(`Playing ${song} yeahh!`);  
 }  
}  

class PlaylistService {  
 create(list) {  
 console.log(`Remembering the song ${list} I want to play - can do.`);  
 }  
}  

class SharingService {  
 share(song) {  
 console.log(`Sharing this awesome ${song} with my friends`);  
 }  
}  

class JamService {  
 startSession(playlist, participant) {  
 console.log(`Starting a jam session with my friend ${participant} and this playlist ${playlist}`);  
 }  
}  

class StreamingService {  
 stream(song, deviceName) {  
 console.log(`Streaming ${song} to my other device ${deviceName}`);  
 }  
}  

class Mp3Player {  
 playMusic(song) {  
 MusicService.play(song);  
 }  
 createPlaylist(list) {  
 PlaylistService.create(list);  
 }  
}  

class SpotifyClient {  
 playMusic(song) {  
 MusicService.play(song);  
 }  
 createPlaylist(list) {  
 PlaylistService.create(list);  
 }  
 shareMusic(song) {  
 SharingService.share(song);  
 }  
 startJamSession(playlist, participant) {  
 JamService.startSession(playlist, participant);  
 }  
 streamMusicToOtherDevice(song, deviceName) {  
 StreamingService.stream(song, deviceName);  
 }  
}

Принцип інверсії залежностей (DIP)

pic

Цей принцип дуже цінується і запозичений звідси

Визначення: Залежіть від абстракцій, а не від конкретних реалізацій.

У реальному житті я б дійсно сподівався на зворотне 😂

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

Приклад порушення принципу: Типовий випадок для кожного розробника програмного забезпечення — це використання бази даних з користувачами. До того, як Мартін Фаулер і пізніше Spring принесли на ринок свою революцію з ін’єкцією залежностей, ось так, ймовірно, люди робили це раніше.
Недолік полягає в тому, що кожен новий тип бази даних спричинятиме зміни в класі UserService.

class Database {  
 connect() {  
 console.log("Підключення до бази даних...");  
 }  
}  

class UserService {  
 constructor() {  
 this.database = new Database(); // Прямо залежить від конкретної бази даних  
 this.database.connect();  
 }  

 getUser(id) {  
 this.database.getUser(id);  
 console.log("Отримання користувача з бази даних...");  
 }  
}

Правильний приклад коду: Щоб зробити це більш "універсальним", використовується абстрактний інтерфейс для всіх типів баз даних.
Тому дозволяється використовувати конкретні реалізації будь-якого типу бази даних без необхідності змінювати сам клас UserService.

// Оголошуємо універсальний інтерфейс для бази даних  
class IDatabase {  
 connect() {  
 throw new Error("Метод не реалізовано.");  
 }  
}  

class SQLDatabase extends IDatabase {  
 connect() {  
 console.log("Підключення до SQL бази даних...");  
 }  
}  

class MongoDatabase extends IDatabase {  
 connect() {  
 console.log("Підключення до Mongo бази даних...");  
 }  
}  

class UserService {  
 constructor(database) {  
 this.database = database; // залежить від абстракції  
 this.database.connect();  
 }  

 getUser(id) {  
 this.database.getUser(id);  
 console.log("Отримання користувача з бази даних...");  
 }  
}  

// Використання  
const sqlDatabase = new SQLDatabase();  
const userServiceSQL = new UserService(sqlDatabase);  

userServiceSQL.getUser("RandomHashThatLooksCool");  

const mongoDatabase = new MongoDatabase();  
const userServiceMongo = new UserService(mongoDatabase);  

userServiceMongo.getUser("AnotherRandomHashThatLooksCool");

І, на кінець...

Якщо ви читаєте це, ви або дісталися до кінця, або пропустили решту.
В будь-якому випадку, сподіваюся, вам сподобалося читати і ви дізналися щось нове. Я, наприклад, отримав нові знання, працюючи над цією статтею (дякую, ChatGPT), заглиблюючись у думки спільноти (дякую, Stackoverflow) і інтерпретуючи спадщину нашого дорогого дядечка Боба.

Перекладено з: SOLID Code Stories for a Six Year Old

Leave a Reply

Your email address will not be published. Required fields are marked *