2

When talking about having a rich domain model successfully, in real applications, it needs to, somehow, access some abstraction of complex functionality (instead of being a row state calculator the way we see in most examples).

But, is there any problem in using abstract classes with polymorphic properties to do this, like this:

Interface and implementation

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

class Sha256PasswordHasher implements IPasswordHasher {
  hashPassword(password: string) {
    return password;
  }
}

Abstract class and implementation

abstract class AbstractAccount {
  protected abstract passwordHasher: IPasswordHasher;

  private _email: string;
  get email() {
    return this._email;
  }

  protected hashedPassword: string;
  constructor() {}

  login(email: string, password: string) {
    this._email = email;
    const hashedPassword = this.passwordHasher.hashPassword(password);

    if (this.hashedPassword === hashedPassword) {
      return true;
    }

    throw new Error("Invalid password");
  }

  register(email: string, password: string) {
    this._email = email;
    this.hashedPassword = this.passwordHasher.hashPassword(password);
  }
}

class Account extends AbstractAccount {
  passwordHasher = new Sha256PasswordHasher();
}

The only, drawback I saw in this is that domain models cannot consume other domain models directly (since they will be abstract), but this can be easily solved by declaring abstract factories Factories that return this other domain model.

Obs 1: By domain model, I'm not liming this question to how they are used in a DDD context, but I'm talking about the patterns that Martin Fowler defines as a pattern to isolate domain logic into objects that represent business entities.

Obs 2: Note that I'm not asking how to make domain models consume abstractions, but if there is a problem with this way of consuming them.

0

1 Answer 1

4

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

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

  1. Why isn't IPasswordHasher passed as a constructor parameter to Account?

  2. And once IPasswordHasher is passed 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 the application. The problem is introducing an unnecessary abstraction that can be eliminated by utilizing dependency injection.

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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