web-dev-qa-db-ja.com

OOP:オブジェクトを親クラスに埋め込む場合のインターフェース設計

小さなアプリの設計中に、どちらも機能する2つの可能な設計案ができましたが、次の点で、どちらかが他の方法よりも優れていると感じていますOOP原則、コードを回避するにおいなど.

ここでのシナリオは、次のように、親クラスにオブジェクトが埋め込まれている場合です。

_class Balance {
   private amount: number;
}

class Wallet {
   private balance: Balance; // the embedded 'Balance' object
}
_

そして、私の主な関心事は、埋め込まれたBalanceオブジェクトを含む操作を管理する方法です。これはWalletインターフェイスを介して実行するか、Balanceインスタンスに直接アクセスして実行する必要があります(たとえば、getter関数を介して)。

最初の選択肢:

残高はWalletクラスインターフェースを介して管理されます。

_class Balance {
   private amount: number;

   // constructor

   public getAmount(): number {
      return this.amount;
   }

   public setAmount(amount: number): void {
      this.amount = amount;
   }
}

class Wallet {
   private balance: Balance;

   // constructor 

   public add(amount): void {
      if (amount > 0) {
         this.balance.setAmount(amount);
      }
   }

   public remove(amount): void {
      const total = this.balance.getAmount();

      if (total >= amount) {
         this.balance.setAmount(total - amount);
      }
   }

   public getBalance(): number {
      return this.balance.getAmount();
   }
}
_

使用法:

_const wallet: Wallet = new Wallet(0);

wallet.add(12);
wallet.remove(10);

const total: number = wallet.getBalance(); // total = 2
_

2番目の選択肢:

天びんは、独自のクラスインターフェイスを介して直接管理されます(最初にWalletクラスのgetter関数を使用して天びんにアクセスする必要があります:wallet.getBalance())。

_class Balance {
   private amount: number;

   // constructor

   public getAmount(): number {
      return this.amount;
   }

   public addAmount(amount: number): void {
      if (amount > 0) {
         this.amount = this.amount + amount;
      }
   }

   public removeAmount(amount: number): void {
      if (amount >= this.amount) {
         this.amount = this.amount - amount;
      }
   }
}

class Wallet {
   private balance: Balance;

   // constructor 

   public getBalance(): Balance {
      return this.balance;
   }
}
_

使用法:

_const wallet: Wallet = new Wallet(0);

wallet.getBalance().addAmount(12);

const walletBalance: Balance = wallet.getBalance();
walletBalance.removeAmount(10);

const total: number = wallet.getBalance().getAmount(); // total = 2
_

考え:

さて、ここでの最初の選択肢が進むべき道であることは明らかです:Walletは内部に存在するものをかなりうまくカプセル化します(バランスの詳細はこのクラスの背後に隠されています)、2番目の選択肢と比較しても、使用法はよりクリーンで簡単に見えます。

しかし、このような1つ以上のオブジェクトをオブジェクトに埋め込んでしまうような場合に、この種のシナリオに取り組む方法について少し混乱する傾向があります。埋め込まれたオブジェクトのいずれかでいくつかのアクションを実行できます。これはどのように管理する必要がありますか?ホルダーのインターフェースを介してそれを行う必要がありますか?オブジェクトインスタンスに直接?

私がクールではないと思うのは、間違いなく次のようなものになることです。

_class B {
   foo();
   bar();
   baz();
}

class A {
   b: B;

   boo() {
      this.b.foo();
   }

   Zoo() {
      this.b.bar();
      this.b.baz();
   }
}

const a = new A();

a.boo();
a.Zoo();
_

その結果、機能の羨望コードの臭いが発生します...

この種のシナリオに取り組むために不可欠だと思われる設計上のアドバイス、コメント、ヒントなどはありますか?

1
charliebrownie

実際、その最後の例は正当です。そのおそらくアダプター。コマンド、ファサード、または...でもかまいません。

