データオブジェクトタイプを不変に設計する必要があるかどうかをどのように決定しますか?


23

不変の「パターン」はその長所から気に入っています。過去には、不変のデータ型(一部、ほとんど、またはすべて)を使用してシステムを設計することが有益であることがわかりました。多くの場合、そうすることで、作成するバグが少なくなり、デバッグがはるかに簡単になります。

しかし、私の仲間は一般的に不変を避けています。彼らはまったく経験がありません(それからはほど遠い)が、データオブジェクトを古典的な方法で書き込みます-すべてのメンバーのゲッターとセッターを持つプライベートメンバー。その後、通常、コンストラクターは引数をとらないか、単に便宜上いくつかの引数をとります。多くの場合、オブジェクトの作成は次のようになります。

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

たぶん彼らはそれをどこでもします。たぶん、それらがどれほど重要であっても、それらの2つの文字列を受け取るコンストラクタを定義することすらありません。そして多分、それらは後でそれらの文字列の値を変更せず、変更する必要はありません。明らかに、これらのことが真実であれば、オブジェクトは不変として設計されるでしょう。(コンストラクターは2つのプロパティを取り、セッターはまったく取りません)。

オブジェクト型を不変として設計する必要があるかどうかをどのように決定しますか?判断するための適切な基準はありますか?

私は現在、自分のプロジェクトのいくつかのデータ型を不変に切り替えるかどうかを議論していますが、ピアにそれを正当化する必要があり、型のデータは(めったに)変更されない可能性があります-いつでも変更できますそれは不変の方法です(新しいものを作成し、変更したいものを除いて古いオブジェクトからプロパティをコピーします)。しかし、これが単に不変物が透けて見えるのが私の愛なのか、それとも実際にそれらの必要性/利益があるのか​​はわかりません。


1
アーランのプログラムと問題全体が解決され、すべてが不変です
ザカリーK

1
@ZacharyK私は実際に関数型プログラミングについて何か言及することを考えていましたが、関数型プログラミング言語の経験が限られているため控えました。
リケット

回答:


27

後方に近づいているようです。デフォルトは不変です。オブジェクトを不変にするのは、不変オブジェクトとして機能させる必要がある/できない場合のみです。


11

不変オブジェクトの主な利点は、スレッドの安全性を保証することです。複数のコアとスレッドが標準である世界では、この利点は非常に重要になりました。

しかし、可変オブジェクトの使用は非常に便利です。それらはうまく機能し、別のスレッドから変更しない限り、何をしているのかをよく理解していれば、かなり信頼できます。


15
不変オブジェクトの利点は、推論しやすいことです。マルチスレッド環境では、これは非常に簡単です。単純なシングルスレッドアルゴリズムでは、さらに簡単です。たとえば、状態が一貫しているかどうかを気にする必要はありません。特定のコンテキストで使用するために必要なすべてのミューテーションをオブジェクトが渡しましたか。など。
9000

-1、@ 9000に同意します。スレッドセーフティは二次的です(不変に見えるが、メモ化などの理由で内部的に可変状態にあるオブジェクトを検討してください)。また、可変のパフォーマンスを引き出すことは時期尚早な最適化であり、ユーザーが「何をしているのかを知る」ことを必要とする場合、何でも守ることができます。自分が何をしているのかを常に知っていれば、バグのあるプログラムを書くことはありません。
ドーバル14年

4
@Doval:意見の相違はありません。9000は絶対に正しいです。不変オブジェクト推論するの簡単です。それが、並行プログラミングに非常に役立つ理由の一部です。他の部分は、状態の変化を心配する必要がないことです。早すぎる最適化はここでは関係ありません。不変オブジェクトの使用はパフォーマンスでなく設計に関するものであり、事前のデータ構造の不適切な選択は早すぎる悲観化です。
ロバートハーヴェイ

@RobertHarvey「前もってデータ構造の選択が不適切」とはどういう意味かわかりません。ほとんどの可変データ構造には不変バージョンがあり、同様のパフォーマンスを提供します。リストが必要で、不変リストと可変リストを使用する選択肢がある場合、それがアプリケーションのボトルネックであり、可変バージョンのパフォーマンスが向上することが確実になるまで、不変リストを使用します。
ドーバル14年

2
@Doval:岡崎を読みました。HaskellやLispなどの機能パラダイムを完全にサポートする言語を使用していない限り、これらのデータ構造は一般的に使用されません。そして、不変の構造がデフォルトの選択であるという概念に異議を唱えます。ビジネスコンピューティングシステムの大部分は、依然として可変構造(つまり、リレーショナルデータベース)を中心に設計されています。不変のデータ構造から始めることは良い考えですが、それでも非常に象牙の塔です。
ロバートハーヴェイ

6

オブジェクトが不変かどうかを判断するには、主に2つの方法があります。

a)オブジェクトの性質に基づいて

