4

Imagine a device class that represents a physical real world mobile device, with fields like Enabled, Platform, Model IMEI, etc. Then, an operation class, which is something that needs to be done to a certain device, like DeviceEnablementOperation or LockOperation. This is a 'one device to many operations' relationship, and an operation cannot exist without being related to an existing device. Can I model this in such a way that the system will not allow certain types of operations to devices with certain platforms? For example, an Android device would not allow a DeviceEnablementOperation attached to it, while a Windows Phone device would.

Our current modeling started out considering only Android devices. At the time, we created the concept of operations as a poor man's event source, so that we could track what was done against a certain device via our API. Later on, we started supporting iOS devices too, and we introduced the Platform field using an enumeration class to differentiate them, like this:

public enum PlatformType : short
{
    /// <summary>
    /// Device is an Android.
    /// </summary>
    Android = 0,

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

    //// Can not place the name iOS.

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

At that point in time, the requirements were being met correctly, but now we need to restrict certain types of operations to certain platforms. The current model does not automatically restrict this, so the current implementation considers a hardcoded list of supported operations for each platform, and uses this to reject the creation of some operations to certain devices.

What I'd like to do is change the modelling so that this restriction is more robustly enforced. I thought about using inheritance and creating AndroidDevice, IosDevice and WindowsPhoneDevice classes, but I still fail to see how I could restrict the relationship with operations themselves to cover this requirement. A device has operations, but not all devices support all operations.

The modelling for the operations themselves is very straightforward inheritance, like this:

public abstract class Operation
{
    protected Operation()
    {
        this.Logs = new List<OperationLog>();
    }

    public DateTime? CreationDate { get; set; }

    public virtual Device Device { get; set; }

    public int DeviceId { get; set; }

    public OperationStatus GeneralStatus { get; set; }

    public int Id { get; set; }

    public DateTime? LastUpdate { get; set; }

    public ICollection<OperationLog> Logs { get; internal set; }

    public DateTime? ReceiveDate { get; set; }
}

And then derived operation types, like the ones I mentioned:

public sealed class LockOperation : Operation
{
    /// <summary>
    /// Gets or sets a value indicating whether the device must be locked or not.
    /// </summary>
    public bool Lock { get; set; }

