Should I use inheritance to differentiate objects even if they have the same fields

domain-modelinheritance

Consider this simple class that models a real world mobile device:

/// <summary>
///     Model that represents a device.
/// </summary>
public class Device
{
    public DateTime CreationDate { get; set; }

    public bool Enabled { get; set; }

    /// <summary>
    ///     Gets or sets the device's hardware identifier.
    /// </summary>
    /// <remarks>Commonly filled with data like device's IMEI.</remarks>
    public string HardwareId { get; set; }

    public int Id { get; set; }

    public DateTime? LastCommunication { get; set; }
}

Now suppose a new requirement arrived and I need to start differentiating a device by platform, like Windows Phone, Android and iOS. The most common option I think would be to create an enumeration class:

/// <summary>
///     Defines a type of device.
/// </summary>
enum PlatformType
{
    /// <summary>
    ///     Device is an Android.
    /// </summary>
    Android,

    /// <summary>
    ///     Device is an Windows platform.
    /// </summary>
    Windows,

    /// <summary>
    ///     Device is an iOS platform.
    /// </summary>
    Ios,
}

and then add a new property to the device class:

public class Device
{
    ...

    public PlatformType Platform { get; set; }

    ...
}

Another approach would be to create a inheritance hierarchy on the device class itself:

public class AndroidDevice : Device { }

public class IosDevice : Device { }

public class WindowsPhoneDevice : Device { }

When should I choose one of those approaches over the other? Should I always choose the second one or is there a reason not to?

Best Answer

In general I prefer composition over inheritance. That has several reasons:

  • Humans are bad at dealing with complexity. And dealing with high inheritance trees is complexity. I want light structures on my brain, which I could overlook easily. The same goes for dozens of types even derived from one base class.

  • You are able to switch moving parts out. You could have a simple device, which does runOperatingSystem(), instead of making two different device-classes, you make only one and give it an Operating System. If you want to test behavior, you could inject a mockOperatingSystem and see, if it does, what it should.

  • You are able to extend behaviour, simply by injecting more behavioral components.

In terms of abstraction, you are better off, designing a generic device type: In Python the design would look like the following

class Device:
    def __init__(self, OS, name):
        self.OS=OS
        self.name=name
    def browseInterNet(self):
        self.OS.browseInterNet()

class Android:
    def browseInterNet(self):
        print("I'm browsing")

You have a generic device which runs an operating system. Every userinteraction is delegated to this OS. Perhaps you want to test only the browsing call dependent on any operating systen, you could easily swap it out.

The next step for this design would be, to create a configuration-object, which takes the common parameters (CreationDate, HardwareId and so on). Inject this configuration and the appropriate OS via constructor injection and you are done.

You define common behaviour in a contract (interface), which determines, what you could do with a phone and the operation system deals with the implementation.

Translated to everyday language: If you text with your phone, there is no difference in doing it with an iPhone, Android or Windows in that respect, that you are texting, although the mechanisms from OS to OS differ. How they deal technically with it is uninteresting for the device. The OS runs the texting app, which itself takes care of its implementation. It is all a question of abstracting commonalities.

On the other hand: this is only one way of doing it. It depends on you and your model of the domain.


From the comments:

But you have to agree with me that this would require one to create a lot of wrapper methods to make the API simpler

This depends on what exactly you want to model. To extend the given example of texting:

Say, you simply have some basic jobs, you want the device to do, you define an API for that; in our case simply the method sendSMS(text).You have then the Device, where the message "send text" is called upon. The device in turn delegates that call to the used operating system, which does the actual sending.

If you want to model more than a handful of services your device offers, you have to make bloated API.

This is a sign, that your abstraction level is too low.

The next step would be to abstract the concept of an app out of the system.

In principle, you have a device which interacts with the inner logic of an app, which runs on an operating system. You have input, which is processed and changes the display of the device.

In terms of MVC, e.g. your keyboard is the controller, the display is the view and there is the model within the app, which is modified. And since the view observes the model it reflects any changes.

With such an abstract concept, you are very flexible to build / model a lot of use cases.

I also am not seeing exactly how you would handle the operations concept here. There are still many types of operations, each with differing parameters, that can or can't be applied to a given device.

As said above:it is all a matter of abstraction. The more power you want, the more abstract your model has to be.

Where would the OS be located now after this abstraction?

Taking the example further, you have to develop several abstractions / patterns, which help in this case.

  • Mediator-Pattern: The operating system acts as a mediator, i.e. it takes signals in form of commands, sends it to the app and takes in response commands to e.g. update the view

  • Command-Pattern: The command pattern is the form of abstraction, which is used to describe the communication flow between components. Say, the user presses A, than this could be abstracted as Keypressed-command with a value of A. Another command would be update display with the value of keypress.value or in this case A.

  • MVC The display as the view, the keyboard as the controller and in between the (app-)model.

Your imagination is your limit.

This is the kind of stuff, OOP was invented for originally: simulating independent components and their interaction

Related Topic