In this post, we will look at the interface segregation principle within the SOLID principles.
What is the interface segregation principle?
If you have checked out the previous post where I talk about the Liskov substitution principle, you might have noticed that at some point we mentioned interfaces.
And that is what we are going to focus on in this post, because interfaces help us fix problems that can arise from a poor implementation of the Liskov substitution principle.
As I mentioned before, SOLID principles interact with each other and that is why it is hard to see them clearly separated.
The interface segregation principle states that "Clients should not be forced to depend on methods they do not use"
A really easy way to understand this principle is to think about who owns the interface. For me, the easiest way is to see the interface as a dependency.
For example, our client class uses an interface, and this interface is a dependency. What this means is that if our client class does not need a particular method, the interface should not include it.
An interface should be defined by the client that consumes the interface, and not by the class that implements it. Because of this, we can have smaller interfaces, which are much easier to handle.
It's very common to create interfaces that have a single member. So if you are new at a job and see many interfaces with a single member, don't worry, it's quite common.
Example of interface segregation
For this example, we will use the same code we've been using throughout this section or course.
As you might have noticed, the last part of the previous post is the goal we want to achieve here, which is to eliminate the InformacionFichero
method. We want to get rid of it because we don't use it in our AlmacenamientoSQL
class. Here we have the IAlmacenamiento
interface.
public interface IAlmacenamiento
{
void Guardar(string titulo, string contenido);
string Leer(string titulo);
FileInfo InformacionFichero(string titulo);
}
But this time, we will do it differently. As I said, we should let our client, in this case the ArticulosServicio
class, define the interface.
The first thing we are going to do is move the responsibility of reading the file information we're checking into a method inside our ArticuloServicio
class.
public FileInfo GetFicheroInformation(string titulo)
{
return _almacenamiento.InformacionFichero(titulo);
}
We also need to change the if
statement so it reads from our new method.
if (!GetFicheroInformation(titulo).Exists)
return null;
This action shows how we can further break down our IAlmacenamiento
interface, since now we have a method that isn't directly related. To use this as an example, let's extract this functionality into a new Interface, which will have just one method, to get the file information.
public interface IFicheroInformacion
{
FileInfo GetInformacion(string titulo);
}
This way, we can make a change which will add this new interface to our ArticuloServicio
class, and from the method we just created, we call our new IFicheroInformacion
interface and not IAlmacenamiento
.
public class ArticulosServicio
{
private readonly Cache _cache;
private readonly ILogging _logging;
private readonly IAlmacenamiento _almacenamiento;
private readonly IFicheroInformacion _ficheroInformacion;
public ArticulosServicio(IFicheroInformacion ficheroInformacion)
{
_logging = new DatabaseLog();
_almacenamiento = new AlmacenamientoSQL();
_cache = new Cache();
_ficheroInformacion = ficheroInformacion;
}
public void GuardarArticulo(string contenido, string titulo)
{
_logging.Info($"vamos a insertar el articulo {titulo}");
_almacenamiento.Guardar(titulo, contenido);
_logging.Info($"articulo {titulo} insertado");
_cache.Add(titulo, contenido);
}
public string ConsultarArticulo(string titulo)
{
_logging.Info($"Consultar artículo {titulo}");
string contenido = _cache.Get(titulo);
if (!string.IsNullOrWhiteSpace(contenido))
{
_logging.Info($"Artículo encontrado en la cache {titulo}");
return contenido;
}
_logging.Info($"buscar articulo en el sistema de archivos {titulo}");
if (!GetFicheroInformation(titulo).Exists)
return null;
return _almacenamiento.Leer(titulo);
}
public FileInfo GetFicheroInformation(string titulo)
{
return _ficheroInformacion.GetInformacion(titulo);
}
}
Based on the use case from the previous post, we are going to do the same thing. We are going to use our class AlmacenamientoSQL
instead of AlmacenamientoFichero
, so getting the file information doesn't make sense.
For this, what we're going to do is refactor the IAlmacenamiento
interface and remove InformacionFichero
since it's not needed for our use case.
public interface IAlmacenamiento
{
void Guardar(string titulo, string contenido);
string Leer(string titulo);
}
In SQL, we also don't need the method we just created. So we should remove it and also remove the reference to the IFicheroInformacion
interface.
But now, it might fail if we use the file system storage class (AlmacenamientoFichero
). What we have to do is move that interface as a parameter in the constructor of our AlmacenamientoFichero
class, since it is the client class of IFicheroInformacion
.
public class AlmacenamientoFichero : IAlmacenamiento
{
readonly string path = "C:/temp";
private readonly IFicheroInformacion _ficheroInformacion;
public AlmacenamientoFichero(IFicheroInformacion ficheroInformacion)
{
_ficheroInformacion = ficheroInformacion;
}
public void Guardar(string titulo, string contenido)
{
File.WriteAllText($"{path}/{titulo}.txt", contenido);
}
public string Leer(string titulo)
{
return File.ReadAllText($"{path}/{titulo}.txt");
}
public FileInfo InformacionFichero(string titulo)
{
if (!GetFicheroInformation(titulo).Exists)
return null;
return new FileInfo($"{path}/{titulo}.txt");
}
private FileInfo GetFicheroInformation(string titulo)
{
return _ficheroInformacion.GetInformacion(titulo);
}
}
We should do the same for all the examples.
CONCLUSION
A very common practice is to have interfaces with only one member. Honestly, to me this seems a bit too much, because depending on how the system is, it's not necessary. For example, in the case of logs, you would have one interface to save error logs, another one to save fatal logs, and so on.
This approach has the advantage of being very easy to extend, but you need to use common sense. If you apply this to a project where you've been working for a few years, and you make one interface per method, you could end up with millions of interfaces, so the best option in my opinion is to group them by functionality, as long as they don't become mega-interfaces.
If there is any problem you can add a comment bellow or contact me in the website's contact form