web-dev-qa-db-ja.com

C#の不変オブジェクトパターン-どう思いますか?

私はいくつかのプロジェクトの過程で、不変(読み取り専用)オブジェクトと不変オブジェクトグラフを作成するためのパターンを開発しました。不変オブジェクトには、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; }
  }
}

これは単純なクラスには問題ありませんが、より複雑なクラスには、コンストラクターを介してすべての値を渡すという概念は好きではありません。プロパティにセッターを設定する方が望ましく、新しいオブジェクトを作成するコードが読みやすくなります。

では、セッターを使用して不変オブジェクトをどのように作成しますか?

さて、私のパターンでは、オブジェクトは、単一のメソッド呼び出しでフリーズするまで、完全に可変であるものとして始まります。オブジェクトがフリーズされると、それは永久に不変のままになります-再び可変オブジェクトに変えることはできません。オブジェクトの変更可能なバージョンが必要な場合は、単にクローンを作成します。

では、コードに移りましょう。次のコードスニペットでは、パターンを最も単純な形式に煮詰めようとしました。 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が使用されます。

私はこのパターンが大好きになり、大きなメリットを見つけました。では、私が知りたいのは、似たようなパターンを使用している人がいるかどうかです。はいの場合、それを文書化した優れたリソースを知っていますか?私は基本的に、潜在的な改善と、このトピックにすでに存在する可能性のある標準を探しています。

47
Lars Fastrup

詳細については、2番目のアプローチは「ポップシクル不変性」と呼ばれます。

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()チェックを追加するための実行可能なオプションである可能性があります。

(用語/メソッド名を変更した場合はお詫びします-SOは、返信を作成するときに元の投稿を表示しません)

31
Marc Gravell

別のオプションは、ある種のBuilderクラスを作成することです。

