I have tought Object Oriented Programming for a number of years and in my time completed more than a thousand oral exams at bachelor level computer science and software engineering students. Overall, it seems object oriented programming can be difficult to comprehend, but clearly the concept of interfaces is one of the harder topics.
Interfaces is a threshold concept
Threshold concepts are often very difficult to understand, but are just as enlightening as they are hard. Or in other words, understanding of a threshold concept will significantly transform your perception of a given subject. Take the subject of object oriented programming. Once you understand the concept of interfaces, you will have transformed your overall perception of object oriented programming. Note, since interfaces are not orthogonal to, say abstract classes, interfaces are not the only way to reach this level of insight. The main point here is, the concept that interfaces cover is a language agnostic difficult concept.
Interfaces as contracts
In order to understand difficult concepts analogies are often useful: relating something new to something you already know and understand well, will help you understand this new thing. Interfaces as contracts is a popular analogy when teaching Object Oriented Programming, and almost all students use this analogy when explaining what interfaces are. The problem is that very often, half of the story is missing and the link between the analogy and the related concept disappears. In this post, I will present the complete story along with examples.
What is a contract
A contract, as far as the relation to interfaces go, is a signed agreement between two parties which states shared agreement. As an example, a musician can sign a contract with a record label. The contract could state that the musician must produce 10 songs for the record label. It might also state that the musician will get paid, however, for the analogy this is less important. Conceptually, the same contract could be used by many record companies and musicians; any musician can sign a contract with any record label, although the outcome might be questionable. Software companies need software developers, and they too use contracts, however software developers develop software.
Object oriented programs
Object oriented programs consist of objects that communicate through messages. In class-based languages like C# every object is an instance of a specific class. Calling a method on an object conceptually sends a message to the object. The object may change state and respond through the return value. Classes contain implementations of the methods and defines the state of objects created from that class.
Software company model
We could model a fantasy software company and fantasy software developers using classes:
class SoftwareCompany
{
public void Hire(SoftwareEngineer se){...}
}
class SoftwareEngineer
{
public University GratuatedFrom { get; }
public void DevelopSoftware()
{
Work();
}
}
In this fantasy model, the company only hires software engineers and all software engineers have university degrees. It would be hard although not impossible for the company to hire computer scientists; not impossible, because a CompSci-class could inherit from the SoftwareEngineer-class. Hard because the computer scientist must work exactly like a software engineer. And it would be impossible for the company to hire self-taught software developers without university degrees! The problem here is, the requirement is concrete: we hire only software engineers, or imitations of. There are various ways of mitigating the problem, but no matter what, the above allow only hiring of something that strictly is-a software engineer, possibly through inheritance. The is-a relation is key here.
Contracts
To solve the above problem, we could let the software company hire everyone who can-do software development. We could (conceptually) form a contract: if you can-do software development, you are potentially hired. As will be clear, a suitable probation period is probably in order; bear with me, if I took this analogy too far.
As you guessed, interfaces can be used to form a contract for such a can-do relationship; such an interface could look like this:
interface IDeveloper
{
void DevelopSoftware();
}
Now, we can let the software refer to the interface or contract instead of its concrete implementation. All we need to know, is that we hire developers:
class SoftwareCompany
{
public void Hire(IDeveloper se){}
}
Implementations
We can now hire anyone if they claim to be developers, that is, if they signed the contract – even if they are really bad at software development, like the phony developer below – he just throws exceptions instead of doing any actual work. Interfaces only states capability, not how it is done.
class SoftwareEngineer : IDeveloper
{
public void DevelopSoftware()
{
...
}
}
class SelftaughtDev : IDeveloper
{
public void DevelopSoftware()
{
...
}
}
class ComputerScientist : IDeveloper
{
public void DevelopSoftware()
{
...
}
}
class PhonyDeveloper : IDeveloper
{
public void DevelopSoftware()
{
throw new Exception();
}
}
We now depend on interfaces instead of concrete classes, and we can hire any type of developer, even those who are not software engineers!
Abstraction is key
Object oriented is all about abstractions. The abstractions above let us focus on just the level of detail needed to solve the problem at hand. We can depend on abstractions instead of concrete implementations and as a result, we have inverted our dependencies. Even more important, we have future proofed our application – we can hire anyone that can-do software development.
What makes interfaces special
But aren’t interfaces not just abstract classes with all abstract members? this is of course true, but with languages without support for multiple inheritance, interfaces redeem this by allowing multiple interface-implementations. That is, you can implement more interfaces but still only inherit from a single class.
True to the analogy, this would allow an employee to be both IFrontEndDeveloper and IBackEndDeveloper at the same time.
Practical use of interfaces
Another common issue in understanding interfaces is, when would I use interfaces? There are basically two situations: when you introduce a dependency or when you are providing a dependency.
Introducing abstract dependency
Consider an application you just started developing, you know you want logging functionality because just writing to the console is a thing of the past. Then again, you cannot decide how to log messages from within the application. It could be file logging, network logging, plain console logging or a mix of those and more. Moreover, you know from experience that your choice is going to change, and even that you might change decision during runtime. This sounds difficult? Perhaps, but correct use of abstractions, and specifically in this example, interfaces will ease this task. Consider the following CoolApp-class
class CoolApp
{
public ILogger Logger { get; set; }
public App(ILogger logger)
{
Logger = logger;
}
public void SaveWork()
{
try
{
WriteDocumentToFile();
Logger.LogInfo("Work saved");
}
catch(Exception e)
{
Logger.LogCritical($"Save failed: {e.Message}");
}
}
private void WriteDocumentToFile() { ... }
}
Upon creation of a CoolApp-instance, we have to provide an instance of an object that can-do ILogger. This interface looks like this:
interface ILogger
{
void LogInfo(string m);
void LogCritical(string m);
}
The CoolApp class is completely oblivious to any logging implementation, it only knows where to log and what should be logged. Not the nitty gritty details about how to store, print, or send messages. Not to mention how to format, timestamp, prefix, or postfix messages. Someone else handles those details as is best. It is when we create an instance of CoolApp we make the decision; and we might even change this decision at runtime. The initialization could look like this:
CoolApp app = new CoolApp(new ConsoleLogger());
In this example, everything is logged to the console. But how do we create such loggers?
Providing dependencies (through interface implementation)
CoolApp declares a dependency. This dependency is of something that can do logging, notably a class implementing the ILogger interface. So to use CoolApp we must provide an implementation of ILogger; in other words, we must create a class that is compatible with CoolApp’s dependency requirement of ILogger. To put it differently, a class that signs the contract that CoolApp holds and promises to honor.
And this is a simple implementation simply logging to the console while prefixing with importance:
class ConsoleLogger : ILogger
{
public void LogCritical(string m)
{
Console.WriteLine($"[Critical] {m}");
}
public void LogInfo(string m)
{
Console.WriteLine($"[Info] {m}");
}
}
Another implementation could decorate ConsoleLoggers with colors:
class ColorConsoleLogger : ILogger
{
ILogger Logger { get; set; }
public ColorConsoleLogger(ILogger logger){ Logger=logger; }
public void LogCritical(string m)
{
Console.ForegroundColor = ConsoleColor.Red;
Logger.LogCritical(m);
Console.ResetColor();
}
public void LogInfo(string m)
{
Console.ForegroundColor = ConsoleColor.White;
Logger.LogInfo(m);
Console.ResetColor();
}
}
Actually any ILogger, but for it to make sense, it should use the console. Remember, throwing NotImplementedException in the implementing methods, is a valid implementation from the compilers point of view. A FileLogger could look like this
class FileLogger : ILogger
{
string F { get; }
public void LogCritical(string m) { File.AppendAllText(F, $"[Critical] {m}"); }
public void LogInfo(string m) { File.AppendAllText(F, $"[Info] {m}"); }
}
And last but not least a logger that combines any combination of loggers could look like this:
class MultiLogger : ILogger
{
ILogger[] Loggers { get; }
public MultiLogger(params ILogger[] loggers)
{
Loggers = loggers;
}
public void LogCritical(string m)
{
foreach (var item in Loggers)
{
item.LogCritical(m);
}
}
public void LogInfo(string m)
{
foreach (var logger in Loggers)
{
logger.LogInfo(m);
}
}
}
Conclusion
Interfaces are hard to understand for many students and could be considered one of the difficult threshold concepts. In my experience, even the analogies used to ease the learning process by relating to well understood concepts can be hard to understand. I think the difficulty arises from the fact that interfaces by them selves provide nothing. It is only when used in larger complex systems their use become evident. In this post I have tried to clear up the analogy, explain how it fits with interfaces and finally given examples of the use of interfaces in an application. Interfaces provide many advantages, some highlighted here, but the key takeaway here is: interfaces means flexibility.