Yes, an interface defines a contract where the implementor of the interface must provide an implementation for all interface members. Usually you would prefer adding dependencies to an interface type over dependencies to concrete types. Which is especially interesting for you is the benefit of enabling polymorphism by introducing interfaces like IUpdatable
.
1) The compiler will report an error when an implementation is missing
It's impossible to forget to provide an implementation of a public member when the defined member is defined in the imlemented interface. When a class must implement a certain API in order to be able to participate in a certain logic, an interface can gurantee this. For example when you need to implement a media player, the interface IMediaPlayer
will ensure that all the required members like Play()
, Pause()
, Stop()
, ElapsedTime
are implemented. The author does no longer need to remember all the required members that make a valid media player.
More important, the compiler will force the author to implement the members or the code won't compile.
This adds welcomed robustness to the application.
2) Get IDE support by using code generators
Declaring a type to implement an interface, allows to use code auto-generation. All method bodies and properties are generated automatically. That adds comfort.
3) Interfaces help to add extensibility to the application
Following the OO principles like the Dependency Inversion principle of SOLID principles, helps to add extensibility to your application. Developing against interfaces (or abstractions of the concrete type) allows the code to be independent of the actual implementations and therefore reusable and easy to extend (adding new types)
Bad:
// Only operates on MainWindow instances.
// This implementation makes extending the application difficult.
// If a new window type is added to the application or the MainWindow type is replaced,
// this method needs to be changed or we have to provide overloads.
public void UpdateWindow(MainWindow window)
{
window.Update(data);
}
Good:
// Operates on every type as long it implements IUpdatable.
// This is a highly reusable version that adds extensibility to the application:
// If new implementations are added to the application,
// this method does not need any changes.
public void UpdateWindow(IUpdatable updatable)
{
updatable.Update(data);
}
4) Interfaces enable polymorphism
At run time, objects of an interface implementation may be treated as objects of the same type in places such as method parameters and collections or arrays.
This means you are able to provide a single implementation that for example iterates over a collection of particular interface typed elements, where each element is treated anonymously as the common base type - the interface type.
Bad:
In order to iterate over all different types of Window
to call the Update()
defined on the derived type, we need to know every possible Window subtype. Such code is difficult to extend as adding new types require to modify the code. Appart from this, polymorphism is not possible.
class MainWindow : Window
{
public void Update(object data)
{ ... }
}
class SettingsWindow : Window
{
public void Update(object data)
{ ... }
}
class DependingType
{
List<Window> Windows { get; }
private void UpdateAllWindows()
{
object newData = new object();
// Needs modification in case a new Window type e.g., UserProfileWindow is added to the application
foreach (Window window in this.Windows)
{
if (window is MainWindow mainWindow)
{
mainWindow.Update(newData);
}
else if (window is SettingsWindow settingsWindow)
{
settingsWindow.Update(newData);
}
}
}
}
Good:
Since the actual Window
types are all unknown (anonymous), this solution does only depend on the implemented interface of those types. Such a solution is highly extensible. Furthermore, it enables polymorphism.
interface IUpdateable
{
void Update(object data);
}
class MainWindow : Window, IUpdatable
{
public void Update(object data)
{ ... }
}
class SettingsWindow : Window, IUpdatable
{
public void Update(object data)
{ ... }
}
class DependingType
{
List<IUpdatable> Updatables { get; }
private void UpdateAllWindows()
{
object newData = new object();
// Use polymorphism to invoke Update() on each type.
// This needs no modification in case we add e.g. a UserProfileWindow
foreach (IUpdatable updatable in this.Windows)
{
updatable.Update(newData);
}
}
}
5) Interfaces help to improve testability
APIs that depend on interfaces, are easy to test. The interface allows to fake behavior of the tested method or to mock the type.
Bad:
The following example depends on the concrete class FileSystemReader
and therfore always executes the reading the complete file system. This takes time, makes our test slow and does not provide anything to the effective unit test.
class FileSystemReader
{
// Enumerate the complete! filesystem
public List<string> GetAllFilePaths()
{
var filesystemPaths = new List<string>();
foreach (DriveInfo drive in DriveInfo.GetDrives())
{
filesystemPaths.AddRange(Directory.GetFileSystemEntries(drive.RootDirectory));
}
return filesystemPaths;
}
}
class MainWindow : Window
{
public AddPrefixToAllFiles(string fileFilter, string prefix, FileSystemReader fileSystemReader)
{
var allFilePaths = FileSystemReader.GetAllFilePaths();
// TODO::filter allFilePaths for the requested files
List<string> filteredFilePaths = FilterFilesystemItems(fileFilter, allFilePaths);
// This is the logic we want really want to test
foreach (string filePath in filteredFilePaths)
{
filePath = prefix + filePath;
}
}
}
class Test
{
public void TestMainWindowAddPrefixToAllFiles()
{
var mainWindow = new MainWindow();
// Since AddPrefixToAllFiles only accepts a concrete implementation, we can't fake it
FileSystemReader fileSystemReader = new FileSystemReader();
// Takes very long to execute as we are also implicitly testing the FileSystemReader class
List<string> prefixedFiles = mainWindow.AddPrefixToAllFiles("*.txt", "log_", fileSystemReader);
// TODO::Assert prefixedFiles elements
}
}
Good:
The following version depends on the IFileSystemReader
interface rather than on the concrete FileSystemReader
class. This allows to provide a FileSystemReaderFake
fake implementation to replace the longrunning FileSystemReader
class.
// The interface definition to enable fakes
interface IFileSystemReader
{
List<string> GetAllFilePaths();
}
// The fake class that provides a performance friendly implementation of IFileSystemReader
class FileSystemReaderFake : IFileSystemReader
{
public List<string> GetAllFilePaths()
{
// Fake the filesystem enumeration
return new List<string> { "first.txt", "second.txt", "third.txt" };
}
}
// Now depends on the IFileSystemReader interface
class MainWindow : Window
{
public AddPrefixToAllFiles(string fileFilter, string prefix, IFileSystemReader fileSystemReader)
{
var allFilePaths = fileSystemReader.GetAllFilePaths();
// TODO::filter allFilePaths for the requested files
List<string> filteredFilePaths = FilterFilesystemItems(fileFilter, allFilePaths);
// This is the logic we really want to test
foreach (string filePath in filteredFilePaths)
{
filePath = prefix + filePath;
}
}
}
class Test
{
public void TestMainWindowAddPrefixToAllFiles()
{
var mainWindow = new MainWindow();
// Since AddPrefixToAllFiles() now accepts an interface, we can provide a fake implementation
IFileSystemReader fileSystemReaderFake = new FileSystemReaderFake();
// Executes fast as we are only testing the AddPrefixToAllFiles logic
List<string> prefixedFiles = mainWindow.AddPrefixToAllFiles("*.txt", "log_", fileSystemReaderFake);
// TODO::Assert prefixedFiles elements
}
}
.Update()
in each class either way, so that's not really part of the question. Instead, seems like you're basically just asking if you should define aninterface
that connects the methods.