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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With