wiki:DependencyInjection

Version 5 (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.
    • ok, the explanation was a bit too simplified. Instead, what about you start with 255 occurances of a singleton, then you decide that in 120 of these you want implementation A, and in the other cases you want implementation B. With a singleton, you would need a global state to tell it which implementation you want, and a case distinction inside the singleton. And if in one request you need to switch between A and B for different components, it will be a hell to keep track of the global state. With injected objects, we can just make two implementation classes A and B, and give them to the components as they need it.
    • And, about PPostHandler: It is part of the PT lib, so we don't really want to change it. If it was a normal passed-around object, we could make do the modifications in a subclass and pass around an object of that type.
      • The point about the PT lib is fair, however: if at any given point you have 225 code-pieces that uses a given class and you want to change the 120 of them ... you'll have to change the 120 of them. Unless you start decoupling things through the use of a registry or perhaps a factory. The point is, it's not the singleton that gives you the problem, it's the fact that you're using one thing in 225 places but only want to use it in 120 places.

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.
    • I don't agree that logging debugging is a solution for a flawed architecture. Especially, a log on localhost or test.bw might not reveal problems you get on production, due to some other global state.
      • What flawed architechture? The articles cited do not prove singletons as bad in themselves and neither have you. To the contrary, if used right they serve their purpose great. Also, "Especially, a log on localhost or test.bw might not reveal problems you get on production, due to some other global state." goes for ALL DEBUGGING! That again relates to debugging as such, not to singletons. In other words, if debugging doesn't show the problem on different platforms, switching to something else than singletons is not going to help.
    • And, the problem is not only the global write access, but also global read access. If you change the singleton, you will have hundreds of places that break, because they depend on the old behaviour. If you instead change a passed-around object, you can carefully give the new version only to those components which have already adapted to the new behaviour, and give an old version to the other components.
      • That supposes you know which components have changed. If you do know this, would you not also be able to change the behaviour of reading vars so you call a function to get a var, and the function determines if the caller is the new version or not? Or, you could implement a new method in the singleton, keeping all the old functionality but adding new that new objects can use - designing it better this time. Again, the point is, singletons are simply not bad in themselves - it's a question of how they're used.
  • This being said, there's a very good point to considering other patterns for NEW functionality. But before going back and redesigning everything, perhaps we should consider whether it's worth doing just because some articles are against the use of singletons (how many actual bugs do we have that relate to singletons? How much have the singletons slowed productivity down? I haven't spent enough time with the code, so I'd really like to see concrete examples of how the singletons have given us problems).

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.