Я работаю над игрой, и мы хотим обернуть физический движок, чтобы его можно было легко заменить.
Итак, мы хотим создать иерархию абстрактных классов в качестве шаблона, указав все, что движок должен поддерживать и работает как интерфейс для остальной части игры. Затем мы хотим создать конкретные классы, реализовать эти абстрактные классы и обернуть фактический движок. Проблема в том, что это создает множество ромбов, например:
Where Shape and Circle contain engine-independent functionality but also some engine-dependent abstract methods and P2Shape and P2Circle are wrappers for the P2 engine and implement those engine-specific methods. And from a broader perspective, we don't just have these two classes but a whole hierarchy tree of (not purely) abstract template classes and a whole hierarchy tree of concrete implementations. Each of the concrete classes "is-a" -> its abstract template and "is-a" -> its base class in the hierarchy:
We are probably not the first people to encounter this problem and this is probably some symptom of bad design, so I am curious about the proper way to implement this. To make it a bit more interesting, we are working in TypeScript which does not support multiple inheritance.
Here are the approaches I considered:
- Favor composition over inheritance
Let's say a Shape has a position and a Circle has a radius. In my opinion, a Circle clearly "is a" Shape that has a position and a radius, it doesn't "have a" radius and a shape that has a position. Also, if I access the position I don't want to say myCircle.shape.position. Shapes are a textbook example of inheritance hierarchies. The other option would be that a Shape has a P2Shape but then all engine-specific stuff has to be squeezed in there and the type of the field has to be any or generic. Or a P2Shape has a Shape but then the rest of the game cannot work with it because it is unaware of the P2Shape type.
- Используйте интерфейсы вместо абстрактных классов
TypeScript поддерживает отдельная концепция интерфейсов, где множественное наследование и проблема ромба не являются проблемой. Но там мы не можем поместить какую-либо реализацию в независимые от двигателя части.
Примеси
Мне кажется, что концепция миксинов заключается в создании набора модульных и многократно используемых компонентов, которые можно объединить в классы, использующие эту функциональность. В нашем случае это будет означать, что P2Shape и Circle являются модульными компонентами и P2Circle использует их. Мне это почему-то не кажется семантически правильным.
Все три решения кажутся несколько неудовлетворительными. Любая помощь приветствуется!