Question:
Colleagues, I do not fully understand one of the recommendations in the .NET design guidelines .
It says:
DO prefer protected accessibility over public accessibility for virtual members. Public members should provide extensibility (if required) by calling into a protected virtual member.
The public members of a class should provide the right set of functionality for direct consumers of that class. Virtual members are designed to be overridden in subclasses, and protected accessibility is a great way to scope all virtual extensibility points to where they can be used.
that is
Prefer to make virtual members (like methods) protected rather than public. Public members of a class must provide extensibility (if needed) by invoking a protected virtual member.
The public members of the class should provide the desired, correct functionality for client code. Virtual members are designed to be overridden in descendant classes, and security access is a good technique for limiting the visibility of places to extend to only those who will use it.
After reading this text, I still do not understand what the problem could be if the family of virtual functions is declared public. For example, in this code:
class Human : IDisposable
{
IDisposable property = new Property();
public virtual Dispose()
{
property.Dispose();
}
}
class Spy : Human
{
IDisposable spyGadgets = new SpyGadgets();
public virtual Dispose()
{
base.Dispose();
spyGadgets.Dispose();
}
}
What could be the problem with such code? What is the documentation trying to warn me about? If everything is fine with this case, then in what case are problems possible?
Please provide a meaningful example with code if possible (not with the Foo
and Bar
classes).
Thanks a lot for the replies! I had a hard time choosing which one to tick, because all the answers are very good, and shed light on the problem from different angles.
Answer:
These are recommendations for framework developers. Obviously, framework developers will release new versions of their frameworks. It is also clear that one of the most important tasks for them is to maintain backward compatibility, to the extent possible. This means that the clients that use their code should be limited as much as possible. That is, it is we (well, or only I am such a hand-ass) who are used to writing classes to be inherited – but if we are writing a framework, then we need a good enough reason to make the class inherited. You also need a good reason to make the method virtual. But now you made the public method virtual, and now clients can inherit, overload the method and run their own code using our framework – and we can no longer control this. I mean, by making a public method virtual, we give the client the right to decide what our API will do, and we can’t change anything without breaking backward compatibility. However, by making a protected method virtual, we don't guarantee the client that the method won't become unusable in the future if our public API changes. Thus, clients that have overloaded a protected method remain backward compatible even if the public method logic has changed.
I guess I need to add an example, although I'm not a master of examples 🙂 Let's say there are the following classes:
public class CsvWriter1<T>
{
protected void WriteHeader(TextWriter stream);
protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
protected virtual void WriteInternal(IEnumerable<T> obj, TextWriter stream)
{
WriteHeader(stream);
WriteBody(obj, stream);
}
public void Write(IEnumerable<T> obj, TextWriter stream)
{
WriteInternal(obj, stream);
}
}
public class CsvWriter2<T>
{
protected void WriteHeader(TextWriter stream);
protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
public virtual void Write(IEnumerable<T> obj, TextWriter stream)
{
WriteHeader(stream);
WriteBody(obj, stream);
}
}
Now let's imagine that in the next version of our wonderful framework, we need to write objects in CSV without a header. That is, the header is no longer needed. And you can't override it anymore. What to do? For the CsvWriter1
class, everything is simple
public class CsvWriter1<T>
{
protected void WriteHeader(TextWriter stream);
protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
protected virtual void WriteInternal(IEnumerable<T> obj, TextWriter stream)
{
WriteHeader(stream);
WriteBody(obj, stream);
}
protected void WriteInternalNew(IEnumerable<T> obj, TextWriter stream)
{
WriteBody(obj, stream);
}
public void Write(IEnumerable<T> obj, TextWriter stream)
{
WriteInternalNew(obj, stream);
}
}
In the case of the CsvWriter2
class, we are in a puddle. Since the specified change for it will be considered incompatible, and of course it will break the logic of client classes.