binllionを作っていて1つのstructが大きくなりすぎたり、テストが書きづらくて全く書いてなかったりとイケテナイ感がでてきていました。
なにか解決方法はないかと調べてみるとSOLID原則というものがあることを知りました。
元々はオブジェクト指向プログラミング向けのガイドラインのようなものらしいのでRustで使う際にそのまま適用はできなさそうですが、どのように当てはめればよいのか考えてみたいと思います。
単一責任の原則 (Single-responsibility principle)
クラスは一つの責任だけを持つべき、多くの責務を持つべきでない。
これは言語に関係なく適用できます。機能を分割して部品単位で作っていくようなイメージですね。
開放閉鎖の原則(Open/closed principle)
クラスはどんどん拡張できるようになっているべき、ただしそれに対して修正が発生しても影響は限定的になるべき。
関数やメソッドはガンガン追加していけるけど、それらで依存関係を持ってしまうとNG、みたいなことでしょうか。例えば、関数Aは関数Bを内部で呼び出す、と。関数Bに修正が入ると関数Aに影響が出るから原則に反している、という理解ですかね。
継承しているときの親クラスの関数やメソッドの変更の子クラスへの伝播の話のようにも思えます。
関数やメソッドはなるべく独立して動作するように設計すべしということですね。これは言語に関係なく適用できます。
リスコフの置換原則(Liskov substitution principle)
親クラスと子クラスは同じ機能を提供できなくてはならない、つまり置き換え可能であるべき。
オブジェクト指向プログラミングの継承の話だと思いますが、Rustを含めた近代的プログラミング言語では継承はあまり使わないほうが良いという考え方になっていると思います。
Rustにも継承の機能はありますがtraitで機能を保証していくのが一般的でしょうか。
インターフェース分離の原則 (Interface segregation principle)
何でもできる汎用インターフェイスよりも機能別のインターフェイスがよい。
単一責任の原則のインターフェイスバージョンみたいなことでしょうか。全てのクラスをカバーするようなインターフェイスではなく、各クラスそれぞれのインターフェイスをつくるようにすればよいと。
これは言語に関係なく適用できます。
依存性逆転の原則(Dependency inversion principle)
モジュールAからモジュールBを直接呼び出すな、間に抽象的な層(インターフェイス)を挟め。つまりモジュールA –> インターフェイス –> モジュールB、とすればどちらも抽象的なインターフェイスに依存する形になる。
これは理解が難しかったのですが、モジュールBで使用しているライブラリやクレートを何らかの理由(例えば開発終了など)で別のものに置き換えた場合、モジュールAに影響が出ないようにインターフェイスに合うように作り直すようなケースで考えると納得がいきました。
Rustであればクレート間での呼び出しはtraitを経由して呼び出すような設計にして、traitは徹底的に抽象化するような感じですね。
まとめ
- 関数やインターフェイスは粒度を細かくするべき
- 大きな機能は作らないで小さな機能に分けて関数やメソッドを作るべき
- インターフェイスも同様
- 抽象的なインターフェイス層を通じて機能を呼び出すべき
- モジュール間
- traitを活用すべし
- 継承は使わない
- 関数やメソッド間で依存関係ができないようにする
こうすることで結果的に密結合にならず独立性が高めることができるということですね。部品化することでテストも書きやすくなりそうです。
Rustではtraitを活用してインターフェイス層を作って、モジュールをクレート化することで独立性を高めることができそうです。
クレートはcargoのworkplaceを使うとクレートをまとめ上げることができるようです。こちらを使って再設計してみたいと思います。