OOP Design – How to Prevent Dependency Injection from Killing Object-Oriented Programming

dependency-injectionobject-oriented-design

Recently lots of people adopted DI in their projects (I am working with aspnet core). The problem I have is that DI turns my code towards procedural paradigm. For example in OOP way I would do:

class Something
{
    Something(Other object, Dependency dep); // created manually or by factory obj

    DoStuff();
}

However with DI I cannot create Something class directly, because I need to satisfy dependencies. Now my code generally looks like that:

class Something
{
    Something(Dependency dep); // resolved from di container

    DoStuff(Object obj); // executed manually
}

As a result my objects have no methods, they are just bags of properties that are processed by different services. On the other hand I would have to create giant objects with lots of dependencies just to execute simple task, that needs 1% of created object graph.

How to fix my design?
This approach has obvious downsides, for instance I cannot pass Something class around, because it needs Object to DoStuff, so I have to pack it into some other class. There are many other problems, like functionality scattered across many files, difficulties in implementing security (because you can request "inner" service from DI) or encapsulation (Object must have all properties public because they are manipulated by external class.


Edit:
Small example for better illustration:

Api I would write before DI:

class ShoppingCartFactory {
    ShoppingCart Create(id);
}

// encapsulates cart logic
class ShoppingCart {
    // internal dependencies passed to cart from factory
    ShoppingCart(DeliveryLoader, ItemMetadataLaoder);


    // methods encapsulation functionality
    Modify(item, quantity);
    Save();
    Empty();
    GetDeliveryOptions(); // loads possible transports from db
    LoadItemMetadata(); // loads prices, availability etc. for items

    // public properties
    ImmutableList<CartItem> items; // immutable list of items
    Delivery selectedDelivery; // transport selected by client
}

When client clicks shopping cart delivery options are displayed only once, and subsequent requests do not need them. Therefore with this approach I have to create ShoppingCartDeliveryCreator dependency for each request.

So instead I have to write something like that:

// instead of having properties on cart Object now I have 3 separate classes
// in previous example user of api had no idea how cart works inside
// now he needs to find all classes associated with cart
// also service class needs to take id in each method
// or needs some method like `Contextualize(id)`
class ShoppingCartService {
    // executes logic on db
    ShoppingCart Read(id);
    ShoppingCart Modify(id, item, quenatity);
    ShoppingCart Empty(id);
    ShoppingCart Save(id, shoppingCart);
}
class ShoppingCartDeliveryCreator (...)
class ShoppingCartItemMetadataLoader (...)

// no logic
class ShoppingCart {
    List<CartItem> items; // cannot be immutable, because Service needs to manipulate it
    Delivery selectedDelivery; // same
}

You cannot do anything with ShoppingCart class, You always need one of the services. Also as Derek Elkins pointed out

One of the consequences of systematically using Dependency Injection is that your class hierarchy becomes completely flat.

I am not sure if this is good thing. I made lots of research and experiments, however I am not convinced to this style of programming. To get any idea of how something works You need to examine all that code, guess what it is doing and then you may have thin idea how it works. With old example you could just look on the class and You would see all the options, or You could check all classes that take ShoppingCart.

Best Answer

As Doc Brown mentioned, using Dependency Injection containers is not Dependency Injection. Dependency Injection is just parameterization, and Dependency Injection containers are just meant to "simplify" wiring up dependencies. Your issue seems to be mostly driven by (perceived) limitations of whatever DI container framework you're using.

I don't understand how your objects being "bags of properties" is a "result" of dependency injection. Some objects are going to just be "bags of properties" and thus will have no dependencies. These are often called "POJO/POCO" objects for "Plain Old Java/C# Objects". Obviously, this can't be all your objects as nothing would need dependencies then.

You also mention "giant objects with lots of dependencies" that "needs [only] 1% of [the] created object graph". This is somewhat contradictory. If it only needs 1% of the "created object graph", why is it depending on that remaining 99%? If your dependencies need dependencies for things other than what you need of the dependencies, this suggests that your dependencies aren't well-factored. In OOD terms, a violation of the Single Responsibility Principle. One of the consequences of systematically using Dependency Injection is that your class hierarchy becomes completely flat. Any "hierarchy" that is needed arises from how things are wired together (which no longer needs to be strictly hierarchical). Each of your classes should depend on only what it needs via relatively narrow interfaces. If each class provides only one "service", then dependencies will never instantiate more of the object graph than they need. There are plenty of good reasons for class to provide multiple "services", and this can lead to dependencies that are extraneous to some downstream consumer of the "service", but this will be very obvious and presumably done for some explicit reason. If not, factor the class into multiple classes that each provide only one of the "services". (The "services" usually correspond pretty closely to the interfaces a class implements. So if you have a class that implements multiple interfaces, this is an indication that the class is providing multiple "services".)

At any rate, for the limited example of code you provided, there's a pretty clear solution. Firstly, it's quite possible that whatever DI container framework you're using explicitly provides for this, but let's assume not. What do consumers of Something actually need? The need something that will give them Somethings when given Others. (Or to be more precise, something that will give them an instances of an interface representing whatever "services" Somethings provide. They shouldn't care if they actually are Somethings.). That is, they need a SomethingFactory. So classes that need Somethings should depend on SomethingFactory and then use that to create Somethings. In code:

class Something : ISomething {
    public Something(Other object, Dependencies dependencies) {...}
    // ...
}

interface ISomethingFactory {
    ISomething CreateSomething(Other object);
}

class SomethingFactory : ISomethingFactory {
    private readonly Dependencies _dependencies;
    public SomethingFactory(Dependencies dependencies) {
        _dependencies = dependencies;
    }
    public ISomething CreateSomething(Other object) {
        return new Something(object, _dependencies);
    }
}

class SomethingConsumer {
    public SomethingConsumer(ISomethingFactory somethingFactory) {...}
}

This approach gives you almost exactly the same experience as directly writing new Something(object) plus not having to worry about extraneous dependencies. There is no reason from what you've described (or generally) why Dependency Injection would lead to you needing to make all properties public or use a "procedural style" or anything like that. You could take any OO program written "naturally" and mechanically convert it to one using dependency injection. Simply go through what you would "naturally" write in "OO" style, and replace any uses of new with either passed in dependencies or factories as I've illustrated above. Other than having to pass around some extra parameters, the structure of the code will be practically identical to what it was before. If you feel you need to make much more extensive changes than this, you are misunderstanding something. (This may suggest more extensive changes, but they should not be necessary.)

As a note, the fact that you are using any particular DI container should not be evident from the vast majority of your code. You should not be passing around the DI container and instantiating objects with it willy-nilly. The DI container should only be used (if used at all) to simplify the top-level code that wires everything together. I'm not sure what you mean by "you can request 'inner' service from DI", but it sounds like you're doing something like this.