内部データ構造を常に完全にカプセル化する必要がありますか?


11

このクラスを考慮してください:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

このクラスは、データを保存するために使用する配列を、関心のあるクライアントコードに公開します。

作業中のアプリでこれを行いました。sのChordProgressionシーケンスを格納するクラスがありました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;
    }

}

データ構造自体を公開する代わりに、データ構造によって提供されるすべての操作は、データ構造に委任するパブリックメソッドを使用して、データ構造を囲むクラスによって直接提供されるようになりました。

データ構造が変更された場合、これらのメソッドのみを変更する必要がありますが、変更後もすべてのクライアントコードは機能します。

配列よりも複雑なコレクションでは、内部データ構造にアクセスするためだけに3つ以上のメソッドを実装するために、囲むクラスが必要になる場合があることに注意してください。


このアプローチは一般的ですか?これについてどう思いますか?他にどんな欠点がありますか?内部データ構造に委任するために、少なくとも3つのパブリックメソッドを実装クラスで実装することは合理的ですか?

回答:


14

次のようなコード:

   public Thing[] getThings(){
        return things;
    }

あなたのアクセス方法は内部データ構造を直接返すだけで何もしないので、あまり意味がありません。同様に宣言Thing[] thingsすることもできますpublic。アクセス方法の背後にある考え方は、クライアントを内部の変更から隔離し、クライアントが実際のデータ構造を操作できないようにするインターフェイスを作成することです。ただし、インターフェイスで許可されている慎重な方法は除きます。すべてのクライアントコードが壊れたときに発見したように、アクセス方法はそれをしませんでした-それは無駄なコードです。多くのプログラマは、すべてをアクセスメソッドでカプセル化する必要があることをどこかで学んだため、そのようなコードを書く傾向があると思いますが、それは私が説明した理由によるものです。アクセス方法が何の目的にも役立たないときに「フォームに従う」ためだけに行うのは、単なるノイズです。

カプセル化の最も重要な目標のいくつかを達成する提案されたソリューションを絶対にお勧めします:クライアントにクラスの内部実装の詳細から隔離し、内部データ構造に触れることを許可しない堅牢で目立たないインターフェースを提供しますあなたが適切であると判断した方法で期待する-「必要最小限の特権の法則」。CLR、STL、VCLなどの人気のある大規模なOOPフレームワークを見ると、まさにその理由で、提案したパターンが広まっています。

あなたはいつもそうするべきですか?必ずしも。たとえば、基本的にメインワーカークラスのコンポーネントであり、「前向き」ではないヘルパークラスまたはフレンドクラスがある場合、それは必要ありません-それは多くの不必要なコードを追加することになる行き過ぎです。そして、その場合、私はアクセス方法をまったく使用しません-説明したように、それは無意味です。データ構造は、それを使用するメインクラスのみにスコープされる方法で宣言するだけです(ほとんどの言語はそれを行う方法をサポートしています)friend、またはメインワーカークラスなどと同じファイルで宣言します。

あなたの提案でわかるのは、コーディングするのにもっと手間がかかるという唯一の欠点です(そして今、あなたはコンシューマクラスを再コーディングする必要があります-しかし、とにかくそれをしなければならない/しなければなりませんでした)。 -あなたはそれを正しくする必要があり、時にはそれはより多くの作業を必要とします。

優れたプログラマーを優れたものにしていることの1つは、余分な仕事に価値があるときとそうでないときを知っていることです。長い目で見れば、今余分に投入することは将来大きな利益をもたらすでしょう-このプロジェクトでなければ、他のプロジェクトでも。ロボットで所定の形式に従うだけでなく、正しい方法でコーディングし、それについて頭を使うことを学びます。

配列よりも複雑なコレクションでは、内部データ構造にアクセスするためだけに3つ以上のメソッドを実装するために、囲むクラスが必要になる場合があることに注意してください。

包含クラスを通じてデータ構造全体を公開している場合、IMOでは、単により安全なインターフェース(「ラッパークラス」)を提供するためではない場合、そのクラスがまったくカプセル化されている理由を考える必要があります。あなたは、包含クラスがその目的のために存在しないと言っているので、おそらくあなたの設計について正しくない何かがあるでしょう。クラスをより慎重なモジュールに分割し、階層化することを検討してください。

クラスには明確で目立たない目的が1つあり、その機能をサポートするインターフェイスを提供する必要があります。一緒に属していないものを一緒にバンドルしようとしている可能性があります。それを行うと、変更を実装しなければならないたびに事態が破壊されます。クラスが小さく、慎重になればなるほど、物事を簡単に変更できるようになります。ThinkLEGO。


