C#の不変オブジェクトパターン-どう思いますか? [閉まっている]
-
06-07-2019 - |
質問
いくつかのプロジェクトの過程で、不変(読み取り専用)オブジェクトと不変オブジェクトグラフを作成するパターンを開発しました。不変オブジェクトには、100%スレッドセーフであるという利点があるため、スレッド間で再利用できます。私の仕事では、Webアプリケーションでこのパターンを頻繁に使用して、構成設定や、メモリに読み込んでキャッシュする他のオブジェクトを作成します。キャッシュされたオブジェクトは、不意に変更されないことを保証するため、常に不変である必要があります。
もちろん、次の例のように不変オブジェクトを簡単に設計できます:
public class SampleElement
{
private Guid id;
private string name;
public SampleElement(Guid id, string name)
{
this.id = id;
this.name = name;
}
public Guid Id
{
get { return id; }
}
public string Name
{
get { return name; }
}
}
これは単純なクラスでは問題ありませんが、より複雑なクラスでは、コンストラクターを介してすべての値を渡すという概念を好みません。プロパティにセッターを設定する方が望ましいため、新しいオブジェクトを作成するコードは読みやすくなります。
では、セッターを使用して不変オブジェクトをどのように作成しますか?
まあ、私のパターンでは、オブジェクトは、1回のメソッド呼び出しでフリーズするまで、完全に変更可能として開始されます。オブジェクトが凍結されると、そのオブジェクトは永久に不変のままになります。再び変更可能なオブジェクトに変えることはできません。オブジェクトの可変バージョンが必要な場合は、単純に複製します。
さて、ここでいくつかのコードについて説明します。次のコードでは、パターンを最も単純な形式に要約しようとしています。 IElementは、すべての不変オブジェクトが最終的に実装する必要がある基本インターフェイスです。
public interface IElement : ICloneable
{
bool IsReadOnly { get; }
void MakeReadOnly();
}
Elementクラスは、IElementインターフェイスのデフォルトの実装です。
public abstract class Element : IElement
{
private bool immutable;
public bool IsReadOnly
{
get { return immutable; }
}
public virtual void MakeReadOnly()
{
immutable = true;
}
protected virtual void FailIfImmutable()
{
if (immutable) throw new ImmutableElementException(this);
}
...
}
不変オブジェクトパターンを実装するために、上記のSampleElementクラスをリファクタリングしましょう:
public class SampleElement : Element
{
private Guid id;
private string name;
public SampleElement() {}
public Guid Id
{
get
{
return id;
}
set
{
FailIfImmutable();
id = value;
}
}
public string Name
{
get
{
return name;
}
set
{
FailIfImmutable();
name = value;
}
}
}
MakeReadOnly()メソッドを呼び出してオブジェクトが不変としてマークされていない限り、IdプロパティとNameプロパティを変更できるようになりました。不変になると、セッターを呼び出すとImmutableElementExceptionが発生します。
最終メモ: 完全なパターンは、ここに示すコードスニペットよりも複雑です。また、不変オブジェクトのコレクションおよび不変オブジェクトグラフの完全なオブジェクトグラフのサポートも含まれています。完全なパターンを使用すると、最も外側のオブジェクトでMakeReadOnly()メソッドを呼び出すことにより、オブジェクトグラフ全体を不変にすることができます。このパターンを使用してより大きなオブジェクトモデルの作成を開始すると、漏れやすいオブジェクトのリスクが高まります。漏れやすいオブジェクトとは、オブジェクトに変更を加える前にFailIfImmutable()メソッドを呼び出せないオブジェクトです。リークをテストするために、ユニットテストで使用する汎用リークディテクタクラスも開発しました。リフレクションを使用して、すべてのプロパティとメソッドが不変の状態でImmutableElementExceptionをスローするかどうかをテストします。 つまり、ここではTDDが使用されています。
私はこのパターンが大好きになり、大きな利点を見つけました。それで、私が知りたいのは、あなたの誰かが同様のパターンを使用している場合です。はいの場合、それを文書化する優れたリソースを知っていますか?基本的に、潜在的な改善点と、このトピックに既に存在する可能性のある標準を探しています。
解決
詳細については、2番目のアプローチは<!> quot; popsicle immutability <!> quot;と呼ばれます。
Eric Lippertには、こちら。私はまだCTP(C#4.0)に慣れていますが、オプションの/名前付きパラメーター(.ctorに対して)がここで行うことは興味深いようです(読み取り専用フィールドにマップされた場合)... [更新:このこちら]にブログを掲載しました]
情報については、おそらくこれらのメソッドを作成しないでしょうvirtual
-サブクラスがそれをフリーズ不可にできるようにしたくないでしょう。追加のコードを追加できるようにする場合は、次のようなものをお勧めします。
[public|protected] void Freeze()
{
if(!frozen)
{
frozen = true;
OnFrozen();
}
}
protected virtual void OnFrozen() {} // subclass can add code here.
また、AOP(PostSharpなど)は、ThrowIfFrozen()チェックをすべて追加するための実行可能なオプションです。
(用語/メソッド名を変更した場合はおologiesび申し上げます-返信の作成時に元の投稿が表示されない)
他のヒント
別のオプションは、ある種のBuilderクラスを作成することです。
たとえば、Java(およびC#や他の多くの言語)では、文字列は不変です。複数の操作を実行して文字列を作成する場合は、StringBuilderを使用します。これは変更可能であり、完了すると、最終的なStringオブジェクトが返されます。それ以降は不変です。
他のクラスでも同様のことができます。不変のElementがあり、次にElementBuilderがあります。ビルダーが行うことは、設定したオプションを保存するだけです。その後、ファイナライズすると、不変の要素が構築されて返されます。
もう少しコードですが、不変であるはずのクラスにセッターを置くよりもきれいだと思います。
変更ごとに新しいSystem.Drawing.Point
を作成する必要があるという事実に対する最初の不快感の後、数年前にこの概念を完全に受け入れました。実際、デフォルトではすべてのフィールドをreadonly
として作成し、説得力のある理由がある場合にのみ変更可能に変更します<!>#8211;驚くほどめったにありません。
ただし、クロススレッドの問題についてはあまり気にしません(これが関連するコードはほとんど使用しません)。セマンティックな表現力のおかげで、はるかに優れていると思います。不変性は、誤って使用するのが難しいインターフェースの典型です。
あなたはまだ状態を扱っているので、オブジェクトが不変になる前に並列化されていると噛まれることもあります。
より機能的な方法は、各セッターでオブジェクトの新しいインスタンスを返すことです。または、可変オブジェクトを作成して、コンストラクターに渡します。
ドメイン駆動設計と呼ばれる(比較的)新しいソフトウェア設計パラダイムは、エンティティオブジェクトと値オブジェクトを区別します。
エンティティオブジェクトは、従業員、クライアント、請求書など、永続データストア内のキー駆動型オブジェクトにマッピングする必要があるものとして定義されます。オブジェクトのプロパティを変更すると、変更をデータストアのどこかに保存し、同じ<!> quot; key <!> quotを持つクラスの複数のインスタンスが存在する必要があります。 1つのインスタンスの変更が他のインスタンスを上書きしないように、それらを同期するか、データストアへの永続性を調整する必要があります。エンティティオブジェクトのプロパティを変更するということは、参照しているオブジェクトを変更するのではなく、オブジェクトについて何かを変更していることを意味します...
値オブジェクトは、不変と見なすことができるオブジェクトであり、そのユーティリティはプロパティ値によって厳密に定義されており、複数のインスタンスについては、アドレスや電話番号など、何らかの方法で調整する必要はありません。または車の車輪、または文書の文字...これらはプロパティによって完全に定義されています...テキストエディタの大文字の「A」オブジェクトは、他のすべての大文字の「A」オブジェクトと透過的に交換できますこのドキュメントでは、他のすべての「A」と区別するためのキーは必要ありません。この意味では不変です。「B」に変更すると(電話番号オブジェクトの電話番号文字列を変更するように、いくつかの可変エンティティに関連付けられているデータを変更するのではなく、文字列の値を変更するときと同じように、ある値から別の値に切り替えます...
System.Stringは、セッターと変更メソッドを備えた不変クラスの良い例です。ただし、各変更メソッドは新しいインスタンスを返します。
@Cory Foyと@Charles Bretanaによる、エンティティと値の違いがあるポイントの拡大。値オブジェクトは常に不変である必要がありますが、オブジェクトが自分自身をフリーズさせたり、コードベース内で任意にフリーズさせたりできるとは思いません。それには本当に悪臭があり、オブジェクトが正確に凍結された場所、凍結された理由、およびオブジェクトの呼び出し間で状態が解凍から凍結に変わる可能性を追跡するのが難しくなるのではないかと心配しています。
それは、時々(可変)エンティティを何かに与え、それが変更されないようにしたいということではありません。
したがって、オブジェクト自体を凍結する代わりに、ReadOnlyCollection <!> lt;のセマンティクスをコピーすることもできます。 T <!> gt;
List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();
オブジェクトは、必要なときに変更可能な部分を取り、必要に応じて不変にすることができます。
ReadOnlyCollection <!> lt;に注意してください。 T <!> gt; ICollection <!> ltも実装します。 T <!> gt;インターフェースにAdd( T item)
メソッドがあります。ただし、インターフェイスにはbool IsReadOnly { get; }
も定義されているため、コンシューマは例外をスローするメソッドを呼び出す前に確認できます。
違いは、IsReadOnlyを単にfalseに設定できないことです。コレクションは読み取り専用であるか、読み取り専用ではなく、コレクションの有効期間中は変更されません。
C ++がコンパイル時に提供するconst-correctnessがあればいいのですが、それはそれ自身の問題のセットを持ち始めており、C#がそこに行かないことを嬉しく思います。
ICloneable -次を参照すると思いました。
ICloneableを実装しない
パブリックAPIでICloneableを使用しないでください
これは重要な問題であり、それを解決するために、より直接的なフレームワーク/言語のサポートが必要です。あなたが持っている解決策には、多くの定型文が必要です。コード生成を使用して、ボイラープレートの一部を自動化するのは簡単かもしれません。
すべてのフリーズ可能なプロパティを含む部分クラスを生成します。このために再利用可能なT4テンプレートを作成するのは非常に簡単です。
テンプレートは入力にこれを使用します:
- 名前空間
- クラス名
- プロパティ名/タイプタプルのリスト
そして、以下を含むC#ファイルを出力します。
- 名前空間宣言
- 部分クラス
- 各プロパティ、対応する型、バッキングフィールド、ゲッター、およびFailIfFrozenメソッドを呼び出すセッター
フリーズ可能なプロパティのAOPタグも機能しますが、より多くの依存関係が必要になりますが、T4はVisual Studioの新しいバージョンに組み込まれています。
これに非常によく似ているもう1つのシナリオは、INotifyPropertyChanged
インターフェイスです。その問題の解決策は、この問題に適用される可能性があります。
このパターンに関する私の問題は、不変性にコンパイル時の制限を課していないことです。コーダーは、たとえば、オブジェクトをキャッシュまたは別のスレッドセーフでない構造に追加する前に、オブジェクトが不変に設定されていることを確認する責任があります。
だから、このコーディングパターンを、次のように、汎用クラスの形式でコンパイル時の制限を加えて拡張します。
public class Immutable<T> where T : IElement
{
private T value;
public Immutable(T mutable)
{
this.value = (T) mutable.Clone();
this.value.MakeReadOnly();
}
public T Value
{
get
{
return this.value;
}
}
public static implicit operator Immutable<T>(T mutable)
{
return new Immutable<T>(mutable);
}
public static implicit operator T(Immutable<T> immutable)
{
return immutable.value;
}
}
これを使用する方法のサンプルを次に示します。
// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements =
new List<Immutable<SampleElement>>();
for (int i = 1; i < 10; i++)
{
SampleElement newElement = new SampleElement();
newElement.Id = Guid.NewGuid();
newElement.Name = "Sample" + i.ToString();
// The compiler will automatically convert to Immutable<SampleElement> for you
// because of the implicit conversion operator
elements.Add(newElement);
}
foreach (SampleElement element in elements)
Console.Out.WriteLine(element.Name);
elements[3].Value.Id = Guid.NewGuid(); // This will throw an ImmutableElementException
要素のプロパティを単純化するためのヒント:自動プロパティとprivate set
を使用し、データフィールドを明示的に宣言しないでください。例:
public class SampleElement {
public SampleElement(Guid id, string name) {
Id = id;
Name = name;
}
public Guid Id {
get; private set;
}
public string Name {
get; private set;
}
}
チャンネル9の新しいビデオです。インタビューの36:30からのAnders HejlsbergがC#の不変性について話し始めています。彼はポプシクル不変性の非常に良いユースケースを提供し、これがあなたが現在あなた自身を実装するために必要なものである方法を説明します。 C#の将来のバージョンで不変のオブジェクトグラフを作成するためのより良いサポートについて考える価値があると彼が聞くのは私の耳に聞こえる音楽でした
議論されていない特定の問題に対する他の2つのオプション:
-
プライベートプロパティセッターを呼び出すことができる独自のデシリアライザーを構築します。最初はデシリアライザーを構築するための努力ははるかに多くなりますが、それによって事態はよりクリーンになります。コンパイラはセッターを呼び出そうとさえしないようにし、クラス内のコードを読みやすくします。
-
XElement(または他のフレーバーのXMLオブジェクトモデル)を受け取り、そこからデータを取り込むコンストラクターを各クラスに配置します。クラスの数が増えるにつれて、これはソリューションとしてすぐに望ましくなくなります。
MutableThingとImmutableThingのサブクラスを持つ抽象クラスThingBaseを作成するのはどうですか? ThingBaseにはすべてのデータが保護された構造に含まれ、フィールドには読み取り専用のパブリックプロパティが、構造には読み取り専用のプロテクトプロパティが提供されます。また、ImmutableThingを返すオーバーライド可能なAsImmutableメソッドも提供します。
MutableThingは、プロパティを読み取り/書き込みプロパティでシャドウし、デフォルトのコンストラクターとThingBaseを受け入れるコンストラクターの両方を提供します。
Immutableなものは、AsImmutableをオーバーライドして自分自身を返すだけの封印されたクラスです。また、ThingBaseを受け入れるコンストラクターも提供します。
オブジェクトを変更可能な状態から不変の状態に変更できるというアイデアは好きではありません。その種のデザインのポイントは私には負けそうです。いつそれをする必要がありますか? VALUESを表すオブジェクトのみが不変であるべきです
オプションの名前付き引数をnullableと一緒に使用して、ほとんど定型のない不変のセッターを作成できます。プロパティを本当にnullに設定したい場合は、さらに問題が発生する可能性があります。
class Foo{
...
public Foo
Set
( double? majorBar=null
, double? minorBar=null
, int? cats=null
, double? dogs=null)
{
return new Foo
( majorBar ?? MajorBar
, minorBar ?? MinorBar
, cats ?? Cats
, dogs ?? Dogs);
}
public Foo
( double R
, double r
, int l
, double e
)
{
....
}
}
次のように使用します
var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);