不変クラスはどの時点で負担になりますか?


16

データモデルを保持するクラスを設計するとき、不変オブジェクトを作成すると便利ですが、どの時点でコンストラクターパラメーターリストとディープコピーの負荷が大きくなりすぎて、不変の制限を放棄する必要があるのでしょうか?

たとえば、名前付きのものを表す不変のクラスを次に示します(C#構文を使用していますが、原則はすべてのOO言語に適用されます)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

名前付きのものは、新しい名前付きのものに作成、クエリ、コピーできますが、名前は変更できません。

これはすべて良いですが、別の属性を追加したい場合はどうなりますか?コンストラクターにパラメーターを追加し、コピーコンストラクターを更新する必要があります。これはあまり手間がかかりませんが、複雑なオブジェクトを不変にしたいときに、問題が発生するのはわかります。

クラスに他の複雑なクラスを含むmay属性とコレクションが含まれている場合、コンストラクターのパラメーターリストは悪夢のように思えます。

では、どの時点でクラスが複雑すぎて不変にならないのでしょうか?


私は常にモデル内のクラスを不変にする努力をしています。あなたが巨大で長いコンストラクタパラメータリストを持っているなら、多分あなたのクラスが大きすぎて分割できるでしょうか?下位レベルのオブジェクトも不変であり、同じパターンに従っている場合、上位レベルのオブジェクトは(あまり)影響を受けません。既存のクラスを変更して不変にするのは、ゼロから始めたときにデータモデルを不変にするよりもはるかに難しいと思います。
誰も

1
:あなたはこの質問で提案ビルダーパターンで見ることができるstackoverflow.com/questions/1304154/...
Antの

MemberwiseCloneを見たことがありますか?新しいメンバーごとにコピーコンストラクターを更新する必要はありません。
ケビンクライン

3
@Tonyコレクションとそれに含まれるすべてが不変の場合、ディープコピーは必要ありません。浅いコピーで十分です。
mjcopple

2
余談ですが、私は通常、クラスが「かなり」不変である必要があるが、完全ではないクラスで「1回だけ」フィールドを使用します。これにより、巨大なコンストラクターの問題は解決しますが、不変クラスの利点のほとんどは提供されます。(つまり、内部クラスコードは値の変更を心配する必要はありません)
Earlz

回答:


23

彼らが負担になるとき?非常に迅速(特に選択した言語が不変性に対する十分な構文サポートを提供しない場合)

不変性は、マルチコアのジレンマなどの特効薬として販売されています。しかし、ほとんどのオブジェクト指向言語の不変性により、モデルとプロセスに人工的なアーティファクトとプラクティスを追加する必要があります。複雑な不変クラスごとに、同等に複雑な(少なくとも内部的に)ビルダーが必要です。どのように設計しても、依然として強い結合が導入されます(したがって、導入する正当な理由があります)。

すべてを小さな非複雑なクラスでモデル化することは必ずしも可能ではありません。そのため、大規模なクラスと構造の場合、それらを人為的にパーティション分割します。これは、ドメインモデルで理にかなっているためはなく、コード内の複雑なインスタンス化とビルダーを処理する必要があるためです。

JavaやC#のような汎用言語で不変性の考えを取りすぎて、すべてが不変になると、さらに悪くなります。その結果、そのようなものを容易にサポートしない言語でs-expressionコンストラクトを強制する人々が表示されます。

エンジニアリングとは、妥協とトレードオフによるモデリングの行為です。誰かがすべてがXまたはY関数型言語(完全に異なるプログラミングモデル)で不変であることを読んだために、命令によってすべてを不変にします。これは受け入れられません。それは良いエンジニアリングではありません。

小さく、場合によっては単一のものを不変にすることができます。理にかなっている場合は、より複雑なものを不変にすることができます。しかし、不変性は特効薬ではありません。バグを削減し、スケーラビリティとパフォーマンスを向上させる機能は、不変性の唯一の機能ではありません。それは適切な工学実践の機能です。結局のところ、人々は不変性のない優れたスケーラブルなソフトウェアを書いてきました。

不変性は、ドメインモデルのコンテキストで意味をなさない範囲で行われた場合、理由なしに行われた場合、非常に高速な負荷になります(偶然の複雑さを増します)。

私は、それを避けるようにします(構文言語が適切にサポートされているプログラミング言語で作業している場合を除きます)。


6
ルイス、シンプルでありながら健全なエンジニアリングの原則の説明で書かれた、よく書かれた、実用的に正しい答えが、最先端のコーディング流行を使用しているものほど多くの票を獲得しない傾向があることに気付きましたか?これは素晴らしい、素晴らしい答えです。
Huperniketes

3
おかげで:)私は自分自身の傾向に気づいたが、それは大丈夫です。流行のファンボーイがコードを解約し、後でより良い時間単位のレートで修復できるようになりました、ハハ:) jk
...-luis.espinal