たとえば、Java(およびC#や他の多くの言語)では、文字列は不変です。文字列を作成するために複数の操作を実行する場合は、StringBuilderを使用します。これは変更可能です。これで、最終的なStringオブジェクトが返されます。それ以降は不変です。

他のクラスでも同様のことができます。不変の要素があり、次にElementBuilderがあります。ビルダーが行うのは、設定したオプションを保存することだけです。それを確定すると、ビルダーは不変の要素を作成して返します。

これはもう少しコードですが、不変であるはずのクラスにセッターを置くよりもクリーンだと思います。

17
Herms

変更のたびに新しいSystem.Drawing.Pointを作成する必要があるという事実についての最初の不快感の後、私は数年前にこの概念を完全に受け入れました。実際、私は現在、すべてのフィールドをデフォルトでreadonlyとして作成し、やむを得ない理由がある場合にのみ変更可能に変更しています。これは驚くほどまれです。

ただし、クロススレッドの問題についてはあまり気にしません(これに関連するコードを使用することはめったにありません)。セマンティックな表現力のおかげで、私はそれをはるかに良く見つけました。不変性は、誤って使用するのが難しいインターフェイスの縮図です。

9
Konrad Rudolph

あなたはまだ状態を扱っているので、オブジェクトが不変になる前に並列化されていると、まだ噛まれる可能性があります。

より機能的な方法は、各セッターでオブジェクトの新しいインスタンスを返すことです。または、可変オブジェクトを作成してコンストラクターに渡します。

8
Cory Foy

ドメイン駆動設計と呼ばれる(比較的)新しいソフトウェア設計パラダイムは、エンティティオブジェクトと値オブジェクトを区別します。

エンティティオブジェクトは、従業員、クライアント、請求書など、永続データストア内のキー駆動型オブジェクトにマップする必要があるものとして定義されます。オブジェクトのプロパティを変更すると、次のことが必要になります。変更をデータストアのどこかに保存します。同じ「キー」を持つクラスの複数のインスタンスが存在する場合は、それらを同期するか、1つのインスタンスの変更が他のインスタンスを上書きしないようにデータストアへの永続性を調整する必要があります。 。エンティティオブジェクトのプロパティを変更するということは、オブジェクトについて何かを変更していることを意味します。参照しているオブジェクトを変更するのではありません。

値オブジェクトotohは、不変と見なすことができるオブジェクトであり、そのユーティリティはプロパティ値によって厳密に定義され、複数のインスタンスについては、アドレス、電話番号、ホイールなどの方法で調整する必要はありません。車の場合、またはドキュメント内の文字...これらはプロパティによって完全に定義されます...テキストエディタの大文字の「A」オブジェクトは、ドキュメント全体で他の大文字の「A」オブジェクトと透過的に交換できます。他のすべての「A」と区別するためにキーは必要ありません。この意味では、「B」に変更すると(電話番号オブジェクトの電話番号文字列を変更するのと同じように)、不変であるためです。ある可変エンティティに関連付けられたデータを変更すると、ある値から別の値に切り替わります...文字列の値を変更するときと同じように...

6
Charles Bretana

これは重要な問題であり、それを解決するためのより直接的なフレームワーク/言語のサポートが見たいです。あなたが持っている解決策は多くの定型文を必要とします。コード生成を使用して、定型文の一部を自動化するのは簡単かもしれません。

すべてのフリーズ可能なプロパティを含む部分クラスを生成します。このために再利用可能なT4テンプレートを作成するのはかなり簡単です。

テンプレートはこれを入力に使用します。

  • 名前空間
  • クラス名
  • プロパティ名/タイプタプルのリスト

そして、以下を含むC#ファイルを出力します。

  • 名前空間宣言
  • 部分クラス
  • 対応するタイプ、バッキングフィールド、ゲッター、およびFailIfFrozenメソッドを呼び出すセッターを含む各プロパティ

フリーズ可能なプロパティのAOPタグも機能する可能性がありますが、より多くの依存関係が必要になりますが、T4は新しいバージョンのVisualStudioに組み込まれています。

これに非常によく似た別のシナリオは、INotifyPropertyChangedインターフェースです。その問題の解決策は、この問題に適用できる可能性があります。

System.Stringは、セッターと変更メソッドを備えた不変クラスの良い例ですが、各変更メソッドが新しいインスタンスを返すだけです。

4
dalle

エンティティと値の間に違いがある@CoryFoyと@CharlesBretanaによるポイントを拡張します。値オブジェクトは常に不変である必要がありますが、オブジェクトが自分自身をフリーズしたり、コードベースで任意にフリーズできるようにする必要があるとは思いません。臭いがひどいので、オブジェクトがどこでフリーズしたのか、なぜフリーズしたのか、オブジェクトを呼び出す間に解凍状態からフリーズ状態に変化する可能性があるため、追跡が困難になるのではないかと心配しています。 。

それは、(可変の)エンティティを何かに与えて、それが変更されないようにしたい場合があるということではありません。

したがって、オブジェクト自体をフリーズする代わりに、ReadOnlyCollection <T>のセマンティクスをコピーすることも可能です。

_List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();
_

オブジェクトは、必要なときに変更可能として参加し、必要に応じて不変にすることができます。

ReadOnlyCollection <T>は、インターフェイスにAdd( T item)メソッドを持つICollection <T>も実装していることに注意してください。ただし、インターフェイスには_bool IsReadOnly { get; }_も定義されているため、コンシューマーは例外をスローするメソッドを呼び出す前に確認できます。

違いは、IsReadOnlyをfalseに設定することはできないということです。コレクションは読み取り専用であるか、読み取り専用ではなく、コレクションの存続期間中は変更されません。

コンパイル時にC++が提供するconst-correctnessがあればいいのですが、それ自体に問題が発生し始め、C#がそこに行かなくてよかったです。


ICloneable-私は次のことをもう一度参照すると思いました:

ICloneableを実装しないでください

パブリックAPIでICloneableを使用しないでください

Brad Abrams-設計ガイドライン、マネージコード、および.NET Framework

4
Robert Paulson

このパターンに関する私の問題は、不変性にコンパイル時の制限を課していないことです。コーダーは、オブジェクトがキャッシュまたは別のスレッドセーフでない構造に追加する前に、オブジェクトが不変に設定されていることを確認する責任があります。

そのため、このコーディングパターンを、次のようなジェネリッククラスの形式でコンパイル時の制約を使用して拡張します。

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
3
Jos Bosmans

サブクラスMutableThingとImmutableThingを持つ抽象クラスThingBaseを持つのはどうですか? ThingBaseには、保護された構造内のすべてのデータが含まれ、フィールドのパブリック読み取り専用プロパティとその構造の保護された読み取り専用プロパティが提供されます。また、ImmutableThingを返すオーバーライド可能なAsImmutableメソッドも提供します。

MutableThingは、読み取り/書き込みプロパティでプロパティをシャドウイングし、デフォルトのコンストラクターとThingBaseを受け入れるコンストラクターの両方を提供します。

不変のものは、AsImmutableをオーバーライドして単に自分自身を返す封印されたクラスです。また、ThingBaseを受け入れるコンストラクターも提供します。

2
supercat

オプションの名前付き引数を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);
2
bradgonesurfing

要素のプロパティを単純化するためのヒント: 自動プロパティ with 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;
  }
}
2
spoulson

オブジェクトを可変状態から不変状態に変更できるという考えは好きではありません。そのようなものは、私には設計のポイントを打ち負かすようです。いつそれをする必要がありますか? VALUESを表すオブジェクトのみが不変である必要があります

2
Andrew Bullock

議論されていないあなたの特定の問題のための他の2つのオプション:

  1. プライベートプロパティセッターを呼び出すことができる独自のデシリアライザーを構築します。最初にデシリアライザーを構築するための努力ははるかに多くなりますが、それは物事をよりクリーンにします。コンパイラーは、セッターを呼び出そうとすることさえ防ぎ、クラス内のコードは読みやすくなります。

  2. XElement(または他の種類のXMLオブジェクトモデル)を取得し、そこから自身を設定するコンストラクターを各クラスに配置します。明らかに、クラスの数が増えると、これは解決策としてすぐに望ましくなくなります。

2
Neil

これはチャンネル9の新しいビデオで、インタビューの36:30からAnders HejlsbergがC#の不変性について話し始めています。彼は、アイスキャンディーの不変性の非常に優れたユースケースを示し、これが現在自分で実装する必要があるものであると説明しています。 C#の将来のバージョンで不変のオブジェクトグラフを作成するためのより良いサポートについて考える価値があると彼が言うのを聞いたのは私の耳には音楽でした

エキスパートからエキスパートへ:Anders Hejlsberg-C#の未来

2
Lars Fastrup