Typescript Class Mixins

Итак, давайте поговорим о #typescript, его суперспособностях и о том, как его можно использовать для генерации новых классов на лету и инстанцирования классов, которых на самом деле не существует. Мы будем создавать летающих собак и гордых уток.

Пожалуй, одна из самых увлекательных особенностей #typescript — это его миксины, которые позволяют лепить классы (и не только их). В этой статье мы узнаем, как можно использовать mixin базового класса для создания четырех различных производных классов. Это было довольно забавное открытие.

Изображение обложки для этой статьи не случайно, в какой-то степени оно вдохновило статью. Итак, давайте подумаем, как можно использовать #typescript для создания экземпляров собаки, рыбы, курицы и утки. Если подумать, то у них явно должен быть какой-то суперкласс. Они также реализуют различные способы передвижения. Для простоты предположим, что некоторые функции реализованы одинаково для всех классов, т.е. ходьба одинакова для собаки и курицы (хотя в реальности куры не ходят на четырех ногах).

Теперь мы можем предположить суперкласс, который будет нашей базой:

class Animal {
  constructor(public readonly name: string) { }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Простое и понятное животное, у которого есть имя.
Наши животные реализуют различные способы передвижения, которые можно описать с помощью интерфейсов #typescript:

interface IFly {
  fly(): void;
}

interface IWalk {
  walk(): void;
}

interface ISwim {
  swim(): void;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Так, собака реализует IWalk, а курица реализует и IFly, и IWalk.

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

interface IConstructor<T> { new(...args: any[]): T; }
Вход в полноэкранный режим Выход из полноэкранного режима

Когда конструктор generic готов, следующим шагом будет создание трех миксинов, которые обогатят переданный класс Animal новыми функциональными возможностями:

function Walker<T extends IConstructor<Animal>>(animal: T) {
  return class extends animal implements IWalk {
    walk(): void {
      console.log(`I am a ${this.name} and I can walk!`);
    }
  }
}

function Swimmer<T extends IConstructor<Animal>>(animal: T) {
  return class extends animal implements ISwim {
    swim(): void {
      console.log(`I am a ${this.name} and I can swim!`);
    }
  }
}

function Flyer<T extends IConstructor<Animal>>(animal: T) {
  return class extends animal implements IFly {
    fly(): void {
      console.log(`I am a ${this.name} and I can fly!`);
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, теперь для создания собаки нам не нужно создавать отдельный класс, расширяющий Animal, а можно изготовить его, передав класс Animal в миксин Walker:

const dog = new class extends Walker(Animal) { }('dog');
dog.walk(); // "I am a dog and I can walk!" 
Вход в полноэкранный режим Выйти из полноэкранного режима

То же самое будет справедливо для рыбы и курицы, последняя получит два метода:

const fish = new class extends Swimmer(Animal) { }('fish');
fish.swim(); // "I am a fish and I can swim!" 

const chicken = new class extends Flyer(Walker(Animal)) { }('chicken');
chicken.fly(); // "I am a chicken and I can fly!" 
chicken.walk(); // "I am a chicken and I can walk!" 
Войти в полноэкранный режим Выход из полноэкранного режима

Более того, такой подход позволяет добавить еще больше свойств/методов на результирующий экземпляр и даже переопределить расширенные методы, давайте создадим утку и переопределим ее метод fly:

const proudDuck = new class extends Flyer(Swimmer(Walker(Animal))) {
  public override fly(): void {
    console.log('I don't fly as a chick, I fly like a duck!')
  }
}('duck');
proudDuck.fly(); // "I don't fly as a chick, I fly like a duck!" 
proudDuck.swim(); // "I am a duck and I can swim!" 
proudDuck.walk(); // "I am a duck and I can walk!" 
Вход в полноэкранный режим Выход из полноэкранного режима

Что еще более интересно в этом подходе с использованием миксинов, так это то, что теперь мы можем создать даже летающую собаку, если захотим:

const flyingDog = new class extends Flyer(Walker(Animal)) { }('a flying dog!');
flyingDog.walk(); // "I am a a flying dog! and I can walk!" 
flyingDog.fly(); // "I am a a flying dog! and I can fly!" 
Войти в полноэкранный режим Выйти из полноэкранного режима

И мы по-прежнему сохраняем типы и intellisense. Удивительно, не правда ли? 🙂
P.S. код для статьи доступен здесь

Оцените статью
devanswers.ru
Добавить комментарий