0

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

A Mehmeto
  • 1,594
  • 3
  • 22
  • 37
  • Could you elaborate on why you consider these questions problems? It's not that I don't have opinions on this, but to be able to help, it's better if I can help you address *your* problems, rather than the ones I imagine that you have. – Mark Seemann Jan 28 '23 at 08:23
  • That's a good question that made me think. The first quick answer is that I am new to DDD and I am not sure I am doing it right. As I understand it as of now, DDD focus on modeling the business logic. Consequently, I have the feeling that creating an `idProvider` and a `randomNumberProvider` prop to my `Dice` aggregate is wrong, as real dice doesn't have such features. Those providers feel more like external technicalities that should live in the usecase. Typically, before trying to mock those non-determistic providers, I would have just called `uuid() ` and `Math.random()` directly with DI. – A Mehmeto Jan 28 '23 at 16:36
  • Indeed. Keep pursuing that thought. Does a die even need to have an ID? Who or what rolls a die? Is it the die that rolls itself, or is there an external agent that does it? Where does the randomness come from? – Mark Seemann Jan 28 '23 at 17:13
  • Let's do it. I'd say that the die need an ID, as this specific game requires 10 dies that can be rolled or not depending on business rules that I know I'm going to develop soon after. The diece is being rolled by the player, technically he's triggering an event from the interface. The randomness comes from the die itself in my mind, the player can't control die value (neither the randomness) as per business rules. – A Mehmeto Jan 28 '23 at 17:36
  • I start understanding that I have 2 slightly different issues. Providing basic value from the Aggregate to a child entity (as an ID or a Date). It's pretty obvious that the entity should not give himself those values. But I'm still clueless on how to do that without the Aggregate Root knowing about the dependency. And there is a slightly different problem with the randomness part, which is an intrinsic feature of a Die, so maybe it's not so wrong to inject the randomNumberProvider at both the instantiation of the dice and the die. – A Mehmeto Jan 28 '23 at 19:26
  • 1
    I can't comment on the die ID, since I don't know which game you're implementing, so let's not pursue that any further... I can't imagine which game it might be, though... Regarding randomness, I agree that one can view it from more than one perspective, and I see what you mean by the die being the source of randomness. I would have viewed the source of randomness as essentially *chaos* - small imperfections in the fabric of reality, essentially. That's not necessarily more correct, but which model is the most useful? *All models are wrong, but some models are useful* - George Box – Mark Seemann Jan 28 '23 at 19:27

1 Answers1

0

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.

A Mehmeto
  • 1,594
  • 3
  • 22
  • 37