1
回答ありがとうございます。質問:内部データ構造に、おそらく5つのパブリックメソッドがある場合、クラスのパブリックインターフェイスですべてが機能する必要がある場合はどうでしょうか。たとえば、JavaのArrayListには、以下のメソッドがありますget(index)add()size()remove(index)、とremove(Object)。提案された手法を使用して、このArrayListを含むクラスには、内部コレクションに委任するために5つのパブリックメソッドが必要です。プログラムでのこのクラスの目的は、このArrayListをカプセル化することではなく、何か他のことを行うことです。ArrayListは単なる詳細です。[...]
アビブコーン

内部データ構造は単なる普通のメンバーであり、上記の手法を使用する場合、追加の5つのパブリックメソッドを特徴とするクラスを含む必要があります。あなたの意見では-これは合理的ですか?また、これは一般的ですか?
アビブコーン

@Prog- 内部データ構造に、おそらく5つのパブリックメソッドがある場合はどうでしょうか...設計-パブリッククラスが適切なインターフェイスを提示しすぎている、または提示していない。クラスには非常に控えめで明確に定義された役割があり、そのインターフェースはその役割とその役割のみをサポートする必要があります。クラスを分割して階層化することを検討してください。クラスは、カプセル化の名前であらゆる種類のオブジェクトを含む「キッチンシンク」であってはなりません。
ベクトル

ラッパークラスを介してデータ構造全体を公開している場合、IMOは、より安全なインターフェイスを提供するためだけではない場合、そのクラスがなぜカプセル化されるのかを考える必要があります。収容クラスはその目的のために存在しないと言っているので、この設計に関して正しくないことがあります。
ベクトル

1
@Phoshi- 読み取り専用がキーワードです -それに同意できます。しかし、OPは読み取り専用の話ではありません。たとえばremove、読み取り専用ではありません。私が理解しているのは、提案された変更前の元のコードのように、OPはすべてを公開したいpublic Thing[] getThings(){return things;}ということです。それは私が嫌いなことです。
ベクトル

2

あなたは尋ねました:内部データ構造を常に完全にカプセル化すべきですか?

簡単な回答:はい、ほとんどの場合ですが、常にではありませ

ロングアンサー:クラスは次のカテゴリに従うと思います。

  1. 単純なデータをカプセル化するクラス。例:2Dポイント。XおよびY座標を取得/設定する機能を提供するパブリック関数を作成するのは簡単ですが、内部データをあまり手間をかけずに簡単に隠すことができます。このようなクラスの場合、内部データ構造の詳細を公開する必要はありません。

  2. コレクションをカプセル化するコンテナクラス。STLには古典的なコンテナクラスがあります。私もその中に考慮std::stringstd::wstringます。彼らは抽象化に対処するための豊富なインタフェースを提供しますがstd::vectorstd::stringstd::wstringにも生データへのアクセスを取得する機能を提供します。私はそれらを貧弱に設計されたクラスと呼ぶのを急ぐことはないでしょう。これらのクラスが生データを公開する理由はわかりません。しかし、私の仕事では、パフォーマンス上の理由で数百万のメッシュノードとそれらのメッシュノード上のデータを処理する際に、生データを公開する必要があることがわかりました。

    クラスの内部構造を公開することに関する重要なことは、緑の信号を出す前に、長く真剣に考える必要があるということです。インターフェイスがプロジェクトの内部にある場合、将来変更するのは高価ですが、不可能ではありません。インターフェイスがプロジェクトの外部にある場合(他のアプリケーション開発者が使用するライブラリを開発しているときなど)、クライアントを失うことなくインターフェイスを変更することは不可能です。

  3. 本質的にほとんど機能するクラス。例:std::istreamstd::ostream、STLコンテナのイテレータ。これらのクラスの内部詳細を公開するのはまったくばかげています。

  4. ハイブリッドクラス。これらは、いくつかのデータ構造をカプセル化するクラスですが、アルゴリズム機能も提供します。個人的には、これらはよく考えられていないデザインの結果だと思います。ただし、それらを見つけた場合は、ケースごとに内部データを公開することが理にかなっているかどうかを判断する必要があります。

結論として、クラスの内部データ構造を公開する必要があると判断したのは、パフォーマンスのボトルネックになったときだけです。


STLが内部データを公開する最も重要な理由は、多くのポインターを期待するすべての関数との互換性だと思います。
思源レン

0

生データを直接返す代わりに、このようなものを試してください

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; }
  ...
}

これで、適切なカプセル化が行われ、実装の詳細が隠され、後方互換性が提供されます(有料)。


