Шаблоны проектирования в JavaScript

Feb 5, 2019

Основы при создании паттернов

Паттерн проектирования - часто встречающиеся решения определенных проблем возникающие при разработке ПО.

Основные темины

Композиция - разложение программы на более мелкие части, пригодные для повторного использования. В контексте ООП JavaScript достигает композиции возможностью экземплярам класса быть значениями атрибутов других объектов.

Полиморфизм является одним из принципов объектно-ориентированного программирования (ООП). Это практика проектирования объектов, где реализовано совместное использование поведения и возможность переопределять общее поведения на специфичное. Обычно полиморфизм реализуется при помощи наследования.

Плюса от паттернов:

  • Быстрее решает проблемы
  • Чистый код (так как решения стандартизированы)
  • Эффективное общение между программистами

Структура паттерна:

  • Название
  • Проблема
  • Решение
  • Последствия

Классификация паттернов

  1. Шаблоны создания
  2. Структурные паттерны - построение связей между объектами.
  3. Поведенческие паттерны - поведение - общение объектов между собой.

Все паттерны в той или иной степени решают проблему зависимостей в приложении.

Шаблоны создания

Шаблоны создания - создание объектов без внесения в программу лишних зависимостей.

Одиночка (Singleton)

Одиночка - позволяет создать только один экземпляр с глобальной точкой доступа к этому экземпляру.

class Singleton {
    constructor() {
        // the class constructor
        if(!Singleton.instance) {
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    publicMethod() {
        console.log('Public Method');
    }
}

const instance = new Singleton();

// prevents new properties from being added to the object
Object.freeze(instance);

export default instance;

Фабрика

Фабричная функция — это функция, которая принимает несколько аргументов и возвращает новый объект, состоящий из этих аргументов. В JavaScript любая функция может возвращать объект. Если она делает это без ключевого слова new, то её можно назвать фабричной.

Фабрика - отделяет клиента от конкретных классов, экземпляры которых он хочет получить.

Фабрика позволяет создавать объекты не напрямую (через ключевое слово new), а через вызов методы у какого-то другого объекта. То есть вызов через new происходит, только это делается 'под капотом'. Фабрика используется для того чтобы избавить пользователя от зависимостей от класса/классов. Плюс файбрика используется в том случае, когда клиент не знает какой класс будет использоваться.

То есть мы выносим логику по созданию объектов в конкретный модуль.

class Car {
    constructor(doors = 4, state = "brand new", color = "silver") {
        this.doors = doors
        this.state = state
        this.color = color
    }
}

class Truck {
    constructor(state = "used", wheelSize = "large", color = "blue") {
        this.state = state
        this.wheelSize = wheelSize
        this.color = color
    }
}

class VehicleFactory {
    vehicleClass = Car
    createVehicle = (type, props) => {
        switch(type) {
            case "car":
                return new this.vehicleClass(props.doors, props.state, props.color)
            case "truck":
                return new this.vehicleClass(props.state, props.wheelSize, props.color)
        }
    }
}

// Let's build a vehicle factory!

const factory = new VehicleFactory()
const car = factory.createVehicle( "car", {
    doors: 6,
    color: "green"
})

console.log(JSON.stringify(car))

const truck = factory.createVehicle( "truck", {
    state: "like new",
    color: "red",
    wheelSize: "small"
})

console.log(JSON.stringify(truck))

// Let's build a truck factory!

class TruckFactory extends VehicleFactory {
    vehicleClass = Truck
}

const truckFactory = new TruckFactory()
const bigTruck = truckFactory.createVehicle( "truck", {
    state: "omg ... so bad",
    color: "pink",
    wheelSize: "so BIG"
})

console.log(JSON.stringify(bigTruck))

В этом примере можно изначально в классе VehicleFactory вместо вызова this.vehicleClass вызывать непосредственно нужный класс.

Строитель (Builder)

Строитель - позволяет создавать сложные объекты пошагово. Строитель позволяет использовать один код для получения разных объектов.

Зачем использовать - отделить сложную логику создания от финального представления.

class ProductBuilder {
    constructor() {
        this.name = 'A Product';
        this.price = 9.99;
        this.category = 'other';
    }

    withName(name) {
        this.name = name;
        return this;
    }

    withPrice(price) {
        this.price = price;
        return this;
    }

    withCategory(category) {
        this.category = category;
        return this;
    }

    build() {
        return {
            name: this.name,
            price: this.price,
            category: this.category,
        }
    }
}

console.log(
  new ProductBuilder()
    .withName('Harry Potter')
    .withCategory('book')
    .build()
)
// =>
//    {
//      name: 'Harry Potter',
//      price: 9.99,
//      category: 'book'
//    }

Обратите внимание, что при создании экземпляра мы не используем сложный вызов в конструкторе. Это нерационально, если, например, у вас объект имеет под 100 свойство.

Если объект простой, то шаблон строитель усложнит код.

В чем отличие строителя от фабрики: шаблон строитель позволяет конфигурировать объект, фабрика же создает объект за один шаг. То есть фабрика, как правило, не конфигурирует объект при его создании.

Источник: An Exploration of JavaScript Builders

Неплохой пример с еще более высокой абстракцией director: Паттерн "Строитель" в JavaScript

Прототип

Прототип (вшит в язык) - позволяет копировать объекты не вдаваюсь в подробности их реализации.

Клонирование без учета прототипа:

class HumanBeing {
    constructor(config) {
        this.skinColor = config.skinColor;
        this.hairColor = config.hairColor;
        this.height = config.height;
        this.weight = config.weight;
        this.gender = config.gender;
        // And more data.
    }
    clone() {
        return new HumanBeing(Object.assign({}, this));
    }
}

export default HumanBeing;
import HumanBeing from './HumanBeing';

var me = new HumanBeing({
    skinColor: 'pale',
    hairColor: 'brown',
    height:'173cm',
    weight: '100kg',
    gender: 'male'
});

var clone = me.clone();

console.log(`Are original and clone the same instance? ${me === clone}`);

for(let key in me) {
    console.log(`Are both ${key} property values in original and clone the same value? ${me[key] === clone[key]}`);
}

Клонирование с учетом прототипа:

export default class Rect {
    constructor(width, height, fillStyle, context) {
        this.width = width;
        this.height = height;
        this.fillStyle = fillStyle;
        this.context = context;
    }

    render(x, y) {
        this.context.fillStyle = this.fillStyle;
        this.context.fillRect(x, y, this.width, this.height);
    }

    clone() {
        // получаем прототип
        const prototype = Object.getPrototypeOf(this);
        // создаем новый объект на основе прототипа
        const cloned = Object.create(prototype);

        cloned.width = this.width;
        cloned.height = this.height;
        cloned.fillStyle = this.fillStyle;
        cloned.context = this.context;

        return cloned;
    }
}
Добавить комментарий