Search code examples
c#interfacesolid-principlessingle-responsibility-principleinterface-segregation-principle

Interface segregation and single responsibility principle woes


I'm trying to follow the Interface Segregation and Single Responsibility principles however I'm getting confused as to how to bring it all together.

Here I have an example of a few interfaces I have split up into smaller, more directed interfaces:

public interface IDataRead
{
    TModel Get<TModel>(int id);
}

public interface IDataWrite
{
    void Save<TModel>(TModel model);
}

public interface IDataDelete
{        
    void Delete<TModel>(int id);
    void Delete<TModel>(TModel model);
}

I have simplified it slightly (there were some where clauses that hindered readability).

Currently I am using SQLite, however, the beauty of this pattern is that it will hopefully give me the opportunity to be more adaptable to change should I choose a different data storage method, like Azure for example.

Now, I have an implementation for each of these interfaces, here's a simplified example of each one:

public class DataDeleterSQLite : IDataDelete
{
    SQLiteConnection _Connection;

    public DataDeleterSQLite(SQLiteConnection connection) { ... }

    public void Delete<TModel>(TModel model) { ... }
}

... 

public class DataReaderSQLite : IDataRead
{
    SQLiteConnection _Connection;

    public DataReaderSQLite(SQLiteConnection connection) { ... }

    public TModel Get<TModel>(int id) { ... }
}

// You get the idea.

Now, I'm having a problem bringing it all together, I'm certain the general idea is to create a Database class which uses interfaces as opposed to the classes (the real implementation). So, I came up with something like this:

public class Database
{
    IDataDelete _Deleter;
    ...

    //Injecting the interfaces to make use of Dependency Injection.
    public Database(IDataRead reader, IDataWrite writer, IDataDelete deleter) { ... }
}

The question here is how should I expose the IDataRead, IDataWrite, and IDataDelete interfaces to the client? Should I rewrite the methods to redirect to the interface? Like this:

//This feels like I'm just repeating a load of work.
public void Delete<TModel>(TModel model)
{
    _Deleter.Delete<TModel>(model);
}

Highlighting my comment, this looks a bit stupid, I went to a lot of trouble to separate the classes into nice, separated implementations and now I'm bring it all back together in one mega-class.

I could expose the interfaces as properties, like this:

public IDataDelete Deleter { get; private set; }

This feels a little bit better, however, the client shouldn't be expected to have to go through the hassle of deciding which interface they need to use.

Am I completely missing the point here? Help!


Solution

  • When we talk about interface segregation, (and even for single responsibility) we talk about making entities that do a set of operations which are logically related and fit together to form a meaningful complete entity.

    The idea is, a class should be able to read an entity from database, and update it with new values. But, a class should not be able to get weather of Rome and update a stock value in NYSE!

    Making separate interfaces for Read, Write, Delete is bit extreme. ISP doesn't literally impose a rule to put just one operation in an interface. Ideally, an interface which can read, write, delete makes a complete (but not bulky with not-related operations) interface. Here, the operations in an interface should be related not dependent on each other.

    So, conventionally, you can have an interface like

    interface IRepository<T>
    {
        IEnumerable<T> Read();
        T Read(int id);
        IEnumerable<T> Query(Func<T, bool> predicate);
        bool Save(T data);
        bool Delete(T data);
        bool Delete(int id);
    }
    

    This interface you can pass on to client code, which makes complete sense to them. And it can work with any type of entity which follows a basic set of rules (e.g. each entity should be uniquely identified by an integer id).

    Also, if your Business/Application layer classes depend just on this interface, rather than the actual implementating class, like this

    class EmployeeService
    {
        readonly IRepository<Employee> _employeeRepo;
    
        Employee GetEmployeeById(int id)
        {
            return _employeeRepo.Read(id);
        }
    
        //other CRUD operation on employee
    }
    

    Then you business/application classes become completely independent of the data store infrastructure. You get the flexibility to choose whichever data store you like, and just plug them into the codebase with an implementation of this interface.

    You can have OracleRepository : IRepository and/or MongoRepository : IRepository and inject the correct one through IoC as and when required.