Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

How-To's

Super SuperClasses – Code smells series

This post is part of a 10-week series by Dino Esposito (@despos) around a common theme: code smells and code structure.

In our previous post, we looked at refactoring our code in a way that makes it more extensible, using dependency injection (DI) and composition. This week, let’s contrast that with a classical object-oriented approach: inheritance. While composition vs. inheritance could be a code smell, it’s well worth spending some time on the differences between both.

In this series:

The funny thing about object-oriented inheritance is that right after having been celebrated as the new deal of software engineering in the early 1990s, it was fingered as a hurdle on the way to effective software design.

Composition over inheritance is an old dilemma that dates back to 1994 and the Gang of Four’s popular book, Design Patterns: Elements of Reusable Object-oriented Software. More recently, the authors of the Go language cut the story short deciding not to support inheritance at all, in favor of composition.

There are a few agreed facts around the debate between inheritance and composition. Inheritance means that one type derives from another and all of its capabilities are exposed through the derived type. Well, to be precise, the list of visible capabilities is filtered by the syntax rules of the programming language of choice.

The idea behind inheritance is that some commonality exists between parent and derived class. The commonality, though, must be at the business level, not at the design level. And it should not violate the Liskov Substitution Principle – a way of ensuring that inheritance is used correctly.

When a base type is substituted by a subtype, both must have the same behavior. An example could be inheriting a Square from a Rectangle. Mathematically, a Square is indeed a Rectangle, so inheritance is probably ok. However what if a method using a Rectangle is passed a Square? It may have the expectation that setting the Width does not change the Height, but with a Square, it does. This may introduce an unexpected bug! And thus inheriting a Square from a Rectangle may not be the best approach. The same goes for Bird and Penguin, too.

Composition is much more pragmatic as it more closely resembles the real world. The real world is full of composite objects in which distinct parts work together just because they are assembled together, but each remaining a distinct and standalone object.

In short, composition specifies a “has a” relation. Inheritance specifies an “is a” relation. A real-world example would be a car. A Car has an Engine (composition). A Car is an Automobile (inheritance).

Inheritance versus composition is a dilemma to keep constantly in mind. The proven way to deal with that is to arrange the inheritance chain in a bottom-up fashion rather than top-down.

Let’s say you have a class that expresses some business rules for booking an asset in some enterprise asset management scenario.

public class AssetBookingRule
{
    public int SlotLengthInMins { get; set; }
    public int MaxConsecutiveSlots { get; set; }
}

Let’s say that during development you end up with a few similar classes. In particular, one of the newly added rule classes also needs a validity interval.

public class AssetCostRule
{
    public decimal Fee { get; set; }
    public DateTime? From { get; set; }
    public DateTime? To { get; set; }
}

Although you started with two distinct root classes that work well as standalone entities, some business commonality now emerges and it is probably enough to extract a superclass. In ReSharper and Rider, we can do this with the Refactor This action (Ctrl+Shift+R) or from the context menu.

Extract superclass using ReSharper or Rider

With a simple and largely automatic refactoring operation, all of your related classes will now share the same root.

public class AssetBookingRule : AssetRuleBase
{
    public int SlotLengthInMins { get; set; }
    public int MaxConsecutiveSlots { get; set; }
}

public class AssetRuleBase
{
    public DateTime? From { get; set; }
    public DateTime? To { get; set; }
}

public class AssetCostRule : AssetRuleBase
{
    public decimal Fee { get; set; }
}

The interesting thing is that you also end up with an even more flexible design, as now all rule classes have, or may have, a validity interval. Your expressiveness is greatly improved at nearly zero cost.

Now compare this bottom-up approach with the classic top-down. In a bottom-up approach you compose together classes or stack them up in a hierarchy on a per-need basis.

In a top-down approach, instead, you need a careful preliminary analysis of the dependencies and may end up with interconnections you don’t need. Not to mention that subtle interconnections up in the inheritance chain may always occur.

Stay tuned for next week, when we will discuss how null pointers lead us to opportunities to improve our code base.

Download ReSharper 2018.1.4 or Rider 2018.1.4 and give them a try. They can help spot and fix common code smells! Check out our code analysis series for more tips and tricks on automatic code inspection with ReSharper and Rider.

image description