4
特効薬?番号。C#/ Javaの少し厄介な価値はありますか(実際、それほど悪くはありません)。絶対に。また、不変性におけるマルチコアの役割はごくわずかです...本当の利点は推論の容易さです。
マウリシオシェファー

@Mauricio-あなたがそう言うなら(Javaでの不変性はそれほど悪くない)。1998年から2011年までJavaに取り組んできたので、私は違うことを請うでしょう。単純なコードベースでは些細な例外ではありません。しかし、人々は異なる経験をしており、私は自分のPOVには主観がないわけではないことを認めています。申し訳ありませんが、同意できません。ただし、不変性に関しては推論の容易さが最も重要なことであることに同意します。
luis.espinal

13

可能な限りクラスが不変であることを主張する段階を経ました。ほぼすべて、不変配列などのビルダーがありました。あなたの質問への答えは簡単だとわかりました:どの時点で不変クラスが負担になりますか? 非常に迅速に。何かをシリアル化する場合は、すぐにシリアル化を解除できるようにする必要があります。つまり、それは可変でなければなりません。ORMを使用するとすぐに、それらのほとんどはプロパティが変更可能であると主張します。等々。

最終的に、そのポリシーを、変更可能なオブジェクトへの不変のインターフェイスに置き換えました。

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

オブジェクトには柔軟性がありますが、これらのプロパティを編集してはならないことを呼び出し元のコードに伝えることができます。


8
些細なことはさておき、命令型言語でプログラミングする場合、不変のオブジェクトは非常に迅速に痛みになる可能性があることに同意しますが、不変のインターフェイスが同じ問題を本当に解決するのか、それともまったく問題を解決するのかはわかりません。不変オブジェクトを使用する主な理由は、いつでもどこでもオブジェクトを投げることができ、他の誰かがあなたの状態を壊すことを心配する必要がないためです。基礎となるオブジェクトが可変である場合、特にそれを可変にした理由がさまざまなものがそれを変化させる必要があるためである場合、その保証はありません。
アーロンノート

1
@Aaronaught-興味深い点。私はそれが実際の保護よりも心理的なものだと思います。ただし、最後の行には誤った前提があります。可変性を維持する理由は、さまざまなものがインスタンス化してリフレクションを介して入力する必要があり、インスタンス化された後に変異するのではないということです。
pdr

1
@Aaronaught:同じ方法IComparable<T>でif X.CompareTo(Y)>0Y.CompareTo(Z)>0thenが保証されX.CompareTo(Z)>0ます。インターフェイスにはコントラクトがあります。IImmutableList<T>インスタンスが外部に公開される前に、すべてのアイテムとプロパティの値を「石に設定」する必要があることを契約が指定している場合、すべての正当な実装がそうします。IComparable実装が推移性に違反することを妨げるものは何もありませんが、そうする実装は違法です。場合はSortedDictionary誤動作を嫡出与えられたときにIComparable、...
supercat

1
@Aaronaught:IReadOnlyList<T>(1)インターフェース文書にそのような要件が記載されておらず、(2)最も一般的な実装でList<T>あるが読み取り専用はない場合、なぜの実装は不変であると信頼する必要がありますか?私の用語について曖昧な点ははっきりしていません。コレクション内のデータが読めれば読めます。外部参照を変更するコードによって外部参照が保持されていない限り、含まれるデータを変更できないと約束できる場合、読み取り専用です。期間を変更できないことを保証できる場合、不変です。
supercat

