Better Cohesion

One of the issues that I’ve come across multiple times this year is abstracting multi-purpose hardware. When I say multi-purpose here, I’m talking about custom hardware that has multiple, unrelated, functions. A multi-function printer/scanner/copier (multi-function device, hence, MFD) is a good enough example, so I’ll stick with that throughout this post, even though I’ve never owned or programmed a multi-function printer. (The one limitation of the MFD example is that, generally, you can’t do any of these functions simultaneously. Most of the hardware I worked with absolutely required the separate functions to operate simultaneously.)

Ideally (academically, at least), we would want to abstract our printer/scanner/copier into three classes, Printer, Scanner and Copier. That way, we can give an instance of Printer to the application that’s printing, and it won’t affect some other application that is using an instance of Scanner. Our Printer.Open() and Scanner.Open() functions perform different operations as you’d expect and everyone is happy.

Well, not everyone. The person who has to write these three classes is planning a murderous rampage against whoever decided that cohesion trumps ease, or possibility, of implementation. The problem is that while we have three logical devices, there is only one physical device. All commands given to Printer, Scanner or Copier need to be passed through the same USB connection to the actual hardware.

With our three separate objects, these commands could literally come at any time, which means we need locking on the hardware connection. Inter-process synchronisation is expensive (in the Windows world we’re looking at a mutant/mutex (kernel object) versus a critical section (compare-and-swap operation)). Then there’s the other problem of device sharing between processes which, if solved at all, involves more synchronisation.

The best solution to this comes from COM. Once you get past the horrible syntax (brought about by creating an object model with no language support), you find that COM makes it easy to tie an object instance to a separate process (server) and let other processes access it transparently. However, this isn’t the really juicy bit.

“Cohesion” normally refers to how focused an object is. When separate, the Printer class is dedicated solely to operating the printer function, and hence has high cohesion. Every member of Printer relates to the printer. Every line of code in Printer relates to the printer. If something is going wrong with the printer, look inside Printer.

If we combine all of our Printer, Scanner and Copier classes into a single Device class, instead of Printer.Open() and Scanner.Open(), we have Device.OpenPrinter() and Device.OpenScanner() (or worse, Device.Open(enum DeviceType type)). The interface is now massively fragmented and client-side cohesion has gone out the window. On the bright side, all the hardware communication is now in the one place, making the internal cohesion high.

The fundamental problem is that cohesion from the implementer’s point-of-view is different to cohesion from the user’s point-of-view. The implementer loves having all the synchronisation and driver communication in the one place, while the user doesn’t understand why they need to dig through all these printer methods just to get to Device.MakeColourCopy(1).

So how does COM solve this? In short, by not allowing access to the class. If we create an instance of Device in C++ or C#, we have access to every member. If we create an instance of Device using COM, we get an opaque object pointer until we request an interface. How does this look in C#?

interface IPrinter
{
    void OpenPrinter();
    void ClosePrinter();
    void Print(string text);
}
 
interface IScanner
{
    void OpenScanner();
    void CloseScanner();
    Bitmap Scan();
}
 
class Device : IPrinter, IScanner
{
    public void OpenPrinter() { }  // reordered slightly, because it happens to make
    public void OpenScanner() { }  // more sense to put the "Open"s together
    public void ClosePrinter() { }
    public void CloseScanner() { }
    public void Print(string text) { }
    public Bitmap Scan() { }
}
 
Device device = new Device();
// Only use 'device' to assign to 'printer' and 'scanner'
IPrinter printer = (IPrinter)device;
IScanner scanner = (IScanner)device;

So we create an instance of Device, but the restrict our view of the interface to that part which we are interested in, either IPrinter or IScanner. If instead of Device we instantiate a class specific to our hardware, it doesn’t have to implement IScanner, and anyone trying to use it as a scanner will get a nice little exception.

The one limitation here is that the interface of Device exposes all members, which means we must avoid naming collisions. COM doesn’t have this issue, because it never exposes the entire interface, only the part which has been requested. Luckily, C# can do the same.

interface IPrinter
{
    void Open();
    void Close();
    void Print(string text);
}
 
interface IScanner
{
    void Open();
    void Close();
    Bitmap Scan();
}
 
class Device : IPrinter, IScanner
{
    void IPrinter.Open() { }
    void IScanner.Open() { }
    void IPrinter.Close() { }
    void IScanner.Close() { }
    void IPrinter.Print(string text) { }
    Bitmap IScanner.Scan() { }
}

This is known as explicit interface implementation, and it means that the following are true:

Device device = new Device();
IPrinter printer = (IPrinter)device;
IScanner scanner = (IScanner)device;
 
device.Open();      // compiler error
printer.Open();     // calls IPrinter.Open()
scanner.Open();     // calls IScanner.Open()

So now, the only way to access the printer or scanner is to instantiate a Device and specifically request access to the subset that is required. IPrinter has high cohesion, since it only contains members related to printing, as does IScanner. Meanwhile, a murderous rampage is avoided as the developer is also allowed an implementation with high cohesion.

3 Responses to “Better Cohesion”

  1. pimaster - December 17th, 2009

    Out of interest, are the casts actually necessary?
    In a Java world, since Device implements IPrinter, you should be safe to go IPrinter printer = new Device();
    Or is the specific casting related to COM?

    I’d also consider using something like a DeviceFactory that allows you to get a printer or scanner scanner whilst sharing a single instance of the device. This would enable you to keep printer commands in a Printer class and scanner commands in the Scanner class whilst still only passing the commands to the one Device object.

  2. zooba - December 18th, 2009

    Good point. Typically I include the casts because I use ‘var’ (type inference) rather than specifying the type. It’s probably just my style. Having said that, when using explicit implementation, you need to cast a Device to an IPrinter or IScanner – otherwise you can’t access anything.

    The device factory idea is effectively a singleton with multiple ‘get instance’ methods. My intense dislike of singleton will get an airing another day, but my bigger concern with the factory approach is that it makes it less obvious that it is in fact the same object. IMO, this is taking the abstraction too far. (I’ve just noted this down as a topic for a future post, so it will get a more thorough discussion soon.)

  3. pimaster - December 18th, 2009

    Sweet, looking forward to the follow ups