後方互換性のための賢いアイデア。しかし、これで適切なカプセル化ができたので、実装の詳細を非表示にします -実際にはそうではありません。クライアントはまだのニュアンスに対処する必要がありますList。単純にデータメンバーを返すアクセスメソッドは、物事をより堅牢にするためのキャストを使用しても、カプセル化IMOとしてはあまりよくありません。ワーカークラスは、クライアントではなく、すべてを処理する必要があります。クライアントがしなければならない「ダンバー」は、より堅牢になります。余談として、私はあなたが質問...答えはわからない
ベクトル

1
@Vector-あなたは正しいです。返されるデータ構造は依然として変更可能であり、副作用により情報の隠蔽が失われます。
BobDalgleish

返されるデータ構造は依然として変更可能であり、副作用によって情報の隠蔽が破壊されます -はい、それも-それは危険です。私は、クライアントから何が必要かという点で単純に考えていました。それが質問の焦点でした。
ベクトル

@BobDalgleish:元のコレクションのコピーを返してみませんか?
ジョルジオ

1
@BobDalgleish:適切なパフォーマンス上の理由がない限り、内部データ構造への参照を返して、ユーザーが非常に悪い設計決定としてそれらを変更できるようにすることを検討します。オブジェクトの内部状態は、適切なパブリックメソッドを介してのみ変更する必要があります。
ジョルジオ

0

これらのことにはインターフェースを使用する必要があります。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

を使用することもできます。これはCollectionListランダムアクセスなしでコレクションをサポートできるよりも少ないメソッドを提供するかIterable、ストリームをサポートすることもできますが、アクセスメソッドに関してはあまり提供しません...


-1-妥協が不十分で、特に安全ではないIMO:内部データ構造をクライアントに公開し、「Javaコレクション...はおそらく Listを実装する可能性高い」ため、それをマスクして最善を期待しています。ソリューションが真にポリモーフィック/継承ベースである場合-すべてのコレクションが常にList派生クラスとして実装される場合、それはより理にかなっていますが、「最高を期待する」だけでは良い考えではありません。「優れたプログラマーは、一方通行で両方向を見る」。
ベクトル

@Vectorはい、将来のJavaコレクションが実装するList(またはCollection、または少なくともIterable)と想定しています。それがこれらのインターフェースの全体のポイントであり、Java配列がそれらを実装しないのは残念ですが、Javaのコレクションの公式インターフェースであるため、Javaコレクションがそれらを実装すると仮定することはそれほど遠くありません-そのコレクションが古い場合を除きますよりもList、その場合、AbstractListでラップするのは非常に簡単です。
イダンアリー

あなたはあなたの仮定が事実上真実であることが保証されていると言っているので、OK-あなたが説明するのに十分で、私はJavaの男ではないので、私は(私が許可されたら)下票を削除します浸透による場合を除きます。それでも、私は内部データ構造を公開するというこの考えを、それがどのように行われたかに関係なくサポートしておらず、OPの質問(カプセル化に関するもの)に直接回答していません。すなわち、内部データ構造へのアクセスを制限します。
ベクトル

1
@Vectorはい、ユーザーがキャストできListにしArrayListますが、実装は100%を保護することができるようにそうではありません-あなたは、常にアクセスprivateフィールドにリフレクションを使用することができます。そうすることは眉をひそめますが、キャスティングも眉をひそめます(それほどではありません)。カプセル化のポイントは、悪意のあるハッキングを防止することではありません-むしろ、ユーザーが変更したい実装の詳細に依存しないようにすることです。Listインターフェイスを使用すると、まさにそれができます。クラスのユーザーは、変更される可能性のListある具体的なArrayListクラスではなく、インターフェイスに依存できます。
イダンアリー

常にリフレクションを使用してプライベートフィールドに確実にアクセスできます。誰かが悪いコードを書き、デザインを破壊したい場合は、そうすることができます。むしろ、ユーザーを防ぐためです...-それがカプセル化の理由の1つです。もう1つの方法は、クラスの内部状態の整合性と一貫性を確保することです。問題は「悪意のあるハッキング」ではなく、厄介なバグにつながる貧弱な組織です。「最低限必要な特権の法則」-クラスのコンシューマーに必須のものだけを与える-もう必要ありません。内部データ構造全体を公開することが必須である場合、設計上の問題があります。
ベクトル

-2

これは、外部データ構造から内部データ構造を隠すために非常に一般的です。いつか特別にDTOで過剰になります。ドメインモデルにはこれをお勧めします。公開する必要がある場合は、不変のコピーを返します。これとともに、get、set、removeなどのこれらのメソッドを持つインターフェイスを作成することをお勧めします。


1
これは3つの以前の回答に比べて大幅のオファーは何もしていないようだ
ブヨ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.