Τα προθέματα και τα επιθήματα δεν ανήκουν στα ονόματα των interfaces
Η χρήση του προθέματος I
ή του
επιθήματος Interface
στα interfaces, καθώς και
του Abstract
στις abstract κλάσεις, είναι ένα
antipattern. Δεν έχει θέση στον καθαρό κώδικα. Η
διάκριση των ονομάτων των interfaces στην
πραγματικότητα συσκοτίζει τις αρχές του OOP,
εισάγει θόρυβο στον κώδικα και προκαλεί
περιπλοκές κατά την ανάπτυξη. Οι λόγοι
είναι οι εξής.

Τύπος = κλάση + interface + απόγονοι
Στον κόσμο του OOP, τόσο οι κλάσεις όσο και τα interfaces θεωρούνται τύποι. Αν χρησιμοποιήσω έναν τύπο κατά τη δήλωση μιας property ή μιας παραμέτρου, από την άποψη του προγραμματιστή δεν υπάρχει διαφορά μεταξύ του αν ο τύπος στον οποίο βασίζεται είναι κλάση ή interface. Αυτό είναι ένα υπέροχο πράγμα, χάρη στο οποίο τα interfaces είναι στην πραγματικότητα τόσο χρήσιμα. Αυτό δίνει νόημα στην ύπαρξή τους. (Σοβαρά: σε τι θα χρησίμευαν τα interfaces αν αυτή η αρχή δεν ίσχυε; Προσπαθήστε να το σκεφτείτε.)
Δείτε αυτόν τον κώδικα:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Ο κατασκευαστής λέει: “Χρειάζομαι μια
φόρμα και έναν μεταφραστή.” Και δεν τον
ενδιαφέρει καθόλου αν θα λάβει ένα
αντικείμενο GettextTranslator
ή
DatabaseTranslator
. Και ταυτόχρονα, ως
χρήστης, δεν με ενδιαφέρει καθόλου αν το
Translator
είναι interface, abstract κλάση ή
συγκεκριμένη κλάση.
Δεν με ενδιαφέρει καθόλου; Στην πραγματικότητα όχι, ομολογώ ότι είμαι αρκετά περίεργος, οπότε όταν εξετάζω μια ξένη βιβλιοθήκη, ρίχνω μια ματιά στο τι κρύβεται πίσω από τον τύπο και περνάω το ποντίκι πάνω του:

