Home > OS >  How to correctly implement this design?
How to correctly implement this design?

Time:04-02

I came across this problem in an interview where I was asked to design a simple architecture for Vehicle infrastructure for all vehicles.

Problem statement was something like - Consider a base class 'Vehicle' for all kinds of vehicles (cars, trucks, bikes, autos, cycles ...). Now we have multiple requirements for different kind of vehicles- Eg. Car, Jeep, .. have AC whereas bike, cycles, trucks does not have AC.

Also we have a long list of vehicles(all vehicles) and we specifically want to perform some operations based on whether this vehicle has ac or not. How can we design this.

I was able to come up with an approach where I created an interface for AC Functionality and have vehicles that have ac implement these.

class Vehicle
{
}

inteface IACFunctionality
{
void PerformSomeOperation();
}

class Car : Vehicle, IACFunctionality
{
//Implementation
}

class Bike : Vehicle
{
//No implementation for ac
}

public void UpdateAllACDevices(List<Vehicle> vehicles)
{
// Check if vehicle type implements IACFunctionality and perform operation
foreach(var vehicle in vehicles)
{
IACFunctionality acVehicle = vehicle as IACFunctionality;
if(acVehicle != null)
   acVehicle.PerformSomeOperation();
}
}

This was all good until he asked me - consider if few cars did not have AC. How will you integrate this change ?

I decided to go ahead with bit shady approach to have 2 separate classes for Car - one that implemented IACFunctionality and one that didn't.

class ACCar : Vehicle, IACFunctionality 
{}

class NonACCar : Vehicle
{}

Now he further asked what if there are multiple such requirements like AC - music system, sunroof, etc. I realized this wont fit according to my approach, but based on interface segregation principle, I didn't want to have something defined for a class which it does not require.

What would be correct approach to proceed with this design ?

CodePudding user response:

My approach would start by creating an interface IXxxFunctionality for each functionality. An interface would be added only on classes that may implement it, excluding classes where the interface is completely meaningless.

Besides exposing its own methods and properties, each interface would also expose a boolean property IsXxxSupported (or CanXxx) that says whether the functionality is actually supported. By querying such property, you know that calling any member of the interface is safe (they do not throw NotSupportedException or other appropriate exception).

When you need to call the desired functionality on objects statically typed with the base class, you can use the following pattern:

foreach (var vehicle in vehicles)
{
    if (vehicle is IACFunctionality acVehicle && acVehicle.IsACSupported)
    {
        // do stuff with acVehicle without worries!
    }
}

From a theoretical point of view, in such way you are respecting both interface segregation principle and Liskov substitution principle, since:

  • Each interface not applied where it is not needed and you are not polluting the base class.
  • The members of the interface throw NotSupportedException if and only if IsXxxSupported returns false. In fact, you are not violating the contract of a superclass in a subclass (unless you provide a bad implementation intentionally), because the contract allows the exception under a specific condition.

Generally speaking, exposing a IsXxxSupported property next to the real "actionable" members is an approach that can be leveraged also in other contexts. This includes abstract classes that provide some basic features together with other optional ones that are delegated to their concrete subclasses.

This kind of pattern can be found in many places both in the .NET and in widespread libraries. The first example that comes into my mind is the enter image description here

  • Related