Παράδοση εξαρτήσεων
Τα ορίσματα, ή στην ορολογία του DI “εξαρτήσεις”, μπορούν να παραδοθούν στις κλάσεις με τους ακόλουθους κύριους τρόπους:
- παράδοση μέσω κατασκευαστή
- παράδοση μέσω μεθόδου (του λεγόμενου setter)
- ρύθμιση μεταβλητής
- με μέθοδο, annotation ή attribute inject
Τώρα θα δείξουμε τις διάφορες παραλλαγές με συγκεκριμένα παραδείγματα.
Παράδοση μέσω κατασκευαστή
Οι εξαρτήσεις παραδίδονται τη στιγμή της δημιουργίας του αντικειμένου ως ορίσματα του κατασκευαστή:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Αυτή η μορφή είναι κατάλληλη για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της, καθώς χωρίς αυτές δεν θα είναι δυνατή η δημιουργία της παρουσίας.
Από την PHP 8.0, μπορούμε να χρησιμοποιήσουμε μια συντομότερη μορφή σύνταξης (constructor property promotion), η οποία είναι λειτουργικά ισοδύναμη:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Από την PHP 8.1, η μεταβλητή μπορεί να επισημανθεί με τη σημαία
readonly
, η οποία δηλώνει ότι το περιεχόμενο της μεταβλητής δεν θα
αλλάξει πλέον:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Το DI container παραδίδει αυτόματα τις εξαρτήσεις στον κατασκευαστή χρησιμοποιώντας autowiring. Τα ορίσματα που δεν μπορούν να παραδοθούν με αυτόν τον τρόπο (π.χ. strings, αριθμοί, booleans) τα γράψτε στη διαμόρφωση.
Constructor hell
Ο όρος constructor hell περιγράφει την κατάσταση όπου ένας απόγονος κληρονομεί από μια γονική κλάση, της οποίας ο κατασκευαστής απαιτεί εξαρτήσεις, και ταυτόχρονα ο απόγονος απαιτεί εξαρτήσεις. Ταυτόχρονα, πρέπει να αναλάβει και να παραδώσει και τις γονικές:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Το πρόβλημα προκύπτει τη στιγμή που θα θέλαμε να αλλάξουμε τον
κατασκευαστή της κλάσης BaseClass
, για παράδειγμα, όταν προστεθεί
μια νέα εξάρτηση. Τότε είναι απαραίτητο να τροποποιηθούν και όλοι οι
κατασκευαστές των απογόνων. Κάτι που καθιστά μια τέτοια τροποποίηση
κόλαση.
Πώς να το αποτρέψουμε αυτό; Η λύση είναι να προτιμάμε τη σύνθεση έναντι κληρονομικότητας.
Δηλαδή, θα σχεδιάσουμε τον κώδικα διαφορετικά. Θα αποφεύγουμε τις αφηρημένες
Base*
κλάσεις. Αντί η MyClass
να αποκτά μια συγκεκριμένη
λειτουργικότητα κληρονομώντας από την BaseClass
, θα της παραδοθεί
αυτή η λειτουργικότητα ως εξάρτηση:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Παράδοση μέσω setter
Οι εξαρτήσεις παραδίδονται καλώντας μια μέθοδο, η οποία τις
αποθηκεύει σε μια ιδιωτική μεταβλητή. Η συνήθης σύμβαση ονομασίας
αυτών των μεθόδων είναι η μορφή set*()
, γι' αυτό ονομάζονται setters,
αλλά μπορούν φυσικά να ονομάζονται και οτιδήποτε άλλο.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Αυτός ο τρόπος είναι κατάλληλος για προαιρετικές εξαρτήσεις που δεν είναι απαραίτητες για τη λειτουργία της κλάσης, καθώς δεν εγγυάται ότι το αντικείμενο θα λάβει πραγματικά την εξάρτηση (δηλαδή ότι ο χρήστης θα καλέσει τη μέθοδο).
Ταυτόχρονα, αυτός ο τρόπος επιτρέπει την επανειλημμένη κλήση του setter
και την αλλαγή της εξάρτησης. Εάν αυτό δεν είναι επιθυμητό, προσθέτουμε
έναν έλεγχο στη μέθοδο, ή από την PHP 8.1 επισημαίνουμε την property
$cache
με τη σημαία readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Η κλήση του setter ορίζεται στη διαμόρφωση του DI container στο κλειδί setup. Και εδώ χρησιμοποιείται η αυτόματη παράδοση εξαρτήσεων μέσω autowiring:
services:
- create: MyClass
setup:
- setCache
Ρύθμιση μεταβλητής
Οι εξαρτήσεις παραδίδονται γράφοντας απευθείας στη μεταβλητή μέλους:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Αυτός ο τρόπος θεωρείται ακατάλληλος, επειδή η μεταβλητή μέλους
πρέπει να δηλωθεί ως public
. Και επομένως δεν έχουμε έλεγχο ότι η
παραδοθείσα εξάρτηση θα είναι πράγματι του συγκεκριμένου τύπου (ίσχυε
πριν την PHP 7.4) και χάνουμε τη δυνατότητα να αντιδράσουμε στη νέα
εκχωρημένη εξάρτηση με δικό μας κώδικα, για παράδειγμα, να αποτρέψουμε
την επακόλουθη αλλαγή. Ταυτόχρονα, η μεταβλητή γίνεται μέρος του
δημόσιου interface της κλάσης, κάτι που μπορεί να μην είναι επιθυμητό.
Η ρύθμιση της μεταβλητής ορίζεται στη διαμόρφωση του DI container στην ενότητα setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Ενώ οι τρεις προηγούμενοι τρόποι ισχύουν γενικά σε όλες τις αντικειμενοστραφείς γλώσσες, η έγχυση με μέθοδο, annotation ή attribute inject είναι ειδική αποκλειστικά για τους presenters στο Nette. Αυτά συζητούνται σε ξεχωριστό κεφάλαιο.
Ποιον τρόπο να επιλέξω;
- ο κατασκευαστής είναι κατάλληλος για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της
- ο setter είναι αντίθετα κατάλληλος για προαιρετικές εξαρτήσεις, ή εξαρτήσεις που μπορεί να χρειαστεί να αλλάξουν περαιτέρω
- οι δημόσιες μεταβλητές δεν είναι κατάλληλες