    /// <summary>
    /// Gets or sets the device's lock password.
    /// </summary>
    public string Password { get; set; }
}

and

public sealed class EnablementOperation : Operation
{
    /// <summary>
    /// Gets or sets a value indicating whether the device should be enabled or disabled.
    /// </summary>
    public bool Enabled { get; set; }
}

Is it possible to model this to better reflect the new requirement that I want? I'd somehow need a way to tell if a given operation is supported for a given device, and only allow the relationship to exist if the operation was supported.

I suppose I could try something using generics and inheritance on both the device and operation classes, for example:

public abstract class Device<TOperation>
{
    public ICollection<TOperation> Operations { get; set; }
    ...
}

public sealed class AndroidDevice : Device<AndroidOperation> {...}

public abstract class Operation {...}

public abstract class AndroidOperation {...}

public sealed LockOperation : AndroidOperation {...}

But this particular approach would need multiple inheritance to work, and since I'm using C# it is not possible. I could then try using interfaces to differentiate operations, but them I suspect it would be a problem when mapping this to a database later using an ORM.

I also can't really break everything into their own completely isolated classes, because I need the general concept of "devices that have operations", regardless of platform or operation type. That's why the base classes exist in our current model.

4
  • Do you intend to make your operations mostly device agnostic, so your Lock operation just needs a platform field as a parameter? Or do you have, for example, individual Lock operations for each supported device?
    – Doc Brown
    May 5, 2015 at 20:37
  • This is actually a duplicate of a deleted question. Since the other question is no longer searchable, I guess we should keep this one around.
    – user22815
    May 5, 2015 at 23:03
  • @DocBrown great question... as I said, we started only supporting android devices, so everything was platform agnostic at first. The problem started to appear when we had to support other platforms. At that point, most operations are the same (it's the same lock operation for both android and ios for instance, since the only "argument" for this operation is the lock password, this does not change across platforms), but it is quite possible that some operations are platform specific, yes. We are actually still not sure how to model those.
    – julealgon
    May 6, 2015 at 13:23
  • @DocBrown We already have a "LocationMonitorConfigurationOperation" right now to configure our own gps monitoring process on the devices, and even though the operation is "the same" across devices, the parameters needed by our Android library are different from the ones iOS needs. Right now, we decided to create separate operations for that case. That is: there is a AndroidLocationMonitorConfigurationOperation and a IosLocationMonitorConfigurationOperation. I'm really not liking that at the moment, because to me, conceptually, they are the same operations.
    – julealgon
    May 6, 2015 at 13:25

3 Answers 3

2

If you worry about ORM persistence, then here's a possible solution:

Each operation declares its supported platform, similar to interface approach but use flags property instead (please note I change your enum to flags and add an option for unknown type)

[Flags]
public enum PlatformType : short
{
    Unknown = 0,
    /// <summary>
    /// Device is an Android.
    /// </summary>
    Android = 1,

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

    //// Can not place the name iOS.

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

public abstract class Operation
{
    public string Name { get; set; }
    public abstract PlatformType SupportedPlatforms { get; }

    //Other properties
}

public sealed class DeviceEnablementOperation : Operation
{
    public override PlatformType SupportedPlatforms
    {
        get
        {
            return PlatformType.Windows | PlatformType.Ios;
        }
    }

    //Other properties
}

The abstract devices class will contain list of operations and ensure to accept only operations it supports (in constructor and AddOperation() method). It will throw exception immediately when violated so that developer can recognize quickly:

public abstract class Device
{
    public PlatformType Platform { get; private set; }
    private ICollection<Operation> _operations;

    protected Device(PlatformType platform)
    {
        Platform = platform;
        _operations = new List<Operation>();
    }

    protected Device(PlatformType platform, ICollection<Operation> operations)
    {
        Platform = platform;

        var violatedOperation = operations.FirstOrDefault(o => !OperationSupportedByThisDevice(o));
        if(violatedOperation != null)
        {
            throw new ApplicationException(string.Format("Operation {0} not supported by platform {1}", violatedOperation.Name, Platform));
        }

        _operations = operations;
    }

    public void AddOperation(Operation operation)
    {
        if(!OperationSupportedByThisDevice(operation))
        {
            throw new ApplicationException(string.Format("Operation {0} not supported by platform {1}", operation.Name, Platform));
        }

        _operations.Add(operation);
    }

    private bool OperationSupportedByThisDevice(Operation operation)
    {
        return operation.SupportedPlatforms.HasFlag(Platform);
    }
}

public class AndroidDevice : Device
{
    public AndroidDevice() : base(PlatformType.Android)
    {
    }

    public AndroidDevice(ICollection<Operation> operations) : base(PlatformType.Android, operations)
    {
    }
}

So if an android device is created like this, it will throw exception since DeviceEnablementOperation is not supported by android device:

var androidDevice = new AndroidDevice(new List<Operation> { new DeviceEnablementOperation() });

If you worry about the flags usage for PlatformType for device (.i.e. an device can be mistakenly assigned with multiple platforms), then create a separate flags enum for operation, but you get the idea

5
  • Two comments on your code: 1) In the second device constructor you are enumerating operations twice. Instead of Any and First you can use FirstOrDefault and throw an exception if the result is not null. 2) In OperationSupportedByThisDevice you could use Enum.HasFlag May 6, 2015 at 5:16
  • Ok I see where you are coming from. I'd like a constraint a bit stronger than this (like compile time type safety that restricted this) but I like what you did. There is just only one thing I'm not very fond of here, and it's that you are using a flags enum in a context where flags do not make sense. One could create a device with platform equal to `PlatformType.Android | PlatformType.WindowsPhone", and it would be legal. This opens up more validation problems that need to be handled in code.
    – julealgon
    May 6, 2015 at 13:03
  • @julealgon Yes, I noticed that problem and also mentioned in my answer that if it's a concern, you can create a separate flag for operation, e.g. OperationPlatform, leave the PlatformType unchanged. But if you want a compile time constraint then I haven't thought of any better way than Ewan's answer, though that is not ORM-friendly May 7, 2015 at 3:17
  • I'm sorry, I completely missed that last part, so my previous comment was unnecessary. I also just realized that I could return a collection of PlatformType instead of having to create a new enumeration too, and just change the test to .Contains instead of .HasFlag. I'm liking your solution here even if it does not restrict this at compile time, will think about implementing it.
    – julealgon
    May 7, 2015 at 14:12
  • Also, we could even remove the whole PlatformType enum by switching the SupportedPlatforms property on the Operation base class to a IsSupportedOn(Device) method. The device OperationSupportedByThisDevice method would then pass itself to that method in the operation, who would then, based on the provided device, check if it is supported or not (by using it's type instead of the enumeration).
    – julealgon
    May 7, 2015 at 14:18
1

How about adding interfaces for platform specific Operations? ie.

public interface IAndroidOperation : IOperation {}
public interface IIoSOperation : IOperation {}

Then have the operations implement the interfaces they are good for

public class LockOperation : IAndroidOperation, IIoSOperation {}

Then have the device only allow adding of the correct interface

public class AndroidDevice : Device
{
    public List<IAndroidOperation> Operations {get;,set;}
}

Update:

you are right the exposure of the generic Operations is non trivial. There are a number of approaches. I have included a simple one below, which is not perhaps ideal. but does avoid having to make your own IEnumberable

using System.Linq;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject2
{
    public interface IOperation 
    {
        void DoStuff();
    }
    public interface IAndroidOperation : IOperation { }
    public interface IoSOperation : IOperation { }

    public class LockOperation : IAndroidOperation
    {
        public void DoStuff(){}
    }

    public class BendOperation : IoSOperation
    {
        public void DoStuff() { }
    }
    public interface IDevice
    {
        IEnumerable<IOperation> GetOperations();
    }
    public abstract class Device<T> : IDevice where T : IOperation
    {
        public abstract List<T> Operations { get; set; }

        public IEnumerable<IOperation> GetOperations()
        {
            return this.Operations.Select(o => (IOperation)o);
        }
    }

    public class IoSDevice : Device<IoSOperation>
    {
        public override List<IoSOperation> Operations { get; set; }
    }

    public class AndroidDevice : Device<IAndroidOperation>
    {
        public override List<IAndroidOperation> Operations { get; set; }
    }

    public class TestMe
    {
        [TestMethod]
        public void test()
        {
            List<IDevice> devices = new List<IDevice>();
            AndroidDevice a = new AndroidDevice();
            IoSDevice i = new IoSDevice();

            devices.Add(a);
            devices.Add(i);

            LockOperation l = new LockOperation();
            BendOperation b = new BendOperation();

            a.Operations = new List<IAndroidOperation>();
            i.Operations = new List<IoSOperation>();

            a.Operations.Add(l); //ok
            //a.Operations.Add(b); //compilation error
            i.Operations.Add(b); // ok

            foreach (IDevice d in devices)
            {
                foreach (IOperation o in d.GetOperations())
                {
                    o.DoStuff();
                }
            }

        }
    }
}
6
  • Ok, but how do I access the operations for a given Device, without having to cast them to a specific inherited class? As I said in the OP, "I need the general concept of "devices that have operations", regardless of platform or operation type. That's why the base classes exist in our current model."
    – julealgon
    May 7, 2015 at 14:04
  • I did skim over this bit. you have a problem in the Generic Collections don't like you casting from one to another. I was thinking the ideal case you would create your own IEnumerable which would expose the Get as a IOperation. I'll see if i can think of a better approach
    – Ewan
    May 7, 2015 at 14:49
  • just found this : 25hoursaday.com/weblog/2007/08/09/… which outlines a few methods, it seems the most elegant is to use arrays rather than Generic Collections
    – Ewan
    May 7, 2015 at 14:58
  • re ORM friendly : i dont tend to use them, but since the key thing here are the interfaces, which allow mutiple inheritance, there is room to inherit from Device/Operation obejects which expose a Type = this.GetType property if required by your ORM
    – Ewan
    May 7, 2015 at 15:20
  • About the array/collection topic, I think this hasn't been an issue in C# since the .Net 4 release. You can see that generics has co and contravariance here.
    – julealgon
    May 7, 2015 at 15:33
1

I think interfaces in the form of the wrapper pattern are the way to go.

If you have an IPlatform interface and IAndroid, IOS (pun intended), etc. that extend it, you can write operations as wrapper objects following the wrapepr pattern.

Disclaimer: this is pseudo code, it's been too long since I did C#, you get the idea

Here's an operation that works on all devices.

class BreakDisplayOperation implements IPlatform {

BreakDisplayOperation (IPlatform platform) { // this is the  constructor

One that only works on IWindows as in your .

class DeviceEnablementOperation implements IWindows {

DeviceEnablementOperation (IWindows platform) { // constructor

The idea behind this is to delegate all method calls of the interface to the object received in the constructor, except for those that you want to manipulate by the operation.

IWindows (or IPlatform) probably includes a getter method for enable

bool isEnabled(); // or do this via get keyword, which C# has, if I remember correctly.

DeviceEnablementOperation overrides this method an returns a value it defines itself, instead of receiving that value by delegating the call to the wrapped IWindows object, like so

class DeviceEnablementOperation implements IWindows {

bool isEnabled()
{
    return true;
}

}

again, other things that are not manipulated by the operation are just delegated to the wrapepd object (assuming a member with same name as the constructor parameter:

class DeviceEnablementOperation implements IWindows {

    bool isEnabled() // modified
    {
        return true;
    }

    IMEI getIMEI() // delegated
    {
        return platform.getIMEI()
    }
}

In the above examples, you'd have to do the delegation for all Operations again and again. To avoid this boilerplate, create super classes that do all delegations, then each specific Operation can override methods as necessary.


This all works because the interface inheritance branches, making individual platforms incompatible types because they are siblings in the hierarchy.

I know this abstracts the problem in a different way as you are currently doing it. It's a bit like Platform, Device and Operation all merge into the same thing.

You can apply many operations by wrapping the wrappers.


To get a list of all operations, add the getOperations(); method.

The super class of all Platforms will return a new empty list object. The super class of all Operations will again delegate that call to the wrapped object (which will eventually be a Platform), add itself to the list and return the list. (for possible other wrappers to ad themselves)


The problems of this approach:

  • somebody could implement both interface to merge them, breakign the separation
  • an IPlatform wrapper will be of that type, to use a specific wrapper again, a cast is necessary. You can work around this by duplicating the general purpose wrappers specifically typed for each platform. Not sure if generics could help you with this, but I have the feeling they do.
3
  • This is very interesting. I admit I still have not understood 100% of what you said there, but I'll certainly analyze it more thoroughly. Using this approach, how would a operation that supported N platforms work? Say that the iOS display was indestructible (:P), so BreakDisplayOperation could only be applied to IAndroid and IWindows. Since you are using a kind of wrapper/decorator here, how would that work with multiple inferface implementations?
    – julealgon
    May 7, 2015 at 15:44
  • @julealgon sorry, if the answer is confusing. Try to wrap your head around the wrapper/decorator pattern first. ;) I guess that interfaces are not enough for more specialized type restrictions as I said in the problem section. I don't know much about generics in c#, but it looks like an "or" constraint like this is not possible. The only thing in that direction is probably overlaoding the constructor for the accepted types, but I think thisin turn will require casting.
    – null
    May 7, 2015 at 16:44
  • @julealgon you can do this by duplicating the Operation class with each one implementing one of the allowed interfaces.
    – null
    May 8, 2015 at 17:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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