Галоўная / Артыкулы / Розумэнне прынцыпаў SOLID праз практычныя прыклады коду

Розумэнне прынцыпаў SOLID праз практычныя прыклады коду

Этыя інструкцыі раз’ясняюць усе пяць прынцыпаў SOLID за дапамою конкрэтных прыкладаў коду, паказваючы, як яны прыменяюцца ў рэальных проектах і застосунках на React.

1903 слоў

Калі вы ствараеце програмнае забезпечэнне, правільная робота коду — гэта толькі палова задача.

Калі прыложэнне пачинае растаць, яго код часта становіцца важкім для чытання, змены і падтрымання правільной роботы. Незначныя змены ў аднай частцы системы можу таямна нашкодзіць чамусь несвязанам.

Гэта самэй той тип проблем, якія прагледзеліся ў прынцыпах SOLID.

SOLID — група пяці прынцыпаў проектавання з об’ектно-оріентаванага програмавання, якія спрыяюць стварэнню коду, які:

  • Лёгкі для падтрымання
  • Лёгкі для тэставання
  • Лёгкі для расширэння
  • Мае меншы ступень взаімазв’язку
  • Лёгкі для розумення камандай

Абревіатура складаецца з:

S — Прынцып адной адпаведнасці

O — Прынцып адкрытасці/закрытасці

L — Прынцып замены Ліскова

I — Прынцып сепарацыі інтэрфейса

D — Прынцып інверсіі залежнасцяў

Разберамо кожны з іх на простых прыкладах.

1. S — Прынцып адной адпаведнасці

«Клас должен маты толькі адну прычыну для змены».

Проста кажучы, кожны клас чы рэжым должен быць створаны наваколо адной задачы.

Разглядзім клас User, які адпаведнае за:

  • Зберагчы данні корыстніка
  • Взаімадзею з базай дадзеных
  • Адправку электранаў
  • Стварэнне атласоў

Гэта занадто многа для адного класу.

class User {
  createUser() {
    // create user
  }
saveToDatabase() {
    // save user
  }
  sendEmail() {
    // send email
  }
  generateReport() {
    // generate report
  }
}

Якщо трэба зменіць логіку адправкі электранаў, вам даводзіцца правяць клас User.

Якщо змінюецца обробка базай дадзеных, вам знову даводзіцца працаваць з тым жа класам.

Болей чысты падход — раздзеліць гэтыя задачы.

class User {
  createUser() {
    // create user
  }
}
class UserRepository {
  saveToDatabase() {
    // database logic
  }
}
class EmailService {
  sendEmail() {
    // email logic
  }
}
class ReportService {
  generateReport() {
    // report logic
  }
}

За дапамою такага падзелу кожны клас тепер адпрацоўвае толькі адну задачу.

Чаму гэта корыстна?

Калі змянюецца якая-небудзь вакалідатыва, вы адразу знаеце, яку частку коду трэба зменіць.

Адна задача на клас означае адну прычыну для яго змены.

2. O — Прынцып адкрытасі/закрытасі

«Програмныя ентытэты должны быць адкрытымі для расшырэння, але закрытымі для змены».

Формулаванне звучыць абстрактна, але сама ідея — ні.

Мета — адагульнаваць новыя функцыяналы без паўтарачога перапісвання вучальнага коду.

Возьмім прыклад обробкі платэжаў:

function processPayment(type, amount) {
  if (type === "card") {
    // card payment
  } else if (type === "upi") {
    // UPI payment
  } else if (type === "paypal") {
    // PayPal payment
  }
}

Тепер заўважкім, што вам трэба падтрымаць:

  • Stripe
  • Razorpay
  • Apple Pay
  • Google Pay

Кожны новы варыянт робіць гэту функцыю большай і хаотычней.

Лепшая стратэгія — це стварыць адзін клас для кожнага спосабу платежа.

