Якого розміру мають бути класи/об’єкти?

Часто чую про те, що усе повинно бути маленьким і деякий час сам керувався таким евристичним правилом. В Ruby спільноті, наприклад існувало (чи ще існує, давно не пишу на Ruby) правило, що класи мають бути не більше 100 строк, а методи – не більше п’яти.

Така гранулярність, що аргументована дурними правилами призводить лише до підвищення складності коду. Щось інше має керувати тим, якого розміру мають бути класи/об’єкти та методи.

Загальні відомості про ООП

Перш за все необхідно розуміти навіщо необхідні об’єкти! Більшість розробників цього не розуміють! Об’єкти – це один із підходів до вирішення проблем великого стану і високої сплутаності (high coupling) та для організації коду згідно принципу high cohesion – високої згуртованості, згідно якому, те, що використовується/змінюється разом – має знаходитися разом. Об’єкти інкапсулють одну або декілька пов’язаних змінних, які у контексті об’єкта називаються властивостями. Об’єкти забезпечують

knight in armour
Об’єкти, як і сталеві лати, мають захищати те, що міститься всередині.

В імперативному програмуванні до цих проблеми підходять через розбиття коду на відносно незалежні модулі. У функціональному – намагаються відмовитися від стану. В об’єктно-оріентованому стан інкапсулюють у об’єкті та надають інтерфейс для безпечної зміни стану. При цьому стан – це деталь реалізації, а об’єкт – це темна скринька, як і, наприклад, функція. Аби підкреслити це – був сформульований принцип TDA (Tell, Don’t Ask – Наказуй, а не запитуй), згідно якому, об’єкт має лише отримувати команди та не надавати інформації про свій внутрішній стан.

Більше про ООП можна дізнатися з моїх публікацій:

Приклад

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

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

У найпростішому варіанті ми порушуємо TDA, тому що користувач має доступ до його властивостей. Щодо показників лічільника – ми можемо легко копіювати його значення та надавати користувачу і в такий спосіб гарантувати, що користувач не зможе самостійно змінити лічильник. Також користувач повинен мати можливість безпечно користуватися власне самою колекцією – для цього кожного разу, коли користувач потребує доступу до колекції елементів – нам необхідно копіювати колекцію, що не ефективно.

Наступний крок – це усунення необхідності копіювання колекції. Ми насправді ніколи не користуємося одномоментно цілою колекцією. Нас цікавлять поодинокі її елементи. Таким чином ми можемо додати методи – ітератори, які дозволяють уникнути необхідності копіювати цілу колекцію: #each, #map, #filter, #all?, #any?, #selec t та інші, але насправді достатньо лише #each, на основі якого може бути реалізована функціональність усіх інших. Метод #each (чи будь який інший із зазначених вище ітераторів) також дозволяє взагалі уникнути звернення до лічильника елементів у колекції, оскільки #each сам перевіряє чи не було досягнуто кінця колекції. Таким чином маємо наступний інтерфейс:

  • #add(elem 't)
  • #delete(idx int)
  • #each((elem 't, idx int) -> ()) // () використовуємо як літерал типу unit, Тип unit має одне едине значення – unit, яке записується як ()

Реалізація зазначена вище не є безпечною для конкурентного програмування. Наш наступний крок – це зробити інтерфейс, безпечний для конкурентного програмування. Небезпечними операціями є ті, що змінюють внутрішній стан, тобто #add та #delete. Аби зробити їх безпечними, вони мають виконувати свою роботу не безпосередньо в своєму коді, а додавати відповідні завдання у чергу, елементи якої обробляються один за одним всередині об’єкта.

Простий приклад реалізації об’єкта на мові Golang можна знайти у моїй публікації Що я розумію під об’єктною орієнтованістю.

То ж якого розміру має бути клас/об’єкт?

Розмір класу/об’єкту залежить від того як багато потенційно небезпечних операцій над змінними ми маємо в не ОО коді, які бажаємо зробити безпечними через відповідний ОО код. Якщо така операція лише одна – клас/об’єкт можуть містити один-два десятки строк. Якщо таких операцій багато – клас/об’єкт можуть містити тисячу строк і навіть більше. Але є одне але!

Чи всі (припустимо) декілька десятків операцій є достатньо універсальними, чи, можливо, деякі із них дуже специфічні і мають дуже обмежене використання? Якщо контекст використання тієї чи іншої операції досить обмежений – необхідно створювати нові класи/об’єкти, які можуть значно перетинатися функціональністю із вже існуючими. Це необхідно для того, аби тримати специфічні операції близько до контекстів їх використання та не переускладнювати глобальний контекст методами/сигнатурами занадто специфічними та такими, що в якийсь момент можуть стати мертвим кодом.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *