Patterns - worldscapes/engine GitHub Wiki

This section describes design patterns regularly used in the Engine.

API objects

API object encapsulates logic related to some responsibility with the state needed for it. It's used to provide API to connect Engine parts together while allowing different implementations.

This pattern is implemented using Typescript abstract classes to reexport nested API object methods.

While state encapsulation can be done using closures, it's much easier to work with objects when making APIs. This way we see the object as a namespace or module.

When using API objects entities should rely on dependency injection. Actual API objects can be created in the entity itself, but they still should be a way to provide custom implementation from outside.

After a test it looks like using classes for API makes them hard to keep simple and permits more dirty state manipulation. It's hard to keep mindset of namespace when working with class.

constructor(private ecr: ECR = new BasicECRImplementation()) {}

Structures

Structure is a class that provides a convenient way to create data structures with custom typing. Why are they needed:

  1. Convenient way to create objects
  2. Provide built-in functionality to serialize object type and make it persistent

Another approach to do that could be builder functions that take data and return plain objects, but this way data type cannot be checked and we need to check object structure.

What differs Structures from regular OOP classes?

  1. They should not rely on inheritance, since it brings a lot of complications and design problems into the app. If you just need polymorphism without type checking, you can implement a common interface.
  2. We don't check class by prototype, but only by class name
  3. They should not contain any logic at all. Everything it does is using a constructor to create a data structure to be used further, class is needed for type checking. Any additional logic should be implemented elsewhere.

To represent the inheritance chain array of types can be used. It can be useful when we need to define objects which are from some subset (Component, Resource, Command, and so on). Such inheritance does not bring problems because we don't inherit logic.

Pros

  • Faster type checking because it simply compares string literals
  • Type is serialized from a box and can be sent via network or saved
  • Plain objects proved to be created much faster, which allows avoiding overhead when working with prototypes

Inheritance vs Composition

Whenever it's possible Composition should be used over Inheritance. The problem with Inheritance is that you cannot inherit several pieces of functionality at once, which at some point leads to feature blocking.

Inheritance is a nice tool to implement abstraction which allows having several implementations of one API.

Example

abstract class SomeAbstractClass {
  abstract functionalityYouNeed();
}

// Instead of
class SomeBadImplementation extends SomeAbstractClass {
  functionalityYouNeed() { // Implementation };

  someAdditionalFunctionality() {};
}

// Better to go with
class SomeGoodImplementation extends SomeAbstractClass {
  functionalityYouNeed() { // Implementation };
}

class ThatComposesFunctionalityYouNeed {

  // Put your implementation into a field
  someImplementation: SomeAbstractClass = new SomeGoodImplementation(); // Can be replaced or injected later
  
  // More fields can be added later

  someAdditionalFunctionality() {};
}