class CardPayment {
  pay(amount) {
    console.log(`Card payment: ${amount}`);
  }
}
class UpiPayment {
  pay(amount) {
    console.log(`UPI payment: ${amount}`);
  }
}
class PaypalPayment {
  pay(amount) {
    console.log(`PayPal payment: ${amount}`);
  }
}

Калі такая структура ўжо існуе, дадзенне новага варыянта платежа не вымагае змены класоў, якія ўжо напісаны.

class StripePayment {
  pay(amount) {
    console.log(`Stripe payment: ${amount}`);
  }
}

Первісныя реалізацыі застаюцца такімі ж, якімі былі.

Ідея

Развіваюце систему, дадзяўшы новы код, а не пастойчасна рэдагуючы вярны код.

3. L — Прынцып замены Ліскова

«Падтыпы должны быць заменяймыя на свае базовыя тыпы».

У сутнасці гэты прынцып стаўіць:

Якщо B ёсць падтыпам A, то трэба, каб было можна заменіць B там, дзе вжываецца A, і програма продовжвала правільна працаваць.

Класычны прыклад — птахі.

Сказаўмо, вы вядомы:

class Bird {
  fly() {
    console.log("Flying");
  }
}

Потым яго расшырваюце:

class Sparrow extends Bird {
  fly() {
    console.log("Sparrow is flying");
  }
}

Пакіям, усё гаразд.

Але што будзе з пінгвіном?

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins cannot fly");
  }
}

Это адкрывае недагодзіну ў дыяграме прыемаў.

Якщо іншыя часткі коду прыпускаюць, што кожны Bird можа летаць, то выдача ей экзампляра Penguin спануліць гэтыя прыпускі.

Болей разумны дыяграма прыемаў выделяе поведзінку летання ў околачыне сама по сабе.

class Bird {
  eat() {
    console.log("Eating");
  }
}
class FlyingBird extends Bird {
  fly() {
    console.log("Flying");
  }
}
class Sparrow extends FlyingBird {}
class Penguin extends Bird {}

Такім чынам, пінгвіны больш не змушаны падтрымваць поведзінку, якая не стосуецца іх.

Урок

Хадзіце за мету не ствараць іерархіяў спадчынасці, якія не маюць логічнага сэнсу.

Падклас павінен праверна працаваць там, дзе апэктываўцы спадчынной класа чакаюць, што ён працаваць будзе.

4. I — Прынцып сегрэгаціі інтарфейсаў

“Кліенты не павінны быць змушаныя завісіць ад методаў, якія яны не выкарыстоўваюць.”

Уявіце сабе інтарфейс, створаны так:

print()
scan()
fax()
copy()

Уявіце сабэйскі прынтар, які можа толькі, ну, друкуваць.

Чаму ж адзін такі прынтар павінен рэалізаваць таксама функцыі scan(), fax() і copy()?

Для цього няма жадных важлівых прычын.

Кращы падход — раздзеліць інтэрфейс згідна з тым, каму насправды служыць кожная з можнасцей.

Напрыклад:

class Printer {
  print() {
    console.log("Printing...");
  }
}
class Scanner {
  scan() {
    console.log("Scanning...");
  }
}
class FaxMachine {
  fax() {
    console.log("Faxing...");
  }
}

Просты прынтар павінен рэалізаваць толькі тые функцыі, якія ён дзейсна падтрымлеў.

У сучасным JavaScript

JavaScript не мае формальных інтэрфейсоў, як Java чы C#, але основная ідея застаецца тая ж.

Яго можна практыкаваць за дапамою:

  • Маленькіх модуляў
  • Маленьких API
  • Композіцыі
  • Адзельных служб
  • Сфармаваных компонентаў React

У працоўнасці замест таго, каб аб’еднаваць усё ў адную вялікую службу:

userService.getUser();
userService.createUser();
userService.deleteUser();
userService.sendEmail();
userService.generateReport();

Раздзеліце адпавядачы:

userService.getUser();
userService.createUser();
emailService.sendEmail();
reportService.generateReport();

Урок

