Question

Here is my problem: I want to read input from different HID devices such as a gamepad, racing well, joystick, etc. Pretty much any game controller. The issue is that they all have different inputs.

The gamepad has buttons, switches and sticks while the racing well might have a gear stick. I managed to abstract all these different components into just 3 so instead of having a base class with all possible combinations:

abstract class Device
    {
    public Buttons Buttons;
    public Axes Axes;
    public Switches Switches;
    public GearSticks GearSticks;
    //many more
    }

I can now have:

abstract class Device
{
public Buttons Buttons;   //on or off
public Axes Axes;         //range [-100%:100%]
public Switches Switches; //multiple states
}

At first I was happy with this since it seemed to cover all possible types of input and so my class can stay closed while being open to extension via all the concrete implementations since everything can be abstracted to just 3 types of input.

BUT then I though to myself what if I'm just delaying the inevitable? What if one day I will have to add another field to my Device class? It does not support something like a trackball!

Is there a way I can future proof this class? The way I see it I would end up with something like this:

public Device1 : Device
{
//buttons
//trackball
}

public Device2 : Device
{
//Switch
//Axis
}

public Device3 : Device
{
//trackball
//switch
}

And I would have to keep adding properties to my base class every time there is something new to implement.

Was it helpful?

Solution

I am pretty sure this can be done by introducing a concept like an abstract InputChannel, and devices having a configurable list of input channels. An input channel will have a name, a type, maybe some meta data, and it need to be able to produce some "state" which fits to that type. There might be predefined channels like a button, an axe or a switch, or some new channel you currently don't know (but might be added later by introducing a new InputChannel child class).

This way, a device will become some kind of meta model, and you will also need a way to manage the device's states, which have to correspond to the list of input channels of the device.

However, that kind of generic approach has a certain risk of overengineering, also known as Inner-platform effect. For example, it may not be easy to add specific functionality to such a generic device, or specific events, or interactions between different input channels. It may also be harder to use and understand for a user of your generic device library.

Note that it is not always beneficial to create the most abstract solution possible. Changing requirements in hardware do typically need way more effort to be implemented in the hardware itself than in corresponding software, so often it is better to stick to a more specific solution in software, and change the software when necessary.

OTHER TIPS

The idea behind the open-closed principle is that you are less likely to break existing functionality if you implement new functionality through inheritance rather than modification of an existing class. And you can do so, by inheriting from your Hid. In a year and a couple of months you can create a Hid2020 that inherits from Hid and adds support for the trackball that will be invented in Q4 of 2019. After the invention and popularization of the squeeze detector in 2023 you can create a Hid2024 class that descends from Hid2019.

That would be the defensive approach. But it would also be a bit sloppy from a clean design perspective. In your case I would not lose any sleep over violating O and just change the base class as the world around you changes. It does not seem like the implementation for trackball or any other new type of control will impact the way you handle button presses or switch status changes now.

Licensed under: CC-BY-SA with attribution
scroll top