1
@supercat:ちなみに、マイクロソフトは私に同意します。彼らは不変のコレクションパッケージをリリースし、それらがすべて具象型であることに気付きました。なぜなら、抽象クラスまたはインターフェースが本当に不変であることを保証することはできないからです。
アーロンノート14

8

これに対する一般的な答えはないと思います。クラスが複雑になるほど、クラスの状態変更について推論するのが難しくなり、クラスの新しいコピーを作成するのにコストがかかります。そのため、ある程度の(個人的な)レベルの複雑さを超えると、クラスを不変にしたり維持したりするのが苦痛になります。

注意デザインが臭いされている複雑すぎるクラス、または長いメソッドのパラメータリストにかかわらず不変の、それ自体。

したがって、通常、推奨される解決策は、そのようなクラスを複数の別個のクラスに分割し、各クラスをそれ自体で可変または不変にすることができます。これが実行可能でない場合は、変更可能にすることができます。


5

すべての不変フィールドをinnerに格納すると、コピーの問題を回避できますstruct。これは基本的に、記憶パターンのバリエーションです。次に、コピーを作成するときは、思い出をコピーします。

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}

内部構造は必要ないと思います。MemberwiseCloneを実行する別の方法です。
コーディズム

@Codism-はい、いいえ。クローンを作成したくない他のメンバーが必要になる場合があります。ゲッターの1つで遅延評価を使用し、結果をメンバーにキャッシュした場合はどうなりますか?MemberwiseCloneを実行する場合、キャッシュされた値を複製し、キャッシュされた値が依存するメンバーの1つを変更します。状態をキャッシュから分離する方が簡単です。
スコットホイットロック

内部構造の別の利点に言及する価値があるかもしれません:それはオブジェクトが他の参照が存在するオブジェクトその状態をコピーすることを容易にします。OOPのあいまいさの一般的な原因は、オブジェクト参照を返すメソッドが、受信者の制御外で変更される可能性のあるオブジェクトのビューを返すかどうかです。オブジェクト参照を返す代わりに、メソッドが呼び出し元からオブジェクト参照を受け取り、状態をそれにコピーすると、オブジェクトの所有権がより明確になります。このようなアプローチは...自由に継承タイプでうまく動作しませんが
supercat

...変更可能なデータ所有者には非常に便利です。また、このアプローチにより、「並列」の可変クラスおよび不変クラス(抽象的な「読み取り可能な」ベースから派生)を簡単に作成し、コンストラクターが相互にデータをコピーできるようになります。
supercat 14

3

ここでいくつかの作業をしています。不変データセットは、マルチスレッドのスケーラビリティに最適です。基本的に、1セットのパラメーターがクラスの1つのインスタンス(どこでも)になるように、メモリーをかなり最適化できます。オブジェクトは決して変更されないため、そのメンバーへのアクセスを同期することについて心配する必要はありません。よかったです。ただし、指摘するように、オブジェクトが複雑になるほど、ある程度の可変性が必要になります。私はこれらの線に沿った推論から始めます:

  • オブジェクトの状態変更できるビジネス上の理由はありますか?たとえば、データベースに格納されているユーザーオブジェクトは、そのIDに基づいて一意ですが、時間の経過とともに状態を変更できる必要があります。一方、グリッド上の座標を変更すると、元の座標ではなくなるため、座標を不変にすることが理にかなっています。文字列でも同じです。
  • 一部の属性を計算できますか?つまり、オブジェクトの新しいコピーのその他の値が、渡すコア値の関数である場合、コンストラクターで、またはオンデマンドで計算できます。これにより、コピーまたは作成時に同じ方法でこれらの値を初期化できるため、メンテナンスの量が削減されます。
  • 新しい不変オブジェクトを構成する値の数は?ある時点で、オブジェクトの作成の複雑さが自明ではなくなり、その時点でオブジェクトのインスタンスが増えることが問題になる可能性があります。例には、不変のツリー構造、3つ以上のパラメーターが渡されたオブジェクトなどが含まれます。パラメーターが多いほど、パラメーターの順序を乱したり、間違ったパラメーターを無効にする可能性が高くなります。