これらのオブジェクトは作成された後も変わらないことがわかっているため、これらの状況を簡単にキャッチできます。たとえば、RequestHistoryエンティティがあり、それが構築された後は、本質的に履歴エンティティは変更されません。これらのオブジェクトは、不変クラスとして簡単に設計できます。Request Objectは状態を変更でき、時間の経過などで誰に割り当てられるかは変更できますが、要求履歴は変更されないため、要求オブジェクトは変更可能であることに注意してください。たとえば、送信された状態から割り当てられた状態に移動したときに先週作成された履歴要素があり、この履歴エンティティは決して変更できません。したがって、これは古典的な不変のケースです。

b)設計の選択、外部要因に基づいて

これはjava.lang.Stringの例に似ています。文字列は実際には時間の経過とともに変化する可能性がありますが、設計上、キャッシング/文字列プール/同時実行性の要因により、不変になっています。同様に、アプリケーションでキャッシュ/同時実行性および関連するパフォーマンスが不可欠な場合、キャッシュ/同時実行性などはオブジェクトを不変にする上で良い役割を果たします。ただし、すべての影響を分析した後、この決定は非常に慎重に行う必要があります。

不変オブジェクトの主な利点は、タンブルウィードパターンの影響を受けないことです。つまり、オブジェクトは存続期間中に変更を受け取らず、コーディングとメンテナンスが非常に簡単になります。


4

私は現在、自分のプロジェクトのいくつかのデータ型を不変に切り替えるかどうかを議論していますが、ピアにそれを正当化する必要があり、型のデータは(めったに)変更されない可能性があります-いつでも変更できますそれは不変の方法です(新しいものを作成し、変更したいものを除いて古いオブジェクトからプロパティをコピーします)。

プログラムの状態を最小化することは非常に有益です。

クライアントクラスから一時的に保存するために、クラスの1つで可変値タイプを使用するかどうかを尋ねます。

はいと答えた場合、その理由を尋ねますか?可変状態は、このようなシナリオには属しません。それが実際に属する状態を強制的に作成し、データ型のステートフルネスを可能な限り明示的にすることは、優れた理由です。


4

答えはやや言語に依存します。コードはJavaのように見えますが、この問題は可能な限り困難です。Javaでは、オブジェクトは参照によってのみ渡すことができ、クローンは完全に壊れています。

簡単な答えはありませんが、小さな値のオブジェクトを不変にしたいことは確かです。Javaは文字列を不変にしましたが、日付とカレンダーを不変にしました。

したがって、小さな値オブジェクトを不変にし、コピーコンストラクタを実装してください。Cloneableのことは忘れてください。設計がひどくて役に立たないのです。

大きな値のオブジェクトの場合、それらを不変にするのが不便な場合は、コピーしやすくします。


CまたはC ++を記述するときにスタックとヒープを選択する方法に非常に似ています:)
リケット

@Ricket:それほどIMOではありません。スタック/ヒープはオブジェクトの寿命に依存します。C ++では、スタック上に可変オブジェクトを置くことは非常に一般的です。
ケビンクライン

1

そして多分、それらは後でそれらの文字列の値を変更せず、変更する必要はありません。明らかに、これらのことが真実であれば、オブジェクトは不変として設計されるでしょう。

多少直感に反して、後で文字列を変更する必要がないことは、オブジェクトが不変かどうかは関係ないという非常に良い議論です。プログラマーは、コンパイラーがそれを強制するかどうかにかかわらず、それらを事実上不変として扱います。

不変性は通常は害を与えませんが、常に助けになるとは限りません。オブジェクトが不変性の恩恵を受けるかどうかを判断する簡単な方法は、オブジェクトのコピーを作成するか、変更する前にミューテックスを取得する必要がある場合です。変更されない場合、不変性は実際には何も買わず、時には物事をより複雑にします。

あなたはやる無効な状態でオブジェクトを構築するリスクについての良い点を持っているが、それは本当に不変性とは別の問題です。オブジェクトは、可変であり、構築後に常に有効な状態になります。

この規則の例外は、Javaは名前付きパラメーターもデフォルトパラメーターもサポートしないため、オーバーロードされたコンストラクターを使用するだけで有効なオブジェクトを保証するクラスを設計するのが面倒になる場合があることです。2つのプロパティの場合はそれほどではありませんが、コードの他の部分の類似するがより大きなクラスでそのパターンが頻繁に発生する場合は、一貫性についても言及する必要があります。


可変性についての議論では、不変性とオブジェクトを安全に公開または共有できる方法との関係を認識できないことに興味があります。深く不変のクラスオブジェクトへの参照は、信頼できないコードと安全に共有できます。参照を保持するすべての人が、オブジェクトを変更したり、そうする可能性のあるコードに公開したりすることを絶対に信頼できない場合、可変クラスのインスタンスへの参照を共有できます。変異する可能性のあるクラスインスタンスへの参照は、通常は共有しないでください。オブジェクトへの唯一の参照は、宇宙のどこにでも存在している場合...
supercat

