Virtual by default is better
I’ve taken quite a few paradigm shifting journeys in my life. Some have been quick insights, instant moments of clarity. Others have been slow processes, spanning over several years. One such slow journey for me has been how I look upon virtual methods of object oriented languages.
Java methods are virtual by default. Unless explicitly marked as final, a method may be overridden in a deriving class.
class A {
a() { … }
final b() { … }
}
Class B extends A {
// B::a overrides A::a
a() { … }
// Compile error, A::b is non-virtual
b() { … }
}
C# on the other hand, takes the opposite approach. There you have to explicitly mark a method as virtual, and an overriding method must be marked with the override keyword.
class A {
virtual a() { … }
b() { … }
}
class B {
override a() { … }
// This won’t compile since A::b is non-virtual.
// Without the override keyword though,
// B::b only hides A::b which produces a
// warning instead of an error.
override b() { … }
}
In that sense C# is more expressive, and intention is better communicated with the explicit marks. On the other hand, Java is more flexible and less restrictive.
I used to favor the non-virtual by default approach. Partly because I was kind of born with it – my first OOP language was Object Pascal and Delphi – but also because I appreciate the better control as to how my components are being used. The ability to restrict polymorphism is good because usage in a way that wasn’t intended can create a lot of mess.
To give one non-obvious example, consider a library class A with a method a.
class A {
a { … }
}
A library user decides he needs to add functionality to A, so he creates a descender B with a method b.
class B: A {
b { … }
}
Everything works just fine, but two year later a new version of the library is released. In this new version a new method b was added to class A. Suddenly B::b overrides A::b although this was not the original intention. In a language like C# compiling class B will result in an error (since b is not virtual), but in Java and most other object-oriented languages the upgrade may have introduced a subtle bug.
My standpoint regarding virtual methods used to be that the need for control outweighs the need for flexibility, but some five years ago I started to re-evaluate that opinion. The reason was automated testing. I have slowly come to realize that the C# object model is a pain, at least when it comes to putting a piece of legacy code under a testing harness. While there are plenty of situations where sub classing is a bad idea, unit-testing is not one of them. Sub classing can be a powerful dependency breaking technique, and it allows for testing of code that isn’t otherwise testable without refactoring.
One (contrived) example:
class SpecialConnection {
//…
send_string(String s) {
//…
};
//…
}
class SomeLegacyClass {
SpecialConnection _conn;
SomeLegacyClass(SpecialConnection connection) {
//…
}
//…
create_and_send_string() {
String s;
// some code that builds
// the string to send
_connection.send_string(s);
}
//…
}
How can I test the create_and_send_string method? Well, if SpecialConnection::send_string is virtual the answer is easy, just subclass SpecialConnection and stub out the method.
Class FakeSpecialConnection: SpecialConnection {
Send_string(String s) {
if (s != “expected value”)
throw new Exception(…);
}
}
And the test code could be like this:
SomeLegacyClass c = new SomeLegacyClass(new FakeSpecialConnection());
c.create_and_send_string();
On the other hand, if send_string is not a virtual method we must do some refactoring (like extracting an interface from SpecialConnection) before we can do testing without a real connection. One could argue that the code in the example is poorly designed, and that we should refactor it anyway. That’s true, but a prerequisite for safe refactoring is thorough unit-testing, something we don’t have at this point, which is why we’re trying to get this piece of code into a testing harness in the first place; so that we could make safe refactorings.
When it comes to testing, virtual methods means more options, better options, safer options. That is the reason why I have converted, and now value that flexibility more than I fear possible regression.
Cheers!