Καθολική κατάσταση και singletons
Προειδοποίηση: Οι ακόλουθες κατασκευές είναι σημάδι κακώς σχεδιασμένου κώδικα:
Foo::getInstance()
DB::insert(...)
Article::setDb($db)
ClassName::$var
ήstatic::$var
Εμφανίζονται κάποιες από αυτές τις κατασκευές στον κώδικά σας; Τότε έχετε την ευκαιρία να τον βελτιώσετε. Ίσως σκέφτεστε ότι πρόκειται για συνήθεις κατασκευές, τις οποίες βλέπετε ίσως και σε παραδείγματα λύσεων διαφόρων βιβλιοθηκών και frameworks. Αν ισχύει αυτό, τότε ο σχεδιασμός του κώδικά τους δεν είναι καλός.
Τώρα σίγουρα δεν μιλάμε για κάποια ακαδημαϊκή καθαρότητα. Όλες αυτές οι κατασκευές έχουν ένα κοινό: χρησιμοποιούν καθολική κατάσταση. Και αυτή έχει καταστροφική επίδραση στην ποιότητα του κώδικα. Οι κλάσεις λένε ψέματα για τις εξαρτήσεις τους. Ο κώδικας γίνεται απρόβλεπτος. Μπερδεύει τους προγραμματιστές και μειώνει την αποδοτικότητά τους.
Σε αυτό το κεφάλαιο θα εξηγήσουμε γιατί συμβαίνει αυτό και πώς να αποφύγετε την καθολική κατάσταση.
Καθολική σύζευξη
Σε έναν ιδανικό κόσμο, ένα αντικείμενο θα έπρεπε να μπορεί να
επικοινωνεί μόνο με αντικείμενα που του έχουν περαστεί απευθείας. Εάν
δημιουργήσω δύο αντικείμενα A
και B
και ποτέ δεν περάσω
αναφορά μεταξύ τους, τότε ούτε το A
, ούτε το B
, μπορούν να
φτάσουν στο άλλο αντικείμενο ή να αλλάξουν την κατάστασή του. Αυτό
είναι ένα πολύ επιθυμητό χαρακτηριστικό του κώδικα. Είναι παρόμοιο με
το να έχετε μια μπαταρία και μια λάμπα. η λάμπα δεν θα ανάψει αν δεν τη
συνδέσετε με την μπαταρία με ένα καλώδιο.
Αυτό όμως δεν ισχύει για τις καθολικές (στατικές) μεταβλητές ή τα
singletons. Το αντικείμενο A
θα μπορούσε ασύρματα να φτάσει στο
αντικείμενο C
και να το τροποποιήσει χωρίς καμία μεταβίβαση
αναφοράς, καλώντας το C::changeSomething()
. Εάν το αντικείμενο B
αρπάξει επίσης το καθολικό C
, τότε τα A
και B
μπορούν να αλληλεπιδράσουν μέσω του C
.
Η χρήση καθολικών μεταβλητών εισάγει στο σύστημα μια νέα μορφή
ασύρματης σύζευξης, η οποία δεν είναι ορατή από έξω. Δημιουργεί ένα
παραπέτασμα καπνού που περιπλέκει την κατανόηση και τη χρήση του
κώδικα. Για να κατανοήσουν πραγματικά οι προγραμματιστές τις
εξαρτήσεις, πρέπει να διαβάσουν κάθε γραμμή του πηγαίου κώδικα. Αντί
απλώς να εξοικειωθούν με τα interfaces των κλάσεων. Επιπλέον, πρόκειται για
μια εντελώς περιττή σύζευξη. Η καθολική κατάσταση χρησιμοποιείται
επειδή είναι εύκολα προσβάσιμη από οπουδήποτε και επιτρέπει, για
παράδειγμα, την εγγραφή στη βάση δεδομένων μέσω της καθολικής
(στατικής) μεθόδου DB::insert()
. Αλλά όπως θα δείξουμε, το πλεονέκτημα
που προσφέρει είναι ασήμαντο, ενώ αντίθετα οι επιπλοκές που προκαλεί
είναι μοιραίες.
Από την άποψη της συμπεριφοράς, δεν υπάρχει διαφορά μεταξύ καθολικής και στατικής μεταβλητής. Είναι εξίσου επιβλαβείς.
Απόκοσμη δράση από απόσταση
“Απόκοσμη δράση από απόσταση” (Spooky action at a distance) – έτσι ονόμασε περίφημα το 1935 ο Άλμπερτ Αϊνστάιν ένα φαινόμενο στην κβαντική φυσική που του προκαλούσε ανατριχίλα. Πρόκειται για την κβαντική διεμπλοκή, της οποίας η ιδιαιτερότητα είναι ότι όταν μετράτε πληροφορίες για ένα σωματίδιο, επηρεάζετε αμέσως το άλλο σωματίδιο, ακόμα κι αν απέχουν εκατομμύρια έτη φωτός. Αυτό φαινομενικά παραβιάζει τον θεμελιώδη νόμο του σύμπαντος ότι τίποτα δεν μπορεί να ταξιδέψει γρηγορότερα από το φως.
Στον κόσμο του λογισμικού, μπορούμε να ονομάσουμε “απόκοσμη δράση από απόσταση” μια κατάσταση όπου εκκινούμε μια διαδικασία, την οποία θεωρούμε απομονωμένη (επειδή δεν της περάσαμε καμία αναφορά), αλλά σε απομακρυσμένα σημεία του συστήματος συμβαίνουν απροσδόκητες αλληλεπιδράσεις και αλλαγές κατάστασης, για τις οποίες δεν είχαμε ιδέα. Αυτό μπορεί να συμβεί μόνο μέσω της καθολικής κατάστασης.
Φανταστείτε ότι εντάσσεστε σε μια ομάδα προγραμματιστών ενός έργου που έχει μια εκτεταμένη, ώριμη βάση κώδικα. Ο νέος σας προϊστάμενος σας ζητά να υλοποιήσετε μια νέα λειτουργία και εσείς, ως σωστός προγραμματιστής, ξεκινάτε γράφοντας ένα τεστ. Επειδή όμως είστε νέοι στο έργο, κάνετε πολλά διερευνητικά τεστ του τύπου “τι θα συμβεί αν καλέσω αυτή τη μέθοδο”. Και δοκιμάζετε να γράψετε το ακόλουθο τεστ:
function testCreditCardCharge()
{
$cc = new CreditCard('1234567890123456', 5, 2028); // ο αριθμός της κάρτας σας
$cc->charge(100);
}
Εκτελείτε τον κώδικα, ίσως αρκετές φορές, και μετά από λίγο παρατηρείτε ειδοποιήσεις στο κινητό σας από την τράπεζα ότι κάθε φορά που εκτελείται, χρεώνονται 100 δολάρια από την πιστωτική σας κάρτα 🤦♂️
Πώς στο καλό μπόρεσε το τεστ να προκαλέσει πραγματική χρέωση χρημάτων; Η λειτουργία με πιστωτική κάρτα δεν είναι εύκολη. Πρέπει να επικοινωνήσετε με μια web υπηρεσία τρίτου μέρους, πρέπει να γνωρίζετε τη διεύθυνση URL αυτής της web υπηρεσίας, πρέπει να συνδεθείτε και ούτω καθεξής. Καμία από αυτές τις πληροφορίες δεν περιέχεται στο τεστ. Ακόμα χειρότερα, ούτε καν γνωρίζετε πού βρίσκονται αυτές οι πληροφορίες, και επομένως ούτε πώς να κάνετε mock τις εξωτερικές εξαρτήσεις, ώστε κάθε εκτέλεση να μην οδηγεί ξανά σε χρέωση 100 δολαρίων. Και πώς έπρεπε να γνωρίζετε, ως νέος προγραμματιστής, ότι αυτό που ετοιμαζόσασταν να κάνετε θα οδηγούσε στο να γίνετε 100 δολάρια φτωχότεροι;
Αυτή είναι η απόκοσμη δράση από απόσταση!
Δεν σας μένει παρά να ψάξετε για πολλή ώρα σε πολλούς πηγαίους
κώδικες, να ρωτήσετε παλαιότερους και πιο έμπειρους συναδέλφους, μέχρι
να καταλάβετε πώς λειτουργούν οι συνδέσεις στο έργο. Αυτό οφείλεται
στο ότι, κοιτάζοντας το interface της κλάσης CreditCard
, δεν μπορείτε να
προσδιορίσετε την καθολική κατάσταση που πρέπει να αρχικοποιηθεί.
Ακόμη και η ματιά στον πηγαίο κώδικα της κλάσης δεν σας αποκαλύπτει
ποια μέθοδο αρχικοποίησης πρέπει να καλέσετε. Στην καλύτερη περίπτωση,
μπορείτε να βρείτε μια καθολική μεταβλητή στην οποία γίνεται πρόσβαση
και από αυτήν να προσπαθήσετε να μαντέψετε πώς να την
αρχικοποιήσετε.
Οι κλάσεις σε ένα τέτοιο έργο είναι παθολογικοί ψεύτες. Η πιστωτική
κάρτα προσποιείται ότι αρκεί να την παρουσιάσετε και να καλέσετε τη
μέθοδο charge()
. Κρυφά, όμως, συνεργάζεται με μια άλλη κλάση
PaymentGateway
, η οποία αντιπροσωπεύει την πύλη πληρωμών. Ακόμη και το
interface της λέει ότι μπορεί να αρχικοποιηθεί ξεχωριστά, αλλά στην
πραγματικότητα αντλεί διαπιστευτήρια από κάποιο αρχείο διαμόρφωσης
και ούτω καθεξής. Για τους προγραμματιστές που έγραψαν αυτόν τον
κώδικα, είναι σαφές ότι η CreditCard
χρειάζεται την PaymentGateway
.
Έγραψαν τον κώδικα με αυτόν τον τρόπο. Αλλά για οποιονδήποτε είναι νέος
στο έργο, είναι ένα απόλυτο μυστήριο και εμποδίζει τη μάθηση.
Πώς να διορθώσετε την κατάσταση; Εύκολα. Αφήστε το API να δηλώσει τις εξαρτήσεις.
function testCreditCardCharge()
{
$gateway = new PaymentGateway(/* ... */);
$cc = new CreditCard('1234567890123456', 5, 2028);
$cc->charge($gateway, 100);
}
Παρατηρήστε πώς οι συνδέσεις μέσα στον κώδικα γίνονται ξαφνικά
προφανείς. Με το γεγονός ότι η μέθοδος charge()
δηλώνει ότι
χρειάζεται την PaymentGateway
, δεν χρειάζεται να ρωτήσετε κανέναν πώς
συνδέεται ο κώδικας. Γνωρίζετε ότι πρέπει να δημιουργήσετε την
παρουσία της, και όταν προσπαθήσετε να το κάνετε, θα διαπιστώσετε ότι
πρέπει να δώσετε παραμέτρους πρόσβασης. Χωρίς αυτές, ο κώδικας δεν θα
μπορούσε καν να εκτελεστεί.
Και κυρίως, τώρα μπορείτε να κάνετε mock την πύλη πληρωμών, ώστε να μην χρεώνεστε 100 δολάρια κάθε φορά που εκτελείτε το τεστ.
Η καθολική κατάσταση κάνει τα αντικείμενά σας να μπορούν κρυφά να έχουν πρόσβαση σε πράγματα που δεν δηλώνονται στα API τους, και ως αποτέλεσμα, μετατρέπει τα API σας σε παθολογικούς ψεύτες.
Ίσως να μην το είχατε σκεφτεί έτσι προηγουμένως, αλλά κάθε φορά που χρησιμοποιείτε καθολική κατάσταση, δημιουργείτε μυστικούς ασύρματους διαύλους επικοινωνίας. Η απόκοσμη δράση από απόσταση αναγκάζει τους προγραμματιστές να διαβάζουν κάθε γραμμή κώδικα για να κατανοήσουν τις πιθανές αλληλεπιδράσεις, μειώνει την παραγωγικότητα των προγραμματιστών και μπερδεύει τα νέα μέλη της ομάδας. Εάν είστε εσείς αυτός που δημιούργησε τον κώδικα, γνωρίζετε τις πραγματικές εξαρτήσεις, αλλά οποιοσδήποτε έρθει μετά από εσάς είναι αβοήθητος.
Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection.
Ευθραυστότητα της καθολικής κατάστασης
Στον κώδικα που χρησιμοποιεί καθολική κατάσταση και singletons, δεν είναι ποτέ σίγουρο πότε και ποιος άλλαξε αυτή την κατάσταση. Αυτός ο κίνδυνος εμφανίζεται ήδη κατά την αρχικοποίηση. Ο ακόλουθος κώδικας υποτίθεται ότι δημιουργεί μια σύνδεση βάσης δεδομένων και αρχικοποιεί την πύλη πληρωμών, ωστόσο προκαλεί συνεχώς εξαίρεση και η εύρεση της αιτίας είναι εξαιρετικά χρονοβόρα:
PaymentGateway::init();
DB::init('mysql:', 'user', 'password');
Πρέπει να εξετάσετε λεπτομερώς τον κώδικα για να διαπιστώσετε ότι το
αντικείμενο PaymentGateway
έχει ασύρματη πρόσβαση σε άλλα
αντικείμενα, ορισμένα από τα οποία απαιτούν σύνδεση βάσης δεδομένων.
Δηλαδή, είναι απαραίτητο να αρχικοποιήσετε τη βάση δεδομένων πριν από
την PaymentGateway
. Ωστόσο, το παραπέτασμα καπνού της καθολικής
κατάστασης το κρύβει αυτό από εσάς. Πόσο χρόνο θα είχατε εξοικονομήσει
εάν τα API των μεμονωμένων κλάσεων δεν εξαπατούσαν και δήλωναν τις
εξαρτήσεις τους;
$db = new DB('mysql:', 'user', 'password');
$gateway = new PaymentGateway($db, ...);
Ένα παρόμοιο πρόβλημα εμφανίζεται και κατά τη χρήση καθολικής πρόσβασης στη σύνδεση της βάσης δεδομένων:
use Illuminate\Support\Facades\DB;
class Article
{
public function save(): void
{
DB::insert(/* ... */);
}
}
Κατά την κλήση της μεθόδου save()
, δεν είναι βέβαιο εάν έχει ήδη
δημιουργηθεί η σύνδεση με τη βάση δεδομένων και ποιος φέρει την ευθύνη
για τη δημιουργία της. Εάν θέλουμε, για παράδειγμα, να αλλάξουμε τη
σύνδεση της βάσης δεδομένων κατά την εκτέλεση, ίσως για λόγους δοκιμών,
θα έπρεπε πιθανότατα να δημιουργήσουμε επιπλέον μεθόδους όπως
DB::reconnect(...)
ή DB::reconnectForTest()
.
Ας εξετάσουμε ένα παράδειγμα:
$article = new Article;
// ...
DB::reconnectForTest();
Foo::doSomething();
$article->save();
Πού έχουμε τη βεβαιότητα ότι κατά την κλήση του $article->save()
χρησιμοποιείται όντως η δοκιμαστική βάση δεδομένων; Τι γίνεται αν η
μέθοδος Foo::doSomething()
άλλαξε την καθολική σύνδεση της βάσης
δεδομένων; Για να το διαπιστώσουμε, θα έπρεπε να εξετάσουμε τον πηγαίο
κώδικα της κλάσης Foo
και πιθανώς και πολλών άλλων κλάσεων. Αυτή η
προσέγγιση, ωστόσο, θα έδινε μόνο μια βραχυπρόθεσμη απάντηση, καθώς η
κατάσταση μπορεί να αλλάξει στο μέλλον.
Και τι γίνεται αν μεταφέρουμε τη σύνδεση με τη βάση δεδομένων σε μια
στατική μεταβλητή μέσα στην κλάση Article
;
class Article
{
private static DB $db;
public static function setDb(DB $db): void
{
self::$db = $db;
}
public function save(): void
{
self::$db->insert(/* ... */);
}
}
Αυτό δεν άλλαξε απολύτως τίποτα. Το πρόβλημα είναι η καθολική
κατάσταση και είναι εντελώς αδιάφορο σε ποια κλάση κρύβεται. Σε αυτή
την περίπτωση, όπως και στην προηγούμενη, δεν έχουμε καμία ένδειξη κατά
την κλήση της μεθόδου $article->save()
για το σε ποια βάση δεδομένων
θα γίνει η εγγραφή. Οποιοσδήποτε στο άλλο άκρο της εφαρμογής θα
μπορούσε ανά πάσα στιγμή να αλλάξει τη βάση δεδομένων χρησιμοποιώντας
το Article::setDb()
. Κάτω από τα χέρια μας.
Η καθολική κατάσταση καθιστά την εφαρμογή μας εξαιρετικά εύθραυστη.
Υπάρχει όμως ένας απλός τρόπος για να αντιμετωπίσουμε αυτό το πρόβλημα. Αρκεί να αφήσουμε το API να δηλώσει τις εξαρτήσεις, εξασφαλίζοντας έτσι τη σωστή λειτουργικότητα.
class Article
{
public function __construct(
private DB $db,
) {
}
public function save(): void
{
$this->db->insert(/* ... */);
}
}
$article = new Article($db);
// ...
Foo::doSomething();
$article->save();
Χάρη σε αυτή την προσέγγιση, εξαλείφεται η ανησυχία για κρυφές και απροσδόκητες αλλαγές στη σύνδεση της βάσης δεδομένων. Τώρα έχουμε τη βεβαιότητα για το πού αποθηκεύεται το άρθρο και καμία τροποποίηση του κώδικα μέσα σε μια άλλη άσχετη κλάση δεν μπορεί πλέον να αλλάξει την κατάσταση. Ο κώδικας δεν είναι πλέον εύθραυστος, αλλά σταθερός.
Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection.
Singleton
Το Singleton είναι ένα σχεδιαστικό πρότυπο που, σύμφωνα με τον ορισμό από τη γνωστή δημοσίευση Gang of Four, περιορίζει μια κλάση σε μία μόνο παρουσία και προσφέρει καθολική πρόσβαση σε αυτήν. Η υλοποίηση αυτού του προτύπου συνήθως μοιάζει με τον ακόλουθο κώδικα:
class Singleton
{
private static self $instance;
public static function getInstance(): self
{
self::$instance ??= new self;
return self::$instance;
}
// και άλλες μέθοδοι που εκτελούν τις λειτουργίες της συγκεκριμένης κλάσης
}
Δυστυχώς, το singleton εισάγει καθολική κατάσταση στην εφαρμογή. Και όπως δείξαμε παραπάνω, η καθολική κατάσταση είναι ανεπιθύμητη. Επομένως, το singleton θεωρείται αντι-πρότυπο (antipattern).
Μην χρησιμοποιείτε singletons στον κώδικά σας και αντικαταστήστε τα με
άλλους μηχανισμούς. Τα singletons πραγματικά δεν τα χρειάζεστε. Εάν, ωστόσο,
χρειάζεται να εγγυηθείτε την ύπαρξη μιας μόνο παρουσίας της κλάσης για
ολόκληρη την εφαρμογή, αφήστε το στον DI container. Δημιουργήστε έτσι ένα
application singleton, δηλαδή μια υπηρεσία. Με αυτόν τον τρόπο, η κλάση παύει να
ασχολείται με τη διασφάλιση της μοναδικότητάς της (δηλ. δεν θα έχει
μέθοδο getInstance()
και στατική μεταβλητή) και θα εκτελεί μόνο τις
λειτουργίες της. Έτσι, παύει να παραβιάζει την αρχή της μοναδικής
ευθύνης (single responsibility principle).
Καθολική κατάσταση έναντι δοκιμών
Κατά τη συγγραφή δοκιμών, υποθέτουμε ότι κάθε δοκιμή είναι μια απομονωμένη μονάδα και ότι καμία εξωτερική κατάσταση δεν εισέρχεται σε αυτήν. Και καμία κατάσταση δεν εξέρχεται από τις δοκιμές. Μετά την ολοκλήρωση της δοκιμής, όλη η σχετική κατάσταση με τη δοκιμή θα πρέπει να αφαιρεθεί αυτόματα από τον garbage collector. Χάρη σε αυτό, οι δοκιμές είναι απομονωμένες. Επομένως, μπορούμε να εκτελέσουμε τις δοκιμές με οποιαδήποτε σειρά.
Εάν, ωστόσο, υπάρχουν καθολικές καταστάσεις/singletons, όλες αυτές οι ευχάριστες υποθέσεις καταρρέουν. Η κατάσταση μπορεί να εισέλθει και να εξέλθει από τη δοκιμή. Ξαφνικά, η σειρά των δοκιμών μπορεί να έχει σημασία.
Για να μπορέσουμε καν να δοκιμάσουμε τα singletons, οι προγραμματιστές
συχνά πρέπει να χαλαρώσουν τις ιδιότητές τους, για παράδειγμα
επιτρέποντας την αντικατάσταση της παρουσίας με μια άλλη. Τέτοιες
λύσεις είναι στην καλύτερη περίπτωση ένα hack, που δημιουργεί κώδικα
δύσκολο στη συντήρηση και την κατανόηση. Κάθε δοκιμή ή μέθοδος
tearDown()
, που επηρεάζει οποιαδήποτε καθολική κατάσταση, πρέπει να
αναιρέσει αυτές τις αλλαγές.
Η καθολική κατάσταση είναι ο μεγαλύτερος πονοκέφαλος στις δοκιμές μονάδας (unit testing)!
Πώς να διορθώσετε την κατάσταση; Εύκολα. Μην γράφετε κώδικα που χρησιμοποιεί singletons, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection.
Καθολικές σταθερές
Η καθολική κατάσταση δεν περιορίζεται μόνο στη χρήση singletons και στατικών μεταβλητών, αλλά μπορεί να αφορά και τις καθολικές σταθερές.
Οι σταθερές, η τιμή των οποίων δεν μας προσφέρει καμία νέα (M_PI
)
ή χρήσιμη (PREG_BACKTRACK_LIMIT_ERROR
) πληροφορία, είναι σαφώς εντάξει.
Αντίθετα, οι σταθερές που χρησιμεύουν ως τρόπος για να περάσουμε
ασύρματα πληροφορίες μέσα στον κώδικα, δεν είναι τίποτα άλλο από
κρυφές εξαρτήσεις. Όπως για παράδειγμα το LOG_FILE
στο ακόλουθο
παράδειγμα. Η χρήση της σταθεράς FILE_APPEND
είναι απολύτως σωστή.
const LOG_FILE = '...';
class Foo
{
public function doSomething()
{
// ...
file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND);
// ...
}
}
Σε αυτή την περίπτωση, θα έπρεπε να δηλώσουμε μια παράμετρο στον
κατασκευαστή της κλάσης Foo
, ώστε να γίνει μέρος του API:
class Foo
{
public function __construct(
private string $logFile,
) {
}
public function doSomething()
{
// ...
file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
// ...
}
}
Τώρα μπορούμε να περάσουμε την πληροφορία για τη διαδρομή του αρχείου καταγραφής και να την αλλάξουμε εύκολα ανάλογα με τις ανάγκες, πράγμα που διευκολύνει τις δοκιμές και τη συντήρηση του κώδικα.
Καθολικές συναρτήσεις και στατικές μέθοδοι
Θέλουμε να τονίσουμε ότι η ίδια η χρήση στατικών μεθόδων και
καθολικών συναρτήσεων δεν είναι προβληματική. Εξηγήσαμε σε τι
συνίσταται η ακαταλληλότητα της χρήσης του DB::insert()
και
παρόμοιων μεθόδων, αλλά πάντα αφορούσε μόνο την καθολική κατάσταση, η
οποία είναι αποθηκευμένη σε κάποια στατική μεταβλητή. Η μέθοδος
DB::insert()
απαιτεί την ύπαρξη στατικής μεταβλητής, επειδή σε αυτήν
είναι αποθηκευμένη η σύνδεση με τη βάση δεδομένων. Χωρίς αυτή τη
μεταβλητή, θα ήταν αδύνατο να υλοποιηθεί η μέθοδος.
Η χρήση ντετερμινιστικών στατικών μεθόδων και συναρτήσεων, όπως
DateTime::createFromFormat()
, Closure::fromCallable
, strlen()
και πολλών
άλλων, είναι απολύτως σύμφωνη με το dependency injection. Αυτές οι συναρτήσεις
επιστρέφουν πάντα τα ίδια αποτελέσματα για τις ίδιες παραμέτρους
εισόδου και είναι επομένως προβλέψιμες. Δεν χρησιμοποιούν καμία
καθολική κατάσταση.
Υπάρχουν, ωστόσο, και συναρτήσεις στην PHP που δεν είναι
ντετερμινιστικές. Σε αυτές ανήκει, για παράδειγμα, η συνάρτηση
htmlspecialchars()
. Η τρίτη της παράμετρος $encoding
, εάν δεν
αναφέρεται, έχει ως προεπιλεγμένη τιμή την τιμή της επιλογής
διαμόρφωσης ini_get('default_charset')
. Επομένως, συνιστάται να αναφέρεται
πάντα αυτή η παράμετρος και να αποφεύγεται έτσι η πιθανή απρόβλεπτη
συμπεριφορά της συνάρτησης. Το Nette το κάνει αυτό με συνέπεια.
Ορισμένες συναρτήσεις, όπως strtolower()
, strtoupper()
και
παρόμοιες, στο πρόσφατο παρελθόν συμπεριφέρονταν μη ντετερμινιστικά
και εξαρτώνταν από τη ρύθμιση setlocale()
. Αυτό προκαλούσε πολλές
επιπλοκές, συχνότερα κατά την εργασία με την τουρκική γλώσσα. Αυτή,
δηλαδή, διακρίνει το πεζό και το κεφαλαίο γράμμα I
με και χωρίς
τελεία. Έτσι, το strtolower('I')
επέστρεφε τον χαρακτήρα ı
και το
strtoupper('i')
τον χαρακτήρα İ
, πράγμα που οδηγούσε στο να
αρχίσουν οι εφαρμογές να προκαλούν μια σειρά από μυστηριώδη σφάλματα.
Αυτό το πρόβλημα, ωστόσο, διορθώθηκε στην έκδοση PHP 8.2 και οι
συναρτήσεις δεν εξαρτώνται πλέον από το locale.
Πρόκειται για ένα ωραίο παράδειγμα του πώς η καθολική κατάσταση ταλαιπώρησε χιλιάδες προγραμματιστές σε όλο τον κόσμο. Η λύση ήταν η αντικατάστασή της με dependency injection.
Πότε είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση?
Υπάρχουν ορισμένες συγκεκριμένες καταστάσεις όπου είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση. Για παράδειγμα, κατά τον εντοπισμό σφαλμάτων στον κώδικα, όταν χρειάζεται να εκτυπώσετε την τιμή μιας μεταβλητής ή να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος. Σε τέτοιες περιπτώσεις, που αφορούν προσωρινές ενέργειες οι οποίες θα αφαιρεθούν αργότερα από τον κώδικα, είναι δυνατόν να χρησιμοποιηθεί θεμιτά ένας καθολικά διαθέσιμος dumper ή χρονόμετρο. Αυτά τα εργαλεία, δηλαδή, δεν αποτελούν μέρος του σχεδιασμού του κώδικα.
Ένα άλλο παράδειγμα είναι οι συναρτήσεις για την εργασία με
κανονικές εκφράσεις preg_*
, οι οποίες εσωτερικά αποθηκεύουν τις
μεταγλωττισμένες κανονικές εκφράσεις σε μια στατική cache στη μνήμη.
Όταν λοιπόν καλείτε την ίδια κανονική έκφραση πολλές φορές σε
διαφορετικά σημεία του κώδικα, μεταγλωττίζεται μόνο μία φορά. Η cache
εξοικονομεί απόδοση και ταυτόχρονα είναι για τον χρήστη εντελώς
αόρατη, επομένως μια τέτοια χρήση μπορεί να θεωρηθεί θεμιτή.
Σύνοψη
Συζητήσαμε γιατί έχει νόημα:
- Να αφαιρέσετε όλες τις στατικές μεταβλητές από τον κώδικα
- Να δηλώσετε τις εξαρτήσεις
- Και να χρησιμοποιείτε dependency injection
Όταν σκέφτεστε τον σχεδιασμό του κώδικα, σκεφτείτε ότι κάθε
static $foo
αποτελεί πρόβλημα. Για να είναι ο κώδικάς σας ένα
περιβάλλον που σέβεται το DI, είναι απαραίτητο να εξαλείψετε εντελώς
την καθολική κατάσταση και να την αντικαταστήσετε με dependency injection.
Κατά τη διάρκεια αυτής της διαδικασίας, ίσως διαπιστώσετε ότι είναι απαραίτητο να χωρίσετε την κλάση, επειδή έχει περισσότερες από μία ευθύνες. Μην το φοβάστε. επιδιώξτε την αρχή της μοναδικής ευθύνης.
Θα ήθελα να ευχαριστήσω τον Miško Hevery, του οποίου τα άρθρα, όπως το Flaw: Brittle Global State & Singletons, αποτελούν τη βάση αυτού του κεφαλαίου.