My concrete classes can be one of the type permutations shown below. But, since I cannot inherit from two classes (case 4 and 5) I had to convert one of them to interface which introduce code duplication shown in FIX section below. I know someone will say "use composition", but I don't see how to change the design to take this into account.
Type permutations:
ControllerSimulatorMovingControllerSimulatorRegistersControllerSimulatorControllerSimulator+RegistersControllerSimulatorMovingControllerSimulator+RegistersControllerSimulator
CODE
public abstract class ControllerSimulator
{
protected virtual void OnTurningOn() { }
public void TurnOn()
{
...
}
protected virtual void OnTurningOff() { }
public void TurnOff()
{
...
}
// more base methods
...
}
public abstract class MovingControllerSimulator : ControllerSimulator
{
public virtual void Disable() { }
public void SetMotionDuration(TimeSpan duration)
{
_motionDuration = duration;
}
protected void OnMoveStarted()
{
MoveStarted?.Invoke();
}
// more base methods
...
}
public abstract class RegistersControllerSimulator : ControllerSimulator
{
protected Dictionary<string, object> Registers;
protected abstract void OnRegisterChanged(string regName, object regVal);
public void UpdateRegister(string regName, object regVal)
{
Registers[regName] = regVal;
OnRegisterChanged(...);
}
// more base methods
...
}
FIX
Since I cannot inherit from two classes I decided to convert RegistersControllerSimulator to an interface and create RegistersMovingControllerSimulator and RegistersNonMovingControllerSimulator. But, this requires code duplication which I potentially can solve by moving it to utils.
interface
interface IRegistersControllerSimulator
{
void UpdateRegister(string regName, object regVal);
}
public abstract class RegistersMovingControllerSimulator : MovingControllerSimulator, IRegistersControllerSimulator
{
protected Dictionary<string, object> Registers;
protected abstract void OnRegisterChanged(string regName, object regVal);
public void UpdateRegister(string regName, object regVal)
{
Registers[regName] = regVal;
OnRegisterChanged(...);
}
}
public abstract class RegistersNonMovingControllerSimulator : ControllerSimulator, IRegistersControllerSimulator
{
protected Dictionary<string, object> Registers;
protected abstract void OnRegisterChanged(string regName, object regVal);
public void UpdateRegister(string regName, object regVal)
{
Registers[regName] = regVal;
OnRegisterChanged(...);
}
}
Concrete classes
public class Concrete_Registers_Moving_Controller : RegistersMovingControllerSimulator
{
protected override void OnRegisterChanged(string regName, object regVal)
{
if (regName == "Move")
{
// imitate Move
OnMoveStarted();
// I know `.Wait()` is bad here, but it's just an example
Task.Delay(_motionDuration).Wait();
}
}
public Task StartMoveAsync2()
{
...
OnMoveStarted();
return Task.Delay(_motionDuration);
}
}
public class Concrete_Registers_NonMoving_Controller : RegistersNonMovingControllerSimulator
{
protected override void OnRegisterChanged(string regName, object regVal)
{
if (regName == "Move2")
{
...
// imitate Move
OnMoveStarted();
// I know `.Wait()` is bad here, but it's just an example
Task.Delay(_motionDuration).Wait();
}
}
}
public class Concrete_NonRegisters_Moving_Controller : MovingControllerSimulator
{
public Task StartMoveAsync()
{
OnMoveStarted();
return Task.Delay(_motionDuration);
}
}
public class Concrete_NonRegisters_NonMoving_Controller : ControllerSimulator
{
}
USAGE
var controllerRegMove = new Concrete_Registers_Moving_Controller();
controllerRegMove.UpdateRegister(...);
controllerRegMove.SetMotionDuration(...);
controllerRegMove.StartMoveAsync2();
var controllerRegNonMove = new Concrete_Registers_NonMoving_Controller();
controllerRegNonMove.UpdateRegister(...);
controllerRegNonMove.SomeNonMoveMethod(...);
var controllerNonRegMove = new Concrete_NonRegisters_Moving_Controller();
controllerNonRegMove.SetMotionDuration(...);
controllerNonRegMove.StartMoveAsync();
var controllerNonRegNonMove = new Concrete_NonRegisters_NonMoving_Controller();
controllerNonRegNonMove.SomeNonMoveMethod2(...);
Yes, use composition.
What you can do with inheritance you can also do with composition. The reverse isn't true, because you can compose objects with multiple capabilities, but you can't inherit from more than one base class.
So how would you go about it in this case?
It's not entirely clear to me how the various classes in the OP hierarchy are supposed to work together, but since it sounds as though you have two axes of variability, you'll need two services:
Here I've used Dan North's Thingy naming convention for placeholder names until something better comes to mind.
Now inject those into the Controller class:
You can now vary movement and registers independently.
If you want to reuse methods from one object in another object, you can define the interface methods so that the other classes pass themselves as arguments, but be careful with that, since it indicates two-way coupling that will make it more difficult to maintain the code. There's usually a better alternative to such designs.