ООП. Паттерн стратегії.
Фронтенд розробка JavaScript. -> ООП. Паттерн стратегії.
ООП паттерни в JavaScript.
Після появи ООП програмний світ перетворився з використанням класів та об’єктів.
Однак це в багатьох складних проектах призводило до плутанини через відсутність визнаних методик вирішення типових завдань.
Еріх Гамма вніс у це ясність і відкрив нам світ патернів, які він розділив на 3 категорії:
Шаблони, що породжують - які описують як правильно створювати об’єкти.
Поведінкові - описують взаємодії між класами та об’єктами
Структурні - дають уявлення про правильну архітектуру класів та об’єктів
Паттерн Стратегія (з розряду поведінкових).
Припустимо, створюємо додаток “Зоопарк”.
У якому маємо таку ієрархію класів.
Клас Тварини.
Клас Кіт
Клас Собака
Класи Кіт та Собака є спадкоємцями класу тварини.
Пряме використання класів у js (ES6) не підтримуватиметься старими браузерами
SyntaxError: Unexpected token class(…)
І для цього необхідно використовувати транспілятори типу babel webpack та ін. або поліфіли.
Однак використання конструкції class є зовсім не обов’язковим у js і все можна реалізувати виключно у функціональному стилі.
За допомогою об’єктів та функцій у яваскрипт ми можемо реалізувати будь-яку примху ООП.
Складемо цю програму та відобразимо тварин на сторінці.
Як створити “типу-клас” у js?
var Animal = function() {}
або так
function Animal() {}
При цьому фокус у тому, що ми вважаємо, що цю функцію викликатимуть ключове слово new.
Тому такі функції називають функції-конструктори та назви пишуть (для зручності) з великої великої літери.
Шаблон.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
</style>
<title>Zoo</title>
</head>
<body>
<h1>Zoo</h1>
<div id="root"> </div>
<script src="jquery.min.js" ></script>
<script src="app.js" ></script>
</body>
</html>
Створимо клас (функцію-конструктор) тварини.
Дані.
- Розмір (пікселі)
Опції.
-
Відображення;
-
Переміщення.
Приклад коду.
var Animal = function(){
};
Animal.prototype.move = function() {
console.log('I am mooving')
};
Animal.prototype.size = 100
Animal.prototype.show = function() {
$("#root").append(`<img src="${this.image}" width="${this.size}" />`);
};
Ми закидаємо методи в prototype для того, щоб успадковуватись і при цьому не плодити методи в пам’яті.
Собаки та коти.
Дані.
-
картинка
-
ім’я
Опції.
- Голос
Приклад функції конструктора.
var Dog = function(name,image) {
this.name = name;
this.image = image;
this.voice = function() {
console.log(`gav gav my name is ${this.name} ${this.size}`);
}
}
Dog.prototype = Animal.prototype;
Клієнтський код.
var d = new Dog('Bobik','bobik.png');
d.voice();
d.show();
Перепишемо на класах, використовуючи ES6
Дякувати Богу, він підтримується всіма сучасними браузерами.
class Animal {
size = 50;
show() {
$("#root").append(`<img src="${this.image}" width="${this.size}" />`);
};
move() {
console.log('I am mooving');
};
}
class Dog extends Animal {
constructor(name,image) {
super();
this.name = name;
this.image = image;
}
voice() {
console.log(`gav gav my name is ${this.name} ${this.size}`);
}
}
var d = new Dog('Bobik','bobik.png');
d.show();
Добавим кнопки и оформим все на странице.
class Animal {
size = 150;
show() {
const tpl = `
<div>
<img src="${this.image}" width="${this.size}" />
<p>
<button id="v-button-${this.name}">Voice</button>
<button id="m-button-${this.name}">Move</button>
</p>
</div>
`
$("#root").append(tpl);
$('#m-button-'+this.name).on('click',this.move);
$('#v-button-'+this.name).on('click',()=> {this.voice()});
};
move() {
console.log('I am mooving');
};
}
class Dog extends Animal {
constructor(name,image) {
super();
this.name = name;
this.image = image;
}
voice() {
console.log(`gav gav my name is ${this.name}`);
}
}
var d = new Dog('Bobik','bobik.png');
d.show();
З неочевидного відзначимо:
id=”v-button-${this.name} - ми присвоюємо унікальний ідентифікатор кнопкам для підв’язки колбеків. Поки вважаємо, що ім’я звірів буде унікальним.
$(…).on(‘click’,()=> {this.voice()}) - тут ми обв’язали колбек стрілочною функцією щоб не втратити контекст this т.к. якщо цього не зробити, в ньому виявиться елемент кнопки, і ми не зможемо звернутися до this.name всередині методу voice.
Створюємо клас для котів.
class Cat extends Animal {
constructor(name,image) {
super();
this.name = name;
this.image = image;
}
voice() {
console.log(this);
console.log(`miau miau my name is ${this.name}`);
}
}
Використовуємо класи у клієнтському коді.
var dogObj = new Dog('Bobik','bobik.png');
dogObj.show();
var catObj = new Cat('Murka','murka.png');
catObj.show();
Результат.
Нове завдання від замовника.
Потрібно додати нову функцію стрибка для всіх тварин.
Рішення “В лоб”.
Додаємо метод до базового класу Animal c кнопкою.
class Animal {
size = 150;
show() {
const tpl = `
...
<button id="j-button-${this.name}">Jump</button>
...
`
...
$('#j-button-'+this.name).on('click',()=> {this.jump()});
};
...
jump() {
console.log('I am jumping!!!');
};
}
У результаті всіх дочірніх до Animal об’єктах класів Dog і Cat з’явилася нова кнопка.
Не дивно т.к. ці класи успадковують (розширюють) клас Animal ключовим словом extends.
Проблема.
Як виявилося, у нашому зоопарку є ряд тварин, які виготовлені з дерева і є опудалом і відповідно не можуть стрибати.
class WoodenCat extends Animal {
constructor(name,image) {
super();
this.name = name;
this.image = image;
}
voice() {
console.log(`I am a wooden cat miau miau my name is ${this.name}`);
}
}
var catMaket = new WoodenCat('WoodMurka','catwood.png');
catMaket.show();
При вирішенні з методом у базовому класі у нас стрибають усі.
Перший варіант рішення.
Перевизначати метод у дочірніх класах.
class WoodenCat extends Animal {
....
jump() {
console.log(`I can not jump!`);
}
}
Мета досягнута.
Однак такий підхід має недоліки.
При додаванні нових класів необхідно в кожному перевизначати цей метод і дублювати його.
Але найстрашніше те, що якщо логіка цього методу буде змінюватися, то нам доведеться змінювати його за всіма класами.
Для того щоб цього уникнути, цю функцію виносять в окремий клас і вставляють його екземпляр (об’єкт) усередину класу тварини. При цьому у відповідному методі jump базового класу Animal викликається метод внутрішнього об’єкта.
Цей прийом називається поліморфізму.
Класи, що визначають здатність стрибати.
class CanJump {
jump() {
console.log(`Jump Jump!`);
}
}
class CanNotJump {
jump() {
console.log(`I can not jump!`);
}
}
Зміни базового класу Animal.
class Animal {
...
constructor(canJumpObj){
this.canJump = canJumpObj;
}
...
jump() {
this.canJump.jump();
};
}
“Протягуємо” об’єкти класів CanJump та CanNotJump всередину конструктора базового класу Animal
class Dog extends Animal {
constructor(name,image,canJumpObj) {
super(canJumpObj);
...
}
...
class Cat extends Animal {
constructor(name,image,canJumpObj) {
super(canJumpObj);
...
}
...
class WoodenCat extends Animal {
constructor(name,image,canJumpObj) {
super(canJumpObj);
...
}
...
Таким чином, ми зосередили логіку стрибків в одному місці, інкапсулюючи її.
Також ми забезпечили поліморфність об’єктів класів нащадків і тепер можемо змінювати їхню поведінку на льоту всередині клієнтського коду.
Ось так можна дати можливість стрибати тому, хто її не мав.
var catMaket = new WoodenCat('WoodMurka','catwood.png',new CanNotJump());
catMaket.canJump = new CanJump();
catMaket.show();
Цей підхід (патерн) носить назву Стратегія і звучить так:
Стратегія (англ. Strategy) - поведінковий шаблон проектування, призначений для визначення сімейства алгоритмів, інкапсуляції кожного з них та забезпечення їх взаємозамінності. Це дозволяє вибирати алгоритм шляхом визначення відповідного класу. Шаблон Strategy дозволяє змінювати обраний алгоритм незалежно від об’єктів-клієнтів, що його використовують.