wiki:DependencyInjection

Version 3 (modified by fake51, 11 years ago) (diff)

--

Dependency Injection

See also SingletonPattern.

The Problem

great articles on this:

In a typical software project, we have concerns or services that can nicely be divided into separate components ("applications"), such as forum, messages, signup etc.

However, there are always some remaining services that are used across different applications. Much of this could be described as "infrastructure". Typical examples in our project would be

  • functionality dealing with the user: Is she logged in, or not? Does she have translation or even admin rights? What's her username?
  • functionality dealing with the "population" and the content: Things like the members table or others are needed in more than just one application.
  • configuration, database connection, baseuri, root directory, etc
  • reusable layout components: display of time and date, user avatars, and the entire page layout.
  • typical functions like redirecting a page, etc, that we would like to reuse.
  • knowing which class is defined where

Traditionally, we would provide all these shared services through global symbols, be it plain global variables, singleton classes, or a singleton registry.

BW-Rox have plenty of plain global variables in the old BW code, and even the original index.php has a lot of them. And we have plenty of singletons and a singleton registry (class PVars) in the PlatformPT part.

Why is this bad?

Flexibility argument

Imagine we have a singleton class called PPostHandler (we have). A search in eclipse returns 225 matches for the term PPostHandler, scattered in models, views, controllers and templates.

Now imagine we decide to replace the PPostHandler with something else, that works differently, and has a different classname. We now have to change all the 255 occurances.

  • or do you? You could ALSO just change PPostHandler and have it forward the new object you created. In essence, what you're assuming is that we would HAVE to change all 255 occurrances - but that's obviously not the case.

Side effects

Many of our applications use an object called PVars::getObj('page'), that lives in the global registry PVars. The object receives parameters for the page.php template, and can be set and read from anywhere.

Now imagine something goes wrong with the object - then we would have to check all the different places where the object is modified.

  • This is more of an issue but it does NOT mean that Singletons or Registrys are bad designs. What it means is that you have to use them right. So, if you allow code outside a singleton to change it's vars and all other code depends on it, then log the change. Again, problem solved: you could have a debug function (to switch on or off) that outputs the state of the Singleton on every change, this would also solve the problem. There isn't any basis for assuming that singletons mean we blindly have to look through every piece of code that references the singleton - that is only the case if we implement the singletons badly.

Solutions

Solution I: Shared base class

Some of the services are typical for a special genre of classes, such as controller, models, views. These services can then be provided through a shared base class. For instance, we can make a RoxControllerBase?, that provides methods for redirecting a page, to get the current request or the current post arguments.

The problem is that at some point our shared base class, and even more the resulting class with all its base classes, will grow bigger and bigger, and thus again hard to manage.

To avoid that, we would not implement all that stuff inside the shared base class, but only let the base class provide an interface to the shared functionality. The question is, how does the shared base class get to the services implemented elsewhere?

For instance, a RoxModelBase? might want to use the database connection, or create one using MySQL username and password. Currently it would grab this info "out of the air", reading one of the globals stored in PVars. If we assume that such globals are evil, then how can the RoxModelBase? get the necessary info?

Solution II: Injection of Parameters

The solution is to give the RoxModelBase? the desired info, by passing around a parameter. This can be a parameter in the controller, or in a setter method, like "setSQLUsername($username)", or, more generic, "inject($key, $value)".

One example for a parameter to be injected can be the request, and post and get arrays. If we inject these guys as local variables, and unset the global counterparts, we gain some flexibility and get rid of evtl side effects and dependencies.

The problem is, we will inject tons of different things. Much of this will happen in the bootstrap (index.php, RoxLauncher?, RoxFrontRouter?), but also later in the controllers, where information has to be passed on to the model and view. Like this

// in SignupController::index()
$page = new SignupPage();
$page->inject($key1, $value1);
$page->inject($key2, $value2);
$page->render();

Solution III: Centralizing the parameter injection

It is nasty, if every controller has to inject the same information to the model, view or page object. If it's really the same information, we could centralize it in the RoxControllerBase?, like this

// in RoxFrontRouter::route()
$controller = new $classname();
$controller->inject($key1, $value1);
$controller->inject($key2, $value2);
$controller->index();

// in SignupController::index()
$page = new SignupPage();
$this->renderPage($page);

// in RoxControllerBase::renderPage($page)
$page->inject($key1, $value1);
$page->inject($key2, $value2);
$page->render();

or like this

// in RoxFrontRouter::route()
$controller = new $classname();
$controller->inject($key1, $value1);
$controller->inject($key2, $value2);
$controller->index();

// in SignupController::index()
$page = $this->createPage('SignupPage');
$page->render();

// in RoxControllerBase::createPage($classname)
$page = new $classname();
$page->inject($key1, $value1);
$page->inject($key2, $value2);
return $page;

Now the

A different strategy can be to centralize this stuff in the RoxFrontRouter? (the thing that chooses an application to run), like this

// in RoxFrontRouter::route()
$controller = new $classname();
$controller->inject($key1, $value1);
$controller->inject($key2, $value2);
$page = $controller->index();
$page->inject($key3, $value3);
$page->inject($key4, $value4);
$page->render();

// in SignupController::index()
$page = $this->createPage('SignupPage');
return $page;

There are two problems with this:

  • we still have to explicitly inject a lot of things.
  • eventually we create and inject many things that will not be needed.

Solution IV: Passed-around Registry

Instead of injecting the items one-by-one, we can make one big object and pass it at once. This is similar to the singleton registry described above, but this time it's not a global symbol.

// in RoxFrontRouter::route()
$registry = new LocalRegistry();
$registry->inject($key1, $value1);
$registry->inject($key2, $value2);
$controller = new $classname();
$controller->setRegistry($registry);
$page = $controller->index();
$page->setRegistry($registry);
$page->render();

// in SignupController::index()
$page = $this->createPage('SignupPage');
return $page;

Eventually we could even pass a different registry to controller and view, so to reduce the dependencies.

A problem we still have is, that we create objects that are only needed on a few pages.

Solution V: Dependency Injection Container

Instead of injecting all the finished objects into the controller or page, we want something that creates only those objects that are actually needed. So, instead of the registry, we define a container that can create these objects itself.

// in DIContainer
function getUser() {... return $user;}
function get($classname) {return new $classname;}

// in RoxFrontRouter::route()
$container = new DIContainer();
$controller = new $classname();
$controller->setDIContainer($container);
$page = $controller->index();
$page->setDIContainer($container);
$page->render();

// in SignupController::index()
$page = $this->createPage('SignupPage');
return $page;

See http://www.sitepoint.com/blogs/2008/02/04/dealing-with-dependencies/, "Writing a container".

Solution VI: Container and Factory

This time the container does not create the services itself, but uses a factory object for that task.

Solution VII: Keep most of the existing singletons, but implement logging, debugging and restraints

Make sure that no vars in a singleton can be changed without it being logged (all vars from a singleton should be fetched by a function). Implement logging of functions that change vars in the singleton. Implement some debugging to allow output on change on vars in the singleton.

Upside: you keep the singletons we have now, thus no problems with breaking code.

Action Plan for BW-Rox

Since we don't want to break existing code, everything we do should be backwards compatible. We can begin by providing redundant non-global shared services, while keeping the old global ones.

This way, it is up to the application programmers, if they want to adopt a scheme with fewer global symbols. It will take a long time until we don't need the global ones any more.