Примеси - mixins

В ООП есть один распространенный и полезный прием, позволяющий добавлять в классы наборы методов по умолчанию. О чем здесь речь? Давайте представим, что реализуется магазин, в котором товары могут быть представлены разными классами, никак между собой не связанными:

Затем, создаются объекты на основе этих классов и помещаются в массив list. И, далее, разработчики между собой договариваются: пусть каждый класс содержит следующий предопределенный набор методов:

  • getPrice – для получения цены товара;
  • getWeight – для получения веса товара;
  • getSize – для получения размера товара;
  • getPages – для получения числа страниц в товаре (например, для книг).

Зачем это нужно? Ну, например, далее в модуле может быть выполнена сортировка товара по одному из этих признаков, допустим по цене:

list.sort( (a, b) => a.getPrice() - b.getPrice() );

или по весу:

list.sort( (a, b) => a.getWeight() - b.getWeight() );

или по числу страниц:

list.sort( (a, b) => a.getPages() - b.getPages() );

и так далее. То есть, все эти методы нужны для реализации последующего функционала. Причем, их число может в будущем меняться: один добавляться, другие убираться. Как в этих условиях лучше всего построить программу?

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

class Apple {
         constructor(price, sort) {
                   this.price = price;
                   this.sort = sort;
         }
         getPrice() { return this.price; }
         getWeight() { return 0; },
         getSize() { return 0; },
         getPages() { return 0; }
}
 
class ToyBoy {
         constructor(price, name) {
                   this.price = price;
                   this.name = name;
         }
         getPrice() { return this.price; }
         getWeight() { return 0; },
         getSize() { return 0; },
         getPages() { return 0; }
}
 
class Book { 
         constructor(price, title, pages) {
                   this.price = price;
                   this.title = title;
                   this.pages = pages;
         }
         getPrice() { return this.price; } 
         getPages() { return this.pages; }         
         getWeight() { return 0; },
         getSize() { return 0; },
}

Как видите, мы вынуждены прописывать все эти методы в каждом классе, причем, некоторые из методов определены чисто формально, возвращающие 0. Теперь представьте, что таких классов десятка два и в них постоянно приходится вносить изменения по этим методам. Кажется, перспективы не очень радужные? Но как мы можем здесь облегчить свое положение? Как раз для этого очень хорошо подходит концепция примесей, или, по-английски

Mixins

в которой предлагается создать единый объект (или класс) с методами по умолчанию. Причем, этот объект не будет участвовать в наследовании, а просто скопирует свое содержимое в соответствующие классы:

А, затем, мы просто переопределим нужные из них. Объявление примеси можно определить следующим образом:

let ShopMixin = {
         getPrice() { return 0; },
         getWeight() { return 0; },
         getSize() { return 0; },
         getPages() { return 0; }
};

А классы определить так:

class Apple {
         constructor(price, sort) {
                   this.price = price;
                   this.sort = sort;
         }
}
Object.assign(Apple.prototype, ShopMixin);
Apple.prototype.getPrice = function() { return this.price; }
 
class ToyBoy { 
         constructor(price, name) {
                   this.price = price;
                   this.name = name;
         }
}
Object.assign(ToyBoy.prototype, ShopMixin);
ToyBoy.prototype.getPrice = function() { return this.price; }
 
class Book {
         constructor(price, title, pages) {
                   this.price = price;
                   this.title = title;
                   this.pages = pages;
         }
}
 
Object.assign(Book.prototype, ShopMixin);
Book.prototype.getPrice = function() { return this.price; }      
Book.prototype.getPages = function() { return this.pages; }   

Здесь используется метод assign объекта Object, который и копирует свойства из ShopMixin в соответствующие объекты prototype. Таким образом, каждый создаваемый объект будет иметь доступ к методам по умолчанию.

Как видите, такая реализация выглядит более приемлемой и удобной, причем, нигде не используется механизм наследования, что в ряде случаев как раз и является большим преимуществом.

Далее, можно создать несколько объектов:

list.push(new Apple(100, "Белый налив"));
list.push(new ToyBoy(200, "Кукла"));
 
list.push(new Book(20, "Муму", 100));
list.push(new Book(40, "Евгений Онегин", 90));
list.push(new Book(30, "Мастер и Маргарита", 500));

И выполнять различные сортировки:

list.sort( (a, b) => a.getPrice() - b.getPrice() );
// list.sort( (a, b) => a.getWeight() - b.getWeight() );
// list.sort( (a, b) => a.getPages() - b.getPages() );
 
for(let s of list)
         console.log( s.price );

Объекты с не переопределенными методами по умолчанию встанут в начало списка, остальные будут отсортированы. Например, сортировка по числу страниц, формально, применима только к книгам, но у нас не будет возникать ошибок, если в списке присутствуют и другие типы объектов и все благодаря наличию методов по умолчанию. Вот общий принцип примесей и вот так их можно реализовать в JavaScript.