Παγκόσμια κατάσταση και singletons
Προειδοποίηση: Οι ακόλουθες δομές είναι συμπτώματα κακοσχεδιασμένου κώδικα:
Foo::getInstance()
DB::insert(...)
Article::setDb($db)
ClassName::$var
ήstatic::$var
Συναντάτε κάποια από αυτές τις δομές στον κώδικά σας; Αν ναι, έχετε την ευκαιρία να τον βελτιώσετε. Μπορεί να νομίζετε ότι πρόκειται για κοινές κατασκευές, που συχνά συναντάτε σε παραδείγματα λύσεων διαφόρων βιβλιοθηκών και πλαισίων. Αν αυτό ισχύει, ο σχεδιασμός του κώδικά τους είναι ελαττωματικός.
Εδώ δεν μιλάμε για κάποια ακαδημαϊκή καθαρότητα. Όλες αυτές οι δομές έχουν ένα κοινό: χρησιμοποιούν την παγκόσμια κατάσταση. Και αυτό έχει καταστροφικό αντίκτυπο στην ποιότητα του κώδικα. Οι κλάσεις είναι παραπλανητικές σχετικά με τις εξαρτήσεις τους. Ο κώδικας γίνεται απρόβλεπτος. Δημιουργεί σύγχυση στους προγραμματιστές και μειώνει την αποδοτικότητά τους.
Σε αυτό το κεφάλαιο, θα εξηγήσουμε γιατί συμβαίνει αυτό και πώς να αποφύγετε την παγκόσμια κατάσταση.
Παγκόσμια διασύνδεση
Σε έναν ιδανικό κόσμο, ένα αντικείμενο θα πρέπει να επικοινωνεί μόνο
με αντικείμενα που του έχουν δοθεί απευθείας. Αν
δημιουργήσω δύο αντικείμενα A
και B
και δεν περάσω ποτέ
μια αναφορά μεταξύ τους, τότε ούτε το A
ούτε το B
μπορούν
να έχουν πρόσβαση ή να τροποποιήσουν την κατάσταση του άλλου. Αυτή
είναι μια ιδιαίτερα επιθυμητή ιδιότητα του κώδικα. Είναι σαν να έχουμε
μια μπαταρία και μια λάμπα- η λάμπα δεν θα ανάψει μέχρι να τη συνδέσετε
με ένα καλώδιο στην μπαταρία.
Ωστόσο, αυτό δεν ισχύει για παγκόσμιες (στατικές) μεταβλητές ή singletons.
Το αντικείμενο A
θα μπορούσε χωρίς καλώδιο να προσπελάσει
το αντικείμενο C
και να το τροποποιήσει χωρίς καμία μεταβίβαση
αναφοράς, καλώντας το C::changeSomething()
. Εάν το αντικείμενο B
έχει επίσης πρόσβαση στο παγκόσμιο C
, τότε τα A
και
B
μπορούν να αλληλοεπηρεάζονται μέσω του C
.
Η χρήση παγκόσμιων μεταβλητών εισάγει μια νέα μορφή ασύρματης
σύζευξης που δεν είναι εξωτερικά ορατή. Δημιουργεί ένα προπέτασμα
καπνού που περιπλέκει την κατανόηση και τη χρήση του κώδικα. Για να
κατανοήσουν πραγματικά τις εξαρτήσεις, οι προγραμματιστές πρέπει να
διαβάσουν κάθε γραμμή του πηγαίου κώδικα, αντί να εξοικειωθούν μόνο με
τις διεπαφές των κλάσεων. Επιπλέον, αυτή η εμπλοκή είναι εντελώς
περιττή. Η παγκόσμια κατάσταση χρησιμοποιείται επειδή είναι εύκολα
προσβάσιμη από οπουδήποτε και επιτρέπει, για παράδειγμα, την εγγραφή
σε μια βάση δεδομένων μέσω μιας παγκόσμιας (στατικής) μεθόδου
DB::insert()
. Ωστόσο, όπως θα δούμε, το όφελος που προσφέρει είναι
ελάχιστο, ενώ οι επιπλοκές που εισάγει είναι σοβαρές.
Όσον αφορά τη συμπεριφορά, δεν υπάρχει καμία διαφορά μεταξύ μιας παγκόσμιας και μιας στατικής μεταβλητής. Είναι εξίσου επιβλαβείς.
Η τρομακτική δράση από απόσταση
„Φαινομενική δράση από απόσταση“ – έτσι ονόμασε ο Άλμπερτ Αϊνστάιν ένα φαινόμενο της κβαντικής φυσικής που τον ανατρίχιασε το 1935. Πρόκειται για την κβαντική διεμπλοκή, η ιδιαιτερότητα της οποίας είναι ότι όταν μετράτε πληροφορίες για ένα σωματίδιο, επηρεάζετε αμέσως ένα άλλο σωματίδιο, ακόμη και αν αυτά απέχουν εκατομμύρια έτη φωτός. γεγονός που φαινομενικά παραβιάζει τον θεμελιώδη νόμο του σύμπαντος ότι τίποτα δεν μπορεί να ταξιδέψει γρηγορότερα από το φως.
Στον κόσμο του λογισμικού, μπορούμε να ονομάσουμε „spooky action at a distance“ μια κατάσταση κατά την οποία εκτελούμε μια διαδικασία που νομίζουμε ότι είναι απομονωμένη (επειδή δεν της έχουμε περάσει καμία αναφορά), αλλά απροσδόκητες αλληλεπιδράσεις και αλλαγές κατάστασης συμβαίνουν σε απομακρυσμένες θέσεις του συστήματος για τις οποίες δεν ενημερώσαμε το αντικείμενο. Αυτό μπορεί να συμβεί μόνο μέσω της παγκόσμιας κατάστασης.
Φανταστείτε να ενταχθείτε σε μια ομάδα ανάπτυξης έργου που έχει μια μεγάλη, ώριμη βάση κώδικα. Ο νέος σας επικεφαλής σας ζητά να υλοποιήσετε ένα νέο χαρακτηριστικό και, σαν καλός προγραμματιστής, ξεκινάτε γράφοντας μια δοκιμή. Αλλά επειδή είστε νέος στο έργο, κάνετε πολλές διερευνητικές δοκιμές τύπου „τι συμβαίνει αν καλέσω αυτή τη μέθοδο“. Και προσπαθείτε να γράψετε την ακόλουθη δοκιμή:
function testCreditCardCharge()
{
$cc = new CreditCard('1234567890123456', 5, 2028); // τον αριθμό της κάρτας σας
$cc->charge(100);
}
Εκτελείτε τον κώδικα, ίσως αρκετές φορές, και μετά από λίγο παρατηρείτε ειδοποιήσεις στο τηλέφωνό σας από την τράπεζα ότι κάθε φορά που τον εκτελείτε, χρεώνονται 100 δολάρια στην πιστωτική σας κάρτα 🤦♂️
Πώς στο καλό θα μπορούσε το τεστ να προκαλέσει πραγματική χρέωση; Δεν είναι εύκολο να λειτουργήσει με πιστωτική κάρτα. Πρέπει να αλληλεπιδράσετε με μια διαδικτυακή υπηρεσία τρίτου μέρους, πρέπει να γνωρίζετε τη διεύθυνση URL αυτής της διαδικτυακής υπηρεσίας, πρέπει να συνδεθείτε και ούτω καθεξής. Καμία από αυτές τις πληροφορίες δεν περιλαμβάνεται στη δοκιμή. Ακόμα χειρότερα, δεν γνωρίζετε καν πού υπάρχουν αυτές οι πληροφορίες και, επομένως, πώς να παριστάνετε τις εξωτερικές εξαρτήσεις, ώστε κάθε εκτέλεση να μην οδηγεί σε νέα χρέωση 100 δολαρίων. Και ως νέος προγραμματιστής, πώς υποτίθεται ότι θα γνωρίζατε ότι αυτό που θα κάνατε θα σας οδηγούσε στο να γίνετε κατά 100 δολάρια φτωχότεροι;
Αυτή είναι μια τρομακτική δράση από απόσταση!
Δεν έχετε άλλη επιλογή από το να ψάξετε πολύ πηγαίο κώδικα, ρωτώντας
παλαιότερους και πιο έμπειρους συναδέλφους, μέχρι να καταλάβετε πώς
λειτουργούν οι συνδέσεις στο έργο. Αυτό οφείλεται στο γεγονός ότι, όταν
εξετάζετε τη διεπαφή της κλάσης CreditCard
, δεν μπορείτε να
προσδιορίσετε την παγκόσμια κατάσταση που πρέπει να αρχικοποιηθεί.
Ακόμα και αν κοιτάξετε τον πηγαίο κώδικα της κλάσης δεν θα σας πει ποια
μέθοδος αρχικοποίησης πρέπει να καλέσετε. Στην καλύτερη περίπτωση,
μπορείτε να βρείτε την παγκόσμια μεταβλητή στην οποία γίνεται
πρόσβαση και να προσπαθήσετε να μαντέψετε πώς να την αρχικοποιήσετε
από αυτήν.
Οι κλάσεις σε ένα τέτοιο έργο είναι παθολογικοί ψεύτες. Η κάρτα
πληρωμών προσποιείται ότι μπορείτε απλώς να την ενσαρκώσετε και να
καλέσετε τη μέθοδο charge()
. Ωστόσο, κρυφά αλληλεπιδρά με μια άλλη
κλάση, την PaymentGateway
. Ακόμη και η διεπαφή της λέει ότι μπορεί να
αρχικοποιηθεί ανεξάρτητα, αλλά στην πραγματικότητα αντλεί
διαπιστευτήρια από κάποιο αρχείο ρυθμίσεων κ.ο.κ. Είναι σαφές στους
προγραμματιστές που έγραψαν αυτόν τον κώδικα ότι το CreditCard
χρειάζεται το PaymentGateway
. Έγραψαν τον κώδικα με αυτόν τον τρόπο.
Αλλά για οποιονδήποτε νέο στο έργο, αυτό είναι ένα πλήρες μυστήριο και
εμποδίζει την εκμάθηση.
Πώς να διορθώσετε την κατάσταση; Εύκολα. Αφήστε το API να δηλώσει εξαρτήσεις.
function testCreditCardCharge()
{
$gateway = new PaymentGateway(/* ... */);
$cc = new CreditCard('1234567890123456', 5, 2028);
$cc->charge($gateway, 100);
}
Παρατηρήστε πώς οι σχέσεις μέσα στον κώδικα είναι ξαφνικά προφανείς.
Δηλώνοντας ότι η μέθοδος charge()
χρειάζεται τη διεύθυνση
PaymentGateway
, δεν χρειάζεται να ρωτήσετε κανέναν πώς ο κώδικας είναι
αλληλοεξαρτώμενος. Ξέρετε ότι πρέπει να δημιουργήσετε μια παρουσία
της, και όταν προσπαθείτε να το κάνετε αυτό, πέφτετε πάνω στο γεγονός
ότι πρέπει να παρέχετε παραμέτρους πρόσβασης. Χωρίς αυτές, ο κώδικας
δεν θα μπορούσε καν να εκτελεστεί.
Και το πιο σημαντικό, μπορείτε τώρα να μιμηθείτε την πύλη πληρωμών, ώστε να μην χρεώνεστε 100 δολάρια κάθε φορά που εκτελείτε μια δοκιμή.
Η παγκόσμια κατάσταση προκαλεί στα αντικείμενά σας τη δυνατότητα να έχουν κρυφή πρόσβαση σε πράγματα που δεν έχουν δηλωθεί στα API τους, και ως αποτέλεσμα καθιστά τα API σας παθολογικά ψεύτικα.
Μπορεί να μην το είχατε σκεφτεί με αυτόν τον τρόπο πριν, αλλά κάθε φορά που χρησιμοποιείτε global state, δημιουργείτε μυστικά ασύρματα κανάλια επικοινωνίας. Η ανατριχιαστική απομακρυσμένη δράση αναγκάζει τους προγραμματιστές να διαβάσουν κάθε γραμμή κώδικα για να κατανοήσουν τις πιθανές αλληλεπιδράσεις, μειώνει την παραγωγικότητα των προγραμματιστών και μπερδεύει τα νέα μέλη της ομάδας. Αν είστε αυτός που δημιούργησε τον κώδικα, γνωρίζετε τις πραγματικές εξαρτήσεις, αλλά όποιος έρχεται μετά από εσάς είναι άσχετος.
Μη γράφετε κώδικα που χρησιμοποιεί παγκόσμια κατάσταση, προτιμήστε να μεταβιβάζετε εξαρτήσεις. Δηλαδή, την έγχυση εξαρτήσεων (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 θεωρείται αντιπρότυπο.
Μην χρησιμοποιείτε singletons στον κώδικά σας και αντικαταστήστε τα με
άλλους μηχανισμούς. Πραγματικά δεν χρειάζεστε singletons. Ωστόσο, αν πρέπει
να εγγυηθείτε την ύπαρξη μιας και μόνο περίπτωσης μιας κλάσης για
ολόκληρη την εφαρμογή, αφήστε το στο DI container. Έτσι,
δημιουργήστε ένα singleton της εφαρμογής ή μια υπηρεσία. Αυτό θα σταματήσει
την κλάση από το να παρέχει τη δική της μοναδικότητα (δηλαδή, δεν θα
έχει μια μέθοδο getInstance()
και μια στατική μεταβλητή) και θα
εκτελεί μόνο τις λειτουργίες της. Έτσι, θα σταματήσει να παραβιάζει την
αρχή της ενιαίας ευθύνης.
Παγκόσμια κατάσταση έναντι δοκιμών
Όταν γράφουμε δοκιμές, υποθέτουμε ότι κάθε δοκιμή είναι μια απομονωμένη μονάδα και ότι δεν εισέρχεται σε αυτήν καμία εξωτερική κατάσταση. Και καμία κατάσταση δεν φεύγει από τις δοκιμές. Όταν μια δοκιμή ολοκληρώνεται, κάθε κατάσταση που σχετίζεται με τη δοκιμή θα πρέπει να αφαιρείται αυτόματα από τον garbage collector. Αυτό καθιστά τις δοκιμές απομονωμένες. Επομένως, μπορούμε να εκτελέσουμε τις δοκιμές με οποιαδήποτε σειρά.
Ωστόσο, αν υπάρχουν καθολικές καταστάσεις/συνθήκες, όλες αυτές οι ωραίες υποθέσεις καταρρέουν. Μια κατάσταση μπορεί να εισέλθει και να εξέλθει από μια δοκιμή. Ξαφνικά, η σειρά των δοκιμών μπορεί να έχει σημασία.
Για να δοκιμάσουν καθόλου singletons, οι προγραμματιστές πρέπει συχνά να
χαλαρώσουν τις ιδιότητές τους, ίσως επιτρέποντας την αντικατάσταση
μιας περίπτωσης από μια άλλη. Τέτοιες λύσεις είναι, στην καλύτερη
περίπτωση, χάκερς που παράγουν κώδικα που είναι δύσκολο να συντηρηθεί
και να κατανοηθεί. Κάθε δοκιμή ή μέθοδος tearDown()
που επηρεάζει
οποιαδήποτε παγκόσμια κατάσταση πρέπει να αναιρεί αυτές τις
αλλαγές.
Η παγκόσμια κατάσταση είναι ο μεγαλύτερος πονοκέφαλος στον έλεγχο μονάδων!
Πώς να διορθώσετε την κατάσταση; Εύκολα. Μη γράφετε κώδικα που χρησιμοποιεί 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()
και πολλές
άλλες, είναι απόλυτα συνεπής με την έγχυση εξάρτησης. Αυτές οι
συναρτήσεις επιστρέφουν πάντα τα ίδια αποτελέσματα από τις ίδιες
παραμέτρους εισόδου και επομένως είναι προβλέψιμες. Δεν χρησιμοποιούν
καμία παγκόσμια κατάσταση.
Ωστόσο, υπάρχουν συναρτήσεις στην PHP που δεν είναι ντετερμινιστικές.
Σε αυτές περιλαμβάνεται, για παράδειγμα, η συνάρτηση htmlspecialchars()
.
Η τρίτη της παράμετρος, $encoding
, αν δεν καθοριστεί, έχει ως
προεπιλογή την τιμή της επιλογής διαμόρφωσης ini_get('default_charset')
.
Επομένως, συνιστάται να προσδιορίζετε πάντα αυτή την παράμετρο για να
αποφύγετε πιθανή απρόβλεπτη συμπεριφορά της συνάρτησης. Η Nette το
πράττει αυτό με συνέπεια.
Ορισμένες συναρτήσεις, όπως η strtolower()
, η strtoupper()
, και οι
παρόμοιες, είχαν μη ντετερμινιστική συμπεριφορά στο πρόσφατο παρελθόν
και εξαρτιόνταν από τη ρύθμιση setlocale()
. Αυτό προκάλεσε πολλές
επιπλοκές, τις περισσότερες φορές όταν εργάζονταν με την τουρκική
γλώσσα. Αυτό οφείλεται στο γεγονός ότι η τουρκική γλώσσα κάνει
διάκριση μεταξύ κεφαλαίων και πεζών I
με και χωρίς τελεία. Έτσι
το strtolower('I')
επέστρεφε τον χαρακτήρα ı
και το
strtoupper('i')
επέστρεφε τον χαρακτήρα İ
, γεγονός που οδηγούσε
τις εφαρμογές να προκαλούν μια σειρά από μυστηριώδη σφάλματα. Ωστόσο,
το πρόβλημα αυτό διορθώθηκε στην έκδοση 8.2 της PHP και οι λειτουργίες
δεν εξαρτώνται πλέον από την τοπική γλώσσα.
Αυτό είναι ένα ωραίο παράδειγμα του πώς η παγκόσμια κατάσταση έχει ταλαιπωρήσει χιλιάδες προγραμματιστές σε όλο τον κόσμο. Η λύση ήταν η αντικατάστασή του με την έγχυση εξάρτησης.
Πότε είναι δυνατή η χρήση του Global State;
Υπάρχουν ορισμένες συγκεκριμένες καταστάσεις στις οποίες είναι δυνατή η χρήση καθολικής κατάστασης. Για παράδειγμα, όταν κάνετε αποσφαλμάτωση κώδικα και πρέπει να απορρίψετε την τιμή μιας μεταβλητής ή να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος. Σε τέτοιες περιπτώσεις, οι οποίες αφορούν προσωρινές ενέργειες που θα αφαιρεθούν αργότερα από τον κώδικα, είναι θεμιτό να χρησιμοποιήσετε έναν σφαιρικά διαθέσιμο ντάμπερ ή ένα χρονόμετρο. Τα εργαλεία αυτά δεν αποτελούν μέρος του σχεδιασμού του κώδικα.
Ένα άλλο παράδειγμα είναι οι συναρτήσεις για την εργασία με
κανονικές εκφράσεις preg_*
, οι οποίες αποθηκεύουν εσωτερικά τις
μεταγλωττισμένες κανονικές εκφράσεις σε μια στατική κρυφή μνήμη στη
μνήμη. Όταν καλείτε την ίδια κανονική έκφραση πολλές φορές σε
διαφορετικά μέρη του κώδικα, αυτή μεταγλωττίζεται μόνο μία φορά. Η
κρυφή μνήμη εξοικονομεί απόδοση και είναι επίσης εντελώς αόρατη στον
χρήστη, οπότε μια τέτοια χρήση μπορεί να θεωρηθεί νόμιμη.
Περίληψη
Δείξαμε γιατί έχει νόημα
- Αφαιρέστε όλες τις στατικές μεταβλητές από τον κώδικα
- Δηλώστε εξαρτήσεις
- Και χρησιμοποιήστε την έγχυση εξαρτήσεων
Όταν μελετάτε το σχεδιασμό κώδικα, να έχετε κατά νου ότι κάθε
static $foo
αντιπροσωπεύει ένα πρόβλημα. Προκειμένου ο κώδικάς σας
να είναι ένα περιβάλλον που σέβεται το DI, είναι απαραίτητο να
εξαλείψετε εντελώς την παγκόσμια κατάσταση και να την αντικαταστήσετε
με έγχυση εξαρτήσεων.
Κατά τη διάρκεια αυτής της διαδικασίας, μπορεί να διαπιστώσετε ότι πρέπει να χωρίσετε μια κλάση επειδή έχει περισσότερες από μία αρμοδιότητες. Μην ανησυχείτε γι' αυτό- επιδιώξτε την αρχή της μίας ευθύνης.
Θα ήθελα να ευχαριστήσω τον Miško Hevery, του οποίου άρθρα όπως το Flaw: Brittle Global State & Singletons αποτελούν τη βάση αυτού του κεφαλαίου.