🐥

DartのMixinをコンセプトから整理する

2024/04/30に公開

背景

近頃Flutterで遊ぶ機会が増え、Dartのドキュメントを読み込む時間も増えました。
Dartのドキュメントは非常に丁寧でわかりやすいですが、mixinのコンセプトが腑に落ちず、時間を割いて調査しました。
今回はその内容をシェアします💃

実行環境とマシンの情報
$ system_profiler SPHardwareDataType | grep -E 'Model Name|Chip'
Model Name: MacBook Air
Chip: Apple M2

$ sw_vers
ProductName:            macOS
ProductVersion:         14.4.1
BuildVersion:           23E224

$ dart --version
Dart SDK version: 3.2.6 (stable) (Wed Jan 24 13:41:58 2024 +0000) on "macos_arm64"

調べたこと

クラスと機能追加からコンセプトを理解する

まずは教科書的に抽象クラスCarを定義してみます。これは「車」なのでdriveメソッドを伴うものとします。

abstract class Car {
  void drive();
}

class StandardCar extends Car {
  
  void drive() {
    print('Start driving!');
  }
}

このような形でStandardCarを実装できました。このインスタンスはCarクラスとして扱うことができます。ご存知ポリモーフィズムですね。

void main() {
  Car car = StandardCar();
  car.drive(); // Start driving!
}

しかし突如として「空飛ぶ車」クラスが必要になりました。安直にはCarクラスをextendsするのが簡単です。

abstract class Car {
  void drive();
}

class FlyableCar extends Car {
  
  void drive() {
    print('Start driving, but I can fly...');
  }

  void fly() {
    print('Start flying!');
  }
}

void main() {
  final flyableCar = FlyableCar();
  flyableCar.drive(); // Start driving, but I can fly...
  flyableCar.fly(); // Start flying!
}

はい、問題なく動きます。しかしこのコードが意図するものは

  • 車クラス Car と、空飛ぶ車クラス FlyableCar がある
  • FlyableCarCarfly という「機能」を追加したものである

ということですね。この「機能を追加する」という部分はFlyableという単語でしか表現できていません。

加えて、機能変更が想定できていません。この車は「立って歩く」かもしれませんし、あとから「空を飛ぶ」機能が削られるかもしません。

そのような未来を想定する場合、機能は機能単位で考えるべきです🤔
となると「クラス」という概念とは異なる気がしてきますね[1]

mixin登場

そこで登場するのがmixinです。まずは公式ドキュメントを読んでみましょう。

Mixins are a way of defining code that can be reused in multiple class hierarchies. They are intended to provide member implementations en masse.
Mixinは複数のクラス階層で再利用可能なコードを定義する方法です。これは多くのメンバー実装をまとめてで提供することを目的としています。

provide member implementations en masseということですので、実装、すなわち機能を一括りとして提供できるわけですね!

使い方は簡単で、定義したmixinを追加したいクラスにwithすればOKです[2]

abstract class Car {
  void drive();
}

mixin Flyable {
  void fly() {
    print('Start flying!');
  }
}

class FlyableCar extends Car with Flyable {
  
  void drive() {
    print('Start driving, but I can fly...');
  }
}

void main() {
  final f = FlyableCar();
  f.fly(); // Start flying!
}

多重継承 (呼び方が適切ではないですが) もできます。機能を自由につけ外しができるわけですね。便利!

mixin Movable {
  void move() {
    print('Start moving!');
  }
}

class MiracleCar extends Car with Flyable, Movable {
  
  void drive() {
    print('Start driving, but I can fly...');
  }
}

Flutterのコードを眺める

実践的なコードを見てみましょう。DartといえばFlutter、ということで、Flutterでデバイスのライフサイクルを検知するコードを眺めてみます。

重要な部分はここですね👀

class (省略) extends State<(省略)> with WidgetsBindingObserver {
  ...

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    if (WidgetsBinding.instance.lifecycleState != null) {
      // 処理
    }
  }

  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      // 処理
    });
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  ...
}

このコードはWidgetsBindingObserverというmixinwithしています。このWidgetsBindingObserverはアプリのライフサイクルが変化する度にdidChangeAppLifecycleStateをコールするため、その状態変化 (AppLifecycleState) を監視することができます。

たとえばAppLifecycleState.resumedをチェックすればフォアグラウンドを検知できるというわけですね。

このように、WidgetsBindingObserverという機能をクラスに混ぜ込み、簡単にその処理を拡張することができます。もちろん、複数箇所からクラス階層をまたいで参照できる、という点も大きいですね!

まとめ

今回はmixinのコンセプトと具体的なコードを紹介しました。より具体的にメリットを書き出すと

  1. 処理の再利用性
    • 様々なクラスで共通機能を使い回すことが可能
    • コードの重複を避けることができ、保守性が向上する
  2. 拡張性の向上
    • 新しい機能をクラスに追加する際、既存のクラス階層を変更することなく柔軟に拡張できる

となるでしょうか🤔

少なくとも今後はサンプルコードを恐怖心なく読めるようになれそうです😌

脚注
  1. Carは車という概念と、車ならば当然なせること (drive) というセットで存在していました。しかし「機能」はそれだけでは存在できません。「空を飛ぶ」という行為を存在すると、鳥や飛行機が空を飛んでいる様子を想像するはずで、その概念自体を想像する人はいないでしょう。 ↩︎

  2. mix inは「加える」、「かき混ぜる」という動詞ですから、私ははじめmixinも行為を指すものだと勘違いしてしまいました🫠 mixinはクラスに近い概念であって、mixinwithするもの、ということに注意してください。 ↩︎

Discussion