抽象クラス/インターフェース/ポリモーフィズム

2025-07-28

はじめに

JavaScriptはプロトタイプベースの言語ですが、オブジェクト指向プログラミングの重要な概念である「抽象基底クラス」と「インターフェース」と「ポリモーフィズム(多態性)」を実現することができます。この章では、TypeScriptのインターフェースも含め、これらの概念をJavaScriptでどのように適用するかを詳しく解説します。

インターフェースの概念

インターフェース(Interface)は、クラスが実装すべきメソッドやプロパティの「契約」を定義するものです。純粋なJavaScriptにはインターフェースの構文はありません。(JavaやTypeScriptにはある)。
ただし 「このオブジェクトはこういう形(プロパティやメソッド)を持っているべき」という約束事 を表すときに「インターフェース」という考え方が使われます。

インターフェース (Interface)

  • 完全な抽象化: メソッドの実装を持たない(Java 8以降はdefaultメソッドとstaticメソッドを除く)
  • 多重実装可能: 1つのクラスが複数のインターフェースを実装できる
  • 定数のみ: フィールドは自動的に public static final となる
  • 契約の定義: 「何をすべきか」を定義する

抽象クラス (Abstract Class)

  • 部分的な抽象化: 抽象メソッドと具象メソッドの両方を持てる
  • 単一継承のみ: 1つのクラスは1つの抽象クラスしか継承できない
  • 状態の保持: 通常のフィールドやメソッドを持てる
  • 共通機能の提供: 関連クラス間で共通の実装を提供する

主な違い

特徴インターフェース抽象クラス
実装の有無基本的にない(default除く)一部実装可能
継承/実装多重実装可能単一継承のみ
フィールド定数のみ任意のフィールド
コンストラクタ不可
アクセス修飾子publicのみ各種指定可能

この図は、オブジェクト指向設計におけるこれらの概念の関係性と違いを視覚的に理解するのに役立ちます。

JavaScriptでのインターフェース実装方法

方法1:ダックタイピング(Duck Typing)

JavaScriptでは「もしアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」という哲学(ダックタイピング)でインターフェースを実現します。

// インターフェースの「概念」を定義
// Renderableはrenderメソッドを持つべき
function renderItem(item) {
  // 渡された item が render というメソッドを持っているか
  if (typeof item.render !== 'function') {
    throw new Error('Renderableインターフェースを実装していません');
  }
  item.render();
}

// 使用例
class Circle {
  render() {
    console.log('円を描画');
  }
}

class Square {
  render() {
    console.log('四角形を描画');
  }
}

renderItem(new Circle()); // "円を描画"
renderItem(new Square()); // "四角形を描画"

ダックタイピング(Duck Typing)とは、オブジェクトの型やクラスではなく「持っているメソッドや振る舞い」によって、そのオブジェクトをどう扱えるか判断する考え方です。

この場合は、typeof item.render にて item というオブジェクトの中に render というプロパティを探します。それが存在すれば、その型を返します(例: “function”, “string”, “undefined”)。

もし render プロパティが関数ではなかった場合(存在しない、または文字列や数値などの関数以外の場合)、true になりエラーとなります。つまり、特定の型に属しているかよりも、必要なメソッドやプロパティを持っているかで機能を利用します。

方法2:TypeScriptのインターフェース

TypeScriptでは明示的なインターフェース構文があります。

// インターフェース定義
interface Drawable { // TypeScriptでしっかり型を使う方法 interface
  draw(): void;
  area(): number;
}

// クラス実装
class Circle implements Drawable { // implementsにてinterfaceを実装する
  constructor(private radius: number) {}

  draw() {
    console.log(`半径${this.radius}の円を描画`);
  }

  area() {
    return Math.PI * this.radius ** 2;
  }
}

// 使用例
const circle: Drawable = new Circle(5);
circle.draw();
console.log(circle.area());

TypeScriptは型安全性と開発支援を加えたJavaScriptの上位互換です。TypeScriptは別記事でシリーズ化して詳しく説明します。

方法3:抽象基底クラス

抽象基底クラスとは、直接インスタンス化せず、共通の設計やメソッドの雛形(インターフェース)を子クラスに強制するためのクラスです。

