1

I was going through the source code of an existing project and totally confused about one implementation. The project is created in .Net 6 and it's a frontend MVC project. It's part of a microservice project and data is coming through an API.

Now the thing is, there is a generic class written for API access and it has its own interface. So whichever module service has an API call, its corresponding interface inherits the generic interface which is handling the API calls. But in the implementation of the service call, we need to inherit the concrete class of this Generic API access class otherwise we have to implement the API access logic again. But inheriting the concrete class directly in the service, then it's not loosely coupled right? if its not should we really need an Interface for the generic API class? I know it's a silly question but somehow my brain is not working in the right direction. Please help me out.

PFB the code sample

public interface IBaseService
{
    Task<T> SendAsync<T>(ApiRequest apiRequest);
}

public class BaseService : IBaseService
{
    API Access implementation
}

public interface IProductService : IBaseService
{
    Task<T> CreateProductAsync<T>(ProductDto productDto);
}

public class ProductService : BaseService, IProductService
{
    private readonly IHttpClientFactory _clientFactory;

    public ProductService(IHttpClientFactory clientFactory) : base(clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public Task<T> CreateProductAsync<T>(ProductDto productDto)
    {
        throw new NotImplementedException();
    }
}
8
  • Well, it's obvious that BaseService is there to act as a superclass. Why should it be loosely coupled? Inheritance causes a very strong coupling between classes.
    – Laiv
    Dec 15, 2022 at 9:09
  • @Laiv I agree, it is a superclass and in that case, we don't need an explicit interface for the supercalss that right? at least in this case?
    – Optimus
    Dec 15, 2022 at 9:31
  • 1
    We can't say, because we don't know. This is an example code w/o context at all. I can say that frameworks like Spring Data (Java) in order to allow you to implement very minimal services and repositories with many out-of-the-box methods implemented work this way but then, it's a framework, so much complexity is somewhat legit because it has to be functional in many scenarios. If you are not implementing a library or framework, you are right to ask why your hierarchy is so complex.
    – Laiv
    Dec 15, 2022 at 9:52
  • I also advise running away from designs that try to make things super generic (one fits all solutions) and super reusable via inheritance. The example above could be one of those designs. But w/o enough context I can't say.
    – Laiv
    Dec 15, 2022 at 9:55
  • 1
    Unless is causing trouble, you can ignore it. You can also ask the tech lead or the responsible about this design. Maybe there's a reason for this and, maybe, coupling was not a concern at all. Loosely coupling is not an end. It's a means to an end.
    – Laiv
    Dec 15, 2022 at 10:39

3 Answers 3

2

My guess is that the idea here is for client code to use service-specific interfaces (like your IProductService), that decouple them from the service implementations.

If so, the intended dependency structure is something like this: some client code will polymorphically use an IProductService-typed parameter, and a concrete product service will be supplied to it.

enter image description here

For example, client code might use dependency injection to obtain an IProductService dependency. In this scenario, client code doesn't know anything about ProductService and is not dependent on it.

However, since presumably there's a part of the implementation (and a part of the interface) that's shared between all these services, that was extracted into IBaseService, which was then implemented by the concrete BaseService class.

Might be that IBaseService on its own is useful as a dependency to client code in other contexts, or it might be that it just serves as a form of reuse for defining the other (service-specific) interfaces.

Now, you asked:

But inheriting the concrete class directly in the service, then it's not loosely coupled right?

No, inheriting a concrete class is also a valid way to decouple things, and in fact it's essential for some design patterns. It's just that it provides, generally speaking, a lesser degree of decoupling compared to inheriting an interface or an abstract class - assuming your client code is using the base class directly. It can also be trickier for subclasses to get the implementation right (in terms of Liskov Substitution Principle).

But in your case, from what I can tell, the BaseService class is not meant to be used by clients directly; it just implements the shared members, and doing

ProductService : BaseService, IProductService
{
   // ...
}

saves you from having to type all those shared members (required by the virtue of IProductService inheriting IBaseService) again and again for every concrete class - that is, it helps keep the code DRY.

So this is not a problem, as long as your client code doesn't know anything about (it's not directly using/referencing) the BaseService class.

In the detailed view, you have:

enter image description here

But in the more "zoomed out", coarse grained view, you have this:

enter image description here

Which is basically the same as the picture at the very top.

Now, weather or not there's a better design than this is a different matter, and depends on the specifics of your domain and your requirements. Maybe there's a good reason for this design, maybe there isn't - you'll have to discuss that with your team members.

As others mentioned, an alternative to this is to make the functionality contained in BaseService a separate thing on its own, if possible. A class with a more narrow responsibility, that's not in the service hierarchy - and then inject it as a dependency to the concrete services, such as ProductService. It all depends on what you want decouple, and on how you want to distribute various responsibilities across classes.

1

You have to ask, what is coupled to what.

Here your BaseService is coupled to everything, frankly it looks like a bad design, why not inject?

However, your application is loosely coupled to ProductService. Which is probably the bit that counts.

1

I think it is all about the context of BaseService class, if it just implements Task<T> SendAsync<T>(ApiRequest apiRequest);, you can simply remove it and then implements this method in ProductService class, so the code will be :

public interface IBaseService
{
    Task<T> SendAsync<T>(ApiRequest apiRequest);
}

public interface IProductService : IBaseService
{
    Task<T> CreateProductAsync<T>(ProductDto productDto);
}

public class ProductService : IProductService
{
    private readonly IHttpClientFactory _clientFactory;

    public ProductService(IHttpClientFactory clientFactory) : base(clientFactory)
    {
        _clientFactory = clientFactory;
    }

    Task<T> SendAsync<T>(ApiRequest apiRequest) {
        //API Access implementation
    }

    public Task<T> CreateProductAsync<T>(ProductDto productDto)
    {
        throw new NotImplementedException();
    }
}

If not, i.e. BaseService contains a lot of method, replace inheritance by composition, and pass it as a parameter to ProductService, using polymorphism (parameter should be of type IBaseService and not BaseService, so the code will be :

public interface IBaseService
{
    Task<T> SendAsync<T>(ApiRequest apiRequest);
}

public class BaseService : IBaseService
{
    API Access implementation
}

public interface IProductService : IBaseService
{
    Task<T> CreateProductAsync<T>(ProductDto productDto);
}

public class ProductService : IProductService
{
    private readonly IHttpClientFactory _clientFactory;
    private IBaseService baseService;

    public ProductService(IBaseService baseService, IHttpClientFactory clientFactory) : base(clientFactory)
    {
        _clientFactory = clientFactory;
        this.baseService = baseService;
    }

    public Task<T> CreateProductAsync<T>(ProductDto productDto)
    {
        throw new NotImplementedException();
    }
}

This way you have access to all BaseService methods, without being highly coupled to it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.