Α, τώρα το ξέρω. Και αυτό είναι όλο. Η γνώση του αν πρόκειται για κλάση ή interface θα ήταν σημαντική αν ήθελα να δημιουργήσω την περίπτωσή της, αλλά δεν είναι αυτή η περίπτωση, τώρα απλώς μιλάω για τον τύπο της μεταβλητής. Και εδώ θέλω να είμαι απομονωμένος από αυτές τις λεπτομέρειες. Και σίγουρα δεν θέλω να τις εισάγω στον κώδικά μου! Το τι κρύβεται πίσω από τον τύπο είναι μέρος του ορισμού του, όχι του ίδιου του τύπου.
Και τώρα δείτε τον επόμενο κώδικα:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Αυτός ο ορισμός του κατασκευαστή λέει κυριολεκτικά: “Χρειάζομαι μια abstract φόρμα και ένα interface μεταφραστή.” Αλλά αυτό είναι ανοησία. Χρειάζεται μια συγκεκριμένη φόρμα, την οποία πρέπει να αποδώσει. Όχι μια abstract φόρμα. Και χρειάζεται ένα αντικείμενο που εκτελεί το ρόλο του μεταφραστή. Δεν χρειάζεται ένα interface.
Γνωρίζετε ότι οι λέξεις Interface
και
Abstract
πρέπει να αγνοηθούν. Ότι ο
κατασκευαστής θέλει το ίδιο πράγμα όπως στο
προηγούμενο παράδειγμα. Αλλά… σοβαρά;
Πραγματικά σας φαίνεται καλή ιδέα να
εισαγάγετε στις συμβάσεις ονοματοδοσίας τη
χρήση λέξεων που πρέπει να παραβλέπονται;
Δημιουργεί μια ψευδή εικόνα για τις αρχές
του OOP. Ένας αρχάριος πρέπει να είναι
μπερδεμένος: «Αν ο τύπος Translator
σημαίνει είτε 1) ένα αντικείμενο της κλάσης
Translator
2) ένα αντικείμενο που υλοποιεί
το interface Translator
ή 3) ένα αντικείμενο
που κληρονομεί από αυτά, τι σημαίνει τότε το
TranslatorInterface
?» Σε αυτό δεν μπορεί να
δοθεί λογική απάντηση.
Όταν γράφουμε TranslatorInterface
, παρόλο
που και το Translator
μπορεί να είναι
interface, διαπράττουμε ταυτολογία. Το ίδιο όταν
δηλώνουμε interface TranslatorInterface
. Και ούτω
καθεξής. Μέχρι να προκύψει ένα
προγραμματιστικό αστείο:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Κατασκευαστής
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Εξαιρετική υλοποίηση
Όταν βλέπω κάτι σαν TranslatorInterface
,
είναι πιθανό να υπάρχει και μια υλοποίηση
με το όνομα Translator implements TranslatorInterface
.
Με ωθεί να σκεφτώ: τι κάνει το Translator
τόσο εξαιρετικό ώστε να έχει το μοναδικό
δικαίωμα να ονομάζεται Translator; Κάθε άλλη
υλοποίηση χρειάζεται ένα περιγραφικό
όνομα, για παράδειγμα GettextTranslator
ή
DatabaseTranslator
, αλλά αυτή είναι κατά
κάποιο τρόπο “προεπιλεγμένη”, όπως
υποδηλώνει η προνομιακή της θέση, όταν
ονομάζεται Translator
χωρίς επίθετο.
Μάλιστα, αυτό κάνει τους ανθρώπους
αβέβαιους και δεν ξέρουν αν πρέπει να
γράψουν typehint για το Translator
ή το
TranslatorInterface
. Στον κώδικα του client, τότε
αναμιγνύονται και τα δύο, σίγουρα το έχετε
συναντήσει πολλές φορές (στο Nette, για
παράδειγμα, σε σχέση με το Nette\Http\Request
vs IRequest
).
Δεν θα ήταν καλύτερο να απαλλαγούμε από
την εξαιρετική υλοποίηση και να
διατηρήσουμε το γενικό όνομα Translator
για το interface; Δηλαδή, να έχουμε
συγκεκριμένες υλοποιήσεις με συγκεκριμένο
όνομα + ένα γενικό interface με γενικό όνομα.
Αυτό έχει νόημα.
Το βάρος του περιγραφικού ονόματος τότε
πέφτει αποκλειστικά στις υλοποιήσεις. Αν
μετονομάσουμε το TranslatorInterface
σε
Translator
, η πρώην κλάση μας Translator
χρειάζεται νέο όνομα. Οι άνθρωποι τείνουν
να λύνουν αυτό το πρόβλημα ονομάζοντάς την
DefaultTranslator
, και εγώ είμαι ένοχος. Αλλά
πάλι, τι την κάνει τόσο εξαιρετική ώστε να
ονομάζεται Default; Μην είστε τεμπέληδες και
σκεφτείτε σοβαρά τι κάνει και γιατί
διαφέρει από άλλες πιθανές υλοποιήσεις.
Και τι γίνεται αν δεν μπορώ να φανταστώ πολλαπλές υλοποιήσεις; Τι γίνεται αν μου έρχεται στο μυαλό μόνο ένας έγκυρος τρόπος; Τότε απλά μην δημιουργείτε interface. Τουλάχιστον προς το παρόν.
Ιδού, εμφανίστηκε μια άλλη υλοποίηση
Και να το! Χρειαζόμαστε μια δεύτερη υλοποίηση. Συμβαίνει συχνά. Ποτέ δεν υπήρξε ανάγκη να αποθηκεύονται οι μεταφράσεις με άλλο τρόπο εκτός από έναν δοκιμασμένο, π.χ. σε βάση δεδομένων, αλλά τώρα εμφανίστηκε μια νέα απαίτηση και είναι απαραίτητο να υπάρχουν περισσότεροι μεταφραστές στην εφαρμογή.
Αυτή είναι επίσης η στιγμή που συνειδητοποιείτε σαφώς ποια ήταν η ιδιαιτερότητα του αρχικού μοναδικού μεταφραστή. Ήταν ένας μεταφραστής βάσης δεδομένων, όχι κάποιος προεπιλεγμένος.
Τι να κάνουμε με αυτό;
- Από το όνομα
Translator
θα κάνουμε ένα interface - Την αρχική κλάση θα τη μετονομάσετε σε
DatabaseTranslator
και θα υλοποιεί τοTranslator
- Και θα δημιουργήσετε νέες κλάσεις
GettextTranslator
και ίσωςNeonTranslator
Όλες αυτές οι αλλαγές γίνονται πολύ άνετα
και εύκολα, ειδικά αν η εφαρμογή είναι
χτισμένη σύμφωνα με τις αρχές του dependency
injection. Δεν χρειάζεται να αλλάξετε τίποτα
στον κώδικα, μόνο στη διαμόρφωση του DI
container θα αλλάξουμε το Translator
σε
DatabaseTranslator
. Αυτό είναι υπέροχο!
Μια διαμετρικά αντίθετη κατάσταση θα
προέκυπτε, ωστόσο, αν επιμέναμε στα
προθέματα/επιθήματα. Θα έπρεπε να
μετονομάσουμε τους τύπους σε ολόκληρη την
εφαρμογή από Translator
σε
TranslatorInterface
. Μια τέτοια μετονομασία
θα ήταν καθαρά σκόπιμη για τη συμμόρφωση με
τη σύμβαση, αλλά θα πήγαινε ενάντια στο
νόημα του OOP, όπως δείξαμε πριν από λίγο. Το
interface δεν άλλαξε, ο κώδικας χρήστη δεν
άλλαξε, αλλά η σύμβαση απαιτεί μετονομασία;
Τότε πρόκειται για λανθασμένη σύμβαση.
Αν επιπλέον, με τον καιρό, αποδεικνυόταν ότι μια abstract κλάση θα ήταν καλύτερη από ένα interface, θα μετονομάζαμε ξανά. Μια τέτοια παρέμβαση μπορεί να μην είναι καθόλου ασήμαντη, για παράδειγμα, όταν ο κώδικας είναι κατανεμημένος σε πολλαπλά πακέτα ή χρησιμοποιείται από τρίτους.
Μα όλοι το κάνουν έτσι
Όχι όλοι. Είναι αλήθεια ότι στον κόσμο της PHP, η διάκριση των ονομάτων των interfaces και των abstract κλάσεων έγινε δημοφιλής από το Zend Framework και μετά το Symfony, δηλαδή μεγάλους παίκτες. Αυτή την προσέγγιση υιοθέτησε και η PSR, η οποία παραδόξως δημοσιεύει μόνο interfaces, και παρόλα αυτά σε καθένα αναφέρει στο όνομα τη λέξη interface.
Από την άλλη πλευρά, ένα άλλο σημαντικό
framework, το Laravel, δεν διακρίνει καθόλου τα
interfaces και τις abstract κλάσεις. Δεν το κάνει, για
παράδειγμα, ούτε το δημοφιλές επίπεδο βάσης
δεδομένων Doctrine. Και δεν το κάνει ούτε η
τυπική βιβλιοθήκη της PHP (έχουμε έτσι τα
interfaces Throwable
ή Iterator
, την abstract
κλάση FilterIterator
, κ.λπ.).
Αν κοιτάζαμε τον κόσμο εκτός PHP, για
παράδειγμα, η C# χρησιμοποιεί το πρόθεμα
I
για τα interfaces, αντίθετα στην Java ή την
TypeScript τα ονόματα δεν διακρίνονται.
Δεν το κάνουν λοιπόν όλοι, ωστόσο, ακόμα κι αν το έκαναν, δεν σημαίνει ότι είναι σωστό. Η άκριτη υιοθέτηση όσων κάνουν οι άλλοι δεν είναι λογική, επειδή μπορείτε να υιοθετήσετε και λάθη. Λάθη από τα οποία ο άλλος πολύ πιθανόν θα ήθελε να απαλλαγεί ο ίδιος, απλά είναι πολύ απαιτητικό.
Δεν αναγνωρίζω στον κώδικα τι είναι interface
Πολλοί προγραμματιστές θα αντιτείνουν ότι τα προθέματα/επιθήματα είναι χρήσιμα για αυτούς, επειδή χάρη σε αυτά αναγνωρίζουν αμέσως στον κώδικα τι είναι interface. Έχουν την αίσθηση ότι μια τέτοια διάκριση θα τους έλειπε. Λοιπόν, για να δούμε, αναγνωρίζετε σε αυτά τα παραδείγματα τι είναι κλάση και τι interface;
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
Το X
είναι πάντα κλάση, το Y
είναι interface, είναι σαφές ακόμα και χωρίς
προθέματα/επιθήματα. Φυσικά, το γνωρίζει
και το IDE και στο συγκεκριμένο πλαίσιο θα
σας προτείνει πάντα σωστά.
Αλλά τι γίνεται εδώ:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try {
} catch (A $x) {
}
Σε αυτές τις περιπτώσεις δεν το αναγνωρίζετε. Όπως είπαμε στην αρχή, εδώ δεν πρέπει να υπάρχει διαφορά από την άποψη του προγραμματιστή μεταξύ του τι είναι κλάση και τι interface. Κάτι που ακριβώς δίνει νόημα στα interfaces και τις abstract κλάσεις.
Αν εδώ ήσασταν σε θέση να διακρίνετε την κλάση από το interface, θα αναιρούσατε τη βασική αρχή του OOP. Και τα interfaces θα έχαναν το νόημά τους.
Το έχω συνηθίσει
Η αλλαγή συνηθειών απλά πονάει 🙂 Πόσες φορές ακόμα και η ιδέα. Αλλά για να μην αδικούμε, πολλούς ανθρώπους οι αλλαγές αντίθετα τους ελκύουν και τις ανυπομονούν, ωστόσο για τους περισσότερους ισχύει ότι η συνήθεια είναι δεύτερη φύση.
Αλλά αρκεί να κοιτάξουμε στο παρελθόν, πώς
κάποιες συνήθειες τις πήρε ο χρόνος. Η πιο
διάσημη είναι ίσως η λεγόμενη ουγγρική
σημειογραφία που χρησιμοποιούνταν από τη
δεκαετία του ογδόντα και έγινε δημοφιλής
από τη Microsoft. Η σημειογραφία συνίστατο στο
ότι το όνομα κάθε μεταβλητής ξεκινούσε με
μια συντομογραφία που συμβόλιζε τον τύπο
δεδομένων της. Στην PHP θα έμοιαζε κάπως έτσι
echo $this->strName
ή $this->intCount++
. Από
την ουγγρική σημειογραφία άρχισαν να
απομακρύνονται τη δεκαετία του ενενήντα
και σήμερα η Microsoft στις οδηγίες της
αποθαρρύνει άμεσα τους προγραμματιστές
από αυτήν.
Κάποτε ήταν αδιαχώριστη και σήμερα δεν λείπει σε κανέναν.
Αλλά γιατί να πάμε σε τόσο μακρινό παρελθόν; Ίσως θυμάστε ότι στην PHP ήταν σύνηθες να διακρίνονται τα μη δημόσια μέλη των κλάσεων με κάτω παύλα (παράδειγμα από το Zend Framework). Ήταν την εποχή που υπήρχε ήδη εδώ και καιρό η PHP 5, η οποία είχε τροποποιητές ορατότητας public/protected/private. Αλλά οι προγραμματιστές το έκαναν από συνήθεια. Ήταν πεπεισμένοι ότι χωρίς τις κάτω παύλες θα έχαναν τον προσανατολισμό τους στον κώδικα. «Πώς θα διέκρινα στον κώδικα τις δημόσιες από τις ιδιωτικές μεταβλητές, ε;»
Σήμερα κανείς δεν χρησιμοποιεί κάτω παύλες. Και δεν λείπουν σε κανέναν. Ο χρόνος απέδειξε περίτρανα ότι οι φόβοι ήταν αβάσιμοι.
Παρόλα αυτά, είναι ακριβώς το ίδιο με την ένσταση: «Πώς θα διέκρινα στον κώδικα το interface από την κλάση, ε;»
Έπαψα να χρησιμοποιώ προθέματα/επιθήματα πριν από δέκα χρόνια. Ποτέ δεν θα επέστρεφα, ήταν μια εξαιρετική απόφαση. Δεν γνωρίζω ούτε κανέναν άλλο προγραμματιστή που θα ήθελε να επιστρέψει. Όπως είπε ένας φίλος: «Δοκίμασέ το και σε ένα μήνα δεν θα καταλαβαίνεις ότι το έκανες ποτέ διαφορετικά.»
Θέλω να διατηρήσω τη συνέπεια
Μπορώ να φανταστώ ότι ένας προγραμματιστής θα πει: «Η χρήση προθεμάτων και επιθημάτων είναι πραγματικά ανοησία, το καταλαβαίνω, απλά έχω ήδη χτίσει τον κώδικα έτσι και η αλλαγή είναι πολύ δύσκολη. Και αν άρχιζα να γράφω τον νέο κώδικα σωστά χωρίς αυτά, θα δημιουργούσα μια ασυνέπεια, η οποία είναι ίσως ακόμα χειρότερη από μια κακή σύμβαση.»
Στην πραγματικότητα, ήδη τώρα ο κώδικάς σας είναι ασυνεπής, επειδή χρησιμοποιείτε τη συστημική βιβλιοθήκη της PHP, η οποία δεν έχει καθόλου προθέματα και επιθήματα:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
Και με το χέρι στην καρδιά, ενοχλεί αυτό; Σας πέρασε ποτέ από το μυαλό ότι αυτό θα ήταν πιο συνεπές;
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Ή αυτό;
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Νομίζω πως όχι. Η συνέπεια δεν παίζει τόσο σημαντικό ρόλο όσο θα μπορούσε να φανεί. Αντίθετα, το μάτι προτιμά λιγότερο οπτικό θόρυβο, ο εγκέφαλος την καθαρότητα του σχεδιασμού. Επομένως, η τροποποίηση της σύμβασης και η έναρξη γραφής νέων interfaces σωστά χωρίς προθέματα και επιθήματα έχει νόημα.
Μπορούν να αφαιρεθούν στοχευμένα και από
μεγάλα έργα. Παράδειγμα είναι το Nette Framework,
το οποίο ιστορικά χρησιμοποιούσε προθέματα
I
στα ονόματα των interfaces, από τα οποία
πριν από λίγα χρόνια άρχισε σταδιακά
και με πλήρη διατήρηση της συμβατότητας
προς τα πίσω να απαλλαγεί.
Για να υποβάλετε ένα σχόλιο, συνδεθείτε