...そして、「アイデンティティハッシュコード」を照会したり、ロックに使用したり、「隠されたObject機能」にアクセスしたりすることはなく、オブジェクトを直接変更することは、新しいオブジェクトへの参照で参照を上書きすることと意味的には違いません指定された変更以外は同一です。Javaの最大のセマンティック上の弱点の1つであるIMHOは、変数が何かに対する唯一の非一時的な参照であることをコードが示す手段がないことです。参照は、戻った後にコピーを保持できない可能性があるメソッドにのみ渡すことができます。
supercat 14年

0

私はこれについて過度に低レベルのビューを持っているかもしれませんし、おそらくすべてを不変にするのがそれほど簡単ではないCとC ++を使用しているためですが、不変のデータ型をさらに記述するための最適化の詳細として見ています副作用のない効率的な機能と、システムの取り消しや非破壊編集などの機能を非常に簡単に提供できます。

たとえば、これは非常に高価になる可能性があります。

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... if Meshが永続的なデータ構造として設計されておらず、代わりに、完全にコピーする必要があるデータ型(一部のシナリオではギガバイトに及ぶ可能性があります)を変更する場合でもその一部(頂点の位置のみを変更する上記のシナリオのように)。

したがって、不変性に到達し、データ構造を設計して、その変更されていない部分を浅くコピーし、参照カウントできるようにします。これにより、上記の関数は、スレッドの安全性、例外の安全性、操作の取り消し、非破壊的な適用などを劇的に簡素化する副作用のない機能

私の場合、すべてを不変にするのは(少なくとも生産性の観点から)費用がかかりすぎるので、完全にコピーするには高すぎるクラスのために保存します。これらのクラスは通常、メッシュや画像のような巨大なデータ構造であり、一般に可変インターフェイスを使用して「ビルダー」オブジェクトを通じて変更を表現し、新しい不変のコピーを取得します。そして、クラスの中心レベルで不変の保証を達成しようとするのではなく、副作用のない関数でクラスを使用するのを助けるほどではありません。Mesh上記の不変にしたいのは、メッシュを不変にすることではなく、大量のメモリと計算コストを交換せずに、メッシュを入力して新しいものを出力する副作用のない関数を簡単に作成できるようにすることです。

その結果、コードベース全体に4つの不変のデータ型しかなく、それらはすべて非常に大きなデータ構造ですが、副作用のない関数を作成するためにそれらを頻繁に使用しています。私のように、すべてを不変にするのがそれほど簡単ではない言語で作業している場合、この答えは当てはまるかもしれません。その場合、関数の大部分が副作用を回避するように焦点を移すことができます。その時点で、高価な完全コピーを回避するための最適化の詳細として、選択したデータ構造を不変のPDSタイプにすることができます。一方、次のような関数がある場合:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

...それからベクトルを不変にする誘惑はありません。なぜならそれらは完全にコピーするのに十分に安いからです。ここでベクターを変更されていない部分を浅くコピーできる不変の構造に変えることによって得られるパフォーマンス上の利点はありません。このようなコストは、ベクター全体をコピーするだけのコストを上回ります。


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Fooに2つのプロパティしかない場合、2つの引数を取るコンストラクタを簡単に記述できます。しかし、別のプロパティを追加するとします。次に、別のコンストラクタを追加する必要があります。

public Foo(String a, String b, SomethingElse c)

この時点で、それはまだ管理可能です。しかし、10個のプロパティがある場合はどうでしょうか?10個の引数を持つコンストラクタは必要ありません。ビルダーパターンを使用してインスタンスを構築できますが、これにより複雑さが増します。この時までに、「普通の人がするように、すべてのプロパティにセッターを追加しなかったのはなぜですか」と考えるでしょう。


4
10個の引数を持つコンストラクターは、property7が設定されていないときに発生するNullPointerExceptionよりも悪いですか?ビルダーパターンは、各メソッドで初期化されていないプロパティをチェックするコードよりも複雑ですか?オブジェクトが構築されたときに一度確認するのが良いでしょう。
ケビンクライン

2
何十年も前にまさにこのケースについて言われたように、「10個のパラメータを持つ手順がある
9000

1
10個の引数を持つコンストラクタが必要ないのはなぜですか?どの時点で線を引きますか?10個の引数を持つコンストラクターは、不変性の利点を得るための小さな代償だと思います。さらに、10個のコンマで区切られた1行(必要に応じて複数行またはさらに見栄えの良い10行に広げる)を使用するか、各プロパティを個別に設定するときにとにかく10行を使用します...
Ricket

1
@Ricket:引数を間違った順序で配置するリスクが増えるためです。セッターまたはビルダーパターンを使用している場合、それはほとんどありません。
マイクバランチャック

4
10個の引数を持つコンストラクタがある場合は、それらの引数を独自のクラスにカプセル化すること、および/またはクラス設計を批判的に検討することを検討する時が来たのでしょう。
アダムリア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.