外部からはReadOnlyにして安全な設計を実現する
TODO:めっちゃ面白い導入部を考える
あるオブジェクトに対して「読み込みと書き込みをする」機能と「読み込みだけをする」機能をそれぞれ切り出して公開する(=書き込みする必要のない人には読み込みする機能だけを公開する)ようにしておくと、オブジェクトの依存関係を(全部公開するのに比べて)減らすことができるようになります。
「読み込みと書き込みをする」機能というのは一種の比喩で、かっこいい言い方をすると「副作用のあるインターフェース」ということになります。
読み取り専用のプロパティ
単純にプロパティにアクセス権の差をつけるだけでも、何もしない(全部public)よりは効果があります。大抵のケースではこれだけで十分です。
public class PropertyExample
{
public Something Item { get; private set; }
}
読み取り専用のコレクション
コレクションを公開するプロパティをgetterだけに限定しても、コレクションを参照して副作用のあるインスタンスメソッドを呼ばれてしまっては元も子もないです。
Listの場合
List<T>
は読み取り専用のインターフェースだけをあつめたIReadOnlyList<T>
を実装しているので、外から変更されたくないがコレクションは公開したい場合はIReadOnlyList<T>
にキャストした状態で公開するといいです。
もちろん、呼び出し元でList<T>
に強制的にキャストすればいじれてしまいます。プログラム的にこれを制限する方法はないように思うので、悪いプログラマがチームに紛れ込まないようにするしかないです(悪いプログラマがチームに紛れ込むと設計や言語仕様ではどうにもならないのはC#に限った話ではない…)。
public class ListExample
{
public IReadOnlyList<Something> Items => this.ItemsSource;
private List<Something> ItemsSource { get; set; } = new List<Something>();
}
ObservableCollectionの場合
List<T>
はReadOnly版のinterfaceがありますが、ObservableCollection<T>
(更新通知付きコレクション)にはありません。こちらの場合、interfaceではなく別途ReadOnlyObservableCollection<T>
のインスタンスを作って元ネタのObservableCollectionを指定してやる、というのが必要になります。
public class ObservableCollectionExample
{
public ReadOnlyObservableCollection<Something> Items { get; }
private ObservableCollection<Something> ItemsSource { get; set; } = new ObservableCollection<Something>();
public ObservableCollectionExample()
{
this.Items = new ReadOnlyObservableCollection<Something>(this.ItemsSource);
}
}
公開範囲をもっと細かく設定する
設計的に一番ごちゃごちゃしないのは「クラスの外部に副作用のあるインターフェース(プロパティ、メソッド)を公開しない」なんですが、それだと設計コストがめちゃ高くなるので徹底するのは難しいです。っていうか現実的に可能なんですかね?
経験則として、あるオブジェクトの「状態を知りたい」人と、「状態を変更したい」人では、前者の方が数が多いので、オブジェクト自体は同じでも公開先に応じて公開するものを切り替えられるようになると小回りがきいて便利です。
そういうときには、先に示したList<T>
とIReadOnlyList<T>
のように、同じクラスで読み取りの操作だけを公開するinterface
と書き込み操作も公開するinterface
の2つを実装するようにして、公開先に応じてどちらのinterfaceを見せるか切り替えればよいです。
/// <summary>
/// 副作用のない操作だけを公開するReadOnlyなInterface
/// </summary>
public interface IReadOnlyMultistageExample
{
/// <summary>
/// 副作用のないプロパティ取得
/// </summary>
Something Item { get; }
/// <summary>
/// 副作用のないメソッド呼び出し
/// </summary>
Something GetSomething();
}
/// <summary>
/// 副作用のある操作を公開するInterface
/// </summary>
public interface IMultistageExample : IReadOnlyMultistageExample, IDisposable
{
/// <summary>
/// 副作用のあるプロパティ設定
/// </summary>
new Something Item { get; set; }
/// <summary>
/// 副作用のあるメソッド呼び出し
/// </summary>
Something DoSomething(Something arg);
}
/// <summary>
/// 実体クラス
/// </summary>
public class MultistageExample : IMultistageExample
{
/// <summary>
/// プロパティ
/// </summary>
public Something Item { get; set; }
/// <summary>
/// 副作用のあるメソッド呼び出し
/// </summary>
public Something DoSomething(Something arg)
{
throw new NotImplementedException();
}
/// <summary>
/// 副作用のないメソッド呼び出し
/// </summary>
public Something GetSomething()
{
throw new NotImplementedException();
}
/// <summary>
/// オブジェクトの破棄
/// </summary>
public void Dispose()
{
throw new NotImplementedException();
}
}
どっちのinterfaceで取得したかによって、呼び出せる操作の範囲が変わります。
MultistageExample implement = new MultistageExample();
{
IMultistageExample ime = implement;
ime.Item = new Something();
var item = ime.Item;
ime.DoSomething(new Something());
item = ime.GetSomething();
ime.Dispose();
}
{
IReadOnlyMultistageExample irome = implement;
// irome.Item = new Something(); コンパイルエラー
var item = irome.Item;
// irome.DoSomething(new Something()); コンパイルエラー
item = irome.GetSomething();
// irome.Dispose(); コンパイルエラー
}