双方向関連の実装
Posted by mkamo on 2009年9月5日
自作ライブラリから双方向関連を実装するためのユーティリティを紹介してみる.
クラスFooとクラスBar間に1対1の双方向関連があるとき,Fooが参照するBarとBarが参照するFooは,以下の図のようにそれぞれお互いを参照するような状態になっていなければならない.
しかしFooとBar間の関連を深く考えずに実装をしてしまうと,以下の図のような「foo1: Fooはbar1: Barを参照しているのに,bar1: Barはfoo1: Fooではなくfoo2: Fooを参照している」といった,双方向関連として整合性の取れていない状況に簡単になってしまう.
この記事では,双方向関連の整合性の取れていない状況になりづらくするためのユーティリティを紹介する.
ユーティリティのソースコードは以下の通り.EnsureAssociation()メソッドが双方向関連を保証する.
public static class AssociationUtil {
public enum EnsureResult {
None, /// 何も行われず,関連は変わらなかった
Set, /// 関連が設定された
Unset, /// 関連が解除された
}
public static EnsureResult EnsureAssociation<T>(
T oldValue,
T newValue,
Action<T> fieldSetter,
Action<T> inverseAssociator,
Action<T> inverseUnassociator
)
where T: class
{
if (oldValue == newValue) {
return EnsureResult.None;
}
if (oldValue != null) {
fieldSetter(null);
inverseUnassociator(oldValue);
}
fieldSetter(newValue);
if (newValue != null) {
inverseAssociator(newValue);
return EnsureResult.Set;
} else {
return EnsureResult.Unset;
}
}
}
使い方は以下のとおり.単純なプロパティのsetterの実装では _bar = value; などとするところをEnsureAssociation()メソッドを呼び出すようにしている.これでBarプロパティやFooプロパティを通して値を設定する限り,必ずFooとBar間の双方向関連の整合性が取れた状態になる.(Fooクラス内で_barの値を直接変更した場合などは整合性が崩れてしまうので,そのようなコードには注意しなければならない.)
private class Foo {
private Bar _bar;
public Bar Bar {
get { return _bar; }
set {
AssociationUtil.EnsureAssociation(
_bar,
value,
bar => _bar = bar,
bar => bar.Foo = this,
bar => bar.Foo = null
);
}
}
}
private class Bar {
private Foo _foo;
public Foo Foo {
get { return _foo; }
set {
AssociationUtil.EnsureAssociation(
_foo,
value,
foo => _foo = foo,
foo => foo.Bar = this,
foo => foo.Bar = null
);
}
}
}
これらのクラスを使ったときの振る舞いを示すテストコードは以下のとおり.FooとBar間の双方向関連の整合性が常に取れていることがわかる.
[TestMethod]
public void TestEnsureAssociation() {
var foo1 = new Foo();
var bar1 = new Bar();
var bar2 = new Bar();
Assert.AreEqual(null, foo1.Bar);
Assert.AreEqual(null, bar1.Foo);
Assert.AreEqual(null, bar2.Foo);
foo1.Bar = bar1;
Assert.AreEqual(bar1, foo1.Bar);
Assert.AreEqual(foo1, bar1.Foo);
foo1.Bar = bar2;
Assert.AreEqual(bar2, foo1.Bar);
Assert.AreEqual(null, bar1.Foo);
Assert.AreEqual(foo1, bar2.Foo);
bar2.Foo = null;
Assert.AreEqual(null, foo1.Bar);
Assert.AreEqual(null, bar1.Foo);
Assert.AreEqual(null, bar2.Foo);
}