拡張メソッドを使ったtraitあるいはミックスイン(mixin)的なもの(その1)
「C#クックブック 第3版」をパラパラと見ていて拡張メソッドというものがあることを知りました。
これは使えそうだと思っていたところにScalaにはTraitsというものがあってこれが多重継承あるいはミックスイン(mixin)と同様に活用できるということで注目を集めていることを知りました。
C#でもインターフェイスに対して拡張メソッドを適用することでtrait(あるいは多重継承)と同様に活用することを考えてみました。
そこで試しに「多忙な Java 開発者のための Scala ガイド: trait と振る舞い」のページのプロパティの値の変化通知機能のtraitをC#の拡張メソッドで実装するサンプルを書いてみました。
(C#にはEventがあるので題材としては良くないですが比較ということで、、、)
ObservableSupportインターフェイスは型の宣言だけにしてObservableSupportExtentionクラスで拡張メソッドとして処理を定義しています。
SomeクラスはSuperSomeクラスを継承していますがイベント通知の機能も備えたいのでObservableSupportインターフェイスを実装することでObservableSupportExtentionクラスの処理を多重継承の様に取り込んでいます。
public interface ObservableSupport : IDisposable { } public static class ObservableSupportExtention { private static Dictionary<object, List<Action<string, object>>> listeners = new Dictionary<object, List<Action<string, object>>>(); public static void AddPropertyChangeListener(this ObservableSupport obj, Action<string, object> pcl) { if (!listeners.ContainsKey(obj)) { listeners.Add(obj, new List<Action<string, object>>()); } listeners[obj].Add(pcl); } public static void RemovePropertyChangeListener(this ObservableSupport obj, Action<string, object> pcl) { if (!listeners.ContainsKey(obj)) { return; } listeners[obj].Remove(pcl); } public static void FirePropertyChange(this ObservableSupport obj, string name, object newValue) { if (!listeners.ContainsKey(obj)) { return; } listeners[obj].ForEach(pcl => pcl(name, newValue)); } public static void RemoveObservableSupport(this ObservableSupport obj) { if (!listeners.ContainsKey(obj)) { return; } listeners[obj].Clear(); listeners.Remove(obj); } } public class SuperSome { public void SayHello() { Trace.WriteLine("Hello!"); } } public class Some : SuperSome, ObservableSupport { public Some() { property1 = 1; property2 = "Hello!"; } private int property1; public int Property1 { get { return property1; } set { bool isChange = (property1 != value); property1 = value; if (isChange) { this.FirePropertyChange("Property1", value); } } } private string property2; public string Property2 { get { return property2; } set { bool isChange = !property2.Equals(value); property2 = value; if (isChange) { this.FirePropertyChange("Property2", value); } } } public void Dispose() { this.RemoveObservableSupport(); } } class Program { static void Main(string[] args) { using (Some o1 = new Some()) { o1.AddPropertyChangeListener((name, newValue) => Trace.WriteLine("o1 : " + name + " is changed! : " + newValue.ToString())); o1.Property1 = 2; o1.Property2 = "Good bye!"; o1.SayHello(); } using (Some o2 = new Some()) { o2.AddPropertyChangeListener((name, newValue) => Trace.WriteLine("o2 : " + name + " is changed! : " + newValue.ToString())); o2.Property1 = 5; o2.Property2 = "How are you!"; o2.SayHello(); } } }
実行結果は以下の通りです。
o1 : Property1 is changed! : 2
o1 : Property2 is changed! : Good bye!
Hello!
o2 : Property1 is changed! : 5
o2 : Property2 is changed! : How are you!
Hello!
拡張メソッドではインスタンス変数を使えないのが悩ましい*1ところですがまあ大体こんなところ*2だと思います。
この検証からtraitを使っても静的型言語で型階層構造に組み込む形で機能の再利用を行うものということであれば、ちょっと手間がかかってスマートでないとしても従来からの手段で代替えできるのでそれほどメリットは感じられません。
しかしtraitで「第4回 Scala言語を探検する(2)(2ページ目) | 日経 xTECH(クロステック)」にあるように既存のクラスを拡張しながらミックスインとして型階層に関係なく後付けでスコープを限定して機能が追加できるのであれば従来と違った構造設計の可能性を感じます。
今回の検証でミックスインそしてtraitと.NETの拡張メソッドについての理解が深まった気がします。
この点に関してはまた改めて書きたいと思います。
[2010-04-05]追記:
結局上記の例はtraitの検証コードとしても良くなかったと思います。
やはりインスタンス変数はインスタンスに持たせるべきだと思います。
この課題の解決もそうですが、traitの検証としては「拡張メソッドを使ったtraitあるいはミックスイン(mixin)的なもの(その3) - Jamzzの日々」に書いた「第4回 Scala言語を探検する(2)(2ページ目) | 日経 xTECH(クロステック)」にある「Traitでアスペクト指向を簡易に実現」の例の方が良いと思います。
興味がある方は「拡張メソッドを使ったtraitあるいはミックスイン(mixin)的なもの(その3) - Jamzzの日々」こちらの方を見てください。
あとひょっとしたら参考にされる方がいらっしゃるかもしれないと思うので上記の例をtraitにとらわれないでC#らしく実現するコードを以下に書きます。
C#では言語機能としてeventがあるのでインターフェイスや拡張メソッドなどにより機能を提供する必要がありません。
class SuperSome { public void SayHello() { Trace.WriteLine("Hello!"); } } class Some : SuperSome { public event Action<string, object> Listeners; public Some() { property1 = 1; property2 = "Hello!"; } private int property1; public int Property1 { get { return property1; } set { bool isChange = (property1 != value); property1 = value; if (isChange) { Listeners("Property1", value); } } } private string property2; public string Property2 { get { return property2; } set { bool isChange = !property2.Equals(value); property2 = value; if (isChange) { Listeners("Property2", value); } } } }