このクラスを検討してください:
_class ClassA{
private Thing[] things; // stores data
// stuff omitted
public Thing[] getThings(){
return things;
}
}
_
このクラスは、データを格納するために使用する配列を、関心のあるクライアントコードに公開します。
私が取り組んでいるアプリでこれを行いました。 ChordProgression
sのシーケンスを格納するChord
クラスがありました(他にもいくつかのことをしています)。コードの配列を返すChord[] getChords()
メソッドがありました。データ構造を(配列からArrayListに)変更する必要がある場合、すべてのクライアントコードが壊れました。
これは私に考えさせました-多分次のアプローチがより良いです:
_class ClassA{
private Thing[] things; // stores data
// stuff omitted
public Thing[] getThing(int index){
return things[index];
}
public int getDataSize(){
return things.length;
}
public void setThing(int index, Thing thing){
things[index] = thing;
}
}
_
データ構造自体を公開する代わりに、データ構造によって提供されるすべての操作は、データ構造に委任するパブリックメソッドを使用して、データ構造を含むクラスによって直接提供されるようになります。
データ構造が変更された場合、これらのメソッドのみを変更する必要がありますが、変更後もすべてのクライアントコードは機能します。
配列よりも複雑なコレクションでは、内部データ構造にアクセスするためだけに、包含クラスが4つ以上のメソッドを実装する必要がある場合があります。
このアプローチは一般的ですか?これについてどう思いますか?他にどんな欠点がありますか?内部データ構造に委譲するためだけに、囲んでいるクラスに少なくとも3つのパブリックメソッドを実装させるのは妥当ですか?
次のようなコード:
public Thing[] getThings(){
return things;
}
アクセス方法は内部データ構造を直接返す以外に何もしないので、あまり意味がありません。 Thing[] things
はpublic
になります。アクセスメソッドの背後にある考え方は、クライアントを内部の変更から隔離し、インターフェイスで許可されている慎重な方法を除いて、クライアントが実際のデータ構造を操作できないようにするインターフェイスを作成することです。すべてのクライアントコードが壊れたときに発見したように、アクセスメソッドはそれを実行しませんでした-それは無駄なコードです。多くのプログラマーは、すべてをアクセスメソッドでカプセル化する必要があることをどこかで学んだため、そのようなコードを書く傾向があると思いますが、それは私が説明した理由によるものです。アクセス方法が目的を果たさないときに「フォームに従う」ためだけにそれを行うのは、単なるノイズです。
カプセル化の最も重要な目標のいくつかを達成する提案されたソリューションをお勧めします:クライアントにクラスの内部実装の詳細から隔離し、内部データ構造に触れることを許可しない堅牢で目立たないインターフェースをクライアントに与えるあなたが適切であると決定する方法で期待してください-「最低限の特権の法則」。 CLR、STL、VCLなどの人気の高いOOPフレームワークを見ると、まさにその理由から、提案したパターンは広く普及しています。
常にするべきですか?必ずしも。たとえば、基本的にメインワーカークラスのコンポーネントであり、「正面を向いていない」ヘルパーまたはフレンドクラスがある場合、それは必要ではありません-不要なコードを大量に追加するのはやり過ぎです。そしてその場合、私はアクセス方法をまったく使用しません-説明したように、それは無意味です。データ構造を、それを使用するメインクラスのみをスコープとする方法で宣言します。ほとんどの言語では、その方法をサポートしています-friend
、またはメインワーカークラスと同じファイルで宣言します。
唯一の欠点私は提案でそれをコーディングすることがより多くの仕事であることがわかります(そして今、あなたはあなたの消費者クラスを再コーディングしなければならないでしょう-しかし、とにかくそれをしなければならない/しなければならなかった。)しかし、それは実際にはマイナス面ではありません。正しく実行する必要があり、時にはより多くの作業が必要になります。
優れたプログラマーを優れたものにする1つの点は、余分な作業に価値がある場合とそうでない場合を知っていることです。長期的に見れば、エクストラを追加することで、将来的に大きな利益が得られます。このプロジェクトではないとしても、他のプロジェクトではそうです。ロボットが所定のフォームに従うだけでなく、正しい方法でコーディングして頭を使う方法を学びます。
配列よりも複雑なコレクションでは、内部データ構造にアクセスするためだけに、囲んでいるクラスが4つ以上のメソッドを実装する必要がある場合があります。
包含クラスを通じてデータ構造全体を公開している場合、IMOは、より安全なインターフェイス(「ラッパークラス」)を提供するだけではない場合、そのクラスがカプセル化されている理由を考える必要があります。その目的のために包含クラスが存在しないと言っているので、おそらくデザインに問題があるのでしょう。クラスをより目立たないモジュールに分割し、それらを階層化することを検討してください。
クラスには明確で目立たない目的が1つあり、その機能をサポートするためのインターフェースを提供する必要があります。一緒に属していないものをまとめようとしている可能性があります。これを行うと、変更を実装する必要があるたびに状況が崩れます。クラスが小さくて目立たないほど、変更が簡単になります:レゴだと思います。
質問:内部データ構造を常に完全にカプセル化する必要がありますか?
簡単な回答:はい、ほとんどの場合常にではありません。
長い回答:クラスは次のカテゴリに分類されると思います。
単純なデータをカプセル化するクラス。例:2Dポイント。 X座標とY座標を取得/設定する機能を提供するパブリック関数を作成するのは簡単ですが、あまり問題なく内部データを簡単に非表示にすることができます。そのようなクラスの場合、内部データ構造の詳細を公開することは要求されません。
コレクションをカプセル化するコンテナクラス。 STLには、古典的なコンテナークラスがあります。私が検討します std::string
およびstd::wstring
それらの中にも。それらは抽象化に対処するための豊富なインターフェースを提供しますが、std::vector
、std::string
、std::wstring
は、生データにアクセスする機能も提供します。私はそれらを不十分に設計されたクラスと呼ぶのを急いでいません。これらのクラスが生データを公開する理由がわかりません。ただし、私の作業では、パフォーマンス上の理由から何百万ものメッシュノードとそれらのメッシュノード上のデータを処理するときに、生データを公開する必要があることに気付きました。
クラスの内部構造を公開することの重要なことは、クラスに緑の信号を与える前に、じっくりと考えなければならないということです。インターフェースがプロジェクトの内部にある場合、将来変更するのはコストがかかりますが、不可能ではありません。インターフェイスがプロジェクトの外部にある場合(他のアプリケーション開発者が使用するライブラリを開発している場合など)、クライアントを失うことなくインターフェイスを変更することは不可能です。
ほとんどが本質的に機能するクラス。例:std::istream
、std::ostream
、STLコンテナのイテレータ。これらのクラスの内部の詳細を公開するのはまったく愚かです。
ハイブリッドクラス。これらは、一部のデータ構造をカプセル化するクラスですが、アルゴリズム機能も提供します。個人的には、これらのデザインはよく考えられていないためだと思います。ただし、それらを見つけた場合は、ケースバイケースで内部データを公開することが理にかなっているかどうかを判断する必要があります。
結論として:クラスの内部データ構造を公開する必要があるのは、パフォーマンスのボトルネックになったときだけです。
あなたはそれらのためにインターフェースを使うべきです。 Javaの配列はそれらのインターフェースを実装していないので、あなたの場合は役に立ちませんが、これからはそれを行うべきです:
class ClassA{
public ClassA(){
things = new ArrayList<Thing>();
}
private List<Thing> things; // stores data
// stuff omitted
public List<Thing> getThings(){
return things;
}
}
こうすることで、ArrayList
をLinkedList
やその他に変更できます。すべてのJavaコレクション(配列のほか))があるため、コードを壊すことはありません(疑似?)ランダムアクセスはおそらくList
を実装します。
Collection
を使用することもできます。これは、List
より少ないメソッドを提供しますが、ランダムアクセスなしでコレクションをサポートできます。または、ストリームをサポートすることもできますが、長期的には提供しないIterable
を使用できます。アクセス方法の...
生データを直接返す代わりに、次のようなことを試してください
class ClassA {
private Things[] things;
...
public Things[] asArray() { return things; }
public List<Thing> asList() { ... }
...
}
したがって、あなたは本質的に、世界に望まれるあらゆる面を提示するカスタムコレクションを提供しています。新しい実装では、
class ClassA {
private List<Thing> things;
...
public Things[] asArray() { return things.asArray(); }
public List<Thing> asList() { return things; }
...
}
これで、適切なカプセル化が行われ、実装の詳細が非表示になり、下位互換性が提供されます(有料)。