Τι είναι το DI Container;
Ένα dependency injection container (DIC) είναι μια κλάση που μπορεί να δημιουργήσει και να διαμορφώσει αντικείμενα.
Μπορεί να σας εκπλήξει, αλλά σε πολλές περιπτώσεις δεν χρειάζεστε ένα dependency injection container για να επωφεληθείτε από το dependency injection (συντομογραφία DI). Άλλωστε, ακόμη και στο εισαγωγικό κεφάλαιο δείξαμε το DI με συγκεκριμένα παραδείγματα και δεν χρειαζόταν κανένα container.
Ωστόσο, εάν χρειάζεται να διαχειριστείτε μεγάλο αριθμό διαφορετικών αντικειμένων με πολλές εξαρτήσεις, ένα dependency injection container θα είναι πραγματικά χρήσιμο. Αυτό ισχύει, για παράδειγμα, για τις web εφαρμογές που βασίζονται σε ένα framework.
Στο προηγούμενο κεφάλαιο, παρουσιάσαμε τις κλάσεις Article
και
UserController
. Και οι δύο έχουν κάποιες εξαρτήσεις, δηλαδή τη βάση
δεδομένων και το factory ArticleFactory
. Και για αυτές τις κλάσεις θα
δημιουργήσουμε τώρα ένα container. Φυσικά, για ένα τόσο απλό παράδειγμα, δεν
έχει νόημα να έχουμε ένα container. Αλλά θα το δημιουργήσουμε για να
δείξουμε πώς μοιάζει και πώς λειτουργεί.
Εδώ είναι ένα απλό hardcoded container για το αναφερόμενο παράδειγμα:
class Container
{
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
public function createUserController(): UserController
{
return new UserController($this->createArticleFactory());
}
}
Η χρήση θα έμοιαζε ως εξής:
$container = new Container;
$controller = $container->createUserController();
Απλώς ρωτάμε το container για το αντικείμενο και δεν χρειάζεται πλέον να γνωρίζουμε τίποτα για το πώς να το δημιουργήσουμε και ποιες είναι οι εξαρτήσεις του. όλα αυτά τα γνωρίζει το container. Οι εξαρτήσεις εισάγονται αυτόματα από το container. Σε αυτό έγκειται η δύναμή του.
Το container έχει προς το παρόν όλα τα δεδομένα γραμμένα απευθείας στον κώδικα. Θα κάνουμε λοιπόν το επόμενο βήμα και θα προσθέσουμε παραμέτρους, ώστε το container να είναι πραγματικά χρήσιμο:
class Container
{
public function __construct(
private array $parameters,
) {
}
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['db.dsn'],
$this->parameters['db.user'],
$this->parameters['db.password'],
);
}
// ...
}
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
Οι προσεκτικοί αναγνώστες μπορεί να έχουν παρατηρήσει ένα
συγκεκριμένο πρόβλημα. Κάθε φορά που λαμβάνω ένα αντικείμενο
UserController
, δημιουργείται επίσης μια νέα παρουσία του
ArticleFactory
και της βάσης δεδομένων. Αυτό σίγουρα δεν το θέλουμε.
Θα προσθέσουμε λοιπόν μια μέθοδο getService()
, η οποία θα επιστρέφει
πάντα τις ίδιες παρουσίες:
class Container
{
private array $services = [];
public function __construct(
private array $parameters,
) {
}
public function getService(string $name): object
{
if (!isset($this->services[$name])) {
// το getService('Database') θα καλέσει το createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
Κατά την πρώτη κλήση, π.χ. $container->getService('Database')
, θα ζητήσει από
το createDatabase()
να δημιουργήσει το αντικείμενο της βάσης δεδομένων,
το οποίο θα αποθηκεύσει στον πίνακα $services
και κατά την επόμενη
κλήση θα το επιστρέψει απευθείας.
Θα τροποποιήσουμε και το υπόλοιπο container, ώστε να χρησιμοποιεί το
getService()
:
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createUserController(): UserController
{
return new UserController($this->getService('ArticleFactory'));
}
}
Παρεμπιπτόντως, ο όρος υπηρεσία (service) αναφέρεται σε οποιοδήποτε
αντικείμενο διαχειρίζεται το container. Γι' αυτό και το όνομα της μεθόδου
getService()
.
Έτοιμο. Έχουμε ένα πλήρως λειτουργικό DI container! Και μπορούμε να το χρησιμοποιήσουμε:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('UserController');
$database = $container->getService('Database');
Όπως βλέπετε, η σύνταξη ενός DIC δεν είναι κάτι περίπλοκο. Αξίζει να θυμηθούμε ότι τα ίδια τα αντικείμενα δεν γνωρίζουν ότι τα δημιουργεί κάποιο container. Έτσι, είναι δυνατόν να δημιουργηθεί με αυτόν τον τρόπο οποιοδήποτε αντικείμενο στην PHP χωρίς παρέμβαση στον πηγαίο κώδικά του.
Η χειροκίνητη δημιουργία και συντήρηση της κλάσης του container μπορεί γρήγορα να γίνει εφιάλτης. Στο επόμενο κεφάλαιο, θα μιλήσουμε λοιπόν για το Nette DI Container, το οποίο μπορεί να δημιουργείται και να ενημερώνεται σχεδόν από μόνο του.