How NML implements inheritance in C#

How NML implements inheritance in C#

Inheritance is not the most difficult concept to grasp...

...and it is a skill all developers in modern OOP languages should have well under control. However, as with most things in development, it seems a lot simpler than it turns out to be. One complexity around class inheritance in C# is the effective implementation and usage of abstract classes. There are various kinds of abstract class implementations that I have come across, but there is only one effective version in my experience, and it is the style I encourage NML developers to follow.

Functionality vs Behavior

Abstract classes, and I blame C# developer interviews for this, have become known as:

A way to provide common?functionality?to inheritors

This is indeed something abstract classes do, but it paints a distorted picture of what is expected when it comes to implementation.

A more accurate description is:

A way to implement common?behavior?for inheritors

On the surface of it, they sound basically like the same thing, but they are vastly different in consequence.

Common functionality is about implementing methods and properties that can be called by inheritors, whereas common behaviour is about cementing the rules and expectations of a suite of closely related inheritors.

If this still sounds the same to you, keep reading!

Common functions

What invariably happens when developers look at abstract classes as common functionality, is that they abstract away methods and properties that inheritors call while performing their operations. Developers fall into the trap of thinking whoever else next adds to the suite of classes that inherit from their abstract class, will have the same insights and knowledge that they originally had.

All the abstracting away of methods and properties results in nothing more than just a contained helper class. And helper classes are the number one suspect in bad OOP implementations. More on that another time.

Common Behavior

The whole purpose of related classes is that they promise to behave almost the same. They serve similar purposes in different contexts. They should provide the same function or service to using code, but vary in exactly what output or side-effects they have. What they should not do is implement the same interfaces and abstract classes, but have wildly different output or side effects.

Encapsulating behaviour in abstract classes must entail precisely describing the steps of what the function or service does, in the abstract class, not in the inheritors. An inheriting class’ only job in the relationship is to provide specificity to the behaviour when the abstract class requires it.

What does that all mean?

Here are some rules and guidelines I attempt to encourage at NML when it comes to effective inheritance hierarchies:

Public entry points

Do your concretes have public entry points? If so you are implementing common functionality and not common behaviour. Always keep the public entry point on the most abstract level. Generally, that will be the abstract class.

What that does is force you to encapsulate the shared behaviour that all inheritors must necessarily have as a consequence. If you think: “I need only my version to do X too, but for that, I need to modify the base”, then you need to stop and take a long look at whether you are still implementing the function/service that the inheritance is encapsulating.

Repeated patterns

If you find yourself seeing the same code structures and flow in your concretes, you are probably looking at behaviour that should be moved to a more abstract level. Seeing a try-catch for the same exception around similar-looking code, or sequences of method calls or property assignments, are signs that your concretes have taken hold of some behaviour, and you need to investigate whether you are breaking the DRY principle and need to move code to the base.

Protected methods

When your abstract class has a set of protected methods that are not called from within the abstract class itself, you are almost certainly implementing shared functionality, and not shared behaviour.

There are only 2 reasons for declaring a method or property as protected in an abstract class (OK, there might be more, but very infrequently):

  1. It is declared “protected abstract”, which indicates that there is some action or knowledge that can only be provided by the concrete, as a consequence of the specific variability the concrete is providing.
  2. It is declared “protected virtual” indicating some behaviour or knowledge that a concrete class can use to specify something different when the default is not adequate.

In both the above cases, however, there should always be a call from the class that originally defined the method or property as “protected abstract/virtual”.

Liskov should apply

If your concretes do not adhere to the Liskov substitution principle, you are implementing shared functionality and not common behaviour. Liskov is part of the SOLID principles for a reason, and this is definitely one place where this principle is important to ensure your implementation is robust.

Public entry points (again)

Do you use the “virtual” or “abstract” keywords on your public entry points? If so, you are inviting other developers to break the spirit of the behaviour you are trying to encapsulate. Without the base implementation controlling the core behaviour, nothing really stops an inheritor from adding just that one deviation that steps over the line of the function or service that the suite is supposed to provide.

Conclusion

It is important to think of inheritance hierarchies in terms of behaviour, and not just in terms of variation. The above strategy has served NML well, and it allows us to not only build robust implementations for behaviour that can support variation, but also helps us identify when we are overreaching on an inheritance implementation, and perhaps should rather introduce a different function or service for the requirement.

So if I follow the argument, the only effective use of abstract classes is the Template Pattern? I agree with the majority of what is being said but saying that this is the only effective use is potentially taking it a bit far. The reason that multitudes of patterns exist is because things are not so black and white and different scenarios can lend themselves to other implementations. It’s good to enforce some standards but not at the cost of overlooking another solution that is potentially better suited to the problem space.

Liam Beeton

Software Designer

2 年

I tend to favour composition over inheritance in most of my designs. Low coupling and high cohesion leads to a cheaper cost of feature when responding to change.

Bruce Carlstein

Co-Founder & MD GenSix Digital PTY Ltd | Co-Founder & CMO ScrubBill PTY Ltd

2 年

Awesome Work NML!

Now THAT's an article. Way to go guys!

要查看或添加评论,请登录

NML的更多文章

社区洞察

其他会员也浏览了