1

I am building a wrapper for a library that requires little complex logic. The whole project is 8 builder classes, 4 classes for doing some pre-processing, and a couple visitor classes. Essentially I am just building wrapper classes around a third party library's objects. I was curious whether in a situation like this it is more appropriate to use poor man's di (no framework) or static class dependencies.

For static class dependencies the overhead would be when I have the same static dependency used in multiple classes, you would get some unneeded duplication of instances.

For di, the overhead would be creating the extra builder classes to wire together the objects, in addition to a minor increase is "cognitive load" for those unfamiliar with the concept. For a library this small, without complex business logic, my gut tells me it may be overkill.

Just curious which one you guys would go with, and why. I know the difference in strategies is essentially razor thin.

Dependent Static Classes

public class DoTheThing {

       private static final DependencyOne dependencyOne = new DependencyOne();
       private static final DependencyTwo dependencyTwo = new DependencyTwo();


       public void doSomething(Argument yayImAnArgument){
            dependencyOne.do(yayImAnArgument);
            dependencyTwo.do(yayImAnArgument);
       }

}

vs.

Dependency Injection

public class DoTheThing {

       private final DependencyOne;
       private final DependencyTwo;

       public DoTheThing(DependencyOne dependencyOne, DependencyTwo DependencyTwo){
            this.dependencyOne = dependencyOne;
            this.dependencyTwo = dependencyTwo;
       }

       public void doSomething(Argument yayImAnArgument){
            dependencyOne.do(yayImAnArgument);
            dependencyTwo.do(yayImAnArgument);
       }

}
candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 1
    are you just polling opinion? the DI method has obvious benefits of being able to inject mocks, but I guess you would argue you will never need to? – Ewan Sep 20 '22 at 19:11
  • @Ewan, yep exactly, and that would be the argument, kiss. Maybe also because I am having a hard time finding a name for the objects that would do the wiring and provide the instance. Having a ThingBuilder and then a ThingBuilderBuilder to wire the objects together and provide an instance seems a bit tedious/confusing. – EspressoCoder Sep 20 '22 at 19:18
  • 2
    This approach plays hell with your object lifetimes. Anything marked `static` and initialized will essentially stay in memory until your program exits. Also, you only get one copy of your dependency for all class instances; this won't work if you need a unique copy of the dependency for each object. If you dependency inject, you get more choices. You can, for example, simulate `static` scope by telling your DI container to treat your dependency as a Singleton, and you can change your mind later without having to rewrite your dependent classes. – Robert Harvey Sep 20 '22 at 19:37
  • 1
    Just wanted to point out (as you can see in the candied_orange's answer), that, when it comes to DI, it is in general *not required* to create any extra builder classes. Builders and factories come in handy for other reasons - they are either glorified "custom-made constructors" (they let you have more control over the construction process), or when passed in as a dependency, they offer a way to postpone the construction of an object until all the necessary parameters become available (so in some sense, they can be used for "partial construction"). They aren't required for DI, though. – Filip Milovanović Sep 20 '22 at 20:39

2 Answers2

3

Your

public class DoTheThing {

       private static final DependencyOne dependencyOne = new DependencyOne();
       private static final DependencyTwo dependencyTwo = new DependencyTwo();

       public void doSomething(Argument yayImAnArgument){
            dependencyOne.do(yayImAnArgument);
            dependencyTwo.do(yayImAnArgument);
       }

}

is not much of an improvement over

public class DoTheThing {
       public void doSomething(Argument yayImAnArgument){
            new DependencyOne().do(yayImAnArgument);
            new DependencyTwo().do(yayImAnArgument);
       }
}

Which works just fine until the project changes. It is a Law of Demeter violation because DoTheThing knows how to both build and use these objects.

Here's a classic wiring pattern for pure DI:

static void main(String[] args){
    // Construct and wire together all long lived objects //        
    DoTheThing thingDoer = new DoTheThing(
        new DependencyOne(),
        new DependencyTwo()
    );
    Argument yayImAnArgument = new SnarkyArgument("I know you are, but what am I?");

    // Start the object graph ticking at one entry point //
    thingDoer.doSomething(yayImAnArgument);
}

But wait! This has the same Law of Demeter violation doesn't it? Well sure but we've pushed all the news as far up the call stack as they can go and most importantly separated them from the behavioral business logic in DoTheThing. Which as far as I can tell is just do one before doing two.

By moving construction to main we've put it somewhere safe to change. Nothing else knows about what's happening in main so we aren't likely to break it. All we have to do is build things here that make what we build happy.

Now sure, this is a simple example. With enough going on this construction style gets a little nutty.= That's why creational construction patterns= exist. Because while our news are nicely separated in main this is brain dead procedural code. It is ok to break that up. Just keep the construction separated from the business logic. And sorry but to break it up nicely you really owe us good names.

Or just pretend the code is never going to change. Look I get it. You've never had to deploy anything less than the whole updated project at once. You've never had to change which library you were abstracting. You've never been told any class you touch is going to be subjected to months of artisanal integration testing done by hand. There was a time when I hadn't either. I remember those days. Things were simpler then.

Just know, those days sneak away from you when you're not looking.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • Hi @candied_orange, thank you for the thoughtful response. Follow up question, say a circular dependency is required (not ideal but no way to avoid it). How would you go about injecting it as the below will recurse infinitely? public MyObject getInstance(){ DependencyOne do = new DependencyOne(); DependencyTwo dt = new DependencyTwo(do, getInstance()) } – EspressoCoder Sep 20 '22 at 20:10
  • 1
    @EspressoCoder I'll accept your new question if you'll accept my old answer.[=](https://softwareengineering.stackexchange.com/a/331381/131624) – candied_orange Sep 20 '22 at 20:13
  • accepted it :)) – EspressoCoder Sep 20 '22 at 20:15
  • @EspressoCoder - Could you expand a bit on your circular dependency problem? Not sure if you missed candied_orange's [link](https://softwareengineering.stackexchange.com/a/331381/131624) at the very end of the comment above - the answers discuss some ways to deal with circular dependencies. But your problem seems slightly different - based on your code snippet, you seem to be trying to construct a sort of recursive data structure? If so, what does the object graph look like? There has to be a point where either `dt` or `dt.myObject` is null (where the recursion stops). – Filip Milovanović Sep 20 '22 at 21:16
  • @FilipMilovanović that's not a recursive data structure. that's a recursive getter that's missing a return (and a semicolon). I don't know. Maybe it's Groovy code. – candied_orange Sep 20 '22 at 21:35
  • I thought maybe it was an attempt to create a factory function (missing a `return myObject`) that was meant to produce something that looks like :MyObject -> :DependencyTwo -> :MyObject -> :DependencyTwo -> ... -> null – Filip Milovanović Sep 21 '22 at 05:41
-1

What about implementing both and let the softwaredevelopper that uses your lib decide:

public class DoTheThing {
   private final DependencyOne;
   private final DependencyTwo;

   public DoTheThing(DependencyOne dependencyOne, DependencyTwo DependencyTwo){
        this.dependencyOne = dependencyOne;
        this.dependencyTwo = dependencyTwo;
   }

   public DoTheThing() {
     this(new DependencyOne(), new DependencyTwo());
   }

   public void doSomething(Argument yayImAnArgument){
        dependencyOne.do(yayImAnArgument);
        dependencyTwo.do(yayImAnArgument);
   }
}
k3b
  • 7,488
  • 1
  • 18
  • 31