具体的な処理は持たず、派生クラスで必ず実装すべき抽象メソッドを定義して、コードの設計ルールや共通振る舞いを示します。JavaやC++の「abstract class」に相当します。JavaScriptには文法としては無いですが、ルールを自分で決めて強制することができます。

class AbstractDrawable { // 抽象基底クラスとして宣言する
  draw() {
    throw new Error('drawメソッドを実装してください');
  }

  area() {
    throw new Error('areaメソッドを実装してください');
  }
}

class Triangle extends AbstractDrawable {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }

  draw() {
    console.log('三角形を描画');
  }

  area() {
    return (this.base * this.height) / 2;
  }
}

抽象基底クラスを使う上で、守るべきルールとして以下があります。

  • 抽象基底クラスを直接 new させない
  • 抽象メソッドを定義し、未実装ならエラーを投げる
  • 共通処理は基底クラスで実装し、固有処理だけサブクラスで書かせる

ポリモーフィズム(多態性)の概念

ポリモーフィズム(Polymorphism)とは、同じインターフェースを持つオブジェクトが、異なる実装を持つことができる性質です。

ポリモーフィズムの利点

  1. 拡張性:新しい型を追加しても既存コードを変更せずに動作
  2. 保守性:インターフェースに依存することで実装の変更が容易
  3. 柔軟性:実行時にオブジェクトの型を決定できる

JavaScriptでのポリモーフィズム実装

基本的な例

Animal (基底クラス)
├── Dog (派生クラス)
└── Cat (派生クラス)

class Dog extends Animal {
  makeSound() {
    console.log('ワンワン!');
  }
}

class Cat extends Animal {
  makeSound() {
    console.log('ニャー!');
  }
}

class Cow {
  makeSound() {
    console.log('モー!');
  }
}

// ポリモーフィズムの実現
function animalSound(animal) {
  animal.makeSound();
}

const animals = [new Dog(), new Cat(), new Cow()];
animals.forEach(animal => animalSound(animal));
/*
"ワンワン!"
"ニャー!"
"モー!"
*/

例の核心は、「同じメソッド呼び出しで、オブジェクトの型に応じて異なる動作」を実現していることです。これがポリモーフィズムの本質です。

より実践的な例:支払いシステム

// 支払い方法のインターフェース(概念)
// すべての支払い方法はprocessPaymentメソッドを持つ
class PaymentProcessor {
  process(paymentMethod, amount) {
    paymentMethod.processPayment(amount);
  }
}

// クレジットカード支払い
class CreditCardPayment {
  processPayment(amount) {
    console.log(`クレジットカードで${amount}円支払い`);
    // 実際のクレジットカード処理...
  }
}

// PayPal支払い
class PayPalPayment {
  processPayment(amount) {
    console.log(`PayPalで${amount}円支払い`);
    // 実際のPayPal処理...
  }
}

// 仮想通貨支払い
class CryptoPayment {
  processPayment(amount) {
    console.log(`仮想通貨で${amount}円相当支払い`);
    // 実際の仮想通貨処理...
  }
}

// 使用例
const processor = new PaymentProcessor();
const payments = [
  new CreditCardPayment(),
  new PayPalPayment(),
  new CryptoPayment()
];

payments.forEach(payment => {
  processor.process(payment, 10000);
});
/*
"クレジットカードで10000円支払い"
"PayPalで10000円支払い"
"仮想通貨で10000円相当支払い"
*/

このコードは、支払い処理の共通インターフェースとして PaymentProcessor クラスを定義し、各種支払い方法をそれぞれ別クラス(CreditCardPaymentPayPalPaymentCryptoPayment)で実装した例です。

PaymentProcessor は支払い方法オブジェクトが持つ processPayment メソッドを呼び出すことで多様な支払いに対応し、各支払いクラスは独自に支払い処理の具体的な内容を定義しています。

最後に複数の支払い方法のインスタンスを作成し、process メソッドで順に支払いを実行しているため、それぞれの支払い方法に応じた処理とメッセージがコンソールに表示されます。

インターフェースとポリモーフィズムの組み合わせ

形状描画システム

// インターフェースの定義(概念)
class ShapeInterface {
  draw() {
    throw new Error('drawメソッドを実装してください');
  }

  area() {
    throw new Error('areaメソッドを実装してください');
  }
}

