Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DI

Using Abstractions to Ensure Low Coupling – DI Part 2

0.00/5 (No votes)
28 Feb 2014CPOL2 min read 5.7K  
Using Abstractions to Ensure Low Coupling – DI Part 2

As shown in my previous post, the abstraction of the data layer component handling products – in the form of the IProductRepository interface – was the key to make the business layer independent of the data layer.

Generally, abstractions play a crucial part in ensuring low coupling between software components. These abstractions allow you to define the behaviour of a component without actually caring about the concrete type and implementation behind the abstraction. Low coupling is good for several reasons. It makes your code more extensible, maintainable and, maybe most importantly, more testable. I will come back to the latter in another post.

Of course, you cannot entirely decouple everything. Even in the purest POCO component, there will obviously be dependencies to types in the .NET Framework. Rather, you should strive against the “natural” level of decoupling – whatever that is. The abstractions should form well-defined points of interaction – also called seams - between various components in your system with well-defined responsibilities (adhering to the Single Responsibility Principle).

For example, it definitely does make sense to create a seam between a component handling the business functionality of a product and the component that is responsible for the persistence (i.e. between the ProductService and the ProductRepository in the example in my previous post). If you did not introduce a seam here, it would be very difficult to replace for example a SQL Server database implementation in the data layer with some other kind of technology.

Abstractions are typically defined using interfaces. The IProductRepository could for example look like this:

C#
public interface IProductRepository
{
    void Add(Product product);

    void Update(Product updatedProduct);

    void Remove(Product product);

    IList<Product> GetAll();

    Product Get(Guid id);
}

However, as our application is likely to deal with other entities than products (for example, customers and orders), it would make sense to generalize this interface using C# generics:

C#
public interface IRepository<E> where E : IEntity
{
    void Add(E entity);

    void Update(E updatedEntity);

    void Remove(E entity);

    IList<E> GetAll();

    E Get(Guid id);
}

IEntity is a simple interface ensuring that an entity always has a unique ID and a name:

C#
public interface IEntity
{
    Guid Id { get; }

    string Name { get; }
}

Another fundamental object-oriented design principle is the Interface Segregation Principle:

Clients should not be forced to depend on methods that they do not use.

This basically means that interfaces preferably should be as small and specific as possible. Actually, an interface with a single method can be a very good interface.

You might have some entity services for which you do not want to expose full CRUD functionality – a read-only entity service, so to speak. For this purpose, it would make sense to define a specific IReadOnlyRepository interface:

C#
public interface IReadOnlyRepository<E> where E : IEntity
{
    IList<E> GetAll();

    E Get(Guid id);
}

Then, the IRepository interface could be simplified to an extension of the IReadOnlyRepository interface:

C#
public interface IRepository<E> : IReadOnlyRepository<E>
    where E : IEntity
{
    void Add(E entity);

    void Update(E updatedEntity);

    void Remove(E entity);
}

This is interface segregation in action. You transformed the one big “header” interface into a number of smaller more specific role interfaces.

Now, you have a nice well-defined seam between the product service – or any other entity service – and the corresponding repository.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)