views:

127

answers:

5

I'm wondering if I should change the software architecture of one of my projects.

I'm developing software for a project where two sides (in fact a host and a device) use shared code. That helps because shared data, e.g. enums can be stored in one central place.

I'm working with what we call a "channel" to transfer data between device and host. Each channel has to be implemented on device and host side. We have different kinds of channels, ordinary ones and special channels which transfer measurement data.

My current solution has the shared code in an abstract base class. From there on code is split between the two sides. As it has turned out there are a few cases when we would have shared code but we can't share it, we have to implement it on each side.

The principle of DRY (don't repeat yourself) says that you shouldn't have code twice.

My thought was now to concatenate the functionality of e.g. the abstract measurement channel on the device side and the host side in an abstract class with shared code. That means though that once we create an actual class for either the device or the host side for that channel we have to hide the functionality that is used by the other side.

Is this an acceptable thing to do:

public abstract class ChannelAbstract
{
    protected void ChannelAbstractMethodUsedByDeviceSide()    {  }
    protected void ChannelAbstractMethodUsedByHostSide()      {  }
}

public abstract class MeasurementChannelAbstract : ChannelAbstract
{
    protected void MeasurementChannelAbstractMethodUsedByDeviceSide()   {  }
    protected void MeasurementChannelAbstractMethodUsedByHostSide()     {  }
}

public class DeviceMeasurementChannel : MeasurementChannelAbstract
{
    public new void MeasurementChannelAbstractMethodUsedByDeviceSide()
    {
        base.MeasurementChannelAbstractMethodUsedByDeviceSide();
    }

    public new void ChannelAbstractMethodUsedByDeviceSide()
    {
        base.ChannelAbstractMethodUsedByDeviceSide();
    }
}

public class HostMeasurementChannel : MeasurementChannelAbstract
{
    public new void MeasurementChannelAbstractMethodUsedByHostSide()
    {
        base.MeasurementChannelAbstractMethodUsedByHostSide();
    }

    public new void ChannelAbstractMethodUsedByHostSide()
    {
        base.ChannelAbstractMethodUsedByHostSide();
    }
}

Now, DeviceMeasurementChannel is only using the functionality for the device side from MeasurementChannelAbstract. By declaring all methods/members of MeasurementChannelAbstract protected you have to use the new keyword to enable that functionality to be accessed from the outside.

Is that acceptable or are there any pitfalls, caveats, etc. that could arise later when using the code?

A: 

It sounds to me as though you've not finished identifying which bits of code are shared. Wouldn't it make more sense to keep all the generic / shared stuff in MeasurementChannelAbstract rather than having different methods which the two sides call? I would have thought that those ought to be in inherited classes?

Jon Cage
+6  A: 

You can solve the problem with inheritance, like this:

public abstract class MeasurementChannelAbstract
{
    protected abstract void Method();
}

public class DeviceMeasurementChannel : MeasurementChannelAbstract
{
    public void Method()
    {
        // Device side implementation here.
    }
}

public class HostMeasurementChannel : MeasurementChannelAbstract
{
    public void Method()
    {
        // Host side implementation here.
    }
}

... or by composition, using the Strategy pattern, like this:

public class MeasurementChannel
{
    private MeasurementStrategyAbstract m_strategy;

    public MeasurementChannel(MeasurementStrategyAbstract strategy)
    {
        m_strategy = strategy;
    }

    protected void Method()
    {
        m_strategy.Measure();
    }
}

public abstract class MeasurementStrategyAbstract
{
    protected abstract void Measure();
}

public class DeviceMeasurementStrategy : MeasurementStrategyAbstract
{
    public void Measure()
    {
        // Device side implementation here.
    }
}

public class HostMeasurementStrategy : MeasurementStrategyAbstract
{
    public void Measure()
    {
        // Host side implementation here.
    }
}

It seems to me that you want to divide your inheritance hierarchy between both Standard/Measurement channels and Device/Host channels. One way to do this is with multiple inheritance - but C# doesn't support multiple inheritance (except for interfaces), and in most cases a design based on composition will be simpler.

richj
Timo Kosig
@richj: your example solving the problem using inheritance does not work for me as I have methods in MeasurementChannelAbstract which are only used by the device side or the other way around. I know I could move those to DeviceMeasurementChannelAbstract and HostMeasurementChannelAbstract which would inherit from MeasurementChannelAbstract. The problem is that say e.g. TemperatureChannel which needs to be implemented for both sides would have to inherit from one of these two classes and can not inherit from TemperatureChannelAbstract (where shared data for this channel could be defined).
Timo Kosig
I wonder if approaching the problem from the starting point of interfaces instead of abstract classes would help. The interfaces would be: IChannel, ISidedChannel : IChannel, IMeasurementChannel : IChannel, ISidedMeasurementChannel : ISidedChannel, IMeasurementChannel. Using this idea you can provide one concrete implementation for every type variation, or you could provide one concrete implementation for every type and deal with the variations using Strategies (DeviceSideStrategy, HostSideStrategy, ...).
richj
+5  A: 

To me it looks a bit like you're confusing inheritance and composition. When you have to "blank out"/throw exception when inherited functionality does not overlap sanely, your inheritance graph is missing some intermediate class. And often this is because some functionality just should come from a member instance of an other class instead of being inherited.

Consider also practicality, mapping everything into perfect OOP is not the goal, the goal is a working program that is maintainable without huge pain.

Pasi Savolainen
@Pasi Savolainen: At the moment I can't think of another way how to make sure that code is not repeated on host/device side other than having shared abstract base classes which have functionality for either side. Under the assertion that maintainability has to be easy I would have to go for my aforementioned approach of "blanking out" functionality that is not appropriate for the respective side (device/host). That way I can at least make sure that the implementation of a channels on the device/host side would inherit from the same base class, sharing information.
Timo Kosig
@Timo Kosig: Well, we have a similar situation in our desktop application where a part of business logic has to be embedded in SQL server as stored procedures. These have completely different ways of accessing the actual data (sqlclient vs. nhibernate), but they act on similar objects and are actually communicating. We've solved it partially (subcontractor fucked up) by doing the same calculations on IInterface objects, but use different "loaders" to get these objects that implement the interface. So the logic is shared, but not all functionality must be in all objects. (this is kinda obvious)
Pasi Savolainen
@Pasi Savolainen: I accepted your answer because I feel it comes closest to the situation we're facing here. Just for the record, we're staying with our current approach where we split classes for device/host side implementation early on to avoid the confusion that would come along with keeping code for both sides in the same class. It wasn't an easy decision but thinking of other developers trying to understand the code I think this is the best decision. It doesn't map into perfect OOP but it works and is clearer.
Timo Kosig
+2  A: 

Like Rich implies: Needing only ONE of the members declared ins MeasurementChannelAbstract in one of the concrete implementations is a very clear indication that your interface is badly defined as it has more than one responsibility. This means it is difficult for clients of your code (and readers) to understand the abstractions and see the difference between the various concrete implementations.

This is called the "Single Responsibility Principle" and it is is an important one for good OO design.

(For more on good OO design, I recommend working through all the SOLID principles).

MadKeithV
A: 

Therefore I ask myself if it is OK to have a single chain of inheritance (accepting that some classes will have functionality for both host and device side) and do the separation into host/device channel at the last stage (blanking out the functionality which is not needed)

I think it's ok. But you may blanking out the functionality which is not needed by using different interfaces for SimpleChannel and MeasurementChannel.

Random