昨日会社のメンバーからコーディングについて相談を受けました。
話を聞いていると、オブジェクト指向設計を利用してコードをリファクタリングしたい様子でした。
彼は頑張ってインターフェースやクラスを自分で定義していたんですが、ちょっとぎこちない設計だったので、おいらはStateパターンとFactoryパターンらしきテクニックを使って、改善のお手伝いをしてみました。
そのときの事例をめちゃくちゃデフォルメして説明すると、こんな感じになります。
プログラムはStateに応じて文字や色を変更します。
ボタンを押すとStateに応じて文字や色が変わります。
メンバーが最初に書いていたプログラムのイメージはこんな感じです。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace WindowsApplicationSandbox { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void buttonNormal_Click(object sender, EventArgs e) { this.ChangeTextBox("Normal"); } private void buttonWarning_Click(object sender, EventArgs e) { this.ChangeTextBox("Warning"); } private void buttonError_Click(object sender, EventArgs e) { this.ChangeTextBox("Error"); } private void ChangeTextBox(string state) { this.textBox1.Text = state; switch (state) { case "Normal": this.textBox1.BackColor = SystemColors.Window; this.textBox1.ForeColor = SystemColors.WindowText; break; case "Warning": this.textBox1.BackColor = Color.Yellow; this.textBox1.ForeColor = SystemColors.WindowText; break; case "Error": this.textBox1.BackColor = Color.Red; this.textBox1.ForeColor = Color.White; break; default: break; } } } }
switch文で条件分岐していますね〜。
さあ、これをオブジェクト指向プログラミングのテクニックを使って、条件分岐をなくしてみましょう。
それではまず、このプログラムをこんな感じのクラス構成に分割します。
条件分岐にあたる部分をStateFactoryクラス、Stateクラス、それとStateクラスのサブクラスで実現します。
Stateクラスは抽象クラスです。
各Stateの親クラスになるのと同時に、テキストボックスに文字や色を設定します。
ただし、Stateに応じて変化する文字や色はサブクラスに返してもらうため、抽象メソッド(抽象プロパティ)になっています。
internal abstract class State { internal void ChangeTextBox(TextBox textBox) { textBox.Text = this.Text; //値はサブクラスから受け取る textBox.BackColor = this.BackColor; //値はサブクラスから受け取る textBox.ForeColor = this.ForeColor; //値はサブクラスから受け取る } protected abstract String Text { //具体的な値はサブクラスで定義 get; } protected abstract Color BackColor { //具体的な値はサブクラスで定義 get; } protected abstract Color ForeColor { //具体的な値はサブクラスで定義 get; } }
Stateクラスのサブクラスはこんな感じです。
抽象メソッドを実装し、Stateごとに変わる文字や色だけを返します。
internal class NormalState : State { protected override string Text { get { return "Normal"; } } protected override Color BackColor { get { return SystemColors.Window; } } protected override Color ForeColor { get { return SystemColors.WindowText; } } }
他のサブクラスも同じように実装します。
internal class WarningState : State { protected override string Text { get { return "Warning"; } } protected override Color BackColor { get { return Color.Yellow; } } protected override Color ForeColor { get { return SystemColors.WindowText; } } } internal class ErrorState : State { protected override string Text { get { return "Error"; } } protected override Color BackColor { get { return Color.Red; } } protected override Color ForeColor { get { return Color.White; } } }
クラスを利用する側はどのサブクラスを使うべきかを意識しないように工夫します。
また、条件分岐も使いません。
それなのになぜ処理を分けられるのかというと、オブジェクト指向で重要な「ポリモーフィズム」が使われているからなんですね。
で、ボタンをクリックしたときの処理はこんな感じになります。
private void buttonWarning_Click(object sender, EventArgs e) { //クラスではなくプロパティを選択。分岐もなし this.ChangeTextBox(StateFactory.Warning); } private void ChangeTextBox(State state) { //stateの中身はEmptyStateクラス、WarningStateクラス、ErrorStateクラスのいずれか //どのクラスが渡されるかは実行時に決まる(=ポリモーフィズム) state.ChangeTextBox(this.textBox1); }
どのサブクラスを利用すべきかはStateFactoryクラスにカプセル化してあります。
internal static class StateFactory { private static readonly NormalState normal = new NormalState(); private static readonly WarningState warning = new WarningState(); private static readonly ErrorState error = new ErrorState(); //戻り値の型を親クラス(Stateクラス)にしてあるのがポイント //戻り値の型を抽象化しておくことで、呼び出し側はどのサブクラスも受け入れ可能になる internal static State Normal { get { //NormalStateクラスを返す return normal; } } //戻り値の型は親クラス internal static State Warning { get { //WarningStateクラスを返す return warning; } } //戻り値の型は親クラス internal static State Error { get { //ErrorStateクラスを返す return error; } } }
Warningに変更する場合の実行イメージをシーケンス図で表すとこんな感じです。
完成したプログラムの全体像はこんな感じになります。
このプログラムは最初のプログラムと全く同じ動作をします。
条件分岐がどこにもないのが分かりますか?
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace WindowsApplicationSandbox { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void buttonNormal_Click(object sender, EventArgs e) { this.ChangeTextBox(StateFactory.Normal); } private void buttonWarning_Click(object sender, EventArgs e) { this.ChangeTextBox(StateFactory.Warning); } private void buttonError_Click(object sender, EventArgs e) { this.ChangeTextBox(StateFactory.Error); } private void ChangeTextBox(State state) { state.ChangeTextBox(this.textBox1); } } internal abstract class State { internal void ChangeTextBox(TextBox textBox) { textBox.Text = this.Text; textBox.BackColor = this.BackColor; textBox.ForeColor = this.ForeColor; } protected abstract String Text { get; } protected abstract Color BackColor { get; } protected abstract Color ForeColor { get; } } internal class NormalState : State { protected override string Text { get { return "Normal"; } } protected override Color BackColor { get { return SystemColors.Window; } } protected override Color ForeColor { get { return SystemColors.WindowText; } } } internal class WarningState : State { protected override string Text { get { return "Warning"; } } protected override Color BackColor { get { return Color.Yellow; } } protected override Color ForeColor { get { return SystemColors.WindowText; } } } internal class ErrorState : State { protected override string Text { get { return "Error"; } } protected override Color BackColor { get { return Color.Red; } } protected override Color ForeColor { get { return Color.White; } } } internal static class StateFactory { private static readonly NormalState normal = new NormalState(); private static readonly WarningState warning = new WarningState(); private static readonly ErrorState error = new ErrorState(); internal static State Normal { get { //NormalStateクラスを返す return normal; } } internal static State Warning { get { //WarningStateクラスを返す return warning; } } internal static State Error { get { //ErrorStateクラスを返す return error; } } } }
というわけで、サンプルは以上になります。
ところで「条件分岐はなくなったけどめちゃくちゃコード長くなってるじゃん!」とか「元の方がはるかにわかりやすい!」と思われるかもしれません。
こんな風に元のプログラムがシンプルすぎるとオブジェクト指向のありがたみが伝わりにくいですね。
このへんがオブジェクト指向を本やネットで教えにくい理由なんじゃないかと思います。
実装すべきロジックが複雑であればあるほどオブジェクト指向のメリットを発揮しやすいですが、その分文章では伝えにくくなってしまいます。
だからおいらは以前から「オブジェクト指向を習得するには実際のプロジェクトに携わってオブジェクト指向のメリットを体感するのが一番」だと考えています。
簡単なプログラムであれば従来の構造化プログラミングで十分です(今回のサンプルだってそう)。
しかし、プログラムが一定の複雑さを超えるとだんだんとブロックのネストやコードの重複が増え始めてきます。
こういう場合に構造化プログラミングを超える手法を利用する価値が出てきます。
switch文を一つなくしたぐらいじゃ大したことありませんが、数十、数百というif文やswitch文を持ち、あちこちでたくさんネストしまくっているプログラムから条件分岐がなくなったらどうでしょう?
ロジックは複雑なのにもかかわらず、すごくシンプルなプログラムになると思いませんか?
オブジェクト指向はそれ自体が構造化プログラミング以上に複雑なルールを持っていますが、それらのルールをうまく活用すればプログラムの複雑さを押さえ込むことが出来るのです。(ちょっと矛盾した表現に聞こえますけどね)
たとえば単純なデータであればExcelファイル上で管理できます。
しかし、管理されるデータや運用方法が複雑になってくるとExcelではうまく対処できない事態が発生するようになってきます。
そうなるとExcelでは力不足だということになり、リレーショナルデータベースを使った本格的なシステムに移行したりしますよね。
データベースはExcelよりも複雑なルール(テーブルやビュー、インデックス、SQL等)を持っていますが、それらのルールをうまく活用すれば効率よく情報管理ができます。
構造化プログラミングとオブジェクト指向プログラミングの関係もExcelとデータベースのような関係だ、と言えばちょっとは伝わる・・・かな??
なお、今回紹介したサンプルはあくまでオブジェクト指向を使うと条件分岐がなくなる、という話がメインです。
「こんなの厳密なデザインパターンに従っていない!」とか、「StateクラスがUIに依存しているのはおかしい!」とか、ツッコミどころはあると思いますが、そのあたりはどうかご勘弁を。。。
でもこういう手法を紹介したら、一緒に見ていた技術好きなメンバーから「こういうのは面白い」と言われたり、オリジナルの開発者から「どこで勉強したんですか?」と聞かれたりしました。
その反応が昔オブジェクト指向プログラミングに長けた先輩がいて、その人と一緒に仕事をしたときに感じた自分自身の感想にすごく重なりました。
おいらがその人に影響を受けてたくさん勉強し始めたのと同様、おいらも他のメンバーに影響を与えられる技術者になれれば、と思っております。