I am currently in the process of trying to master C#, so I am reading Adaptive Code via C# by Gary McLean Hall.
He writes about patterns and anti-patterns. In the implementations versus interfaces part he writes the following:
Developers who are new to the concept of programming to interfaces often have difficulty letting go of what is behind the interface.
At compile time, any client of an interface should have no idea which implementation of the interface it is using. Such knowledge can lead to incorrect assumptions that couple the client to a specific implementation of the interface.
Imagine the common example in which a class needs to save a record in persistent storage. To do so, it rightly delegates to an interface, which hides the details of the persistent storage mechanism used. However, it would not be right to make any assumptions about which implementation of the interface is being used at run time. For example, casting the interface reference to any implementation is always a bad idea.
It might be the language barrier, or my lack of experience, but I don't quite understand what that means. Here is what I understand:
I have a free time fun project to practice C#. There I have a class:
public class SomeClass...
This class is used in a lot of places. While learning C#, I read that it is better to abstract with an interface, so I made the following
public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.
public class SomeClass : ISomeClass <- Same as before. All implementation here.
So I went into all some class references and replaced them with ISomeClass.
Except in the construction, where I wrote:
ISomeClass myClass = new SomeClass();
Am I understanding correctly that this is wrong? If yes, why so, and what should I do instead?
Best Answer
Abstracting your class into an interface is something you should consider if and only if you intend on writing other implementations of said interface or the strong possibility of doing so in the future exists.
So perhaps
SomeClass
andISomeClass
is a bad example, because it would be like having aOracleObjectSerializer
class and aIOracleObjectSerializer
interface.A more accurate example would be something like
OracleObjectSerializer
and aIObjectSerializer
. The only place in your program where you care what implementation to use is when the instance is created. Sometimes this is further decoupled by using a factory pattern.Everywhere else in your program should use
IObjectSerializer
not caring how it works. Lets suppose for a second now that you also have aSQLServerObjectSerializer
implementation in addition toOracleObjectSerializer
. Now suppose you need to set some special property to set and that method is only present in OracleObjectSerializer and not SQLServerObjectSerializer.There are two ways to go about it: the incorrect way and the Liskov substitution principle approach.
The incorrect way
The incorrect way, and the very instance referred to in your book, would be to take an instance of
IObjectSerializer
and cast it toOracleObjectSerializer
and then call the methodsetProperty
available only onOracleObjectSerializer
. This is bad because even though you may know an instance to be anOracleObjectSerializer
, you're introducing yet another point in your program where you care to know what implementation it is. When that implementation changes, and presumably it will sooner or later if you have multiple implementations, best case scenario, you will need to find all these places and make the correct adjustments. Worst case scenario, you cast aIObjectSerializer
instance to aOracleObjectSerializer
and you receive a runtime failure in production.Liskov Substitution Principle approach
Liskov said that you should never need methods like
setProperty
in the implementation class as in the case of myOracleObjectSerializer
if done properly. If you abstract a classOracleObjectSerializer
toIObjectSerializer
, you should encompass all the methods necessary to use that class, and if you can't, then something is wrong with your abstraction (trying to make aDog
class work as aIPerson
implementation for instance).The correct approach would be to provide a
setProperty
method toIObjectSerializer
. Similar methods inSQLServerObjectSerializer
would ideally work through thissetProperty
method. Better still, you standardize property names through anEnum
where each implementation translates that enum to the equivalent for its own database terminology.Put simply, using an
ISomeClass
is only half of it. You should never need to cast it outside the method that is responsible for its creation. To do so is almost certainly a serious design mistake.