Get better with interfaces
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.
What is an interface?
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 |
What is a good interface?
A good interface is behavior driven with explicit, readable and understandable API.
It cannot have:
- Functions that are needed to be called in a certain order.
- Functions that require initialization priorhand.
- Functions that are unrelated to one another.
- Only data - this is not an interface.
- Public Data (Like public lists)
It usualy has:
- One concern or concept.
- High cohesion functions.
- Claer API.
- Implements other interfaces.
- Related behavior for implementors.
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:
- Bugs are easier to detect and fix because you are dealing with only 1 behavior at a time.
- Interfaces make the code more composable which helps in upgrading or writing new features.
- Changes are easier to mitigate.
- Managing dependencies is easier with interfaces.
- Provide means to create more reuseable code.
- The code is more testable!
How to avoid interface bloat
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.
What I recommend:
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
2
3public 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
2
3
4public 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
2
3
4public interface IReadable
{
string ReadLine(string filename);
}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.
Exercises
To get better you need to gather experience.
Complete these tasks if you want to examine your knowledge of interfaces:
- Make a list of viable interface in a Coffee Maker
Open to see my answer
- IWaterHeater - To contain heated water for the coffee.
- ICoffeeContainer - Contains Coffee Beans.
- ICoffeGrinder - Something needs to grind the coffee.
- ICoffeeFilter - The filter that makes the coffee after grinding.
- ISteamer - Additional Steam feature for steaming milk or similar products.
- We’ve required to refacotr an alarm system for houses. They have a monolithic interface which needs your refactor please help!
1 | public interface IHouseAlarmHandler |
Open to see my answer
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.
- For your own fun - design a small server which handles requests for picture of kittens.
Think of:
- How to handle caching.
- How to filter images.
- How to handle the IO.
- Add your own features to it like staring a kitty, or adding another picture.
Thank you for reading!