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.

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.

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.

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.

This entry was posted in How-To's and tagged , , , , . Bookmark the permalink.

7 Responses to Super SuperClasses – Code smells series

  1. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #2638

  2. Pingback: Dew Drop - August 7, 2018 (#2782) - Morning Dew

  3. Bradley Uffner says:

    “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!”

    Umm, I’m sorry, but that isn’t a bug, that is EXACTLY what is supposed to happen. If it’s unexpected, the developer should go back to school.

    • The Liskov Substitution Principle is that a derived class should be interchangeable with its base class. A square class is not the same as the rectangle class in its use of height and width, therefore the 2 classes cannot be used interchangeably. If you did not know the underlying logic of Square and Rectangle (and these are examples of more complex forms) then you may try to pass a width and height that differ into a square constructor, in the same you would with a rectangle. To defend against this the square class would have to either ignore one of the properties or have a check to make sure the 2 properties are the same. Therefore writing unnecessary code (and more code means more scope for bugs), to try and shoe horn square into the rectangle shape.

      • Andrew Whitworth says:

        The Square/Rectangle example only fails if your classes are mutable. Considering that they are just value types, they should be immutable and then Liskov will hold for these classes as expected. Don’t be lazy, use immutable value types and you kill two birds with one stone.

    • G says:

      Some of these JetBrains blogs display some rather asinine ignorance that’s surprising and disappointing from a company that makes an IDE. They seem to have read about *reductio ad absurdum*, yet missed the part where it is used to point out fallacies, rather than to support ridiculous over-extension of the favored “code smell”-of-the-day-that-is-actually-good-practice.

    • Tobias Lingemann says:

      The problem is that you can cast the square to a rectangle and use it as such. The user might even be aware that the object is indeed a square and try to set different values for width and height.
      A square doesn’t extend the behavior of a rectangle, it reduces it.

Leave a Reply

Your email address will not be published. Required fields are marked *