// 円クラス
class Circle extends ShapeInterface {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  draw() {
    console.log(`半径${this.radius}の円を描画`);
  }

  area() {
    return Math.PI * this.radius ** 2;
  }
}

// 四角形クラス
class Rectangle extends ShapeInterface {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  draw() {
    console.log(`${this.width}x${this.height}の四角形を描画`);
  }

  area() {
    return this.width * this.height;
  }
}

// 図形描画アプリケーション
class DrawingApp {
  constructor() {
    this.shapes = [];
  }

  addShape(shape) {
    if (typeof shape.draw !== 'function' || typeof shape.area !== 'function') {
      throw new Error('ShapeInterfaceを実装していません');
    }
    this.shapes.push(shape);
  }

  drawAll() {
    this.shapes.forEach(shape => shape.draw());
  }

  calculateTotalArea() {
    return this.shapes.reduce((total, shape) => total + shape.area(), 0);
  }
}

// 使用例
const app = new DrawingApp();
app.addShape(new Circle(5));
app.addShape(new Rectangle(4, 6));
app.addShape(new Circle(3));

app.drawAll();
/*
"半径5の円を描画"
"4x6の四角形を描画"
"半径3の円を描画"
*/

console.log(`総面積: ${app.calculateTotalArea()}`);
// "総面積: 110.68140899333463"

このコードは、JavaScriptで「インターフェースの概念」を擬似的に表現し、図形を扱う仕組みを作った例です。ShapeInterface クラスは drawarea メソッドを持ちますが、未実装のため呼ばれるとエラーを投げ、子クラスに必ずこれらのメソッドを実装させる設計になっています。

CircleRectangleShapeInterface を継承し、それぞれ円と四角形の描画処理と面積計算を具体的に実装しています。DrawingApp クラスは複数の図形を管理し、addShape メソッドで渡されたオブジェクトがインターフェースを満たしているかチェックして登録、drawAll で全図形を描画し、calculateTotalArea で全図形の面積の合計を計算します。最後に円や四角形を追加して描画と総面積表示を行っています。

高度なポリモーフィズムのテクニック

依存性注入(DI)

ポリモーフィズムを活用した依存性注入の例:

依存性注入(DI)とは、あるオブジェクトが必要とする他のオブジェクト(依存先)を、自分で作るのではなく外部から渡してもらう設計パターンです。

// データベース接続インターフェース
class DatabaseConnection {
  connect() {
    throw new Error('connectメソッドを実装してください');
  }

  query(sql) {
    throw new Error('queryメソッドを実装してください');
  }
}

// MySQL実装
class MySQLConnection extends DatabaseConnection {
  connect() {
    console.log('MySQLに接続');
    // 実際の接続処理...
  }

  query(sql) {
    console.log(`MySQLクエリ: ${sql}`);
    // クエリ実行...
    return ['result1', 'result2'];
  }
}

// PostgreSQL実装
class PostgreSQLConnection extends DatabaseConnection {
  connect() {
    console.log('PostgreSQLに接続');
    // 実際の接続処理...
  }

  query(sql) {
    console.log(`PostgreSQLクエリ: ${sql}`);
    // クエリ実行...
    return { rows: ['result1', 'result2'] };
  }
}

// アプリケーションクラス
class App {
  constructor(dbConnection) {
    if (!(dbConnection instanceof DatabaseConnection)) {
      throw new Error('DatabaseConnectionを実装したクラスが必要です');
    }
    this.db = dbConnection;
  }

  run() {
    this.db.connect();
    const results = this.db.query('SELECT * FROM users');
    console.log('結果:', results);
  }
}

// 使用例
const mysqlApp = new App(new MySQLConnection());
mysqlApp.run();
/*
"MySQLに接続"
"MySQLクエリ: SELECT * FROM users"
"結果: ['result1', 'result2']"
*/

const postgresApp = new App(new PostgreSQLConnection());
postgresApp.run();
/*
"PostgreSQLに接続"
"PostgreSQLクエリ: SELECT * FROM users"
"結果: { rows: ['result1', 'result2'] }"
*/

ストラテジーパターン

ポリモーフィズムを活用したストラテジーパターンの例:

