最近、不変オブジェクトの概念に出くわしたので、状態へのアクセスを制御するためのベストプラクティスを知りたいと思います。私の脳のオブジェクト指向部分は、公のメンバーを見て恐怖に襲われたくなりますが、次のような技術的な問題は見られません。
public class Foo {
public final int x;
public final int y;
public Foo( int x, int y) {
this.x = x;
this.y = y;
}
}
フィールドをprivate
として宣言し、それぞれにgetterメソッドを提供する方が快適だと思いますが、状態が明示的に読み取り専用の場合、これは非常に複雑に見えます。
不変オブジェクトの状態へのアクセスを提供するためのベストプラクティスは何ですか?
これは、オブジェクトをどのように使用するかに完全に依存します。パブリックフィールドは本質的に悪ではありません。すべてをデフォルトでパブリックにするのは悪いことです。たとえば、Java.awt.Pointクラスは、そのxフィールドとyフィールドをパブリックにしますが、それらは最終的なものではありません。あなたの例はパブリックフィールドの良い使い方のように見えますが、別の不変オブジェクトのすべての内部フィールドを公開したくない場合もあります。包括的なルールはありません。
私は過去に同じことを考えていましたが、通常は変数をプライベートにし、ゲッターとセッターを使用することになります。そのため、後で同じインターフェイスを維持しながら実装に変更を加えることができます。
これは、ロバートC.マーチンの「クリーンコード」で最近読んだことを思い出させてくれました。第6章では、彼はわずかに異なる視点を示しています。たとえば、95ページで彼は述べています
「オブジェクトは、抽象化の背後にデータを隠し、そのデータを操作する関数を公開します。データ構造はデータを公開し、意味のある関数はありません。」
そして100ページ:
Beanの準カプセル化により、一部のOO純粋主義者は気分が良くなるようですが、通常は他の利点はありません。
コードサンプルに基づくと、Fooクラスはデータ構造のように見えます。したがって、Clean Codeでの議論(私が与えた2つの引用符以上のもの)から理解したことに基づいて、クラスの目的は機能ではなくデータを公開することであり、ゲッターとセッターを持つことはおそらくあまり役に立ちません。
繰り返しになりますが、私の経験では、私は通常、先に進んで、ゲッターとセッターでプライベートデータの「豆」アプローチを使用しました。しかし、繰り返しになりますが、より良いコードを書く方法について本を書くように私に頼んだ人は誰もいなかったので、マーティンは何か言いたいことがあるかもしれません。
オブジェクトがローカルで十分に使用されていて、将来APIの変更を壊す問題を気にしない場合は、インスタンス変数の上にゲッターを追加する必要はありません。しかし、これは一般的な主題であり、不変オブジェクトに固有のものではありません。
ゲッターを使用する利点は、間接参照の1つの追加レイヤーにあります。これは、広く使用されるオブジェクトを設計する場合にmay便利です。ユーティリティは、予測できない将来に拡張されます。
不変性に関係なく、このクラスの実装を公開しています。ある段階で、実装を変更したい場合(または、Pointの例を使用してさまざまな派生を生成したい場合、極座標を使用して同様のPointクラスが必要な場合があります)、クライアントコードはこれにさらされます。
上記のパターンは便利かもしれませんが、一般的には非常にローカライズされたインスタンスに制限します(たとえば、周りの情報のタプルを渡す-一見無関係な情報のオブジェクトが悪いことに気付く傾向がありますカプセル化、または情報isに関連していて、私のタプルが本格的なオブジェクトに変換されます)
覚えておくべき重要なことは、関数呼び出しがユニバーサルインターフェイスを提供することです。任意のオブジェクトが関数呼び出しを使用して他のオブジェクトと対話できます。あなたがしなければならないのは正しい署名を定義することだけです、そしてあなたは去ります。唯一の落とし穴は、これらの関数呼び出しを介してのみ対話する必要があることです。これは多くの場合うまく機能しますが、場合によっては不格好になる可能性があります。
状態変数を直接公開する主な理由は、これらのフィールドでプリミティブ演算子を直接使用できるようにするためです。うまくいくと、読みやすさと利便性が向上します。たとえば、_+
_、または_[]
_を使用してキー付きコレクションにアクセスします。構文の使用が従来の規則に従っている場合、これの利点は驚くべきものになる可能性があります。
演算子はユニバーサルインターフェイスではないという問題があります。非常に特定の組み込み型のセットのみがそれらを使用できます。これらは言語が期待する方法でのみ使用でき、定義することはできません。新しいもの。したがって、プリミティブを使用してパブリックインターフェイスを定義すると、そのプリミティブと、そのプリミティブ(およびそれに簡単にキャストできるその他のもの)のみを使用するようになります。他のものを使用するには、プリミティブを操作するたびにそのプリミティブの周りで踊る必要があり、それはDRYの観点からあなたを殺します:物事は非常にすぐに壊れやすくなります。
一部の言語では演算子がユニバーサルインターフェイスになりますが、Javaではありません。これはJavaの告発ではありません。その設計者は、演算子のオーバーロードを含めないことを意図的に選択しました。演算子の従来の意味にうまく適合しているように見えるオブジェクトを操作している場合でも、実際に意味のある方法でそれらを機能させることは、驚くほど微妙な違いがあります。多くの場合、関数ベースのインターフェイスを読み取り可能で使用可能にする方が、そのプロセスを実行するよりもはるかに簡単であり、多くの場合、最終的にはより良い結果が得られます。使用される演算子。
ただし、はその決定に関係するトレードオフでした。は演算子ベースのインターフェースが実際には関数ベースのインターフェースよりもうまく機能するが、演算子のオーバーロードがなければ、そのオプションは利用できない場合があります。とにかくオペレーターを靴べらにしようとすると、おそらくあなたが本当に石に固執したくないいくつかの設計上の決定にあなたを閉じ込めるでしょう。 Java設計者は、このトレードオフは価値があると考えており、それについては正しいかもしれません。しかし、このような決定は、何らかのフォールアウトなしでは実現できません。このような状況では、フォールアウトが発生します。ヒット。
要するに、問題はあなたの実装を公開することではありません、それ自体。問題はあなた自身をその実装に閉じ込めることです。
実際には、カプセル化を破って、オブジェクトの任意のプロパティを何らかの方法で公開します。すべてのプロパティは実装の詳細です。誰もがこれを行うからといって、それは正しくありません。アクセサーとミューテーター(ゲッターとセッター)を使用しても、それは改善されません。むしろ、カプセル化を維持するためにCQRSパターンを使用する必要があります。
あなたが開発したクラスは、現在の化身では問題ないはずです。この問題は通常、誰かがこのクラスを変更したり、このクラスから継承しようとしたときに発生します。
たとえば、上記のコードを見た後、誰かがクラスBarの別のメンバー変数インスタンスを追加することを考えています。
public class Foo {
public final int x;
public final int y;
public final Bar z;
public Foo( int x, int y, Bar z) {
this.x = x;
this.y = y;
}
}
public class Bar {
public int age; //Oops this is not final, may be a mistake but still
public Bar(int age) {
this.age = age;
}
}
上記のコードでは、Barのインスタンスを変更することはできませんが、外部的には、誰でもBar.ageの値を更新できます。
ベストプラクティスは、すべてのフィールドをプライベートとしてマークし、フィールドのゲッターを用意することです。オブジェクトまたはコレクションを返す場合は、変更できないバージョンを返すようにしてください。
並行プログラミングには不変性が不可欠です。
最終的なプロパティのゲッターを持つ小道具は1つだけです。これは、インターフェイスを介してプロパティにアクセスしたい場合です。
public interface Point {
int getX();
int getY();
}
public class Foo implements Point {...}
public class Foo2 implements Point {...}
それ以外の場合、パブリック最終フィールドはOKです。
パブリックコンストラクターパラメーターからロードされるパブリックfinalフィールドを持つオブジェクトは、それ自体を単純なデータホルダーとして効果的に表現します。このようなデータホルダーは特に「OOPっぽい」ものではありませんが、単一のフィールド、変数、パラメーター、または戻り値で複数の値をカプセル化できるようにする場合に役立ちます。型の目的がいくつかの値を結合する簡単な手段として機能することである場合、そのようなデータホルダーは、実際の値型のないフレームワークでの最良の表現であることがよくあります。
あるメソッドFoo
が呼び出し元に "X = 5、Y = 23、Z = 57"をカプセル化するPoint3d
を与えたい場合、何が起こりたいかという質問を考えてみてください。 X = 5、Y = 23、およびZ = 57であるPoint3d
への参照があります。 Fooが持っているものが単純な不変のデータホルダーであることがわかっている場合、Fooは単に呼び出し元にそれへの参照を与える必要があります。ただし、それが別のものである可能性がある場合(たとえば、X、Y、およびZ以外の追加情報が含まれている可能性があります)、Fooは「X = 5、Y = 23」を含む新しい単純なデータホルダーを作成する必要があります、Z = 57 "であり、呼び出し元にその参照を提供します。
Point3d
を封印し、その内容をパブリックfinalフィールドとして公開することは、Fooのようなメソッドが、それが単純な不変のデータホルダーであると想定し、そのインスタンスへの参照を安全に共有できることを意味します。そのような仮定を行うコードが存在する場合、そのようなコードを壊さずにPoint3d
を単純な不変のデータホルダー以外のものに変更することは困難または不可能な場合があります。一方、Point3d
が単純な不変のデータホルダーであると想定するコードは、他の何かである可能性を処理する必要があるコードよりもはるかに単純で効率的です。
このスタイルはScalaでよく見られますが、これらの言語には決定的な違いがあります。Scalaは niform Access Principle に従いますが、Javaそうではありません。つまり、クラスが変更されない限り、デザインは問題ありませんが、機能を適応させる必要がある場合、いくつかの方法で壊れることがあります。
x
はサブクラスの追加データから計算できます)x
は何らかの理由で負でない必要があります)また、このスタイルを変更可能なメンバー(悪名高いJava.util.Date
など)には使用できないことにも注意してください。ゲッターでのみ、防御的なコピーを作成したり、表現を変更したりする機会があります(たとえば、Date
情報をlong
として保存する)
唯一の目的が意味のある名前で2つの値を相互に結合することである場合、コンストラクターの定義をスキップして要素を変更可能に保つことを検討することもできます。
public class Sculpture {
public int weight = 0;
public int price = 0;
}
これには、クラスをインスタンス化するときにパラメーターの順序を混乱させるリスクを最小限に抑えるという利点があります。制限された変更可能性は、必要に応じて、コンテナー全体をprivate
の制御下に置くことで実現できます。
私はあなたが質問に入れたものと非常によく似た構造をたくさん使用しますが、クラスよりも(時には不変の)データ構造でよりよくモデル化できるものがある場合があります。
すべては、オブジェクトをモデリングしている場合、その動作によって定義されたオブジェクトに依存します。この場合、内部プロパティを公開することはありません。また、データ構造をモデル化していて、Javaにはデータ構造用の特別な構造がない場合、クラスを使用してすべてのプロパティを公開するのは問題ありません。不変性が必要な場合は、finalとコース外のパブリック。
たとえば、ロバート・マーティンは、すばらしい本Clean Codeにこれに関する1つの章を持っていますが、私の意見では必読です。
ただ反映したい 反射 :
Foo foo = new Foo(0, 1); // x=0, y=1
Field fieldX = Foo.class.getField("x");
fieldX.setAccessible(true);
fieldX.set(foo, 5);
System.out.println(foo.x); // 5!
それで、Foo
はまだ不変ですか? :)