Coding to Interfaces vs Abstract Inheritance

dependency-inversioninheritanceinterfacesobject-oriented-designsolid

Inheritance vs coding to an interface is something I have wondered with respect to proper architecture design but actually have not run into an problems when using abstract inheritance over coding to an interface in my own projects.

According to SOLID principles of OOP Design (and general OOP advice), you should code to an interface and never use inheritance (abstract or otherwise) (and yes, I realize I am speaking in the context of absolutes because I find very few arguments otherwise). While this makes complete sense for situations such as having classes such as:

class A {}

class B : A {}

class C : B {}

class D : C {}

class E : D {}

...

In this context, there is a strong coupling between classes that can easily break at any level. I 100% agree that inheritance is the key problem. However, I often wonder if the following would be an acceptable use of inheritance

public abstract class Vehicle
{
    public enum Gear
    {
        Drive,
        Reverse,
        Park,
        Low,
        High,
        Neutral
    }

    public string Color { get; set; }
    // Other common properties here …

    public virtual void Drive()
    {
        if (KeyInIgnition() && HasFuel() && SeatBeltsOn() && GearMode == Gear.Drive)
        {
            // Some logic to move car forward
        }
        // other logic ...
    }

    public virtual void Reverse()
    {
        // Logic here ...
    }
    // ...
}

public class Sedan : Vehicle
{
    // Set base class properties here ...

    public int UniqueSedanMethod() // Let’s say this method does not fit the standard to put something in an interface (i.e. it is completely unique to a sedan)
    {
        // Logic here ...
        return 3;
    }

    // Gets base class logic because driving a sedan does not differ (in the basics) from the driving of an SUV
}

public class SUV : Vehicle
{
    public int CalcAdditionalSeatsAdded()
    {
        // Logic here ...
        return 5;
    }
}

In this context, to me it seems it would make sense to have a logical base class to group the common behaviors all vehicles would share. Drive should not ever be different no matter what car type we are in, however if the need should arise, the method can be completely overrode (and use the default implementation where needed in the override). This seems like the best practice because it is clear, clean, simple, and of course DRY code. However, this makes Sedan, SUV, etc. tightly coupled to the Vehicle base class.

From the opposing view, if I used an interface then I would have to copy paste several lines to each class for its implementation despite the fact they are all the same. This would be a huge violation of DRY and then subsequently having to fix / update a method in all classes if the general behavior changes (i.e. more work and error prone).

From another opposing view, if I put methods like Drive, Reverse, etc. in a service-like class that accepts some form of an Vehicle I would have 2 major problems still.

  1. I would have to code the Sedan / SUV to a common interface still just to get a general parameter else I would have to use generics that would have to be constrained somehow (either interface which makes generics redundant in the first place or the overall class type which is not really ideal since I could pass any class to the method which is definitely not wise).
  2. The Sedan / SUV just depends on this otherwise extraneous dependency service class (and vice verse for the parameters, potentially). This is still tight coupling or rather still dependent.

From another point of view of the first problem, I could create an external service-like class for each vehicle type (i.e SedanService, SUVService, etc.) with the various methods, but this again just makes the services depend on their respective type which is actually what the dependency inversion principle aims to accomplish (at least I think). The problem of not having DRY code arises again because I just exchange putting the details in the actual vehicle type class to putting them in service class. This does not actually solve the problem but rather takes the titanic business strategy of "shuffle the deck chairs and hope the result is better".

With this example and the aforementioned points of view on using the interface / dependency inversion principle, I do not understand the benefits of coding to an interface or otherwise over using inheritance of a common abstract base class that holds the default implementation that should only need to be overrode in rare circumstances without affecting the base or other derivations. The major downside is tightly coupling of the base and derivative along with the potential of introducing brittle inheritance chains if the derivatives are ever inherited from (i.e. which is why I would make them sealed, but for the sake of the example, I did not).

With all of that, my questions are:

Am I misunderstanding the concept of "code to an interface and do not use inheritance" and/or the dependency inversion principle? (Am I misusing those two?) If so, what is the correct interpretation and examples / solution?

If not, is there another way to achieve dependency inversion / loose coupling that I am just not thinking (or aware) of / understanding?

If I am not misunderstanding "code to an interface", dependency inversion, and not unaware of a better solution, and utilizing inheritance correctly then why would inheritance be considered {insert negative and/or demeaning adjective here}? Is there a downside(s) that I am not considering?

Should DRY code and future maintainability be sacrificed for loose coupling for portability and scalability? Seems like a trade-off that is really steep to make without extreme and carefully considered calculations.

P.S.

I understand interfaces are generally for establishing a contract between something and a set of established characteristics and behaviors which culminate in them being a euphemism-like device for stating that an class "has a(n) {insert interface name/noun here}" relationship. Contrastingly, inheritance is that a class "is a(n) {insert base class name/noun here}" with respect to the base class and derivative class's relationship. This is how I am interpreting the terms, so this could be the source of my confusion, but I do not really think so.

Best Answer

Am I misunderstanding the concept of "code to an interface and do not use inheritance"?

Yes you are misunderstanding that, because you seem to be conflating two different principles.

On the one hand, there is the principle of "code to an interface" and it is very unfortunate that languages like C# and Java have an interface keyword, because that is not what the principle refers to. The principle of coding to an interface means that the user of a class only needs to know which public members exist and should not have any knowledge of how the methods are implemented. The interface keyword can help here, but the principle applies equally to languages that don't have the keyword or even the concept in their language design.

On the other hand, there is the principle of "prefer composition over inheritance".
This principle is a reaction to the excessive use of inheritance when Object Oriented design became popular and inheritance of 4 or more levels deep were used and without a proper, strong, "is-a" relationship between the levels.
Personally, I think inheritance has its place in a designer's toolbox and the people that say "never use inheritance" are swinging the pendulum too far in the other direction. But you should also be aware that if your only tool is a hammer, then everything starts to look like a nail.


Regarding the Vehicle example in the question, if users of Vehicle don't need to know if they are dealing with a Sedan or a SUV nor need to know how the methods of Vehicle work, then you are following the principle of coding to an interface.
Also, the single level of inheritance used here is fine for me. But if you start thinking of adding another level, then you should seriously rethink your design and see if inheritance is still the proper tool to use.