Ніколі не прымушвайце компонент, клас чы модуль павяроўвацца на функцыонал, які ёму не патрэбен.

5. D — Прынцып інверсіі залежнасцяў

«Модулі высокага рэвэля не должны павяроўвацца безпосередна на модулі низкага рэвэля. Оба должны павяроўвацца на абстракцыі.»

Этот прынцып існуе, каб зменшыць ступень звязку.

Возьміце гэты прыклад:

class MongoDB {
  save(data) {
    console.log("Saving to MongoDB");
  }
}
class UserService {
  constructor() {
    this.database = new MongoDB();
  }
  saveUser(user) {
    this.database.save(user);
  }
}

Проблема тут у тым, што UserService павяроўваецца безпосередна на MongoDB.

Параліч на PostgreSQL пазней значыць, што трэба будзе зноў змяніць сам UserService.

Лепшы падход — ін’ектавацыя залежнасці.

class UserService {
  constructor(database) {
    this.database = database;
  }
saveUser(user) {
    this.database.save(user);
  }
}

Тепер можна ляйво перадаваць разныя рэалізацыі баз дадзенаў.

const mongoDB = new MongoDB();
const userService = new UserService(mongoDB);

А пазней замена іх ёсць простая:

const postgresDB = new PostgreSQL();
const userService = new UserService(postgresDB);

UserService ніколи не патрэбуе ведаць, яка база дадзеных знаходзіцца за ўсім гэтым.

Чаму гэта корыстна?

Гэта дае вам код, які:

  • Лёгчы для тэставання
  • Лёгчы для замены
  • Менш тыснай сувязаны
  • Лёгчы для падтрымкі з часам

SOLID у рэальным проекте

Следаванне прынцыпам SOLID не значыць стварэння спецыяльных класоў для кожнай дробічкі.

Гэта разлік мае вельмі важлівое значэнне.

SOLID — гэта прыменэнне правільных дизайнерскіх рашэнняў, а не накуплэнне абстракцый проста так.

У дапрыклад, у застосоўцы React гэтыя ідэі сама сабой праявляюцца, калі вы аддзеляеце:

Components
    ↓
Hooks
    ↓
Services
    ↓
API Layer
    ↓
Database

Заведамае завадзіце компонента — гэта пераважна керуванне UI.

Спецыяльны хук можа выконваць логіку стану, якая можа быть перызначаная.

Сэрвіс API можа кераваць комунікацыяй через HTTP.

Бэкенд адпаведае за бізнес-логіку.

Слой базы дадзэных адрабатвае з паставаньем інфармацыі.

Такое раздзелэнне элементаў дапамагае падтрымваць прыемнае кераванне аплікацыяй па мере яе росту.

SOLID і React

Хоць прынцыпы SOLID выйшлі з об’ектно-оріўнтаванага дызайна, калькі з іх ідэй чыста прыменяюцца ў роботе з React.

Адна адпаведальнасць

У замест на стварэнне адного вялікага компонента:

Dashboard.jsx

які намагаецца робіць усё, раздзеліце яго на:

Dashboard
UserProfile
Statistics
RecentOrders
Notifications

Кожны з эых элементаў тады мае набліжна яснейшую задачу.

Адкрыты/Закрыты

Ствараюце компоненты, якія можна перадварабатваць, і якія атрымоўваюць новыя можлівасці через props, у замест на таго, каб ўсё іх внутранне было перапісваўце з кожным разам, калі патрэбна новая версія.

<Button variant="primary">
  Save
</Button>
<Button variant="danger">
  Delete
</Button>

Інверсія залежнасцей

У замест на безпосереднее прыўязванне компонента да адного конкрэтнага спосабу запрашання дадзэных, захавайце логіку API ў сервісе або хуке.

const users = await userService.getUsers();

Сам компонент не павінен ведаць, як фактычна запытка выканана пад спадом.

Чаму важна прынцыпа SOLID

Рэальная практычна корыста ад прынцыпа SOLID мала стосунку да таго, каб код выглядаў элегантна.

