wiki:DependencyInjection

Version 7 (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.
        • Not really, or not always. Consider an object $dog of class Dog, that is passed around to different components. The function $dog->action() will make the dog bark. Now you introduce a DogThatBites?, where the action() command causes the dog to bite. You decide that some of your components should use the DogThatBites?, and the others get a DogThatBarks?. You don't have to change any of your components, because it all works with the $dog->action() method. Ok, sounds a bit hypothetical, but that's the nature of these stupid examples.
          • You're right, sometimes it won't work. Is that reason to change it in all cases? I don't see any reason for this. Again, use the singleton pattern where it makes sense, where it works to your benefit. There are obvious places in our code where we benefit from it.

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 or 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.
        • well, your argument was that we do some debugging to keep track of global state changes. So, if we avoid global things (or reduce them, at least) in the first place, we don't need the debugging. I disagree with the argument to introduce or defend something that could eventually cause problems, and then say it doesn't hurt thanks to our debugging and logging.
          • No, my argument is that we track changes in singleton classes, NOT global state changes. Your argument rests on us using singletons as global vars - if we don't do this then the problem is different. Furthermore, can you point to any one programming paradigm or design pattern that does potentially introduce problems? Do you have a magic bullet somewhere? If not, then I suggest we talk about tradeoffs instead of trying to rule out any and all practices that "could eventually cause problems" - they ALL can.
    • 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?
        • That sounds quite dirty to me! A component's behavior should not depend on the calling component, unless it is given as an argument.
          • It's probably not the easiest way of doing things, but right now (I assume) we're talking about tradeoffs in relation to changing existing code. Plus, I don't see the dirty part in: you can check your parameters or who or what called you and then act on that. Neither of those mean you're dependent on it, as you can always default to standard behaviour. Hence, you're not dependent on the calling component.
      • 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.
        • Sure, that would usually be the quick way to solve the problem, and to get around big code restructuring.
          • What are we discussing here? Rewriting all of the code?
  • 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).
    • One example was PApps (or was it called like that?). It's a PT class, so I don't want to touch it. It used to be responsible for finding an application controller for a given request string. Now the nasty thing about PApps is that it only accepts controllers which are a direct subclass of PAppController. This sucks, because I wanted to introduce a layer in between, class RoxControllerBase? extends PAppController. The singleton nature of PApps makes it hard to do customization by subclassing, so I made a new component doing this job (RequestRouter?). I don't know if this is the best example, but it's the one I currently had in mind.
      • I think it's PApp but I'm not entirely sure. However, if we a) have to use the framework provided and b) can't change it then we do have a problem - but is this caused by singletons or by the fact that we have a large body of code that we just have to live with and can't change? I think you can probably guess where I'm going with this.
    • In general we have less problems with singletons we write ourselves.. because we can always change them.
      • My point exactly.
    • And I agree the singleton isnot the most evil thing we have. Far worse are classes which grow too big.
      • Well, we still disagree then. Singletons are not evil - but we might be using them badly or the wrong way.

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.