仕事としてプログラミングしてて、もう10年来ずっと、いたるところでレガシー化しやすく、テストコードを書けなくしてる最大の原因と思うのがSingletonデザインパターンだと個人的には思う。
ただ、最近DIコンテナなど扱うようになってきて、ようやくSingletonデザインパターンの使い方でGoodなパターンとBadなパターンの二種類が見えてきた。
Javaでかけば以下のようなコード。
public class MyClass { private static final MyClass instance = new MyClass(); private MyClass() { // constructor } public static MyClass getInstance() { return MyClass.instance; } }
何が問題なのか:
なんちゃってアノテーションつければこんな感じ。
@Singleton(scope=Application) public class MyClass { private final ApplicationContext appContext; public MyClass(@Inject final ApplicationContext appContext) { $this->appContext = appContext; // ... } }
利用側:
// ... ApplicationContextImpl appContext = new ApplicationContextImpl(); // ... DIContainer dicon = DIContainer.create(appContext); MyClass singleton = dicon.get(MyClass.class);
どう問題は解決されたのか:
デメリット:
Badパターンでは「インスタンスを一つに制限する」を言語のルールで実現できていたが、逆にGoodパターンではそれを諦めている。
それでは本末転倒ではないのか?
否、「インスタンスを一つに制限する」のは、「何に対して」であるのか、という点が今まではおろそかになっていたと考える。
その前に、そもそもそうした「ルール」をどこで担保するのかについて考えなければならない。
Badパターンでは「ルール」の担保を言語側で担保していた。これは非常に強固で確実だが、同時にテストコードを書くときの障害になる。
個人的には、テストコードを書きづらい設計はどんなにデザインパターン上「正しく」ても、最低最悪のクソコードである。
テストコードがかければ、リファクタリングできる。実装コードを、洗練できる。
しかし、テストコードが書けなければ、リファクタリングする機会が大幅に失われる。実装コードを洗練することが難しくなる。
プログラミングは創造的な作業であるのは確かで、最初から100%の正しさでクラス構造をデザインできることはまずありえない。必ずリファクタリングをする時が来る、あるいは振る舞いを変えるレベル=リファクタリングを超えるレベルで設計を変更する時が来る方が自然と考えたほうが良い。(これについては賛否両論ありそうだけど。)
よって、OOPだろうが関数型だろうが手続き型だろうが、リファクタリングできないソース、端的にはテストコードが書けない設計はどんなにその他が正しく作れれていも、クソコードだと断言する。
ここでSingletonの話に戻ってくると、BadパターンがなぜBadパターンか、それはテストコードを書きづらくて、大抵は書くのをギブアップするような状況になるのが理由となる。(これがconstructorがpublicで、デフォルト実装をsingleton化してるだけで、作ろうと思えば派生クラスとかで色々いじれる状況なら、テストコード用にMock化できるので許容範囲)
つまり自分の意見としては、「インスタンスを一つに制限する」のがテストコードを書きづらくするのなら、そんなルールはゴミ箱に捨てろ。それよりもテストコードを書きやすくしろ、そして書け、書いたテストコードがサンプルコードになり、後続がそれを見て使い方を学べるようにしろ、ルールの担保はコードレビューで担保しろ。ということになる。
そして、「何に対して」インスタンスを一つに制限するのかだが、これは普通に考えればアプリケーションのライフサイクルに揃えるのがSingletonの意図として順当と思われる。
となれば、アプリケーションの開始から終了までの、様々なインスタンスを収めた「文脈=Context」的なインスタンスと同じ長さになるだろうし、またSingletonは左記の「文脈=Context」に含まれるのが順当だろう。
よって、Goodパターンではテストコードの書きやすさを担保しつつ、アプリケーションのライフサイクル(Goodパターンで挙げた例ではDIコンテナ)に合わせて管理できるようにした。これなら、テストコード中でプログラマブルにライフサイクル管理がしやすくなる。
自分の意見になるが、「テストコードを書ける設計になっていること」が最重要となる。
よって、Singletonデザインパターンを実装する場合もテストコードを書ける設計にすることがまず大前提となる。
しかし、Mock化などを考えると言語レベルでの「一つに制限」する「ルール」は採用できない。
そうなると慣れてないプログラマが自分でインスタンスを作ってしまったりする可能性があるが、そのリスクはどう見るか?
そのリスクは、「テストコードによる例示とコードレビューによる担保」で回避すればよい、というのが自分の意見になる。
重要なのは、「何に対してインスタンスを一つに制限しなければならないのか」を明示すること。
実際にDIコンテナやアプリケーションのcontextインスタンスを作り、そこからSingletonインスタンスを取り出すのをテストコードで表現すること。それができる設計にすること。
そうしておくことで、後続のプログラマに対して、そのクラスの使い方、Singletonとしての存在意義をテストコードで表現できる。
もし間違って独自にインスタンスを作るようなコードを書いた場合は、コードレビューで指摘して直してもらう。その際に、テストコードを示してSingletonとしての使い方、アクセス方法、テストコードの書き方を例示することで、後続のプログラマの勉強にもつながる。
もう一度書くと、テストコードを書く障害になるような設計は、どんなに他が正しくてもゴミ箱ゆきというのがここ数年における自分の意見なので、それに基づくと上記のようになった。
以上。
コメント