web-dev-qa-db-ja.com

「変化するものをカプセル化する」と言ったとき、それはどういう意味ですか?

OOP私が出会った原則の1つは次のとおりです。-変化するものをカプセル化します。

フレーズの文字通りの意味、つまり変化するものを隠すことを理解しています。しかし、それがどのようにしてより良い設計に貢献するのか正確にはわかりません。誰かが良い例を使ってそれを説明できますか?

25
Haris Ghauri

次のようなコードを記述できます。

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

または、次のようなコードを記述できます。

pet.speak();

変化するものがカプセル化されている場合、それについて心配する必要はありません。何が必要か、何を使用しているのかを心配するだけで、変化に基づいて実際に必要なことを実行する方法がわかります。

何が変化するかをカプセル化します。何が変化するかを気にするコードを拡散する必要はありません。あなたはペットをそのタイプとして話す方法を知っている特定のタイプに設定し、その後、あなたはどのタイプを忘れてそれをペットのように扱うことができます。どのタイプを尋ねる必要はありません。

タイプにアクセスするにはゲッターが必要なので、タイプはカプセル化されていると思うかもしれません。私はしません。ゲッターは実際にはカプセル化されていません。誰かがあなたのカプセル化を破ったとき、彼らはただ戦うだけです。これらは、デバッグコードとして最もよく使用されるアスペクト指向フックのような素敵なデコレータです。どのようにそれをスライスしても、あなたはまだタイプを公開しています。

この例を見て、私は多態性とカプセル化を融合していると思うかもしれません。私は違います。 「何が変わるか」と「詳細」を融合させています。

あなたのペットが犬であるという事実は詳細です。異なる場合があります。そうではないかもしれません。しかし確かに人から人へと変わるかもしれないもの。このソフトウェアが犬愛好家によってのみ使用されると私たちが信じない限り、犬を細部として扱い、それをカプセル化することは賢明です。このようにして、システムの一部は幸いにも犬に気づかず、「オウムは私たち」とマージしても影響を受けません。

詳細を他のコードから切り離し、分離し、非表示にします。詳細の知識がシステム全体に広がらないようにしてください。そうすれば、「変化するものをカプセル化」することができます。

32
candied_orange

ここで「変化する」とは、「要件の変化により、時間とともに変化する可能性がある」ことを意味します。これは中核的な設計原則です。将来的に個別に変更する必要がある可能性のあるコードやデータを分離して分離することです。単一の要件が変更された場合、理想的には、関連するコードを単一の場所で変更することのみを要求します。ただし、コードベースの設計が適切でない、つまり高度に相互接続されており、要件のロジックが多くの場所に広がっている場合、変更は困難であり、予期しない影響を引き起こす可能性が高くなります。

多くの場所で消費税計算を使用するアプリケーションがあるとします。消費税率が変わった場合、何を選びますか:

  • 消費税率は、消費税が計算されるアプリケーションのあらゆる場所にハードコードされたリテラルです。

  • 消費税率はグローバルな定数で、アプリケーションで消費税が計算されるすべての場所で使用されます。

  • 消費税率が使用される唯一の場所であるcalculateSalesTax(product)と呼ばれる単一のメソッドがあります。

  • 消費税率は、構成ファイルまたはデータベースフィールドで指定されます。

消費税率は、他の要件とは関係なく政治的な決定により変更される可能性があるため、構成に分離して、コードに影響を与えずに変更できるようにすることをお勧めします。しかし、消費税を計算するためのロジックが変更されることも考えられます。製品ごとにレートが異なるため、計算ロジックをカプセル化することもできます。グローバル定数は良い考えのように思えるかもしれませんが、消費税を単一の場所ではなくプログラムのさまざまな場所で使用することを奨励する可能性があるため、実際には悪いです。

次に、コードの多くの場所で使用されている別の定数Piについて考えます。同じ設計原理が成り立ちますか?いいえ、Piは変更されないためです。それを構成ファイルまたはデータベースフィールドに抽出すると、不必要な複雑さが生じるだけです(そして、他のすべてが同じであれば、最も単純なコードを好みます)。不整合を回避して読みやすくするために、複数の場所でハードコードするのではなく、グローバル定数にすることは理にかなっています。

重要なのは、プログラムがどのように機能するかだけを見るとnowであり、消費税率とPiは同等であり、どちらも定数です。何が変化する可能性があるかを検討した場合のみ将来的には、設計でそれらを異なる方法で処理する必要があることに気付きます。

この原則は実際には非常に深いものです。これは、コードベースが行うことになっていることだけにとどまらず、今日を検討する必要があり、変更を引き起こす可能性のある外力を考慮し、さらには要件の背後にあるさまざまな利害関係者。

17
JacquesB

現在の答えはどちらも部分的にしか上手くいかなかったようで、コアアイデアを曇らせる例に焦点を当てています。これも(単独で)OOP=の原則ではなく、一般にソフトウェア設計の原則です。