不変オブジェクトのみをサポートする言語(Erlangなど)では、不変オブジェクトの状態を変更する操作がある場合、最終結果は更新された値を持つオブジェクトの新しいコピーになります。たとえば、アイテムをベクター/リストに追加する場合:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

これは、より複雑なオブジェクトを扱う正しい方法です。たとえば、ツリーノードを追加すると、結果はノードが追加された新しいツリーになります。上記の例のメソッドは、新しいリストを返します。この段落の例でtree.add(newNode)は、ノードが追加された新しいツリーが返されます。ユーザーにとっては、作業が簡単になります。ライブラリの作成者にとって、言語が暗黙的なコピーをサポートしていない場合は退屈になります。そのしきい値はあなた自身の忍耐次第です。ライブラリのユーザーにとって、私が見つけた最も正気な制限は、約3〜4つのパラメータートップです。


可変オブジェクト参照として使用する傾向がある場合(所有者に参照が含まれておらず、決して公開されていないことを意味する)、目的の「変更」コンテンツを保持する新しい不変オブジェクトを構築することは、オブジェクトを直接変更することと同等です。おそらく遅いでしょう。ただし、可変オブジェクトはエンティティとしても使用できます。可変オブジェクトのないエンティティのように動作するものをどのように作成しますか?
supercat 14

0

複数の最終クラスメンバーがあり、それらを作成する必要があるすべてのオブジェクトに公開したくない場合は、ビルダーパターンを使用できます。

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

利点は、異なる名前の異なる値のみを持つ新しいオブジェクトを簡単に作成できることです。


ビルダーが静的であることは正しくないと思います。別のスレッドは、それらを設定した後、を呼び出す前に、静的名または静的値を変更できますnewObject
エリック

ビルダクラスのみが静的ですが、そのメンバは静的ではありません。これは、作成するすべてのビルダーに対して、対応する値を持つ独自のメンバーセットがあることを意味します。クラスは静的である必要があるため、クラスを含むクラスの外で使用およびインスタンス化できます(NamedThingこの場合)
Salandur

あなたの言っていることがわかります。開発者が「成功の落とし穴に陥る」ことはないので、私はこれに関する問題を思い描いています。静的変数を使用するという事実は、もしa Builderが再利用されると、私が述べた事が起こるという本当のリスクがあることを意味します。誰かが多くのオブジェクトを構築し、ほとんどのプロパティが同じであるため、単にを再利用することを決定するかもしれません。Builder実際、依存関係を注入するグローバルシングルトンにしましょう。おっと。主要なバグが導入されました。したがって、インスタンス化されたものと静的なものが混在するこのパターンは悪いと思います。
エリック

1
C#の@Salandur内部クラスは、Java内部クラスの意味では常に「静的」です。
セバスチャンレッド

0

では、どの時点でクラスが複雑すぎて不変にならないのでしょうか?

私の意見では、あなたが示しているような言語で小さなクラスを不変にするのは面倒です。私が使用している小さなここではなく、複雑なあなたがそのクラスに10フィールドを追加し、それが彼らに本当に派手な操作をした場合でも、私はキロバイトだけではメガバイトだけではギガバイトを聞かせて聞かせて取るために起こって疑うので、任意の関数は、あなたのインスタンスを使用して、 classは、オブジェクト全体の安価なコピーを作成して、外部の副作用の発生を回避したい場合に元のオブジェクトの変更を回避できます。

永続的なデータ構造

不変性の個人的な用途は、表示するクラスのインスタンス(100万を格納するものなど)のような小さなデータの集まりを集約する大きな中央のデータ構造ですNamedThings。不変で永続的なデータ構造に属し、読み取り専用アクセスのみを許可するインターフェースの背後にあることにより、コンテナに属する要素は、要素クラス(NamedThing)が処理しなくても不変になります。

安いコピー