Йдзе пра тое, каб будучыя змены былі менш болючымі.

Уявіце проект, які включае:

10 разработчыкаў → 100 функцый → тыясячы файлоў → сталязныя змены

Без ціялесапраўедзенага дызайну адна маленькая патрэба можа спрычыніць каскад некалязаных бягучых проблем.

З чыстым раздзеленнем і слабкай зв’язанасцю змены стаюць набліжэй прыкладнымі.

Прынцыпа SOLID можу памагчы вам:

  • Зменшыць дуплікацыю коду
  • Зменшыць жорсткую зв’язанасць
  • Паўнэчыць тэставанне
  • Удзельніць функцыйях лёгкае расшырэнне
  • Апрыясніць адлучэнне бягучых проблем
  • Паўнэчыць саавершання ў команде
  • Зберагчы вялікія прыкладненнія ў стане для адтрымання

SOLID не значыць чрэзмерную інжыніяроўку

Гэта можа быць найважнейшым вывадам.

Не прыменяйце SOLID як жорсткі список пунктав.

Возьміце простую функцыю, напрыклад:

function add(a, b) {
  return a + b;
}

Яйной не патрэбны пяць класоў, тры інтарфейсы і контэйнер для впрыску залежнасцяў.

Мета ніколі не была зробіць код болей сложным.

Мета — зробіць прыгожа сложны код болей зручным для працы.

Вжывайце SOLID тады, калі сложнасць системы даследжвае додатковую структуру.

Короткая апূহа

S — Адна адпаведальнасць: клас чы ўстановка павінны выкананаць адную галоўную адпаведальнасць.

O — Ачыты/Закрыты: расшырюйце працэздатнасць замест таго, каб парадныя рэдагаваць код, які вже працуе.

L — Адаптаванне па прынцыпе Ліскова: падкласы должны правільна працаваць там, дзе прагчыцацца адпрацоўкі класа-родзіка.

I — Раздзеленне інтэрфейса: не трэба, каб кліенты завіселі ад функцыйнальнасці, якой ў іх няма патрэбы.

D — Інверсія залежнасцей: трэба завісіцца на абстракцыях, а не на конкрэтных реалізаціях.

Заключныя меркі

SOLID ніколі не быў задуманы як пяць правілаў, якія трэба запамятаваць для спраўы на адбор.

Это спосаб размышлення пра проектаванне програмнага забезпечэння.

Працуячы над кодам, корисна зупініцца і запытаць ся:

Чы не прагчыцае гэты модуль рабіць занадта многа роўначасна?

Чы дадаўшы новую функцыю не будзе прымусова патрэбна перапісваць існуючы код?

Чы тут не ствараюцца непатрэбныя залежнасці?

Чы можна тэставаць гэты код без працоў?

Чы гэтае што-небудзь прымушана падтрымляць такую працэздатнасц, якой ёй на самай працы не трэба?

Размышленні над гэтыми пытаннямі зазвычай маюць большое значэнне, чым здатнасц запамятаваць, што значыць кожная літера SOLID.

Хорашыя програмы — это не проста тые, якія зараз працуюць.

Хорашыя програмы — это тые, якія паступова і гармонійна зменшуюцца, а не ператвараюцца на кошмар.

Спадні матэрыялы

  • Чаму абстракцыі фронтэнду таямуча ператвараюцца ў тэхнічны борг — Дазвольце дазнацца, чаму прытэплыя абстракцыі фронтэнду ствараюць скрытую сложнасць, і як выявіць, калі спакульнаныя компоненты, хукі чыста інструменты дэйсна вартуюць стварэння.
  • Шэсьць шаблонаў дизайну JavaScript для падборткі спагетты-кода — У гэтым артыкуле расказваецца пра шэсьць практычных шаблонаў JavaScript — Strategy, Factory, Observer, Adapter, Composition і Pipeline — якія заменяюць заплутаны код на структуру, якая лёгка ў адтрыманні.