DartのMixinをコンセプトから整理する
背景
近頃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
がある -
FlyableCar
はCar
にfly
という「機能」を追加したものである
ということですね。この「機能を追加する」という部分は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
というmixin
をwith
しています。このWidgetsBindingObserver
はアプリのライフサイクルが変化する度にdidChangeAppLifecycleState
をコールするため、その状態変化 (AppLifecycleState) を監視することができます。
たとえばAppLifecycleState.resumed
をチェックすればフォアグラウンドを検知できるというわけですね。
このように、WidgetsBindingObserver
という機能をクラスに混ぜ込み、簡単にその処理を拡張することができます。もちろん、複数箇所からクラス階層をまたいで参照できる、という点も大きいですね!
まとめ
今回はmixinのコンセプトと具体的なコードを紹介しました。より具体的にメリットを書き出すと
- 処理の再利用性
- 様々なクラスで共通機能を使い回すことが可能
- コードの重複を避けることができ、保守性が向上する
- 拡張性の向上
- 新しい機能をクラスに追加する際、既存のクラス階層を変更することなく柔軟に拡張できる
となるでしょうか🤔
少なくとも今後はサンプルコードを恐怖心なく読めるようになれそうです😌
Discussion