Domain Layer and React? Decorators to the Rescue!
Published: 2023-01-14 02:48:14

## Why?

Yeah, why would I try to implement a domain layer within a frontend react application?

**To be able to change and test business rules in isolation from the rest of the system. **


![All by myself](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8eu9hf7bcipap8291s7o.gif)

Business rules doesn't live in backend? Mostly, yes. But there are some rules that are attached to the view, they are about your form behavior, they are about how user may or not interact with your UI without making a request for every single movement.

Some rules are quite simple, like that `isOpen` for a modal. Others can become that big ugly component if your not paying enough attention. We're talking about these last guys. 

There's nothing better than being able to test a complex logic in isolation. There's no UI, there's no API. Just the two of us.

![We can make it if we try](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pb7x0icl83xqrgba3pf9.gif)

Of course, you could isolate the logic inside a hook, and there's an awesome way of testing it with [renderHook](https://github.com/testing-library/react-hooks-testing-library). But yet, you're trapped into react features, if they change their API, you will have to put your hands in your working logic and adapt some stuffs to keep up to date. 

And that's ok if there's no complex domain logic or high life expectancy for the software.

So, repeating the why:

**To be able to change and test business rules in isolation from the rest of the system. **

## How?

I have found [this answer](https://stackoverflow.com/a/71874663) suggesting to wrap the model instance with 2 layers:

- A `ref` layer keeping the mutable instance alive.
- A `projection` layer keeping the instance data as a state and extending writting behaviors with `setState` calls.

For example, assume we have this fancy car model:

```ts
class Car {
  private speed = 0

  speedUp() {
    this.speed++
  }

  speedDown() {
    if(this.speed > 0) {
      this.speed--
    }
  }

  getSpeed() {
    return this.speed
  }
}
```

We should keep the mutable instance with useRef: 

```ts
const useCarRef = () => {
  const carRef = useRef<Car>()

  if(!carRef.current) {
    carRef.current = new Car();
  }

  return carRef.current
}
```

We should also make a projection function that gets a DTO from instance data:

```ts
const projectCar = (car: Car) => {
  return { speed: car.getSpeed() };
}
```

Last, but not least: we'll create the projection by wrapping write methods and saving the projection as state:

```ts
const useCarProjection = (car: Car) => {
  const [projection, setProjection] = useState(projectCar(car));
  return {
      ...projection,
      speedUp: () => {
        car.speedUp();
        setProjection(projectCar(car));
      },
      speedDown: () => {
        car.speedDown()
        setProjection(projectCar(car));
      }
  };
}
```

Finally we could mix it all and render a component:

```tsx
const CarView: React.FC = () => {
  const carRef = useCarRef();
  const car = useCarProjection(carRef);

  return <div>
    <button onClick={car.speedDown}>-</button>
    <span>Speed: {car.speed}</span>
    <button onClick={car.speedUp}>+</button>
  </div>
}
```

![Nice!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0i8bi9f1bvw8at3cas7t.gif)

That's really **nice**! But I was studying some design patterns when I read this solution and I could not help noticing that the [decorator](https://refactoring.guru/design-patterns/decorator) pattern pretends exactly to wrap some original class adding some behavior.

Quick steps: 
  - Create an interface with the class behaviors
  - Create a decorator base class that just wraps an instance and it's methods.
  - Create the specialized decorator that executes base behavior and adds some new.

So, would it fit this application? YES!

First, we should incorporate the `projectCar` function to `Car`. I'll also rename it to `getDTO`.

This is our car now:

```tsx
class Car {
  private speed = 0

  speedUp() {
    this.speed++
  }

  speedDown() {
    if(this.speed > 0) {
      this.speed--
    }
  }

  getDTO() {
    return {
      speed: this.speed
    }
  }
}
```

Neat.

Now, we have to create an interface for our `Car`. That's the contract that will be decorated.

```ts
interface CarDTO {
  speed: number
}

interface CarInterface {
  speedUp(): void;
  speedDown(): void;
  getSpeed(): number
  getDTO(): CarDTO
}

class Car implements CarInterface {
  private speed = 0

  speedUp() {
    this.speed++
  }

  speedDown() {
    if(this.speed > 0) {
      this.speed--
    }
  }

  getSpeed() {
    return this.speed;
  }

  getDTO() {
    return {
      speed: this.speed
    }
  }
}
```

With the contract, we can build the base of our decorator. He's really just a wrapper for the car instance:

```ts
class CarDecorator implements CarInterface {
  constructor(protected wrapped: CarInterface){}
  speedUp(): void {
    this.wrapped.speedUp()
  }
  speedDown(): void {
    this.wrapped.speedDown()    
  }
  getSpeed(): number {
    return this.wrapped.getSpeed()
  }
  getDTO(): CarDTO {
    return this.wrapped.getDTO()
  }
}
```

NOW we can add the true decoration (additional behavior), extending from this base, overriding the write methods (exactly as we did before at `useCarProjection` hook). But first, as the behavior involves triggering another function, we need to receive it and keep it.

```ts
class CarProjectionDecorator extends CarDecorator {
  constructor(
    wrapped: CarInterface,
    private setProjection: (projection: CarDTO) => void 
  ){
    super(wrapped)
  }

  speedUp(): void {
    super.speedUp()
    this.setProjection(this.getDTO())
  }
  speedDown(): void {
    super.speedDown() 
    this.setProjection(this.getDTO())
  }
}
```

The projection decorator only responsability is calling original behavior and adding something more, in this case: state update trigger. Note that decoration could be added to getters as well, but their original behavior already fits our case.

We can still use the hook just to launch the state and deliver the data to the requesting component.

```ts
const useCarProjection = (car: Car) => {
  const [_projection, setProjection] = useState(car.getDTO());
  return new CarProjectionDecorator(car, setProjection)
}
```

The `_projection` is not really needed because all the data is available through `CarProjectionDecorator` getters.

Usage doesn't change much:

```tsx
const CarView: React.FC = () => {
  const carRef = useCarRef();
  const car = useCarProjection(carRef);

  return <div>
    <button onClick={car.speedDown}>-</button>
    <span>Speed: {car.getSpeed()}</span>
    <button onClick={car.speedUp}>+</button>
  </div>
}
```

The only difference is that we are now using the getters instead of accessing directly the DTO properties.

## Conclusion

Our car is now isolated and the behavior addition is being executed in a class with defined responsability with a well known design pattern with the cons of increasing the complexity by adding more abstraction layers and more files.

## Extra - real application

As a reward for reading this all, here's an example of a real application. It's from a RPG character creator in which user will buy his character attribute points. [At github](https://github.com/isaacbatst/t20-sheet-builder-web/blob/a33debf3c217a1e80d8b6077ad0691e4744828ff/src/components/SheetBuilderSection/InitialAttributesDefinitionStep/AttributesLauncherPerPurchase.ts).

Domain class:
```ts
export class AttributesLauncherPerPurchase implements AttributesLauncherPerPurchaseInterface {
  static price: Record<number, number> = {
    [-1]: -1,
    [0]: 0,
    [1]: 1,
    [2]: 2,
    [3]: 4,
    [4]: 7
  }

  static defaultAttributes = {strength: 0 , dexterity: 0, constitution: 0,  intelligence: 0 , wisdom: 0, charisma: 0  }

  private points = 10;

  constructor(private attributes: Attributes = AttributesLauncherPerPurchase.defaultAttributes){}

  confirm(): void {
    if(this.points > 0) {
      throw new Error('POINTS_LEFT')
    }
  }

  increment(attribute: Attribute): void {    
    const currentAttribute = this.attributes[attribute]
    if(this.points === 0) {
      this.attributes[attribute] = currentAttribute;
      return
    }
    if(currentAttribute >= 4) {
      this.attributes[attribute] = 4;
      return 
    }
    const attributeResult = currentAttribute + 1;
    const totalResult = this.points - Math.abs(AttributesLauncherPerPurchase.price[currentAttribute] - AttributesLauncherPerPurchase.price[attributeResult])
    
    if(totalResult < 0) {
      this.attributes[attribute] = currentAttribute
      return
    }
    this.points = totalResult
    this.attributes[attribute] = attributeResult;
  }

  decrement(attribute: Attribute): void {
    const currentAttribute = this.attributes[attribute]
    if(currentAttribute <= -1) {
      this.attributes[attribute] = -1
      return
    };

    const result = currentAttribute - 1;
    this.points += Math.abs(AttributesLauncherPerPurchase.price[currentAttribute] - AttributesLauncherPerPurchase.price[result])
    this.attributes[attribute] = result;
  }

  
  getAttributes(): Attributes {
    return this.attributes
  }

  getPoints() {
    return this.points
  }

  getDTO(): AttributesLauncherPerPurchaseDTO {
    return {
      attributes: this.getAttributes(),
      points: this.getPoints()
    }
  }
}
```

Base decorator:
```ts
// AttributesLauncherPerPurchaseDecorator.ts
export class AttributesLauncherPerPurchaseDecorator implements AttributesLauncherPerPurchaseInterface {
  constructor(protected attributesLauncherPerPurchase: AttributesLauncherPerPurchaseInterface){}
  
  confirm(): void {
    this.attributesLauncherPerPurchase.confirm()
  }
  
  getDTO(): AttributesLauncherPerPurchaseDTO {
    return this.attributesLauncherPerPurchase.getDTO()
  }

  getAttributes(): Attributes {
    return this.attributesLauncherPerPurchase.getAttributes()
  }

  getPoints() {
    return this.attributesLauncherPerPurchase.getPoints()
  }

  increment(attribute: Attribute): void {    
    this.attributesLauncherPerPurchase.increment(attribute)
  }

  decrement(attribute: Attribute): void {
    this.attributesLauncherPerPurchase.decrement(attribute)
  }
}
```

Projection decorator:
```ts
// AttributesLauncherPerPurchaseProjectionDecorator.ts

export class AttributesLauncherPerPurchaseProjectionDecorator extends AttributesLauncherPerPurchaseDecorator {
  constructor(
    attributesLauncherPerPurchase: AttributesLauncherPerPurchaseInterface,
    private setProjection: (projection: AttributesLauncherPerPurchaseDTO) => void
  ){
    super(attributesLauncherPerPurchase)
  }

  confirm(): void {
    super.confirm();
    this.setProjection(this.getDTO())
  }

  increment(attribute: Attribute): void {    
    super.increment(attribute)
    this.setProjection(this.getDTO())
  }

  decrement(attribute: Attribute): void {
    super.decrement(attribute)
    this.setProjection(this.getDTO())
  }
}
```

View
```tsx
const AttributesLauncherPerPurchaseView: React.FC = () => {
  const {sheetBuilderForm} = useSheetBuilderFormContext()
  const attributesLauncher = sheetBuilderForm.getAttributesLauncher()
  const attributes = attributesLauncher.getAttributes();
  return (
    <div>
      <h3 className='mb-3'>Compra de pontos</h3>
      <div>Restante: {attributesLauncher.getPoints()}</div>
      <div className="flex justify-evenly mb-3">
        {Object.entries(attributes).map(([key, value]) => {
          const attribute = key as Attribute;
          return (
            <AttributeInput 
              key={attribute} 
              attribute={attribute} 
              value={value} 
              decrement={() => attributesLauncher.decrement(attribute)} 
              increment={() => attributesLauncher.increment(attribute)} 
            />
          )
        })}
      </div>
    </div>
  )
}
```

That's it, til next app.