ООП. Паттерн стратегії.

Фронтенд розробка 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();

Результат.

start page

Нове завдання від замовника.

Потрібно додати нову функцію стрибка для всіх тварин.

Рішення “В лоб”.

Додаємо метод до базового класу 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();

start page

При вирішенні з методом у базовому класі у нас стрибають усі.

Перший варіант рішення.

Перевизначати метод у дочірніх класах.

class WoodenCat extends Animal {

    ....

    jump() {
        console.log(`I can not jump!`);
    }
}

Мета досягнута.

start page

Однак такий підхід має недоліки.

При додаванні нових класів необхідно в кожному перевизначати цей метод і дублювати його.

Але найстрашніше те, що якщо логіка цього методу буде змінюватися, то нам доведеться змінювати його за всіма класами.

Для того щоб цього уникнути, цю функцію виносять в окремий клас і вставляють його екземпляр (об’єкт) усередину класу тварини. При цьому у відповідному методі 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 дозволяє змінювати обраний алгоритм незалежно від об’єктів-клієнтів, що його використовують.

UML діаграма

start page