Оригінальна публікація: The Four Pillars of Object-Oriented Programming

JavaScript є мовою, що підтримує багато парадигм програмування. Парадигмою програмування називають набір правил, згідно з якими ви пишете код, щоб було легше вирішити певну проблему.

Якраз цим є чотири опори — принципами програмного забезпечення, які допомагають писати чистий об’єктно-орієнтований код.

Цими принципами є:

  • Абстракція
  • Інкапсуляція
  • Успадкування
  • Поліморфізм

Давайте детальніше розглянемо кожен з них.

Абстракція в об’єктно-орієнтованому програмуванні

Щось «абстрагувати» означає приховати деталі імплементації всередині певної обгортки — іноді прототипа, іноді функції. ­Як наслідок, при виклику функції вам не треба розуміти, як саме вона працює.

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

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

function hitAPI(type){
	if (type instanceof InitialLoad) {
		// приклад імплементації
	} else if (type instanceof NavBar) {
		// приклад імплементації
	} else {
		// приклад імплементації
	}
}
Цей код зовсім не використовує принцип абстракції.

Помітили, як у цьому прикладі довелося імплементувати все те, що потрібно для кожного сценарію використання?

Для кожного API, до якого вам потрібно звернутися, необхідно створювати новий блок if зі своєю власною імплементацією. Такий код не є абстрагованим, оскільки вам необхідно турбуватися про імплементацію кожного нового типу, який ви будете додавати. Такий код непридатний до повторного використання і його дуже складно підтримувати.

А що стосується прикладу знизу?

hitApi('www.kealanparr.com', HTTPMethod.Get)

Ви можете просто передати URL та метод HTTP як аргументи до цієї функції і все.

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

В цьому і полягає суть абстракції: в пошуку схожих частин коду та їх обгортанні в загальну функцію або об’єкт для використання в багатьох місцях/вирішення багатьох проблем.

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

Як створити кавову машину з абстракцією

  • Створіть кнопку з заголовком «Зробити каву»

Як створити кавову машину без абстракції

  • Створіть кнопку з заголовком «Закип’ятити воду»
  • Створіть кнопку з заголовком «Налити холодну воду в чайник»
  • Створіть кнопку з заголовком «Насипати одну ложку кави в кружку»
  • Створіть кнопку з заголовком «Помити брудні кружки»
  • І багато інших кнопок

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

Наступна опора об’єктно-орієнтованого програмування покаже нам один зі способів, завдяки якому ми можемо досягнути абстракції, а саме за допомогою інкапсуляції.

Інкапсуляція в об’єктно-орієнтованому програмуванні

Визначенням інкапсуляції є «обмеження чогось у певних рамках, ніби у капсулі». По суті, інкапсуляція — це відбирання доступу до певних частин коду та їх приватизація (інкапсуляцію також часто називають приховуванням інформації).

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

Обмежуйте доступ до частин свого коду. Робіть їх менш відкритими, якщо в них немає першочергової необхідності.

Приватні властивості в JavaScript можливі завдяки замиканням. Ось приклад нижче:

var Dog = (function () {

	// приватна
	var play = function () {
		// імплементація play
	};
    
	// приватна
	var breed = "Dalmatian"
    
	// публічна
	var name = "Rex";

	// публічна
	var makeNoise = function () {
 		return 'Bark bark!';
	};

 	return {
		makeNoise: makeNoise,
		name: name
 	};
})();

Спочатку ми створили функцію, яка одразу викликається (її називають виразом негайно викликаної функції, скорочено IIFE). Це створило об’єкт, до якого будь-хто має доступ, але разом з тим приховали деякі деталі. Ви не зможете викликати метод play та властивість breed, оскільки ми не зробили їх публічними в об’єкті, який повертає функція.

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

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

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

Чому варто надавати перевагу приватності? Чому не можна просто зробити все глобальним?

  • Багато непов’язаних між собою за змістом частин стануть залежними/згрупованими через глобальну змінну.
  • Скоріше за все, ви зміните значення змінних, якщо їхні імена будуть використовуватися декілька разів, що може привести до багів або непередбачуваної поведінки.
  • Ймовірно, ви створите багато спагеті (коду, який тяжко зрозуміти та в якому тяжко побачити, що зчитує та переписує змінні й змінює стан).

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

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

Успадкування в об’єктно-орієнтованому програмуванні

Успадкування дозволяє одному об’єкту отримувати властивості та методи від іншого. У JavaScript це реалізовано у вигляді прототипного успадкування.

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

Коли б нами не використовувалось успадкування, ми намагаємося зробити все так, щоб батьківський об’єкт та об’єкт-нащадок мали високу згуртованість. Згуртованість — це змістовий зв’язок у коді. Наприклад, чи походить тип Bird від DieselEngine?

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

При успадкуванні вам знадобиться більша частина наслідуваного функціоналу (вам не потрібно завжди наслідувати все).

Серед розробників відомий принцип підстановки Лісков. Цей принцип стверджує, що якщо ви можете використати батьківський клас (наприклад, ParentType) будь-де, де ви б використали спадкоємця (наприклад, ChildType) і ChildType успадковує від ParentType — тоді ви пройдете тест.

Головною причиною, з якої ви можете не пройти тест, є видалення об’єктом ChildType методів, які він успадкував від батьківського об’єкта. Це призвело б до TypeError, де методи чи властивості не визначені або є не тим, що ви очікували.

image-146
Виглядає так, ніби стрілки вказують у хибному напрямку. Але «Animal» є основою, тобто батьківським об’єктом.

Ланцюг успадкування — це термін, який використовується для описання потоку успадкування від прототипу об’єкта-основи (того, від якого успадковують всі інші об’єкти) до «кінця» ланцюга успадкування (останній об’єкт, що наслідує; в нашому випадку «Dog»).

Намагайтеся тримати свої ланцюги успадкування чистими та змістовними. Можна дуже легко почати писати антипаттерни під час успадкування (наприклад, антипаттерн крихкої основи). Таке стається, коли ваші прототипи-основи вважаються «крихкими», тому що ви робите «безпечну» зміну до об’єкта-основи, через яку ламаються всі спадкоємці.

Поліморфізм в об’єктно-орієнтованому програмуванні

Поліморфізм — це «можливість відбуватися в декількох різних формах». Саме за це і відповідає четверта опора об’єктно-орієнтованого програмування: за типи в межах окремих ланцюгів успадкування, які можуть робити різні речі.

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

Згідно останнього малюнку, у нас може бути базовий прототип під назвою Animal, який визначає метод makeNoise. Кожен тип, що наслідує від цього прототипу, може переписати makeNoise таким чином, щоб він виконував власну функцію. Наприклад:

// Cтворимо приклади Dog та Animal
function Animal(){}
function Dog(){}

Animal.prototype.makeNoise = function(){
	console.log("Base noise");
};

// Більшість наших Animal мають 4. При потребі це можна змінити
Animal.prototype.legs = 4;

Dog.prototype = new Animal();

Dog.prototype.makeNoise = function(){
	console.log("Woof woof");  
};

var animal = new Animal();
var dog = new Dog();

animal.makeNoise(); // Base noise
dog.makeNoise();    // Woof woof — наслідок переписування
dog.legs;           // 4! — це було успадковано

Dog походить від Animal і може використовувати властивість legs за замовчуванням. Також він може мати власну імплементацію методу, визначеного в Animal, щоб мати власний звук.

Справжня сила поліморфізму полягає у спільності поведінки та можливості її зміни при потребі.

Висновки

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

Якщо вам сподобалася ця стаття, можете підписатися на твіттер автора, де він ділиться власними думками та новими статтями.