このフレーズで「変化」しているのはコードです。クリストフは、それは通常変化する可能性があるものであり、頻繁に予想されるこれ。目標は、コードの将来の変更から身を守ることです。これは インターフェースに対するプログラミング と密接に関連しています。ただし、クリストフはこれを「実装の詳細」に限定するのは正しくありません。実際、このアドバイスの価値は、requirementsの変更が原因であることがよくあります。

これは、カプセル化状態に間接的にのみ関連しています。これは、David Arnoが考えていることです。このアドバイスはカプセル化状態を常に提案するわけではありませんが(多くの場合そうです)、このアドバイスは不変オブジェクトにも適用されます。実際、定数に名前を付けることは、変化するものをカプセル化する(非常に基本的な)形式です。

CandiedOrangeは、「詳細」で「変化するもの」を明示的に調整します。これは部分的に正しいだけです。変化するコードは何らかの意味で「詳細」であることに同意しますが、「詳細」は変化しない場合があります(これをトートロジーにするために「詳細」を定義しない限り)。不変の詳細をカプセル化する理由はあるかもしれませんが、この口述は1つではありません。大まかに言えば、「犬」、「猫」、「アヒル」だけが対処する必要のあるタイプであると確信している場合、この口述はCandiedOrangeが実行するリファクタリングを示唆していません。

別のコンテキストでCandiedOrangeの例をキャストする場合、Cのような手続き型言語があると想定します。次を含むコードがある場合:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

このコードが将来変更されることは当然期待できます。新しいプロシージャを定義するだけで、カプセル化できます。

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

コードのブロックの代わりにこの新しい手順を使用する(つまり、「抽出メソッド」リファクタリング)。この時点で、「牛」タイプまたはspeakプロシージャの更新のみが必要なものを追加します。もちろん、OO言語では、代わりにCandiedOrangeの回答で暗示されているように動的ディスパッチを活用できます。これは、インターフェースを介してpetにアクセスした場合に自然に発生します。条件ロジックの削除動的ディスパッチは、私がこの手続き型レンディションを作成した理由の一部である直交する関心事です。これには、OOPに固有の機能が必要ないことも強調します。OO言語でも、新しいクラスまたはインターフェースを作成する必要があることを必ずしも意味しません。

より典型的な例(これはOOに近いですが、完全にOOではありません)として、リストから重複を削除したいとします。別のリストでこれまでに見た項目を追跡し、見た項目を削除してリストを反復処理することでそれを実装するとします。少なくともパフォーマンス上の理由から、表示されたアイテムを追跡する方法を変更したいと考えるのは妥当です。変化するものをカプセル化するという口述は、見られたアイテムのセットを表すために抽象的なデータ型を構築する必要があることを示唆しています。これで、この抽象Setデータ型に対してアルゴリズムが定義されました。バイナリ検索ツリーに切り替えることにした場合、アルゴリズムを変更したり、気にする必要はありません。 OO言語では、クラスまたはインターフェイスを使用してこの抽象データ型をキャプチャできます。SML/ O'Camlなどの言語では、代わりにSet抽象データ型をモジュールとしてキャプチャできます。

要件駆動型の例として、いくつかのビジネスロジックに関していくつかのフィールドを検証する必要があるとしましょう。現在、特定の要件があるかもしれませんが、それらが進化することを強く疑っています。現在のロジックを独自のプロシージャ/関数/ルール/クラスにカプセル化できます。

これは「変化するもののカプセル化」の一部ではない直交する問題ですが、現在カプセル化されているロジックを抽象化、つまりパラメーター化するのは自然なことです。これは通常、より柔軟なコードにつながり、カプセル化されたロジックを変更するのではなく、代替実装で置き換えることでロジックを変更できます。

16

「変化するものをカプセル化する」とは、変更および進化する可能性がある実装の詳細を隠すことを指します。

例:

たとえば、クラスCourseが、register()できるStudentsを追跡しているとします。 LinkedListを使用して実装し、コンテナを公開して反復を許可できます。

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

しかし、これは良い考えではありません。

  • まず、人々は良い振る舞いに欠け、それをセルフサービスとして使用し、register()メソッドを経由せずに、学生をリストに直接追加できます。
  • しかし、さらに面倒です。これは、使用されるクラスの内部実装の詳細に対する「使用コード」の依存関係を作成します。これにより、たとえば配列、ベクトル、シート番号付きのマップ、または独自の永続データ構造を使用したい場合など、クラスの将来の進化が妨げられる可能性があります。

変化するもの(つまり、変化する可能性のあるもの)をカプセル化すると、使用するコードとカプセル化されたクラスの両方が自由に進化し合うことができます。これがOOPで重要な原則である理由です。

追加の読み:

11
Christophe