ポリモーフィズムを利用して「アルゴリズム(処理方法)をクラスごとに分け、実行時に切り替えられるようにした設計パターンです。具体的には、共通のインターフェースを持つ複数の戦略クラスを用意し、必要に応じて使い分けることで柔軟で拡張しやすいコードを実現します。

// ソート戦略インターフェース
class SortStrategy {
  sort(data) {
    throw new Error('sortメソッドを実装してください');
  }
}

// クイックソート実装
class QuickSort extends SortStrategy {
  sort(data) {
    console.log('クイックソートを実行');
    // 実際のソート処理...
    return [...data].sort((a, b) => a - b);
  }
}

// マージソート実装
class MergeSort extends SortStrategy {
  sort(data) {
    console.log('マージソートを実行');
    // 実際のソート処理...
    return [...data].sort((a, b) => a - b);
  }
}

// バブルソート実装
class BubbleSort extends SortStrategy {
  sort(data) {
    console.log('バブルソートを実行');
    // 実際のソート処理...
    return [...data].sort((a, b) => a - b);
  }
}

// ソートコンテキスト
class Sorter {
  constructor(strategy = new QuickSort()) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    if (!(strategy instanceof SortStrategy)) {
      throw new Error('SortStrategyを実装したクラスが必要です');
    }
    this.strategy = strategy;
  }

  sortData(data) {
    return this.strategy.sort(data);
  }
}

// 使用例
const data = [3, 1, 4, 1, 5, 9, 2, 6];
const sorter = new Sorter();

console.log(sorter.sortData(data)); // クイックソート

sorter.setStrategy(new BubbleSort());
console.log(sorter.sortData(data)); // バブルソート

sorter.setStrategy(new MergeSort());
console.log(sorter.sortData(data)); // マージソート

インターフェースとポリモーフィズムのベストプラクティス

1. 明示的なインターフェースチェック

JavaScriptでは、インターフェースを実装しているか明示的にチェックするのが良いでしょう:

インターフェースチェックは「オブジェクトに必要なメソッドやプロパティがあるかをtypeofやin演算子で確認する」方法で行います。

function implementsInterface(obj, interfaceObj) {
  for (const method in interfaceObj) {          // インターフェースチェック in演算子
    if (typeof obj[method] !== 'function') {    // インターフェースチェック typeof演算子
      return false;
    }
  }
  return true;
}

const DrawableInterface = {
  draw: function() {},
  area: function() {}
};

class Square {
  draw() { /* ... */ }
  area() { /* ... */ }
}

console.log(implementsInterface(new Square(), DrawableInterface)); // true

2. 小さなインターフェースを定義

インターフェース分割の原則(Interface Segregation Principle)に従い、小さなインターフェースを定義します:

インターフェース分割の原則(Interface Segregation Principle)とはクライアント(利用者)が使わないメソッドを持つ大きなインターフェースを実装させるのではなく、小さくて特定用途に絞ったインターフェースに分割すべき、という設計原則です。

// 悪い例: 大きすぎるインターフェース
class BigInterface {
  save() {}
  load() {}
  print() {}
  serialize() {}
  deserialize() {}
}

// 良い例: 小さなインターフェースに分割
class Storable {
  save() {}
  load() {}
}

class Printable {
  print() {}
}

class Serializable {
  serialize() {}
  deserialize() {}
}

3. ポリモーフィズムとコンポジションの組み合わせ

継承よりもコンポジションを優先しつつ、ポリモーフィズムを活用します:

コンポジションとは「必要な機能を持つオブジェクトを内部に持ち、それを組み合わせて新しい機能を作る」方法で、継承より柔軟に部品を組み合わせられます。

class Logger {
  log(message) {
    throw new Error('logメソッドを実装してください');
  }
}

class ConsoleLogger extends Logger {
  log(message) {
    console.log(`[コンソールログ] ${message}`);
  }
}

class FileLogger extends Logger {
  log(message) {
    console.log(`[ファイルログ] ${message} (ファイルに書き込み)`);
  }
}

class ApiService {
  constructor(logger) {
    if (!(logger instanceof Logger)) {
      throw new Error('Loggerを実装したクラスが必要です');
    }
    this.logger = logger;
  }

