9

Dependency Injection forbids casual use of new. It favors good old reference passing. It says to put new as high up the call stack as possible.

Convention over Configuration says the common path should be the easy path. Building the typical version of something shouldn't require much effort.

These ideas seem to be at odds. Is there a way to reconcile them?

For example, can you do better than the following refactoring?

class WelcomePage{
    showAboutDialog(){ 
      new AboutDialog().show();
    }
}

This violates OCP by not being open to extension. However, this:

class WelcomePage{
    AboutDialog aboutDialog;
    WelcomePage(AboutDialog aboutDialog) 
    showAboutDialog(){ 
      aboutDialog.show();
    }
}

Violates Convention over Configuration. Yes you've allowed for extension under OCP. But by taking aboutDialog as a parameter you've forced a configuration situation.

Consider:

class WelcomePage{
    AboutDialog aboutDialog = new AboutDialog();
    WelcomePage(){}
    WelcomePage(AboutDialog aboutDialog){
        this.aboutDialog = aboutDialog;
    } 
    showAboutDialog(){ 
      aboutDialog.show();
    }
}

This follows OCP because now AboutDialog is an overridable default value. It is open to extension. AboutDialog is now the convention but it's not required. The disadvantage to the previous version is that AboutDialog must be deployed with WelcomePage even if it isn't used. The previous one could avoid dragging an implementation with it by making AboutDialog fully abstract.

This is not a solution I'd use when crossing a significant boundary into a unit meant to be independently deployed. But I don't think every class boundary must be treated so formally. Sometimes a class boundary exists just because it's handy to have a class.

It seems overridable default values can achieve Convention over Configuration in Pure Dependency Injection style so long as you're not crossing a deployment boundary.

Is there a method that provides more freedom? A consideration I've ignored? Can we do better?

10
  • Isn't that more or less what DI containers are for? To make instantiating new objects "without new" the easy path?
    – Doc Brown
    Commented Nov 30, 2019 at 23:01
  • @DocBrown are you saying there is no way to do this with pure DI? Commented Nov 30, 2019 at 23:04
  • Well, maybe someone will invent (or maybe someone has already?) a programming language which has built-in DI automatisms, so no separate DI container will be required? But I don't know such a language. However, I found this SO post which should basically answer your question, I guess?
    – Doc Brown
    Commented Nov 30, 2019 at 23:35
  • What if your convention is dependency injection?
    – cbojar
    Commented Dec 1, 2019 at 2:32
  • 1
    @laiv Tons of interfaces with a single implementation never seemed like a particularly testable pattern because you also need the test implementation. With a container you end up doing half convention and half configuration. I'm wondering about a solution that doesn't require a framework, is testable, but doesn't force everything to be done in main. Commented Dec 3, 2019 at 12:44

3 Answers 3

13

Convention over configuration doesn't mean "let's take the easy path;" it means that certain programming decisions are already made by default. It is used by software frameworks to decrease the number of decisions that a developer is required to make, without losing the flexibility of custom configurations. The improved simplicity comes from not having to make configuration decisions every time a framework is used, unless the developer wishes to override them.

Let's look at a very simple example of Convention over Configuration in ASP.NET MVC.

In ASP.NET MVC, you can map the URL http://localhost/Products/List to a controller by merely putting a class called ProductsController in the Controller folder. ASP.NET MVC will do the right thing, happily calling the List method on your ProductsController class, no further effort required. You don't need to decorate the class with attributes to make this work, nor do you need configuration files; your controller class merely needs to follow the convention of having a name that corresponds to some domain (identified in the URL routing) and a suffix of Controller.

Think of Dependency Injection as a mapper between interfaces and concrete types. If you're following TDD principles, your classes should have constructors that take interfaces as parameters. In your aggregate root, you tell the DI container which concrete type corresponds to each interface and, when the DI container sees that interface in one of your class's constructors, it creates an instance of the type you specified for that interface (along with some additional goodies like object lifetime management), and hands that to the constructor parameter automatically.

In other words, DI containers are a form of Convention over Configuration, especially if they use tricks like type discovery and name-mapping ISomeClass to SomeClass to determine which class to instantiate if no programmer-supplied mapping is present.

Convention over Configuration can seem like "spooky action at a distance." If you haven't memorized the conventions, it can be difficult to debug systems that rely heavily on it. As Mark Heath puts it: "'Find all references' will return nothing when these conventions are being used because reflection is typically used at run-time to discover the methods to be called. It can leave developers wondering 'how on earth does this even work'?"

References
Convention over Configuration on Wikipedia
Mark Heath's article on Convention over Configuration

1

I like Robert's answer (I voted for it), but in addition I thought you might be interested in seeing a pattern I use in this sort of situation with some success.

Instead of:

class WelcomePage{
    AboutDialog aboutDialog = new AboutDialog();
    WelcomePage(){}
    WelcomePage(AboutDialog aboutDialog){
        this.aboutDialog = aboutDialog;
    } 
    showAboutDialog(){ 
      aboutDialog.show();
    }
}

You could write it like this:

class WelcomePage{
    IAboutDialog aboutDialog = null;
    WelcomePage(){}
    WelcomePage With(IAboutDialog aboutDialog){
        this.aboutDialog = aboutDialog; 
        return this;
    }
    WelcomePage WithDefaultDependencies(){
        return this.With(new AboutDialog());
    }
    showAboutDialog(){ 
      aboutDialog.show();
    }
}

So in the ordinary (default) case you'd instantiate it like this:

var welcomePage = new WelcomePage().WithDefaultDependencies();

But in your unit testing suite, or if you want to extend, you can do this:

var welcomePage = new WelcomePage().With(new MySpecialAboutDialog());

This makes it pretty easy to use the default configuration, but allows for extension as well.

3
  • 1
    Looks like a mutable variation of the Joshua Bloch Builder. The advantage over my example appears to be that AboutDialog wont be constructed when it's not used. Which might be significant. I like that you didn't make either With static. Supports polymorphism better. Commented Dec 3, 2019 at 2:42
  • 1
    Yep, although it's not quite a builder.... more like a fluent interface
    – John Wu
    Commented Dec 3, 2019 at 2:46
  • 1
    I don't like how easy it is to leave required dependencies null. But it does chain well: return this.With( new AboutDialog().WithDefaultDependencies() ); And it avoids Roberts 'Spooky action at a distance' since 'Find all references' will work just fine. Commented Dec 3, 2019 at 2:51
-2

If you are using a framework such as rails I try to follow the convention in the most common places. Often times I will implement non-idiomatic code with dependency injection. A PORO for a commission calculator would probably be somethimg I would use dependency injection for. It has nothing to do with the base framework, and thus there is not a strong convention for 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.