永続的なデータ構造により、その領域を変換して一意にすることができ、データ構造全体をコピーすることなく元の変更を回避できます。それが本当の美しさです。ギガバイトのメモリを取り、メガバイトのメモリだけを変更するデータ構造を入力する副作用を回避する関数を単純に作成したい場合は、入力に触れずに新しいものを返すために、おかしなもの全体をコピーする必要があります出力。そのシナリオでは、ギガバイトをコピーして副作用を回避するか、副作用を引き起こすため、2つの不快な選択肢から選択する必要があります。

永続的なデータ構造を使用すると、このような関数を記述し、データ構造全体のコピーを作成せずに済みます。関数がメガバイトのメモリを変換するだけの場合、出力に約1メガバイトの追加メモリが必要になります。

重荷

負担については、少なくとも私の場合はすぐに負担があります。人々が話している「一時的」なビルダーを必要としているのは、大規模なデータ構造に手を加えずに効果的に変換を表現できるようにするためです。このようなコード:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
     // Transform stuff in the range, [first, last).
     for (; first != last; ++first)
          transform(stuff[first]);
}

...そして、このように書かなければなりません:

ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
     // Grab a "transient" (builder) list we can modify:
     TransientList<Stuff> transient(stuff);

     // Transform stuff in the range, [first, last)
     // for the transient list.
     for (; first != last; ++first)
          transform(transient[first]);

     // Commit the modifications to get and return a new
     // immutable list.
     return stuff.commit(transient);
}

しかし、これらの2行のコードと引き換えに、関数は同じ元のリストを持つスレッド間で安全に呼び出すことができ、副作用などを引き起こしません。また、この操作を取り消し可能なユーザーアクションにすることも非常に簡単になります。 undoは、古いリストの安価な浅いコピーを保存するだけです。

例外安全またはエラー回復

このようなコンテキストで永続的なデータ構造から得られるほど誰もが恩恵を受けるとは限りません(VFXドメインの中心概念である取り消しシステムや非破壊編集で非常に有用であることがわかりました)。考慮する必要があるのは、例外安全またはエラー回復です。

元の変更関数を例外に対して安全にしたい場合、ロールバックロジックが必要です。そのためには、最も単純な実装ではリスト全体をコピーする必要があります。

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
    // Make a copy of the whole massive gigabyte-sized list 
    // in case we encounter an exception and need to rollback
    // changes.
    MutList<Stuff> old_stuff = stuff;

    try
    {
         // Transform stuff in the range, [first, last).
         for (; first != last; ++first)
             transform(stuff[first]);
    }
    catch (...)
    {
         // If the operation failed and ran into an exception,
         // swap the original list with the one we modified
         // to "undo" our changes.
         stuff.swap(old_stuff);
         throw;
    }
}

この時点で、例外セーフな可変バージョンは、「ビルダー」を使用した不変バージョンよりも計算コストが高く、間違いなく正しく記述するのがさらに困難です。そして、多くのC ++開発者は例外安全性を無視しており、おそらくそれは彼らのドメインにとっては問題ありませんが、私の場合は、例外が発生した場合でもコードが正しく機能することを確認したいです(例外をテストするために意図的に例外をスローするテストを書く安全性)、それが原因で、何かがスローされた場合、関数が関数の途中で引き起こす副作用をロールバックできるようにする必要があります。

アプリケーションをクラッシュさせたり焼き付けたりせずに例外から安全にエラーを回復したい場合は、エラー/例外が発生した場合に関数が引き起こす可能性のある副作用を元に戻す/元に戻す必要があります。そして、ビルダーは実際には、計算時間とともにコストよりもプログラマーの時間を節約できます。

副作用を引き起こさない関数で副作用をロールバックすることを心配する必要はありません!

基本的な質問に戻りましょう。

不変クラスはどの時点で負担になりますか?

それらは常に不変性よりも可変性を中心に展開する言語の負担です。そのため、コストよりも利益のほうが大きい場合に使用すべきだと思います。しかし、十分に大きいデータ構造に十分なレベルで、私はそれが価値のあるトレードオフになる多くの場合があると信じています。

私の場合も、不変のデータ型は数個しかなく、それらはすべて膨大な数の要素(画像/テクスチャのピクセル、ECSのエンティティとコンポーネント、および頂点/エッジ/ポリゴンを格納するための巨大なデータ構造です。メッシュ)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.