  fetchData() {
    try {
      this.logger.log('データ取得開始');
      // API呼び出し...
      this.logger.log('データ取得成功');
    } catch (error) {
      this.logger.log(`エラー: ${error.message}`);
    }
  }
}

// 使用例
const apiWithConsole = new ApiService(new ConsoleLogger());
apiWithConsole.fetchData();

const apiWithFile = new ApiService(new FileLogger());
apiWithFile.fetchData();

TypeScriptでのさらなる活用

TypeScriptではインターフェースが言語機能としてサポートされており、より強力に型安全なポリモーフィズムを実現できます:

// インターフェース定義
interface Vehicle {
  startEngine(): void;
  stopEngine(): void;
  drive(distance: number): void;
}

// クラス実装
class Car implements Vehicle {
  startEngine() {
    console.log('車のエンジンを始動');
  }

  stopEngine() {
    console.log('車のエンジンを停止');
  }

  drive(distance: number) {
    console.log(`${distance}km車で走行`);
  }
}

class Motorcycle implements Vehicle {
  startEngine() {
    console.log('バイクのエンジンを始動');
  }

  stopEngine() {
    console.log('バイクのエンジンを停止');
  }

  drive(distance: number) {
    console.log(`${distance}kmバイクで走行`);
  }
}

// ポリモーフィズムの使用
function testVehicle(vehicle: Vehicle) {
  vehicle.startEngine();
  vehicle.drive(10);
  vehicle.stopEngine();
}

testVehicle(new Car());
/*
"車のエンジンを始動"
"10km車で走行"
"車のエンジンを停止"
*/

testVehicle(new Motorcycle());
/*
"バイクのエンジンを始動"
"10kmバイクで走行"
"バイクのエンジンを停止"
*/

まとめ

この章では、JavaScriptにおけるインターフェースとポリモーフィズムについて学びました:

  1. インターフェース
  • JavaScriptではダックタイピングや抽象クラスで実現
  • TypeScriptでは明示的なインターフェース構文がある
  • クラス間の契約を定義し、疎結合を促進
  1. ポリモーフィズム
  • 同じインターフェースを持つオブジェクトが異なる振る舞い
  • 拡張性と保守性を向上
  • ストラテジーパターンや依存性注入で活用
  1. 実践的な活用
  • 支払いシステム
  • 形状描画アプリケーション
  • データベース接続
  • ソートアルゴリズム
  1. ベストプラクティス
  • 明示的なインターフェースチェック
  • 小さなインターフェース定義
  • コンポジションとの組み合わせ
  1. TypeScriptの活用
  • 型安全なインターフェース実装
  • コンパイル時チェック

インターフェースとポリモーフィズムを適切に使用することで、柔軟で拡張可能、かつ保守性の高いコードを設計できるようになります。JavaScriptの動的な性質を活かしつつ、これらのオブジェクト指向の原則を適用することで、より堅牢なアプリケーションを構築できます。


演習問題

初級問題

問題1: 次のコードの出力結果は何ですか?

class Shape {
  area() {
    return 0;
  }
}

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

const circle = new Circle(5);
console.log(circle.area());

問題2: JavaScriptにおける「ダックタイピング」の概念を説明してください。

問題3: 次のコードの出力結果は何ですか?

class Printer {
  print() {
    console.log('Printing...');
  }
}

class ColorPrinter extends Printer {
  print() {
    console.log('Color printing...');
  }
}

const printer = new ColorPrinter();
printer.print();

中級問題

問題4: 次のコードの出力結果は何ですか?また、その理由を説明してください。

class Animal {
  makeSound() {
    throw new Error('This method must be implemented');
  }
}

class Dog extends Animal {
  makeSound() {
    return 'Woof!';
  }
}

class Cat extends Animal {
  makeSound() {
    return 'Meow!';
  }
}

function animalSound(animal) {
  console.log(animal.makeSound());
}

animalSound(new Dog());
animalSound(new Cat());

問題5: 次のコードを修正して、正しく動作するようにしてください。

class PaymentProcessor {
  process() {
    throw new Error('Process method not implemented');
  }
}

class CreditCardProcessor {
  // ここを実装
}

const processor = new CreditCardProcessor();
console.log(processor.process('1000'));

問題6: 異なるクラスが同じメソッド名を持つ例を3つ作成し、ポリモーフィズムを実現するコードを書いてください。