別の実装をカプセル化することが理にかなっている場合があります。といった:

  • 可変オブジェクトの不変バージョンを提供します。コンストラクターは、受信したBのコピーを作成し、不変の操作のみを公開/サポートできます。
  • ロギングやフィルタリングなどの補助サービスまたはアスペクトサービスを提供します。
  • 事業運営のカプセル化。

これを処理するために何が必要ですか? 目標

真剣に何を目指していますか?

  • Walletは古いBalance指向のコードで動作する必要がありますか?
  • 別のWalletを使用せずにBalanceを実装することには意味がありますか?

最適な設計は1つではなく、トレードオフだけです。トレードオフは後で役立つ場合がありますが、後で妨げられることがよくありますが、すべて目標に到達するのに役立ちます。

したがって、最初の要件は次のとおりです。目標は何ですか。

そうは言っても、物事がどのように移動できるかを理解し、意図を説明するときに実装を指示しないことは助けになります。


いくつかの用語で明確にするために:

  • インターフェイスは、オブジェクトに対する操作を説明するコントラクトです。操作の入力、出力、およびオブジェクトの状態への影響を含みます。インターフェースは、返されたオブジェクトの操作、さらにはグローバル操作またはグローバル状態にまで拡張できます。それらはシステムの意図と考えることができます。

  • 実装は、ある種のデータ構造と、1つ以上のインターフェースをサポートするそのデータ構造に対する操作を記述します。

Walletの実装を検討してください。内部状態を完全に制御する必要があります。そうでなければ、それは最も些細な契約のみをサポートすることができます:何かが起こる可能性があります...

WalletインターフェースがBalanceが返されることを示している場合、Wallet実装は制御を維持できます。

  • Balanceが不変の場合。制御は主にプラットフォームによって実施されます。 Balanceへのサードパーティアクセスは変更できないため、インターフェースを介して公開しても安全です。

  • Balanceを簡単に実装できる場合、WalletBalanceではなくWalletBalanceを返します。このように、操作が発生したときに、Walletの他の部分を適切に更新することにより、Walletのコントラクトを維持できます。

  • Balanceが簡単に作成できる場合、Walletは必要に応じて簡単に作成できます。これは、不変の状態、または回答として返される可変オブジェクトには問題ありません。ただし、Balanceに対する操作がWalletに影響を与えることを意図している場合、Balanceがその状態を維持できるようにするには、Walletが何らかの形式のコールバックをサポートする必要があります。 。

  • Balanceがコピーしやすい場合は、コピーを作成してそれを返すことにより、制御を維持できます。これは、不変の状態、または回答として返される可変オブジェクトには問題ありません。ただし、Balanceに対する操作がWalletに影響を与えることを意図している場合、Balanceがその状態を維持できるようにするには、Walletが何らかの形式のコールバックをサポートする必要があります。 。

そうしないと、制御を維持し、Balance(不変、拡張、構築、またはコピー)の実装を公開することが難しくなります。この場合、実際に要件を実装するのは困難/不可能であるため、インターフェースは設計上かなり壊れている可能性があります。

  • おそらく、インターフェースがBalanceLiteを公開する可能性がありますか?不変、拡張可能、構築可能、またはコピー可能であるという上記のプロパティの1つが必要になります。

    • ただし、1つの注意点は、可変BalanceLiteに拡張された不変のBalanceは、内部状態の一部である場合、直接返されるべきではないということです。通常、サードパーティのコードがそれをキャストしてBalanceインターフェイスに戻し、bad things tmを実行する可能性があるためです。
  • おそらく、インターフェースはBalanceインターフェースを完全に放棄する必要があります。その後、他の操作を通じて十分な能力を獲得する必要があります。

    • Walletコントラクトを制約して、不適切な可能性のあるBalanceインターフェイスを組み込む必要がないため、これが最善の解決策になる可能性があります。
    • 他のコードでBalanceが必要な場合は、最後の例を使用してWalletをラップすることで提供できます。
4
Kain0_0