1

During the design of a small app I ended up with two possible design alternatives that both would work but, somehow, I feel one could be better than the other in terms of following OOP Principles, avoiding code smells, etc.

The scenario here would be having an object embedded in a parent class, for example:

class Balance {
   private amount: number;
}

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

And my main concern is how to manage operations that involve the embedded Balance object: should this be done through the Wallet interface or should it be done accessing the Balance instance directly (through a getter function, for example).

First alternative:

The balance is managed through the Wallet class interface.

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();
   }
}

Usage:

const wallet: Wallet = new Wallet(0);

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

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

Second alternative:

The balance is managed directly through its own class interface (should access it first using a getter function on the Wallet class: 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;
   }
}

Usage:

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

Thoughts:

Okay, it seems obvious that the first alternative here would be the way to go: Wallet encapsulates pretty well what exists under the hoods (the balance details are hidden behind this class) and the usage seems cleaner and easier also compared to the second alternative.

But sometimes, when I get to any case like these where I end up having an object embedding one or more objects I tend to get a bit confused about how to tackle this kind of scenarios. Some actions could be done on any of the embedded objects: how should this be managed? Should we do it through the holder's interface? Directly on the object instance?

What I think would not be cool would be to definitely end up with something similar to this:

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();

which result in a feature envy code smell...

Is there any design advice, comments, tips or anything you consider would be essential to tackle this kind of scenarios?

3

Actually, that last example is legitimate. Its probably an Adaptor. It could also be a Command, or a Facade, or ....

There are cases where it makes good sense to encapsulate another implementation. Such as:

  • offering an immutable version of a mutable object. The constructor could make a copy of a received B, and only expose/support immutable operations.
  • offering ancillary or aspect services such as logging, or filtering.
  • encapsulating a business operation.

What do you need to handle this? A Goal.

Seriously what are you aiming to achieve?

  • Does Wallet need to work with older Balance orientated code?
  • Does it make sense to implement Wallet without deferring to a separate Balance?

There is no one optimal design, just trade-offs. Occasionally the trade-offs help you later, often they hinder you later, but they all help you get to your goal.

So the first requirement is: What is the goal?

That being said, it helps to understand how things can move, and to not dictate implementation when describing intent.


To be clear on a few terms:

  • An interface is the contract describing the operations on an object. Including the operation's inputs, outputs, and effect on the objects state. The interface can extend to operations on returned objects, and even global operations or global state. You can think of them as the intention in your system.

  • An implementation describes some sort of data structure, and the operations on that data structure that support one or more interfaces.

Consider the implementation of Wallet. It must exercise complete control over its internal state. Otherwise it can only support the most trivial of contracts: Anything can happen....

If the Wallet interface states that a Balance is to be returned, a Wallet implementation can maintain control:

  • If Balance is immutable. The control is largely enforced by the platform. As any third-party access to Balance cannot change it, its safe to expose it through the interface.

  • If Balance is easily implemented, then perhaps Wallet does not return Balance but a WalletBalance. This way when an operation occurs, the contract on Wallet can be maintained, by updating other parts of Wallet appropriately.

  • If Balance is easily constructed, the Wallet can simply create it as needed. This is fine for immutable state, or mutable objects passed back as answers. If the operations on the Balance are meant to affect the Wallet though, Balance will need to support some form of callback to allow Wallet to maintain its state.

  • If Balance is easy to copy, then control can be maintained by creating a copy and returning that. This is fine for immutable state, or mutable objects passed back as answers. If the operations on the Balance are meant to affect the Wallet though, Balance will need to support some form of callback to allow Wallet to maintain its state.

Otherwise it is too difficult to maintain control and expose an implementation of Balance (immutable, extended, constructed, or copied). In this case the interface is quite likely broken by design, as its difficult/impossible to actually implement the requirements.

  • Perhaps the interface could expose a BalanceLite? It would need to have one of the above properties of being immutable, extendable, constructable, or copyable.

    • One caveat though is that an immutable BalanceLite, extended into a mutable Balance should not be directly returned if its part of the internal state. Because it usually possible for third-party code to cast it to back to the Balance interface and do bad things tm.
  • Perhaps the interface should abandon the Balance interface entirely. It would then need to gain sufficient capability through other operations.

    • This could be the best solution as you are not constraining the Wallet contract to incorporate a potentially ill suited Balance interface.
    • Should other code require a Balance then your last example could be used to provide it by wrapping a Wallet.
  • 1
    Excellent answer, showing very much that class design without context does not work. – Doc Brown Sep 10 at 6:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.