Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 577
Mike Eason Avatar asked Dec 10 '22 21:12

Mike Eason


2 Answers

Am I completely missing the point here? Help!

I don't think you are completely missing it, you're on the right track, but taking it way too far in this case. All your CRUD functions are totally related to each other so they belong in a single interface that exposes a single responsibility. If your interface exposed CRUD functions AND some other responsibility, then it would be a good candidate to refactor into separate interfaces, in my opinion.

If, as a consumer of your functionality, I had to instantiate different classes for inserts, deletes, etc., I would come looking for you.

like image 198
Big Daddy Avatar answered May 10 '23 21:05

Big Daddy


Going by this example, the power of breaking up each type of operation would be if you wanted to define capabilities of an object based on combinations of the interfaces.

So you could have something that only Gets, and something else that Gets, Saves, and Deletes, and something else that only Saves. You could then pass them into objects whose methods or constructors only call out ISaves, or whatever. That way they aren't worried about knowing how something saves, just that it saves, which is invoked via a Save() method that is exposed by the interface.

Alternatively, you could have a scenario in which the Database implements all of the interfaces, but then it is passed to objects that only care about triggering a write, or read, or update, etc.--and so when it is passed to that object it is passed as the appropriate interface type and the ability to perform other actions is not exposed to the consumer.

With that in mind, it could be very likely that your application does not have a need for that type of functionality. You may not be drawing on data from disparate sources and have a need to abstract one common method of invoking CRUD operations across them, which is what the first would solve, or a need to decouple the concept of the database as a data source, as opposed to an object that supports CRUD ops, which is what the second would solve. So please do ensure this is being employed to meet a need, and not in an attempt to follow best practices--because this is just one way to employ a certain practice, but whether it is "best" can only be determined within the context of the problem it is solving.

like image 24
moarboilerplate Avatar answered May 10 '23 20:05

moarboilerplate