Skip to main content
4 of 5
added 78 characters in body
Greg Burghardt
  • 42k
  • 8
  • 79
  • 132

Can domain models be abstract classes? Sure they can. Polymorphism can be useful in the business layer of your application as well.

However, I do have a few thoughts about the design of the specific code in your question:

  1. Why aren't you passing IPasswordHasher as a constructor parameter to Account?

  2. And once you pass IPasswordHasher as a constructor parameter to a new Account, then the abstract base class becomes unnecessary.

This isn't so much a problem with isolating business logic as it is a basic problem of managing dependencies. So to directly answer observation #2: yes, there is a problem with how the abstraction is being consumed.

There is nothing wrong with IPasswordHasher and Account existing in the business logic/domain layer of your application. The problem is that you have introduced an unnecessary abstraction that can be eliminated by utilizing dependency injection.

You really only need:

interface IPasswordHasher {
  hashPassword(password: string): string;
}

class Sha256PasswordHasher implements IPasswordHasher {
  ...
}

class Account {
  private passwordHasher: IPasswordHasher;

  constructor(passwordHasher: IPasswordHasher) {
    this.passwordHasher = passwordHasher;
  }

  ...
}

Using these classes together:

let passwordHasher = new Sha256PasswordHasher();
let account = new Account(passwordHasher);

The only useful abstraction here is the object that hashes passwords. The Account class is cohesive and concrete. The only difference in behavior is how passwords are hashed, and a constructor parameter for IPasswordHasher allows for that polymorphic relationship.

This doesn't mean you can never have an abstract base class with an abstract property, however an abstract base class that only contains an abstract property without other methods is immediately suspect. It's not immediately wrong, just suspect. It becomes worthwhile to rethink the design of those classes to ensure you are introducing useful abstractions that cannot be expressed by other simpler means in object oriented programming.

The downside with this approach is that you will need to refactor all of the code that initializes Account objects. This isn't a downside of requiring a new constructor parameter, this is the downside of not structuring code for dependency injection to begin with.

As an alternative view of the problem, what if you had:

class OldAndWeakPasswordHasher implements IPasswordHasher {
  hashPassword(password: string): string {
    return password.toUpperCase();
  }
}

Would you ever want to see:

let account = new Account(OldAndWeakPasswordHasher());

If the strength of the password hashing algorithm matters, and you only have one implementation, then consider the password hasher as a hard dependency for Account. The password hasher becomes an implementation detail:

class Account {
  private passwordHasher: Sha256PasswordHasher;

  constructor() {
    this.passwordHasher = new Sha256PasswordHasher();
  }
}

You don't need to pass every dependency into a new object via its constructor. Sometimes you have such a hard dependency between two things that dependency injection doesn't solve any problems, and may in fact introduce problems, like using a weak cipher for passwords by accident. Just know that initializing a dependency in a constructor rather than passing it in makes it impossible to separate the behavior of Account from the password hasher for testing purposes. If you need to mock the password hasher for testing purposes, then you are stuck with dependency injection, and interfaces or abstract classes.

A note about TypeScript and JavasScript

Many unit testing frameworks allow you to spy on and mock any object. The type system in TypeScript is an artificial constraint enforced by the TypeScript compiler, not the JavaScript runtime, so you have a little more flexibility with mocking dependencies in this runtime environment.


Although, to be fair, the type system in any strongly-typed language is an artificial constraint placed on your code by the compiler. The CPU doesn't care in most cases that a property is private. The CPU gets instructions and executes them.

Greg Burghardt
  • 42k
  • 8
  • 79
  • 132