Table of Contents Show
Object-oriented programming is a widely used software paradigm. It’s very powerful, but often it’s misused. This leads to a dependency hell between base classes and subclasses. It’s really hard to find out how everything works together and which code is executed at runtime. Any code change has the potential to break a lot of other stuff.
Example
Imagine this common scenario: You have a well-working component for doing something in your game. For example RotateItem.cs. It’s optimized for rotating items with a mouse and keyboard on Windows.
Now, you port the game to a mobile platform and you have to use touch input for rotating items. The class RotateItem is almost working, just a few tweaks are necessary.
Implementation is easy: Just create a new class TouchRotateItem.cs and derive it from RotateItem. And overwrite some methods.
What’s the problem with subclasses?
The code works fine for now, on both platforms. But at some point, changes will be necessary. For example, there is a complete redesign of the user interface on desktops. Or there is a small bug, e.g. that items are rotated in the wrong direction on desktops.
You change the original RotateItem class and everything works on desktop.
You don’t think about the slightly modified subclass. Maybe you were not involved at all with the implementation of the mobile app. And as long as you don’t switch to the other platform in Unity, you might not even see the subclass in your IDE.
With the example of the wrong direction, you probably have introduced the same bug on mobile platforms now. With a larger refactoring you might have created even more problems. If you are lucky, the code does not compile anymore and you catch it early.
OK, you might not miss the subclass because you have unit tests, code reviews, and all good software development processes. But still, changing the base class will also change the behavior of the derived class. You have to test it on all platforms and there is some risk of introducing unwanted behavior.
Better solutions
Components
Unity uses a component-based architecture. This makes it very easy to favor composition over inheritance.
Divide the RotateItem.cs into separate parts, which only have one responsibility. This could be e.g.:
- RotateItem only deals with the core rotation, completely independent of the user interface.
- TouchRotateInput uses touch input and converts it into a rotation value.
- DesktopRotateInput uses keyboard and mouse input and converts it into a rotation value.
Changing the user interface on the desktop does not affect the code on mobile platforms.
Service Locator
Another solution is using a service locator. Create a custom service for each platform and register it by adding it to the scene. Then the service can be accessed from other parts of the code from the service locator.
In the given example, the input system would be suitable for a service. The RotateItem class then uses the InputSystem to get the current user input.
interface InputSystem
{
float GetRotationalInput();
}
class MobileInputSystem : InputSystem { ... }
class DesktopInputSystem : InputSystem { ... }
class RotateItem : MonoBehaviour
{
private InputSystem _inputSystem;
}
The RotateItem class is independent of the current platform. It does not care about the actual implementation of the InputSystem.
Is subclassing always bad?
There are several problems with subclasses if they are created without a good architecture.
But of course, there are also good use cases for object-oriented programming and polymorphism. For example, different kinds of items or characters are good candidates for subclasses.
It’s always a good practice to follow the SOLID design principles and well-known design patterns.