2

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?

  • I think it's extremely difficult to answer this without knowing more about the existing and new requirements of the class. It looks like a property bag that just provides data rather than behaviour. Is that supposed to be the case? – Ben Aaronson May 8 '15 at 22:52
  • As written, your class has essentially zero behavior. So it is hard to imagine how the behavior would change between ios, android, etc. Subclassing is indicated mainly when behavior changes. Do you anticipate adding methods that actually do something, like gotoStore()? – user949300 May 9 '15 at 1:55
7

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

  • I did think about this before... I agree with you on everything here, really. I feel like having the OS be different actually models the real world better even. It's not the Device that is different, it is it's operating system. But you have to agree with me that this would require one to create a lot of wrapper methods to make the API simpler. 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. – julealgon May 8 '15 at 18:47
  • I've updated the answer – Thomas Junk May 8 '15 at 20:52
  • Trying to make your last abstraction slightly more clearer. So you're saying you would have related functionality composed into its own object (like an app) and you would delegate what events can be called (like sendSMS() or killMonster()) by what app is currently running on the device. These events are then passed to the OS. Where would the OS be located now after this abstraction? – Luminous May 8 '15 at 20:58
  • To stick with the smartphone metaphor, you have a device, which has itself a keyboard-unit and a display-unit (touch as a combination aside). You have then the os component taking commands from the keyboard-unit and taking updates for the view. The os is a "mediator", the keyboard to issue "commands" and the app is the "business logic" part, which gets all the input and tells the os what to do next. – Thomas Junk May 8 '15 at 21:17
4

I would make this choice based on how much of the implementation is shared between platforms.

If 99% of the code is the same on all platforms, then have a private enum, and in the one or two places where it matters just use a switch(Platform) block and you're done.

But if you have to make dozens of platform-dependent system calls inside this class, each with different parameters and semantics and error handling conventions, then you're better off with completely separate classes that merely inherit from a common interface.

The example interface you've shown would almost certainly end up in the latter category.

  • These classes don't do platform-dependent system calls because they are server side representations of the mobile devices, and are not used on the devices themselves. But, even if they have the very same fields (at least for now), we are already facing some different behavior that need to take into account their platform. I first created that question, and it prompted me to ask this more general one. – julealgon May 5 '15 at 20:17
  • In that situation (of the other question), I'd still basically use the inheritance strategy just to differentiate the classes and allow more type safety. For now, I cannot envision leveraging polymorphism or having different fields. – julealgon May 5 '15 at 20:18
  • @julealgon Ah, well, close enough. I definitely agree with leaning towards inheritance since you want to use features that not every platform supports, and thus can't completely hide under the common interface. – Ixrec May 5 '15 at 20:24
  • Could you elaborate on why you would not do this for all cases? You mentioned that if the code is 99% the same you would use the enum approach. Why is that, why not always use inheritance? – julealgon May 5 '15 at 20:31
  • 1
    Wait... I'm not following you now. If 99% of the code is the same, in the case of the inheritance chain this code would obviously be on a base class, and not copied/pasted on each one. Notice that in my example I specified that all subclasses inherited from Device, where all the fields/methods would be. In the extreme scenario (which actually is what we have now), the inherited classes would have nothing declared, just the type. – julealgon May 6 '15 at 13:38
1

The OOP answer is to do the inheritance. I guess the reasoning is to avoid conditional blocks of code such as :

if(type==PlatformType.Android) { ..}
else ....

Which can be replaced with overridden methods on the specific class

However if you just have a data struct with no logic, as may be the case if you have no requirement for extra fields, or are passing to a DB or service. I think the type property is ok.

  • I'll have to disagree with your first statement. This question caused me to get out my design patterns to see what it really says. According to Head First Design Patterns in the OO Principles it says "Favor composition over inheritance." – Luminous May 8 '15 at 20:39
  • really?! I get told composition 'isnt real OO' all the time – Ewan May 8 '15 at 21:18
  • tbh though I think my interpretation of the question as subclass vs type property is correct. composition dosnt solve the problem – Ewan May 8 '15 at 21:27
  • I think 'Head First...'-book and other pattern-only prophets are the reason why many devs hate patterns. Read 'Robert C. Martin - Agile Software Dev' and the reason why and when to use patterns. You guys don't focus on the reason why patterns might be good or become anti-patterns. If there is one switch statement for ios, android, win and no other place checking the type I would always use the simple switch way. If you need to invoke some handlers according to the type in my places your code will smell => change it. – Aitch May 8 '15 at 23:39
  • I think the problem is a property matches the data but 'class type' properties lead to procedual code. I'm sticking with my answer. – Ewan May 9 '15 at 11:07
1

If there is a lot of "common code" (or abstractions that all platforms make use of), another option is composition, rather than inheritance. I believe that anything you can do with inheritance, you can also accomplish through composition (I'm probably going to regret saying "anything"). Inheritance is going to lock you into that base implementation, composition gives you a choice.

Using a contrived example, if you were going to implement a base class that had common functionality such as:

class BaseFoo
{
    public DoThis() {
    }

    public DoThat() {
    }
}

And subclasses:

class MyFoo : BaseFoo {
}

class YourFoo : BaseFoo {
}

Those subclasses are locked into the behavior of the base class. (Yes, you can make methods virtual and override their behavior, but, for the sake of argument, let's say that what the base methods do is extremely complex and you don't want to/can't re-implement it.)

If you instead had each implement the same interface (representing the Foo abstraction) and composed the classes, they are free do implement the abstraction how they see fit:

interface IFoo
{
    void DoThis();
    void DoThat();
}

class MyFoo : IFoo {
    private readonly IThisStrategy _thisStrategy;
    private readonly IThatStrategy _thatStrategy;

    public MyFoo(IThisStrategy thisStrategy, IThatStrategy thatStrategy) {
        _thisStrategy = thisStrategy;
        _thatStrategy = thatStrategy;
    }

    public void DoThis() {
        _thisStrategy();
    }
}

It may take a bit more work to implement the solution through composition, but the benefits include decoupling, flexibility, and reusable components (the strategy implementations). This also has testing benefits as you can unit test individual behaviors that you're injecting without having to test the object at large. And this also avoids polluting your code with a bunch of switch statements everywhere that later cause a maintenance headache should you ever need to support a new platform or change that platform's behavior.

I'm a firm believer in composition over inheritance in a great many cases, especially when inheritance is not being used to actually extend a functional base.

0

Actually none of us can tell you the best way to implement your device type, since we don't know how you're going to (re)use your code. The problem is, that some devs will say, that you should use Device as interface, AbstractDeviceImpl as abstract class and Android, etc. as concrete class which would be nice, but do you need it?

I would say the best approach is to find it out on your own by using TDD. Use your code, see what happens. Try to find out your requirements, reading your tests. Do you want it to be prepared for OCP and very abstract? Or will there be no other devices so simply one device abstraction and 3 concrete classes are your friends?

As I think that there won't be more than those 3 devices, and assume that you only need the type for logging/debugging, I would create a class Device implementing type as read-only string unknown as default. Then create 3 inner classes inheriting Device overriding the type-getter with the corresponding name.

As a matter of fact, using an enum in that way you described it above, is mostly bad OCP violation ending in switch blocks and/or if statements. Adding a new device means, changing the enum and adding a class. That's actually not what an OOP wants. You can always use enums if you need the value itself, but to distinguish between execution paths, enum with more than two elements might produce ugly code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.