C# Interfaces – Understanding Casting to Any Implementation

cimplementationsinterfaces

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 and ISomeClass is a bad example, because it would be like having a OracleObjectSerializer class and a IOracleObjectSerializer interface.

A more accurate example would be something like OracleObjectSerializer and a IObjectSerializer. 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 a SQLServerObjectSerializer implementation in addition to OracleObjectSerializer. 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 to OracleObjectSerializer and then call the method setProperty available only on OracleObjectSerializer. This is bad because even though you may know an instance to be an OracleObjectSerializer, 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 a IObjectSerializer instance to a OracleObjectSerializer 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 my OracleObjectSerializer if done properly. If you abstract a class OracleObjectSerializer to IObjectSerializer, 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 a Dog class work as a IPerson implementation for instance).

The correct approach would be to provide a setProperty method to IObjectSerializer. Similar methods in SQLServerObjectSerializer would ideally work through this setProperty method. Better still, you standardize property names through an Enum 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.

Related Topic