問題7: 次のコードの出力結果は何ですか?

class Database {
  connect() {
    return 'Connected to generic database';
  }
}

class MySQLDatabase extends Database {
  connect() {
    return 'Connected to MySQL database';
  }
}

class MongoDBDatabase extends Database {
  connect() {
    return 'Connected to MongoDB database';
  }
}

function testConnection(db) {
  console.log(db.connect());
}

testConnection(new MySQLDatabase());
testConnection(new MongoDBDatabase());

問題8: JavaScriptでインターフェースを模倣するための一般的なパターンを2つ説明してください。

問題9: 次のコードの出力結果は何ですか?

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

class ErrorLogger extends Logger {
  log(message) {
    console.log(`ERROR: ${message}`);
  }
}

class DebugLogger extends Logger {
  log(message) {
    console.log(`DEBUG: ${message}`);
  }
}

function processLog(logger, message) {
  logger.log(message);
}

processLog(new ErrorLogger(), 'Something went wrong');
processLog(new DebugLogger(), 'Debugging info');

上級問題

問題10: TypeScriptのインターフェースとJavaScriptのダックタイピングの違いを説明し、コード例で示してください。

問題11: 次のコードの出力結果は何ですか?また、その理由を説明してください。

class A {
  method() {
    return 'A';
  }
}

class B {
  method() {
    return 'B';
  }
}

function callMethod(obj) {
  console.log(obj.method());
}

callMethod(new A());
callMethod(new B());
callMethod({ method: () => 'C' });

問題12: ストラテジーパターンを使用して、異なるソートアルゴリズムを切り替えるコードを実装してください。


解答例

初級問題の解答

解答1:

78.53981633974483

解答2:
ダックタイピングとは、オブジェクトの型そのものではなく、持っているメソッドやプロパティに基づいて判断するプログラミングスタイルです。「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」という考え方に基づきます。

解答3:

Color printing...

中級問題の解答

解答4:

Woof!
Meow!

理由: 異なるクラス(DogとCat)が同じメソッド(makeSound)を実装しており、animalSound関数は引数の実際の型を知らなくても正しく動作します。これがポリモーフィズムの例です。

解答5:

class CreditCardProcessor extends PaymentProcessor {
  process(amount) {
    return `Processing credit card payment: $${amount}`;
  }
}

解答6:

class Square {
  area() {
    return this.side ** 2;
  }
}

class Circle {
  area() {
    return Math.PI * this.radius ** 2;
  }
}

class Triangle {
  area() {
    return (this.base * this.height) / 2;
  }
}

function calculateTotalArea(shapes) {
  return shapes.reduce((total, shape) => total + shape.area(), 0);
}

解答7:

Connected to MySQL database
Connected to MongoDB database

解答8:

  1. 抽象基底クラス: 実装されていないメソッドをthrowするクラスを作成
  2. ダックタイピング: 特定のメソッドが存在するかどうかを実行時にチェック

解答9:

ERROR: Something went wrong
DEBUG: Debugging info

上級問題の解答

解答10:

// TypeScriptのインターフェース
interface Flyable {
  fly(): void;
}

class Bird implements Flyable {
  fly() {
    console.log('Bird is flying');
  }
}

// JavaScriptのダックタイピング
class Airplane {
  fly() {
    console.log('Airplane is flying');
  }
}

function makeFly(entity) {
  entity.fly();
}

makeFly(new Bird());
makeFly(new Airplane()); // TypeScriptではエラーだが、JavaScriptでは動作

解答11:

A
B
C

理由: JavaScriptはダックタイピングを採用しているため、オブジェクトの実際の型に関係なく、method()が実装されていれば呼び出すことができます。

解答12:

class BubbleSort {
  sort(array) {
    console.log('Bubble sort');
    return array.slice().sort((a, b) => a - b);
  }
}

class QuickSort {
  sort(array) {
    console.log('Quick sort');
    return array.slice().sort((a, b) => a - b);
  }
}

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

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sort(array) {
    return this.strategy.sort(array);
  }
}

const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort([3, 1, 4, 2]));

sorter.setStrategy(new QuickSort());
console.log(sorter.sort([3, 1, 4, 2]));