I'm building out a small project to try to teach myself as much of the fundamentals as possible, which for me means not using a prefabricated framework (As Jeff once put it, "Don't reinvent the wheel, unless you plan on learning more about wheels" [emphasis mine]) and following the principles of Test Driven Development.
In my quest, I recently ran into the concept of Dependency Injection, which appears essential to TDD. My problem is that I can't quite wrap my head around it. My understanding so far is that it more or less amounts to "have the caller pass the class/method any other classes it may need, rather than letting them create them themselves."
I have two example issues that I'm trying to resolve with DI. Am I on the right track with these refactorings?
Database Connection
I'm planning to just use a singleton to handle the database, as I'm currently not expecting to use multiple databases. Initially, my models were going to look something like this:
class Post {
private $id;
private $body;
public static function getPostById($id) {
$db = Database::getDB();
$db->query("SELECT...");
//etc.
return new Post($id, $body);
}
public function edit($newBody) {
$db = Database::getDB();
$db->query("UPDATE...");
//etc.
}
}
With DI, I think it would look more like this:
class Post {
private $db; // new member
private $id;
private $body;
public static function getPostById($id, $db) { // new parameter
$db->query("SELECT..."); // uses parameter
//etc.
return new Post($db, $id, $body);
}
public function edit($id, $newBody) {
$this->db->query("UPDATE..."); // uses member
//etc.
}
}
I can still use the singleton, with credentials specified in the application setup, but I just have to pass it from the controller (controllers being un-unit-testable anyway):
Post::getPostById(123, Database::getDB);
Models calling models
Take, for example, a post which has a view count. Since the logic to determine if a view is new isn't specific to the Post object, it was just going to be a static method on its own object. The Post object would then call it:
class Post {
//...
public function addView() {
if (PageView::registerView("post", $this->id) {
$db = Database::getDB();
$db->query("UPDATE..");
$this->viewCount++;
}
}
With DI, I think it looks more like this:
class Post {
private $db;
//...
public function addView($viewRegistry) {
if ($viewRegistry->registerView("post", $this->id, $this->db) {
$this->db->query("UPDATE..");
$this->viewCount++;
}
}
This changes the call from the controller to this:
$post->addView(new PageView());
Which means instantiating a new instance of a class that only has static methods, which smells bad to me (and I think is impossible in some languages, but doable here because PHP doesn't allow classes themselves to be static).
In this case we're only going one level deep, so having the controller instantiate everything seems workable (although the PageView class is getting its DB connection indirectly by way of the Post's member variable), but it seems like it could get unwieldy if you had to call a method that needed a class that needed the class that needed a class. I suppose that could just mean that's a code smell too though.
Am I on the right track with this, or have I completely misunderstood DI? Any criticisms and suggestions are greatly appreciated.