В статье описано что такое композиция и как ее использовать при создание взаимоотношений объектов в JavaScript и TypeScript.
Оригинальная статья: Chris Noring — Learn and use Composition in JavaScript and TypeScript
Что означает слово композиция? Давайте начнем с того что это глагол, означает compose — то есть что то составить из чего то. При поиске в словаре вы найдете много значений в том числе и что то про сочинение музыки 🙂 Вы также найдете и это определение:
формировать из разных вещей
Вышесказанное, вероятно, ближе к тому, о чем я собираюсь поговорить дальше — Композиция.
Идея состоит в том, чтобы создать что-то из других частей. Зачем нам это делать? Что ж, проще построить что-то сложное, если оно состоит из множества маленьких частей, которые мы понимаем. Другая причина — возможность повторного использования. Большие и более сложные вещи могут иногда разделяться на части, из которых они состоят, с другими большими и сложными вещами. Вы когда-нибудь слышали об ИКЕА? 😉
В программировании есть несколько типов композиций! Далее мы рассмотрим что такое Наследование, Композиция функций и Композиция объекта (с классами и без классов)
Давайте быстро объясним что такое наследование. Идея наследования состоит в том, чтобы наследовать признаки, поля, а также методы от родительского класса. Класс, наследуемый от родительского класса, называется подклассом. Это наследование позволяет обрабатывать группу объектов одинаковым образом. Рассмотрим приведенный ниже пример:
class Shape { constructor(x, y) { this.x = x; this.y = y; } } class Movable extends Shape { move(dx, dy) { this.x += dx; this.y += dy; } } class Hero extends Movable { constructor(x, y) { super(x, y); this.name = 'hero'; } } class Monster extends Movable { constructor(x, y) { super(x, y); this.name = 'monster'; } } const movables = [new Hero(1,1), new Monster(2,2)]; movables.forEach(m => m.move(2, 3));
В примере мы можем обрабатываем Hero и Monster одинаковым образом, поскольку они имеют общий элемент-предок Movable, который позволяет перемещать их с помощью метода move(). Все это основано на принципе отношений IS-A. Hero IS-A Movable, Monster IS-A Movable.
В последнее время в разные источниках много говорилось о том, что желательнее использовать композицию, а не наследование. Почему? Давайте посмотрим на некоторые недостатки наследования:
class NPC extends Movable { move(dx, dy) { console.log('I wont move') } }
Вышеуказанный код нарушил принцип подстановки Лискова. К счастью наша кодовая база настолько мала, что мы можем легко обнаружить нарушение. В больших кодовых базах вы не сможете определить, когда это произойдет. Представьте, что нарушение происходит на третьем уровне наследования, а всего у вас есть 20 уровней наследования.
Это математический термин, утверждающий согласно Википедии следующее: композиция функций — это операция, которая принимает две функции f и g и производит функцию h такую, что h (x) = g (f (x)).
В программировать мы можем создать такой пример:
let list = [1,2,3]; take(orderByAscending(list), 3)
Где list это x, f(x) это orderByAscending(list) а g(x) это take() c f(x) входным параметром.
Дело в том, что у вас есть много операций, примененных одна за другой к части данных. По сути, вы применяете разные части для создания более сложного алгоритма, который при вызове вычисляет более сложный результат. Но мы не будем тратить время на обсуждение этой версии композиции, просто знайте, что она существует.
Этот тип композиции предназначен для объединения объектов или других типов данных для создания чего-то более сложного, это то с чего мы начали. Это может быть достигнуто различными способами в зависимости от того, какой язык вы используете. В Java и C# у вас есть только один способ создания объектов, используя класс. В других языках, таких как JavaScript, у вас есть объекты, которые можно создавать разными способами, и, таким образом, вы открываете для себя различные способы создания композиции.
Использование классов — это класс, ссылающийся на один или несколько других классов через переменные экземпляра. То есть в этом случае класс будет описывать ассоциацию. Что это означает? У человека четыре конечности, у машины может быть четыре колеса и так далее. Представьте, что на эти классы ссылаются как на части, которые дают вам возможность что-то делать. Давайте посмотрим на пример:
class SteeringWheel { steer(){} } class Engine { run(){} } class Car { constructor(steeringWheel, engine) { this.steeringWheel = steeringWheel; this.engine = engine; } steer() { this.steeringWheel.steer(); } run() { this.engine.run(); } } class Tractor { constructor(steeringWheel) { this.steeringWheel = steeringWheel; } steer() { this.steeringWheel.steer(); } }
То, что мы получаем выше, это, во-первых, автомобиль более сложного класса, состоящий из множества частей: рулевого колеса (steeringWheel) и двигателя (engine), и благодаря этому мы получаем способность управлять автомобилем (steer) и двигаться им (runs). Мы также получаем возможность многократного использования, поскольку мы можем использовать SteeringWheel например в тракторе (Tractor).
JavaScript немного отличается от C# и Java тем, что в нем можно создавать объекты несколькими способами, например как показано ниже:
let myObject = { name: 'a name' }
Object.create()
, Вы можете просто использовать объект, и он будет использоваться в качестве шаблона. Вот так например:const template = { a: 1, print() { return this.a } } const test = Object.create(template); test.a = 2 console.log(test.print()); // 2
new
. Вы можете применить оператор new как к классу, так и к функции, например так: class Plane { constructor() { this.name = 'a plane' } } function AlsoAPlane() { this.name = 'a plane'; } const plane = new Plane(); console.log(plane.name); // a plane const anotherPlane = new AlsoAPlane(); console.log(anotherPlane) // a plane
Существует небольшая разница между этими подходами. Вам нужно проделать немного больше работы, если вы хотите, чтобы наследование работало для функционального подхода среди прочего. На данный момент мы рады, что существуют разные способы создания объектов с использованием new.
Так как нам на самом деле создавать композицию? Для композиции нам нужен способ выражения поведения. Нам не нужно использовать классы, если мы этого не хотим, и вместо этого мы можем перейти непосредственно к объектам. Мы можем выразить нашу композицию следующим образом:
const steer = { steer() { console.log(`steering ${this.name}`) } } const run = { run() { console.log(`${this.name} is running`) } } const fly = { fly() { console.log(`${this.name} is flying`) } }
и использовать их следующим образом:
const steerAndRun = { ...steer, ...run }; const flyingAndRunning = { ...run, ...fly };
Выше мы использовали оператор spread, чтобы объединить различные свойства из разных классов и поместить их в один класс. Результирующий steerAndRun теперь содержит {steer () {…}, run () {…}}, а flyingAndRunning содержит {fly () {…}, run () {…}}.
Затем, используя метод createVehicle(), мы создадим то, что нам нужно:
function createVehicle(name, behaviors) { return { name, ...behaviors } } const car = createVehicle('Car', steerAndRun) car.steer(); car.run(); const crappyDelorean = createVehicle('Crappy Delorean', flyingAndRunning) crappyDelorean.run(); crappyDelorean.fly();
Конечный результат — два разных объекта с разными возможностями.
TypeScript интенсивно использует классы и интерфейсы, поэтому рассмотрим способ выполнения композиции объектов с использованием классов.
Могу ли я выполнить композицию способом, аналогичным использованию оператора spread и объектов?
Да, вы можете. Для этого мы используем концепцию под названием MixIns. Давай начнем:
1. Во-первых, нам нужна эта конструкция:
type Constructor<T = {}> = new (...args: any[]) => T
Мы используем эту конструкцию, чтобы создать Constructor.
2. Затем объявим эту функцию:
function Warrior<TBase extends Constructor>(Base: TBase) { return class extends Base { say: string = 'Attaaack'; attack() { console.log("attacking...") } } }
Эта конструкция возвращает класс, унаследованный от Base. Base является входным параметром нашей функции и имеет тип TBase, который использует только что созданный Constructor.
3. Давайте определим класс, который будет использовать вышеуказанную функцию:
class Player { constructor( private name: string ) {} }
4. Теперь вызовим функцию Warrior:
const WarriorPlayerType = Warrior(Player); const warrior = new WarriorPlayerType("Conan"); console.log(warrior.say); // 'Attaaack' warrior.attack(); // 'attacking...'
5. Мы можем продолжать создавать композицию, создав новую функцию, которая содержит другое поведение, которое мы могли бы хотеть:
function Wings<TBase extends Constructor>(Base: TBase) { return class extends Base { fly() { console.log("flying...") } } }
6. Используем следующую композицию
const WingsWarriorPlayerType = Wings(WarriorPlayerType); const flyingWarrior = new WingsWarriorPlayerType("Flying Conan"); flyingWarrior.fly();
В этой статье описывается, что такое композиция. Кроме того, в ней говориться о том, что композиция предпочтительнее наследования. Так было рассмотрены разные способы создания композиции. В качестве бонуса мы также рассмотрели подход, который вы можете использовать в TypeScript.
Есть несколько замечательных статей на эту тему, которые, я думаю, вы должны прочитать. Посмотрите на следующее:
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Барбара Лисков - женщина, соответственно принцип подстановки Лисков
Полезная статься!