typescriptdomain-driven-designtddin-memorynon-deterministic

How to provide non-determinist value (id, dates, random...) to Domain Entity or Aggregate Root given that its been injected as dependencies


I am working on a frontend repository that implements an hexagonal architecture with domain driven design, using Redux Toolkit.

It is being developed in a TDD fashion. For that purpose, I am using "hand made" mocks. That way, I can pass a real implementation in the SUT, but an InMemory implementation in the test suites.

Examples (you can access the repository here: https://github.com/amehmeto/HexaTenzies):

rollDice.spec.ts

import { ReduxStore } from '../../../../react-view/main'
import { configureStoreWith } from '../../../../app/store'
import { InMemoryIdProvider } from '../../../../infrastructure/idProvider/InMemoryIdProvider'
import { InMemoryRandomNumberProvider } from '../../../../infrastructure/randomNumberProvider/InMemoryRandomNumberProvider'
import { Die } from '../../entities/Die'
import { IdProvider } from '../../ports/IdProvider'
import { rollDice } from './rollDice'
import { Dice } from '../../entities/Dice'

function dieDataBuilder() {
  return new Die('uuid', {
    value: 2,
    isHeld: false,
  })
}

async function triggerRollDiceUseCase(store: ReduxStore) {
  await store.dispatch(rollDice())
  return store.getState().dice.dice
}

describe('Generate Random Dice', () => {
  let store: ReduxStore
  let idProvider: IdProvider
  let randomNumberProvider: InMemoryRandomNumberProvider

  beforeEach(() => {
    idProvider = new InMemoryIdProvider()
    randomNumberProvider = new InMemoryRandomNumberProvider()
    const dependencies = {
      idProvider: idProvider,
      randomNumberProvider: randomNumberProvider,
    }
    store = configureStoreWith(dependencies)
  })


  it('should generate new dice after every roll', async () => {
    const expectedNumberOfDie = 10

    const firstDice = await triggerRollDiceUseCase(store)

    randomNumberProvider.with(0.5)

    const secondDice = await triggerRollDiceUseCase(store)

    expect(firstDice.length).toBe(expectedNumberOfDie)
    expect(secondDice.length).toBe(expectedNumberOfDie)
    expect(firstDice).not.toStrictEqual(secondDice)
  })

The contract

randomNumberProvider.ts

export interface RandomNumberProvider {
  generate(): number
}

The in memory implementation:

InMemoryRandomNumberProvier.ts

import { RandomNumberProvider } from '../../core/dice/ports/randomNumberProvider'

export class InMemoryRandomNumberProvider implements RandomNumberProvider {
  // Should be greater or equal to 0 and less than 1 to simulate Math.random()
  private controlledRandomNumber = 0.3

  generate(): number {
    return this.controlledRandomNumber
  }

  with(number: number): void {
    this.controlledRandomNumber = number
  }
}

The real implementation :

RealRandomNumberProvider.ts

import { RandomNumberProvider } from '../../core/dice/ports/randomNumberProvider'

export class RealRandomNumberProvider implements RandomNumberProvider {
  generate(): number {
    return Math.random()
  }
}

That way, I have control over the non deterministic value on my test. I retrieved those providers in the thunk like so:

import { createAsyncThunk } from '@reduxjs/toolkit'
import { DieViewModel } from '../../entities/Die'
import { Dice } from '../../entities/Dice'
import { ExtraDependencies } from '../../extraDependencies'

export const rollDice = createAsyncThunk<
  DieViewModel[],
  void,
  ExtraDependencies
>(
  `dice/rollDice`,
  async (thunkAPI, { extra: { randomNumberProvider, idProvider } }) => {
    return new Dice(randomNumberProvider, idProvider).roll()
  },
)

What bother me is this line:

return new Dice(randomNumberProvider, idProvider).roll()

I couldn't find a way to design the aggregate root Dice without injecting it those provider, in order to provider an id and a random number to its child entities Die.

Dice.ts

import { RandomNumberProvider } from '../ports/randomNumberProvider'
import { IdProvider } from '../ports/IdProvider'
import { Die, DieViewModel } from './Die'

export class Dice {
  private readonly AMOUNT_OF_DICE = 10
  private readonly dice: Die[]

  constructor(
    private randomNumberProvider: RandomNumberProvider,
    private idProvider: IdProvider,
  ) {
    this.dice = this.initializeDice()
  }

  roll(): DieViewModel[] {
    return this.dice.map((die) => {
      const randomNumber = this.randomNumberProvider.generate()
      die.roll(randomNumber)
      return die.toViewModel()
    })
  }

  public initializeDice(): Die[] {
    return Array(this.AMOUNT_OF_DICE)
      .fill(undefined) // needed to avoid generating die with the same id
      .map(() => this.generateDie())
  }

  private generateDie() {
    const newId = this.idProvider.getNew()
    return new Die(newId)
  }
}

Die.ts

export interface DieViewModel {
  id: string
  props: DieProps
}
interface DieProps {
  value: number
  isHeld: boolean
}

export class Die {
  private readonly MIN_VALUE = 1
  private readonly MAX_VALUE = 6

  constructor(
    public readonly id: string,
    readonly props: DieProps = {
      value: 6,
      isHeld: false,
    },
  ) {
    this.props = props
  }

  public roll(randomNumber: number): void {
    this.props.value = ~~(randomNumber * this.MAX_VALUE) + this.MIN_VALUE
  }

  public hold(): void {
    this.props.isHeld = !this.props.isHeld
  }

  static fromViewModel(dieViewModel: DieViewModel): Die {
    const { id, props } = dieViewModel
    return new Die(id, props)
  }

  toViewModel(): DieViewModel {
    return {
      id: this.id,
      props: {
        value: this.props.value,
        isHeld: this.props.isHeld,
      },
    }
  }
}

I am also concerned but the method roll(randomNumber) of Die which I guess leak some logic (random number) that should be encapsulated.

How can I redesign those Aggregate Root and Entity?

Again, you can access the repository code here: (you can access the repository here: https://github.com/amehmeto/HexaTenzies


Solution

  • Thanks again to @mark-seemann for his socratic questions. Now that I have finish this mini-project, I can reflect on it, and I know can to the conclusion (I reserve the right to change my mind though), that those non-deterministic providers aren't entities props, so I now prefer to inject them trough the public method params when necessary.

    I also prefer to pass the whole dependency in the param than the result of its call as it gives better flexibility.