This guide will cover the idea of interfaces, how to use them correctly and how to get better starting to think in abstractions rather than implementations.
The main language I’m using is C# - Which has good functionality of interfaces.
Other languages support it in various ways but the “Concept” of abstraction still stands.
An interface is an abstract unit which acts as a contract.
An interface hides the implementation and forces it to implement a certain contract.
Thus creating the ability to remain abstract with precise API.
An example:
1 | public interface IHeatable |
The interface doesn’t give us any specific details, what are we heating? A plate? A room?
What kind of heat are we producing?
The implementation answers:
1 | public class AirConditioner :IHeatable |
The idea of “Heat” is true for many electrical devices and non electrical.
For example I could implement a bonefire:
1 | public class BoneFire : IHeatable |
A good interface is behavior driven with explicit, readable and understandable API.
It cannot have:
It usualy has:
What a good interface looks like:
1 | public interface ISensor<TData> |
What a bad interface looks like:
1 | public interface ILoadingManager |
Avoid Managers.
There is a strong tendency for software developers to “Manage” things.
This component doesn’t “manages the loading of lines and unlaoding of lines”.
It should perform “A synchronous load of lines from a source”.
Split behavior across different abstract units because they contain different behavior.
How to fix this interface?
Step 1:
Split into asbtractions.
“ILinesLoader”, “IConverter”, “IPublisher”.
If you need to load from a flie and from web you can introduce new components:
“WebLoader” and “FileLoader”.
Step 2:
Don’t manage data in interfaces.
Let the class decide on caching or not caching and how to unload it.
Step 3:
Move initialization of information to the Constructor or other DI methods.
Any information needed in initialization should be considered through Dependency Injection.
Any “out source” informatino that is reqruied - such as remote configuration, configuration from file , should be kept out of the constructor and dealt seperatly.
(A simlpe example is to read the confiugration before you create the instance, and provide the ctor with the data from the configuration).
The advantages to this approach are great:
We can take this seperation of concerns and split ideas into tiny pieces of code.
This is not desirable because it creates complexity - not reduces it!
Personally I may be a perfectionist and caring for my craft - I want all the code to look tidy and seperated.
However I learned long ago a set of 2 words which answers my conflits: “Good enough”.
A design should not be perfect - it should be “good enough”.
However, Good enough for what?
Clear goals will make the design more understandable because you know what it should accomplish.
With additional guidelines it can create a more sustainable and maintanble code which will raise your code quality.
Avoid tiny pieces of code -
These tend to be forgotten or just put inside another class.
Utilize helper classes for these pieces of code.
When you have many interfaces, group them with a single interface.
1 | public interface IODevice : IReadable, IWriteable, IDisposable, IAsyncReadable, IASyncWriteable |
A Clear idea of IO device is met - a sync and async read and write device.
Yet it still holds the “Idea” rather than implementation, an IODevice may be a DISK, over net, over a pigeon, etc…
Utilize existing interfaces in the framework.
You’ll be surprised how the framework handeled interfaces - most of them are good.
Especially IEnumerable and IDisposable.
Avoid reduplicating List and Dictionary functions.
Don’t expose internal lists, but you can use them.
For example, if a “Context” has IODevices I can set an interface accordingly:
1 | public interface IIOContext : IList<IODevice> |
To create a good interfaces for a set of classes think of “What they all share together”.
Avoid adding things to the interfaces which some of the implementors don’t need.
1 | public interface IReadable |
The readline is fine - but not all implementors of IReadable have a file underneath.
Interfaces with data-only or only get/set methods should be classes.
It’s ok to create an abstract class instead of an interface.
To get better you need to gather experience.
Complete these tasks if you want to examine your knowledge of interfaces:
1 | public interface IHouseAlarmHandler |
public interface IAlarmController
{
bool PerformCheck();
void PauseAlarm();
void ResumeActivity();
event Action<Sesnro> OnAlarm;
}
public class HouseAlarmController : IAlarmController
{
private IList<Sensor> mSensors = new List<Sensor>();
public HouseAlarmController(HouseConfiguration config){}
// ... All other implementation
}
I’ve decided to contain the list of sensors in the implementation.
If the needs arises for adding or removing sensors through a different component,
we can use a factory or pass an IList
I followed the term “Favor composition over inheritance”.
HouseConfiguration is a specific data of a house alarm controller.
Now we can implement different alarm systems!
I’ve renamed the method to “PerformCheck” to be more indicative.
Thank you for reading!