実はオブジェクト指向ってしっくりきすぎるんです! 不変オブジェクトのすゝめ。
バグのないソフトウェアを作りたい
お仕事では主にVB.NETとC#を。趣味のプログラミングでは関数型言語F#を利用しています。私自身のF#スキル(関数型的な考え方)は、まだまだ実践レベルとはとても言えないシロモノだけど、
面白い発見と多くの可能性を感じられる言語なので、F#はさわっていてとても楽しい。
私はこれまでオブジェクト指向言語によるオブジェクト指向プログラミングをこよなく愛してきました。
というのも、「いかにバグを減らすか」、「バグのないソフトウェアを作ること」が私の最大の関心事だからです。
バグの多いコード、あるいは技術的負債の多いコードというのは、コスト的な問題があるばかりか、
開発者の身体や心までもを不健康にし、われわれに大きな不幸をもたらすことを経験的にわかっているからです。
わたしにとってオブジェクト指向技術は、それらの問題を防いだり解決をする手段として適した技術でした。
そしてまた、私が関数型言語を学ぶ理由も、やはりバグのないソフトウェアを作りたいからに他なりません。
健康でありたい。幸せでありたい。そして美しいコードが好きだからです。
関数型言語の小さいようで小さくない、とても大きなウマみ
関数型言語に初めて触れたのはHaskellでした。もう2年以上前になります。.NET Framework に LINQ が登場し、C#でラムダ式を少しずつ利用するようになった頃、
「本物のプログラマはHaskellを使う」という*1記事を読んだのがきっかけでした。
私が知るHaskellで最も特徴的なところと言えば、「遅延評価」およびIOがファーストクラスであること。
あるいはモナドの存在ですが、Haskellを知ってなにより私の目を引いたのは、
「状態の変化(あるいは再代入)という副作用が存在しないので無用なバグを未然に防げる」というところです。
これはHaskell特有のものというわけでもなく、一般的な関数型言語に備わっています。
JavaやVB.NET、C#等のオブジェクト指向言語では、ValueObject(不変オブジェクト)を実装しなければ実現できなかったことが、
関数型言語では、強力で且つ簡単にimmutable表現が可能になることに感動しました。
また、これは関数の再利用性の高さを意味していました。
「これを使えば些細な副作用にびくびくすることがなくなるぞ!すばらしい!」
というのが、私が最初に感じた、関数型言語の小さいようで小さくない、とても大きなウマみです。
バグの多いソフトウェアというのは往々にして副作用の多いプログラムなので、
副作用の少なさを促進することはとても魅力です。
関数プログラミングとオブジェクト指向の長所を組み合わせたプログラミングスタイルを
F#を書くときは、なるべく関数プログラミングな実装を意識しています。でもふと気が付くと、オブジェクト指向な頭に切り替わっている自分がいます。
私がまだ関数型に不慣れなせいもあるのか、オブジェクト指向では当たり前に表現できることが関数型では難しく感じます。
その表現方法(発想をすること)が難しい。というのももちろんありますが、
仮に関数型な書き方をひらめいたとしても、可読性や拡張性、可用性などの面で長期的に考えると、
"関数型よりもオブジェクト指向での表現を選択した方がよい"と感じる場面があります。
関数型言語で書いていても「自分でクラスを作ってオブジェクト指向っぽいことをしたくなる。」ことのなんと多いことか!
関数型言語およびその考え方は、これからのソフトウェア開発では間違いなく必要とされてくる必須技術と感じているけど、
やはりオブジェクト指向もまだまだ手放せません。オブジェクト指向はプログラミングの基本なんです。
いい具合に枯れているんです。
そう。オブジェクト指向ってしっくりきすぎるんです!
あなたのオブジェクト指向には、ValueObject(不変オブジェクト)はありますか?
ところで、あなたのオブジェクト指向には、ValueObject(不変オブジェクト)はありますか?
もしこれを知らない。あるいはその利用が極端に少ないと言うのであれば、
あなたは、オブジェクト指向プログラミングをまだまだ実践出来ていない可能性が高いでしょう。
ThoughtWorksアンソロジー
ThoughtWorksアンソロジーで提案されている「オブジェクト指向エクササイズ9つのルール」の中に、
「すべてのプリミティブ型と文字列型をラップすること」というルールがあります。
現実的に、プリミティブ型と文字列型そのすべてをラップすることは難しいと感じられるかもしれません。
実際、ソフトウェアの規模が大きくなるほどに難しいでしょう。これはなかなか刺激的なルールです。
すべては無理でも、特に重要と感じられる部分に導入するだけでも、
ValueObject(不変オブジェクト)はとても大きな効果を発揮します。
設計、実装、運用、保守などあらゆる面で格段に有利になります。
必ず「不変オブジェクトとして存在するよう設計できないか」を検討する
不変オブジェクトとは、生成されて以降は値および状態が変化することのないオブジェクトです。不変オブジェクトを導入することは、状態変化のある可変オブジェクトを主体とするよりもいくつかの利点があります。
まず、不変オブジェクトは状態が変更されないのでシンプルです。そのオブジェクトを使用する開発者は、
内部状態の遷移を意識する必要がありません。つまり、プログラムの副作用を減らす効果があります。
単純なヒューマンエラーを減少させて、無用なバグの根絶に一役買います。
また、不変オブジェクトはスレッドセーフであり(複数のスレッドからの同時アクセスによりデータ矛盾が発生しない)、
データの複製(クローン)を考える必要がなくなります。キャッシュとして複数クライアントによるデータ共有も可能です。
もちろん、オブジェクトの生存期間の途中でハッシュ値が変わるとマズイというようなシチュエーションでも有効です。
不変オブジェクトには様々なメリットがあります。ですからオブジェクト指向でクラスを設計する際には、
まずそのクラスのインスタンスが「不変オブジェクトとして存在するよう設計できないか」を検討する必要があります。
理由があって可変オブジェクトとして設計する場合は、その可変部分の対象を出来る限り狭く設計するように心がけます。
不変オブジェクトと可変オブジェクトのパフォーマンスに関するトレードオフ
不変オブジェクトを利用すると可変オブジェクトを利用するに比べて
パフォーマンスが低下してしまう。などと言う神話がありますが、それは少し違います。
不変オブジェクトを用いて状態の変更を表現する場合は、そのオブジェクトそのものの状態は変更せずに、
新たに不変オブジェクトを生成する方法をとります。これは、状態変化が可能な可変オブジェクトであれば
生成する必要のないオブジェクトを新しく生成するため、高コストになってしまうのではないかというのが神話の起源です。
しかし、プログラム内でのオブジェクトの使用方法によっては、実際には不変オブジェクトを利用した方がパフォーマンス的にメリットがある場合も少なくありません。
例えば、不変オブジェクトはオブジェクトを防御的にコピーする(クローンの)必要がないため。可変オブジェクトに比べてパフォーマンスが期待できます。
逆に、頻繁に変更されるデータをモデル化している場合には、神話のとおりに不変オブジェクトはパフォーマンスの観点で不利になります。
つまり、そのオブジェクトがどのように利用されるのかによって、そのパフォーマンス性能は異なってくるというわけです。
一概にどちらが不利とは言えません。パフォーマンスに関しては不変オブジェクトと可変オブジェクトの間には、複雑なトレードオフがあります。
不変オブジェクトを導入する場合は、そのことを意識しなければならないことは理解しておく必要があります。
ValueObject(不変オブジェクト)の抽象表現
最後にC#で実装したValueObject(不変オブジェクト)の抽象表現をご覧いただきます。
不変オブジェクトを作成するのに非常に便利なクラスですが、リフレクションを利用しているので、
パフォーマンス的にはとても良いとは言えません。これが基で思うようなパフォーマンスがでないような場合は、
Equalsメソッド等を適宜オーバーライドし、リフレクションを用いない軽い実装をすることで対応します。
「派生クラスのフィールドがすべてreadonlyであるか」について契約する不変表明ができればより良いのですが、
現在のCode Contractsの仕様では不可能ですので、派生クラスにreadonlyではないフィールが存在する場合には、
コンストラクタ内でAssertするようにしています。
※id:YokoKenさんのご指摘でコードを一部修正しました。
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Diagnostics.Contracts; namespace ClassLibrary1 { /// <summary> /// ValueObject(不変オブジェクト)の抽象表現 /// </summary> /// <typeparam name="TSelf"></typeparam> [Serializable] public abstract class ValueObject<TSelf> : IEquatable<TSelf> where TSelf : ValueObject<TSelf> { public ValueObject() { var fields = GetFields(); foreach (var field in fields) { Contract.Assert(field.IsInitOnly,"immutableじゃないっぽいよ"); } } public override bool Equals(object obj) { var other = obj as TSelf; if (object.ReferenceEquals(other, null)) return false; return Equals(obj as TSelf); } public override int GetHashCode() { int hashCode = 11; int multiplier = 29; var fields = GetFields(); foreach (var field in fields.Select((x, i) => new { Value = x, Index = i })) { var value = field.Value.GetValue(this); if (value == null) continue; hashCode *= multiplier; hashCode += (value.GetHashCode() + field.Index); } return hashCode; } public virtual bool Equals(TSelf other) { if (other == null) return false; Type t = GetType(); Type otherType = other.GetType(); if (t != otherType) return false; var fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (var field in fields) { var x = field.GetValue(other); var y = field.GetValue(this); if (x == null && y != null) return false; if (!x.Equals(y)) return false; } return true; } protected IEnumerable<FieldInfo> GetFields() { Type t = GetType(); var fields = new List<FieldInfo>(); while (t != typeof(object)) { fields.AddRange(t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)); t = t.BaseType; } return fields; } public static bool operator ==(ValueObject<TSelf> x, ValueObject<TSelf> y) { if (object.ReferenceEquals(x, null) && object.ReferenceEquals(y, null)) return true; if (object.ReferenceEquals(x, null)) return false; return x.Equals(y); } public static bool operator !=(ValueObject<TSelf> x, ValueObject<TSelf> y) { return !(x == y); } } }
関数型言語についてもしっくりきたい今日この頃です。
次回は、実際にこのValueObject(不変オブジェクト)の抽象表現を利用した実装サンプルを紹介します。
*1:ある意味釣りともとれるタイトル