0

How do you decouple interface implementation from calling code if implementation effects must depend on some parameters in the calling code?

It may sound convoluted, so here is a real-life example:

  • Some online store sells products from different brands;
  • When user purchase is processed, we need to submit data to different storages based on the brand of the purchased product;
  • All such storages are accessible through the same implementation of the same interface, but through different underlying connections.

Following IoC principle, we should have an instance of the storage interface implementation in order processing code without any knowledge about its internals. However, data must be sent to different servers based on products' brand, which means we have to affect that implementation somehow.

If we pass any data (brand data, or repository connection configuration) to the repository, we are either coupling repository interface to the brand entity, or order processing code to repository implementation details.

So how would one implement this scenario following IoC principle? Suggestions of other decoupling patterns are also welcome.

Steven
  • 166,672
  • 24
  • 332
  • 435
mrnateriver
  • 1,935
  • 1
  • 19
  • 19
  • In many cases, the repository interface is generic. So, it is not bound to your data interfaces and vice versa. Your business logic code then USES the data and repository interfaces to perform the required work. – KBO Jun 07 '18 at 05:47
  • Generic or not, repository has to persist some data to different storages based on input from calling code. The question is how to control that without coupling calling code to repository implementation. – mrnateriver Jun 08 '18 at 03:04
  • You do this by separating definition (interfaces) and implementation (classes) in different assemblies. You will have a contract assembly for repository related interfaces (e.g. read-only repository, read/write repository...) and an implementation assembly for that. Normally they are build in different solutions to allow bugfixes/changes/extensions of the implementation without changing the interface assembly version etc. The same for the data to store. Then you inject the repository and data interfaces into the appropriate worker classes, and they don't know their implemetation... – KBO Jun 08 '18 at 06:03
  • You misunderstood the question. I'm not asking how does IoC/DI work, but rather how to use it in this specific scenario. I've posted an answer myself that will likely clear the confusion. But thanks for your input anyway! – mrnateriver Jun 17 '18 at 11:26

1 Answers1

0

I came to the conclusion that in this case code cannot be entirely uncoupled. There’s a coupling between business logic and repository implementation by definition of the task at hand.

However, to simplify further code maintenance, I’ve ended up using the following architecture (in pseudo-code):

Core interfaces:

// Main repository interface
interface OrdersRepositoryInterface {
    store(order: Order): bool;

    // …other methods
}

// An interface of a factory that will be used to create repository instances with different configurations (different underlying storages, for example)
interface OrdersRepositoryFactoryInterface {
    createRepository(configuration: OrdersRepositoryConfigurationInterface): OrdersRepositoryInterface;
}

// An interface of a container that will create different instances of repositories based on specified brand
// An implementation of this interface is the coupling point between business logic and data persistence logic
interface OrdersRepositoryContainerInterface {
    get(brand: Brand): OrdersRepositoryInterface;
}

Repository factory implementation (tightly coupled to repository itself, can be avoided if repository constructor was specified in interface itself, but I personally consider that bad practice):

class OrdersRepositoryImplementation implements OrdersRepositoryInterface {
    constructor(configuration: OrdersRepositoryConfigurationInterface) {
        // …
    }

    store(order: Order): bool {
        // …
    }
}

class OrdersRepositoryFactory implements OrdersRepositoryFactoryInterface {
    createRepository(configuration: OrdersRepositoryConfigurationInterface): OrdersRepositoryInterface {
        return new OrdersRepositoryImplementation(configuration);
    }
}

Repository container implementation:

class OrdersRepositoryContainer implements OrdersRepositoryContainerInterface {
    get(brand: Brand): OrdersRepositoryInterface {
        var factory = IoC.resolve(OrdersRepositoryFactoryInterface);

        var configuration1 = …;
        var configuration2 = …;

        if (brand.slug === "brand1") {
            return factory.createRepository(configuration1);
        } else {
            return factory.createRepository(configuration2);
        }
    }
}

IoC-container binding (if container supports it, it’s probably better to bind classes rather than instances, since automatic Dependency Injection may be used in those implementations’ constructors):

IoC.bindInstance(OrdersRepositoryFactoryInterface, new OrdersRepositoryFactory());
IoC.bindInstance(OrdersRepositoryContainerInterface, new OrdersRepositoryContainer());

And last, but not least, order processing code:

var order = …;

var repositoryContainer = IoC.resolve(OrdersRepositoryContainerInterface);

var repository = repositoryContainer.get(order.brand);

repository.store(order);

This architecture will allow easy replacement of repository resolution logic. For example, repositories for all brands may be unified in the future, in which case only OrderRepositoryContainerInterface implementation will have to be replaced. But still, OrdersRepositoryContainer is still coupled to repositories’ implementation, since it has to know how and where to get their configuration.

I’ll mark this answer as accepted, but I’ll willingly change that if anyone suggests a better idea.

mrnateriver
  • 1,935
  • 1
  • 19
  • 19