Inheritance is a fundamental concept in virtually any language which supports object-oriented programming. How inheritance works varies between languages, but virtually all languages with an object-oriented class system support it in some form. While most programming materials will teach you how to use inheritance, they don’t really touch on when it ends up detrimental to coding.
Inheritance is a tool in a programmer’s toolbox just like any other feature of a language. Tools are (typically) neither good nor bad overall, but good or bad at a specific task. Inheritance is one of the first powerful tools given in most OOP languages for adding complexity which is why it is so dangerous. There are exceptions to this if a tool is poorly designed, but for the sake of this argument, we’re going to ignore those scenarios. A hammer is the wrong tool when working with screws, and inheritance is the wrong tool when working with certain types of problems.
When all you have is a hammer, all you see are nails. Inheritance is the gateway drug for Java and C# bad practices. It’s one of the earliest OOP language features which teaches how to abstract and most coding books treat it with reverence to the point of absurdity. This pattern of abstraction leads to abstract interfaces in a hello world equivalent and other development sins.
Inheritance offers a way to avoid reinventing the wheel, but many amateur or self-taught (and even some professional) programmers end up abusing it. Like any language feature, it should be used to make your program and your development more efficient. Inheritance exists to reduce complexity, to reuse components, and to recycle code. If your usage isn’t helping you do this, then you’re using the tool in the wrong way.
The Problem With Inheritance As Commonly Used
Inheritance is commonly taught along the lines of you build an animal class, then you need to extend that out for a mammal class, which you finally extend into a dog class. If you plan to make a game involving a zoo or similar, this approach can make a lot of sense. The problem comes when you make and extend your animal class but don’t have any other types of animals. While it might make sense for future-proofing the code, it does add a level of complexity without a real gain if there are no concrete plans to actually make use of it.
The other common issue with inheritance is an over-abstraction and multiple levels of inheritance (especially in team projects). I’ve seen and inherited projects which had far more levels of inheritance than make sense with every minor change to a class resulting in its own inherited class. The more levels of inheritance and overloading or overriding and the more convoluted the whole thing gets. A single change to a base class may break a workflow far down the chain in an unpredictable way because the “primitive” classes are still used on their own. These problems just get worse when mixed in with other forms of abstraction such as templates, generics, and interfaces present in some languages.
Any feature in a language will cause more issues than its worth if you don’t understand the reasoning behind it or use it incorrectly. I will include examples which may seem a bit extreme, but the goal is to provide something I have actually seen in an extreme view to show exactly where the breakdown between idea and implementation occurs. Let’s see how we can approach inheritance in a broader way to make it work with your code rather than against it.
Reduce Complexity
Inheritance should make your code cleaner, easier to maintain, and more efficient, not add complexity for the sake of complexity. We talked about an animal class extending into a mammal class and so on earlier. But, does this make any sense if all you use are mammals or dogs or similar?
What exactly are you getting by adding in inheritance? Sometimes, just splitting the objects up so that you can conceptually approach them by their individual units makes sense, but too many levels and it can become unwieldy. If you have an animal and override more than not, is there even a point having an animal class in the first place? The answer will depend on your goals and purposes for your project.
The very general rule of thumb for most programming is that the more abstract you get, the more intuitive the process becomes for the programmer at the expense of performance for the system. If you’re adding complexity to the process, are you either getting better performance that outpaces the cost of maintaining said complexity, or are you making the general process more efficient? This rule of thumb applies to virtually all coding practices and tools, but inheritance (and where it leads) is most often abused.
Example
For instance, does the following make sense?
public class QBOItemRef : QBORef
{
//there might be stuff we add from the rest of this object down the line
}
This snippet adds seemingly pointless complexity, but it is done for a specific purpose. Just because the goal is to remove complexity with inheritance doesn’t always mean we should refactor to avoid it either. The data reflects an external API’s organization, and though we do not add to it or actually use anything unique from the given class, knowing that it is separate and derived from another equivalent data structure makes it easier to maintain for the specific project. Obviously, this specific piece is a bit absurd since our inheritance in the example literally does nothing code-wise, but the purpose of said inheritance is to reduce complexity in working with different object types in an API which performs inheritance in a similar way.
While I could simplify this type of inheritance, it would make our usage of said class less efficient. We could either straight reuse the code from the super class (con: means there are two places to maintain the same code) or remember that this specific class is equivalent (con: easy to lose track of as the project grows in scope). As I mentioned earlier: If you’re adding complexity to the process, are you either getting better performance that outpaces the cost of maintaining said complexity, or are you making the general process more efficient? For this specific process, my addition of complexity with inheritance actually added to efficiency in maintaining our code base by making working with an external API much more intuitive.
You may disagree with the reasoning for an empty inheritance, but the same concept will apply to classes which actually have use. Is it worth deriving a class to add a single data point or should you reconsider your approach? Where do you cross the threshold where it makes sense to use inheritance? There isn’t a right answer, so long as the specific application improves your code or your process.
Reuse Components
Inheritance allows you to reuse components wholesale without having to reinvent the wheel or embed an object in another object. You extend a component which reuses the data and function within it in order to do something new with an object which is conceptually built from the same base. We reused the entirety of the object in my (arguably absurd) example before.
The next thing to consider with inheritance is are you reusing the entirety of the object or just parts? If you’re only inheriting from a class to use a portion, is there a better way to approach the problem? I have personally dealt with code which has multiple levels of inheritance founded on the mistaken principle that inheritance is about reusing any piece rather than the whole.
That isn’t to say that a class needs to make 100% use of its super class, but there should be a conceptual leap that makes sense. How much of the class are you overriding or extending and is there a better way to do it? It can make more sense to either use an interface, generic, or similar rather than just inheriting and overriding portions of a class.
Example
For instance, let’s say we have a bank account object such as the following:
public class BaseAccount
{
protected decimal balance { get; set; }
public BaseAccount()
{
balance = 0;
}
public decimal getBalance()
{
return balance;
}
public virtual string withdrawAmount(decimal amount)
{
if (balance - amount < 0)
return "Unable to process transaction!";
balance -= amount;
return "Withdrew $" + amount.ToString() + " from account, remaining balance of $" + balance.ToString();
}
public virtual string depositAmount(decimal amount)
{
balance += amount;
return "Deposited $" + amount.ToString() + " to account, new balance of $" + balance.ToString();
}
}
Let’s further extend this account system out adding a special overdraft account derived from our base account class:
public class OverdraftAccount : BaseAccount
{
protected decimal minimum { get; set; }
public OverdraftAccount(decimal min)
{
minimum = min;
}
public override string withdrawAmount(decimal amount)
{
if (balance - amount < minimum)
return "Unable to process transaction!";
balance -= amount;
if(balance < 0)
return "Withdrew $" + amount.ToString() + " from account, account is overdrafted, remaining balance of $" + balance.ToString();
return "Withdrew $" + amount.ToString() + " from account, remaining balance of $" + balance.ToString();
}
}
While this code itself makes sense, there are plenty of things we could do to make it more efficient and reuse more components. Arguably, this is a bit of a strawman level of coding for a professional, but I figured no one wanted to read a 750 line example for the sake of proving a point (I left extra virtuals in as well since let’s pretend that there are plans to further extend this even more). We won’t touch on making this more efficient, adding error handling instead of returning just strings, etc.
Hypothetically, we could keep our inheritance, but make the entire process easier by doing the following instead:
public class BaseAccount
{
protected decimal balance { get; set; }
protected decimal minimum { get; set; }
public BaseAccount()
{
balance = 0;
minimum = 0;
}
public decimal getBalance()
{
return balance;
}
public virtual string withdrawAmount(decimal amount)
{
if (balance - amount < minimum)
return "Unable to process transaction!";
balance -= amount;
if (balance < 0)
return "Withdrew $" + amount.ToString() + " from account, account is overdrafted, remaining balance of $" + balance.ToString();
return "Withdrew $" + amount.ToString() + " from account, remaining balance of $" + balance.ToString();
}
public virtual string depositAmount(decimal amount)
{
balance += amount;
return "Deposited $" + amount.ToString() + " to account, new balance of $" + balance.ToString();
}
}
public class OverdraftAccount : BaseAccount
{
public OverdraftAccount(decimal min)
{
minimum = min;
}
}
There may be a good reason to begin with the first and go from there, but you may also prefer to do this instead depending on what else is happening. Reuse existing components in a method or place them where it makes sense. The second makes much more sense if you plan to add an account type with fees for overdraft or similar which further extends out the concept of an overdraft account. Ultimately, neither example is better than the other (assuming we spent the time to shore up the code in both) depending on what the next steps are, but if I were working with the two classes, I would prefer something more akin to the second to reuse as many components in the process as possible without wasting time and effort overriding methods.
Recycle Code
My last example flowed into the process of recycling code. Reusing components is efficient, but recycling code can help too. The difference between reusing and recycling is that one keeps the item in roughly the same state, while the other involves adjustments. Our first set of classes in the previous example recycled the withdrawal method to add a new functionality to it.
Recycling code is like recycling things from the garbage; it can be messy and it can be impractical. That being said, sometimes it can make sense to wholesale copy code out of a class or place it in a separate class (or even super class) to make things more efficient. Inheritance might make it easier in principle, but substantially harder in practice.
I tend to offload non-transformational or abstract process code into its own class or component rather than trying to make things inherit off of a base that doesn’t make sense. If the classes deriving from an object don’t have a common purpose or a common underlying principle, why would you use inheritance? Recycle the code out of these classes and place them in a more efficient structure which is more suited to what you’re doing.
Differentiating the component from the code itself is an exercise in semantics, but sometimes you want to recycle generic pieces of code which form the basis of a component. It’s rare to see someone build a helper method and inherit from it for multiple objects which do something based on that principle, but that being said, it is something I have seen. Recycle the code if it doesn’t make conceptual sense to reuse the components as they are.
Example
I have been recycling the same library for years, albeit with minor changes to it. Originally, the library was a super class, but as it got less and less sane to use an object-oriented approach, I recycled the code into its own static class. The project-specific pieces are done via a data class / interface (not necessarily in the standard sense either) for the given project.
public static class SQLHelper
{
private static string _connection = ConfigurationData._connection;
public static string truncateString(string s, int n) ...
#region SQLBoilerPlate
public static returnStatus _executeStoredProcedure(sqlQueryBlock sql) ...
public static returnStatus _executeSqlQueryBlock(sqlQueryBlock sql) ...
...
}
While I could change this from a static class to a non-static class and use inheritance, there isn’t a compelling reason to do so from the considerations of complexity or maintainability. Aside from the connection string, what else is actually class specific versus remaining the same each time? While I have only placed a portion of the code, the rest of it is just boiler plate code.
Inheritance was the wrong tool for this job so I recycled the pieces which were universal into their own static class. Since nothing will ever change with this in a given project (barring a major change to the infrastructure), there is absolutely no reason to try to use inheritance. This specific version even bears the marks of when it was a super class for other objects with the _ in front of the last two included methods (which were originally protected).
When considering inheritance, what are you actually gaining from it? While I can change over my classes which use this library and inherit from it, what benefit would it provide? If I had more class-specific data, it might pay off, but for my specific usage, there was no compelling reason to rework what I did. By recycling this code from a super class into a static library, I reduced the complexity of the overall system and made something far more scalable for my specific needs.
Conclusion
Inheritance is a powerful tool in object-oriented programming for reducing complexity when used right. The problem comes from adding abstraction and complexity for the sake of complexity. Some developers will feel that a thousand levels of inheritance make sense, but what benefit is it providing and what happens when you’ve been away from the code for a while?
I virtually never use multiple inheritance unless there really isn’t a clean way otherwise. The goal of any language tool is to make programming easier, more maintainable, or more efficient. If you aren’t doing one of those three, you’re almost certainly misusing the feature or tool.
When I first got started learning to program over a decade ago, I had this drilled into me early and often thanks to a good mentor. Inheritance was the first real feature I had that was so wholesale abusable for adding complexity without adding purpose. By tempering my reasoning to try and take an easy way out on some problems, I was forced to approach the problem in a way that made sense.
Inheritance is a powerful tool when used as intended, but it can very easily lead to a runaway effect of adding complexity where it’s not needed. If you feel your code is complex, why is it complex and how does that complexity add to the process? The tool should make your job easier not harder.
Image by jacqueline macou from Pixabay