Полиморфизм

Полиморфизм — способность объектов разных типов отвечать на одно и то же сообщение (вызов метода) по-разному. В JavaScript реализуется через переопределение методов, duck typing и символы.

Зачем нужно

Полиморфизм позволяет писать код, который работает с объектами разных типов через единый интерфейс. Это уменьшает связанность, упрощает расширение системы и делает код универсальным.

Где используется

Обработка коллекций разнотипных объектов, стратегии, плагины, сериализация, рендеринг компонентов, middleware.

Предпосылки

Классы, Наследование, Прототипы

Переопределение методов (Override)

class Shape {
  area {
    throw new Error('Метод area должен быть реализован');
  }

  describe {
    return `${this.constructor.name}: площадь = ${this.area.toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super;
    this.radius = radius;
  }
  area { return Math.PI * this.radius ** 2; }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super;
    this.width = width;
    this.height = height;
  }
  area { return this.width * this.height; }
}

class Triangle extends Shape {
  constructor(base, height) {
    super;
    this.base = base;
    this.height = height;
  }
  area { return 0.5 * this.base * this.height; }
}

// Полиморфный код — работает с любой фигурой
const shapes = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 8)
];

shapes.forEach(shape => {
  console.log(shape.describe);
});
// "Circle: площадь = 78.54"
// "Rectangle: площадь = 24.00"
// "Triangle: площадь = 12.00"

// Функция не знает конкретный тип
function totalArea(shapes) {
  return shapes.reduce((sum, s) => sum + s.area, 0);
}
console.log(totalArea(shapes)); // 114.54

Duck Typing

В JavaScript не нужно формальное наследование — достаточно наличия нужных методов:

// "Если ходит как утка и крякает как утка — это утка"

class Logger {
  log(message) { console.log(`[LOG] ${message}`); }
}

class FileWriter {
  log(message) { /* запись в файл */ }
}

class ApiSender {
  log(message) { /* отправка в API */ }
}

// Принимает любой объект с методом log
function processWithLogging(data, logger) {
  logger.log(`Обработка: ${JSON.stringify(data)}`);
  // ... обработка
  logger.log('Завершено');
}

// Работает с любым "логгером"
processWithLogging({ id: 1 }, new Logger);
processWithLogging({ id: 2 }, new FileWriter);
processWithLogging({ id: 3 }, { log: (msg) => console.log(msg) }); // объект-литерал тоже

Symbol.toPrimitive

Управление преобразованием объекта в примитив:

class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return this.amount;
      case 'string':
        return `${this.amount} ${this.currency}`;
      default: // 'default'
        return this.amount;
    }
  }
}

const price = new Money(100, 'USD');
console.log(`Цена: ${price}`);  // "Цена: 100 USD"  (string)
console.log(price + 50);        // 150              (default → number)
console.log(+price);            // 100              (number)

toString и valueOf

class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString {
    return `Vector(${this.x}, ${this.y})`;
  }

  valueOf {
    return Math.sqrt(this.x ** 2 + this.y ** 2); // длина вектора
  }
}

const v = new Vector(3, 4);
console.log(`${v}`);     // "Vector(3, 4)"
console.log(v + 0);      // 5
console.log(v > 4);      // true

Полиморфизм через интерфейсы (концепция)

JavaScript не имеет интерфейсов как язык, но можно создать проверку:

// Проверка "интерфейса" в runtime
function assertImplements(obj, interface_) {
  for (const method of interface_) {
    if (typeof obj[method] !== 'function') {
      throw new TypeError(
        `Объект не реализует метод ${method}`
      );
    }
  }
}

const Serializable = ['serialize', 'deserialize'];
const Renderable = ['render', 'update'];

class Widget {
  serialize { return JSON.stringify(this); }
  deserialize(data) { return Object.assign(this, JSON.parse(data)); }
  render { return `<div>${this.name}</div>`; }
  update(props) { Object.assign(this, props); }
}

const widget = new Widget();
assertImplements(widget, Serializable); // OK
assertImplements(widget, Renderable);   // OK

Symbol.iterator — полиморфная итерация

class Range {
  constructor(start, end) {
    this.start() = start;
    this.end() = end;
  }

  [Symbol.iterator] {
    let current = this.start();
    const end = this.end();
    return {
      next {
        return current <= end
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
}

// Работает с for...of, spread, деструктуризацией
for (const n of new Range(1, 5)) {
  console.log(n); // 1, 2, 3, 4, 5
}

console.log([...new Range(1, 3)]); // [1, 2, 3]

Паттерн Strategy как полиморфизм

class Sorter {
  constructor(strategy) {
    this.strategy = strategy;
  }

  sort(data) {
    return this.strategy.sort([...data]);
  }
}

const bubbleSort = {
  sort(arr) {
    for (let i = 0; i < arr.length; i++)
      for (let j = 0; j < arr.length - i - 1; j++)
        if (arr[j] > arr[j + 1])
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
    return arr;
  }
};

const quickSort = {
  sort(arr) {
    if (arr.length <= 1) return arr;
    const pivot = arr[0];
    const left = arr.slice(1).filter(x => x <= pivot);
    const right = arr.slice(1).filter(x => x > pivot);
    return [...this.sort(left), pivot, ...this.sort(right)];
  }
};

const sorter = new Sorter(quickSort);
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]

sorter.strategy = bubbleSort; // смена стратегии
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]

Частые ошибки

1. Проверка типа вместо полиморфизма

// Плохо — switch по типу
function getArea(shape) {
  switch (shape.type) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'rect': return shape.width * shape.height;
    // Каждый новый тип — новый case
  }
}

// Хорошо — полиморфизм
function getArea(shape) {
  return shape.area; // каждый объект знает, как считать
}

2. Забытый super при переопределении

class Child extends Parent {
  doSomething {
    // Забыли super.doSomething — логика родителя потеряна
    this.childStuff;
  }
}

3. Слишком жёсткая проверка типов

// Плохо — привязка к конкретному классу
if (logger instanceof ConsoleLogger) { ... }

// Хорошо — duck typing
if (typeof logger.log === 'function') { ... }

Практика

  1. Создай иерархию Shape с полиморфным методом area и perimeter
  2. Реализуй Symbol.toPrimitive для класса Currency
  3. Напиши систему уведомлений с duck typing (email, sms, push — метод send)
  4. Реализуй Symbol.iterator для класса LinkedList
  5. Перепиши switch-based код на полиморфизм

Два вида полиморфизма

«Один полиморфизм — параметрический: функция принимает значения разных типов (дженерики в TypeScript, шаблоны в C++). Другой — полиморфизм подтипов (LSP), он же ООП-полиморфизм».

Параметрический дженерики, обобщённое программирование
Подтипов (LSP) один интерфейс — разные реализации в наследниках

В JS параметрический полиморфизм фактически бесплатен (нет статических типов). Полиморфизм подтипов реализуется через duck typing и переопределение.

Принцип подстановки Барбары Лисков (LSP)

«Вместо родительского класса можно передать любого потомка, и поведение должно остаться корректным. Вместо обычного сокета — веб-сокет с тем же интерфейсом».

Это работает и для классов, и для duck typing: если объект отвечает требуемому контракту (имеет нужные методы), он подходит.

Полиморфизм vs switch case

«Как только видим большой switch case по типу — задумываемся, может тут нужна Стратегия или другой полиморфный паттерн» (Antipatterns).

См. Антипаттерны ООП (Big switch case).

Мономорфизм/полиморфизм/мегаморфизм на уровне V8

Это другая разновидность полиморфизма — на уровне inline-кэшей V8:

  • Mono — одно место обращения видело одну форму объекта (быстро)
  • Poly — несколько форм (2-4) (средне)
  • Mega — больше — V8 деоптимизирует (медленно)

Полиморфизм классов часто приводит к полиморфизму на уровне V8. Об этом подробно: автор, 9JUY3prnCQ4.

Связанные темы

Источники