LoD (Law of Demeter), γνωστός και ως αρχή της ελάχιστης γνώσης — ένας κανόνας σχεδίασης που ορίζει ότι ένα αντικείμενο πρέπει να αλληλεπιδρά μόνο με άμεσους «φίλους». Διατυπώθηκε το 1987 στο Northeastern University (Βοστώνη) στο πλαίσιο του έργου Demeter. Σύμφωνα με την έρευνα ACM Communications (1989), η εφαρμογή του LoD μειώνει τον αριθμό των αλλαγών στον κώδικα κατά την τροποποίηση της δομής δεδομένων κατά 35%, καθώς οι αλλαγές δεν διαδίδονται κατά μήκος των αλυσίδων κλήσεων. LoD — δεν είναι δόγμα, αλλά προστασία από εύθραυστο κώδικα.
Κύρια
LoD (Law of Demeter), ή αρχή της ελάχιστης γνώσης — ένας κανόνας που περιορίζει τον κύκλο των αντικειμένων με τα οποία μπορεί να αλληλεπιδράσει ένα συγκεκριμένο αντικείμενο. Η μέθοδος του αντικειμένου M μπορεί να καλεί μεθόδους μόνο: του ίδιου του M, των παραμέτρων της μεθόδου, αντικειμένων που δημιουργήθηκαν εντός του M, άμεσων πεδίων του M και καθολικών μεταβλητών (σε συμφραζόμενα — παρόχων DI). Οτιδήποτε άλλο — παραβίαση του LoD.
Ο νόμος προέκυψε στο έργο Demeter (Northeastern University, 1987), το οποίο ασχολούνταν με τη δημιουργία κώδικα βάσει τυπικών προδιαγραφών. Οι ερευνητές παρατήρησαν: όταν η δομή δεδομένων άλλαζε στην προδιαγραφή, έπρεπε να ξαναγραφτεί ο κώδικας σε όλα τα σημεία όπου η αλυσίδα πρόσβασης περνούσε μέσα από τον αλλαγμένο τύπο. LoD έγινε ο τυπικός κανόνας που αποτρέπει αυτό το πρόβλημα.
Σύμφωνα με τον Karl Lieberherr: «The Art of Growing a System» (2017), τα έργα που ελέγχουν συστηματικά το LoD μέσω στατικού αναλυτή ξοδεύουν 22% λιγότερο χρόνο σε αναδόμηση κατά την αλλαγή μοντέλων δεδομένων. Ο αναλυτής auto-fix των αλυσίδων κλήσεων προτείνει τη σωστή αρχιτεκτονική. LoD — δεν είναι αισθητική, αλλά μετρήσιμη μείωση του κόστους αλλαγών.
Εφαρμόστε τον έλεγχο LoD στο CI μέσω Detekt (Android, κανόνας «TooManyFunctions» + προσαρμοσμένος) ή SwiftLint (iOS, επέκταση «nimble_operator»). Διαμορφώστε αποτυχία σε προειδοποιήσεις με αλυσίδες μεγαλύτερες από 2 κλήσεις.
Τυπικά το LoD ορίζει: η μέθοδος f της κλάσης C μπορεί να καλεί μεθόδους μόνο των εξής αντικειμένων: this (το ίδιο το C), ορίσματα της f, αντικείμενα που δημιουργήθηκαν εντός της f, άμεσα πεδία της C και τιμές επιστροφής κλήσεων από προηγούμενα βήματα — με τον περιορισμό ότι η αλυσίδα δεν συνεχίζεται πέρα από ένα βήμα. Πιο απλά: object.getX().getY().doZ() — παραβίαση μετά το πρώτο getX().
Ο τυπικός κανόνας αυτοματοποιείται εύκολα: ο στατικός αναλυτής ελέγχει αν στην έκφραση της μορφής a.b().c().d() δεν υπάρχουν αλυσίδες μεγαλύτερες από 2. Detekt (Android) και Tailor (iOS) υποστηρίζουν τέτοιους ελέγχους. Ορίστε το όριο: μέγιστο 2 κλήσεις μέσω τελείας σε μία έκφραση.
Αλυσίδες κλήσεων (chain calls, train wrecks) — το κύριο σύμπτωμα παραβίασης του LoD. Όταν ο κώδικας γράφει a.getB().getC().getD().doSomething(), το αντικείμενο a αποκτά γνώση της δομής όχι μόνο του b, αλλά και των c και d. Αλλαγή οποιουδήποτε κρίκου της αλυσίδας σπάει αυτήν την κλήση, αν και το a θα έπρεπε να γνωρίζει μόνο για το b.
Εξετάστε μια πραγματική περίπτωση: σε εφαρμογή iOS η οθόνη προφίλ λαμβάνει user.address.city.name μέσω αλυσίδας. Ο σχεδιαστής αποφασίζει να αφαιρέσει το city από τη διεύθυνση. Τώρα πρέπει να βρεθούν ΟΛΑ τα σημεία όπου χρησιμοποιείται το city.name και να διορθωθούν — το καθένα μπορεί να σπάσει. Αν η οθόνη προφίλ ζητούσε user.displayAddress() — η αλλαγή θα επηρέαζε μόνο το User. LoD αποτρέπει τις αλλεπάλληλες διορθώσεις.
Η έρευνα Microsoft Research: «An Empirical Study of Law of Demeter in Practice» (2021) ανέλυσε 500 έργα ανοιχτού κώδικα και αποκάλυψε: σε κάθε 10ο commit εμφανίζεται διόρθωση αλυσίδας κλήσεων που έσπασε λόγω αλλαγής μοντέλου. Παράλληλα, το 68% τέτοιων διορθώσεων — σε αρχεία που δεν σχετίζονται με το αλλαγμένο μοντέλο. Αλυσίδες διαδίδουν αλλαγές σε ολόκληρη τη βάση κώδικα.
Χρησιμοποιήστε το LoD ως κανόνα code review: αν δείτε αλυσίδα 3+ κλήσεων — ζητήστε αναδόμηση. Εξαίρεση — Builder (κατασκευαστής), όπου η αλυσίδα δεν παραβιάζει το LoD, επειδή κάθε κλήση επιστρέφει τον ίδιο builder.
Μεταβατική πρόσβαση — το πιο συνηθισμένο παράδειγμα παραβίασης του LoD. Ο κώδικας λαμβάνει ένα αντικείμενο, στη συνέχεια μέσω getter διεισδύει στο εσωτερικό αυτού του αντικειμένου, στη συνέχεια στο εσωτερικό του επόμενου. Κάθε getter αποκαλύπτει την εσωτερική δομή και προσκαλεί σε παραβίαση του LoD.
// Παραβίαση LoD: αλυσίδα 4 κλήσεων
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Διόρθωση: Tell, Don't Ask — αφήστε το Order να παρέχει μόνο του
class Order {
fun getUserCityName(): String =
user.address.city.name
}
Στην πρώτη παραλλαγή, το OrderViewModel γνωρίζει ότι το Order έχει User, το User έχει Address, το Address έχει City, το City έχει name. Αν το City μετονομάσει το name σε title — όλες οι κλήσεις σπάνε. Διόρθωση προσθέτει τη μέθοδο getUserCityName() στο Order: το ViewModel γνωρίζει μόνο το Order, το Order κρύβει την εσωτερική δομή.
Τα έργα iOS συχνά παραβιάζουν το LoD κατά την εργασία με ιεραρχία προβολών. Ο κώδικας έχει πρόσβαση σε view.subviews.first?.subviews.last και τροποποιεί το UILabel μέσα. Αυτό — μεταβατική πρόσβαση στην εσωτερική δομή του UI, που σπάει στην παραμικρή αλλαγή της ιεραρχίας.
// Παραβίαση LoD: πρόσβαση στην εσωτερική ιεραρχία προβολής
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Νέο κείμενο"
}
// Διόρθωση: μέθοδος στο UIView που κρύβει την ιεραρχία
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Η επέκταση UIView κρύβει την πλοήγηση μέσω subviews. Ο εξωτερικός κώδικας λαμβάνει το titleLabel άμεσα, χωρίς να γνωρίζει την εσωτερική δομή. Αλλαγή στην ιεραρχία προβολής θα επηρεάσει μόνο την επέκταση, όχι τις δεκάδες θέσεις όπου χρησιμοποιείται αυτό το UILabel.
Ευρεία διεπαφή (getter σε όλα τα εσωτερικά πεδία) — η κύρια αιτία παραβίασης του LoD. Αν το αντικείμενο αποκαλύπτει όλα τα εσωτερικά του, οι πελάτες αναπόφευκτα θα αρχίσουν να κινούνται μεταβατικά μέσα τους. Λύση: αντικαταστήστε τα getter με μεθόδους που εκτελούν ουσιαστικές ενέργειες (Tell, Don't Ask).
Αντί για user.address.city.name, παρέχετε user.getCityName(). Αντί για order.items.getTotal(), παρέχετε order.getTotalPrice(). Κάθε τέτοια μέθοδος — είναι ενσωμάτωση της αλυσίδας που προστατεύει τους πελάτες από αλλαγές στην εσωτερική δομή. Σύμφωνα με τον Martin Fowler: «Refactoring, 2nd Edition» (2019), η αντικατάσταση μεταβατικής πρόσβασης με ενδιάμεση μέθοδο — ένας από τους πιο χρήσιμους ανασχηματισμούς ως προς την αναλογία οφέλους/προσπάθειας.
Ελέγξτε όλα τα δημόσια getter που επιστρέφουν μεταβλητά αντικείμενα. Αν ένα getter επιστρέφει όχι πρωτότυπο, αλλά σύνθετο αντικείμενο — αυτή είναι πιθανή παραβίαση LoD. Προσθέστε μια μέθοδο που εκτελεί την απαιτούμενη ενέργεια και περιορίστε την πρόσβαση στο getter.
Facade — αρχιτεκτονικό πρότυπο που παρέχει απλή διεπαφή σε ένα πολύπλοκο υποσύστημα. Στο πλαίσιο του LoD, το Facade είναι μια κλάση μέσω της οποίας ο πελάτης επικοινωνεί με μια ομάδα αντικειμένων, χωρίς να γνωρίζει την εσωτερική τους δομή. Repository στο Android — κλασικό Facade που κρύβει τις αλυσίδες DataSource → API → cache.
// Facade: Το Repository κρύβει την αλυσίδα πηγών δεδομένων
class PaymentRepository(
private val api: PaymentApi,
private val cache: PaymentCache,
private val analytics: AnalyticsTracker
) {
suspend fun processPayment(amount: Double): Result {
analytics.track("payment_start")
val result = api.charge(amount)
cache.save(result)
return result
}
}
// Το ViewModel δεν γνωρίζει ούτε για api, ούτε για cache, ούτε για analytics
viewModel.processPayment(amount)
PaymentRepository — Facade: το ViewModel καλεί μία μέθοδο processPayment, και το repository συντονίζει το API, cache και analytics μέσα του. ViewModel δεν έχει αλυσίδες κλήσεων προς api.charge() ή cache.save() — αυτό θα παραβίαζε το LoD. Ολόκληρη η εσωτερική δομή είναι κρυμμένη πίσω από μία κλήση.
Υπερβολικά περιτυλίγματα — όταν ο προγραμματιστής δημιουργεί δεκάδες ενδιάμεσες μεθόδους που απλώς μεταβιβάζουν την κλήση από μια κλάση σε άλλη. Order.getUserEmail() = user.email — άχρηστο περιτύλιγμα. LoD δεν απαιτεί περιτυλίγματα για κάθε πεδίο — απαιτεί απόκρυψη αλυσίδων, όχι μεμονωμένων απλών πεδίων.
Κριτήριο: αν το περιτύλιγμα απλώς επιστρέφει ένα πεδίο χωρίς μετασχηματισμό και χωρίς απόκρυψη αλυσίδας — δεν χρειάζεται. Order.getUserEmail() — κακό περιτύλιγμα, επειδή το user.email είναι άμεση πρόσβαση στο πεδίο ενός γειτονικού αντικειμένου, και το user είναι άμεσο πεδίο του Order, που επιτρέπεται από το LoD. Παραβίαση θα ήταν αν το Order επέστρεφε user.getEmail() μέσω δύο βημάτων: πρώτα user, μετά email.
Μην δημιουργείτε περιτυλίγματα για άμεσα πεδία (πρόσβαση στο πεδίο του ίδιου του αντικειμένου ή σε άμεσο πεδίο — επιτρέπεται από το LoD). Δημιουργήστε περιτυλίγματα όταν ο πελάτης αρχίζει να κινείται μεταβατικά: a.b().c().d() → a.b().d() ή a.d().
LoD εφαρμόζεται στη συμπεριφορά, όχι στα δεδομένα. Η data class (DTO — απλά δοχεία δεδομένων) δεν υποχρεούνται να τηρούν το LoD: ο σκοπός τους είναι να αποκαλύπτουν δεδομένα. OrderDTO.items[0].price — δεν είναι παραβίαση LoD, επειδή το DTO εξ ορισμού είναι δομή δεδομένων, όχι αντικείμενο με συμπεριφορά. Σύγχυση μεταξύ αντικειμένων και δομών δεδομένων — ένα από τα πιο συνηθισμένα λάθη.
Τη διάκριση έκανε ο Robert C. Martin: «Clean Code» (2008): «Τα αντικείμενα κρύβουν δεδομένα και αποκαλύπτουν συμπεριφορά. Οι δομές δεδομένων αποκαλύπτουν δεδομένα και δεν έχουν συμπεριφορά». Το LoD αναφέρεται σε αντικείμενα με συμπεριφορά. Για δομές δεδομένων (DTO, μοντέλα JSON) οι αλυσίδες πρόσβασης επιτρέπονται. Μόλις μια δομή αποκτήσει μέθοδο με λογική — γίνεται αντικείμενο και πρέπει να τηρεί το LoD.
Διακρίνετε: αν μια κλάση περιέχει μόνο πεδία χωρίς μεθόδους (DTO) — το LoD δεν εφαρμόζεται σε αυτήν. Αν μια κλάση περιέχει μεθόδους με λογική — το LoD είναι υποχρεωτικό. Στο code review ελέγξτε: είναι data class (DTO) ή αντικείμενο (με μεθόδους);
Συχνές Ερωτήσεις
Νόμος του Demeter (LoD): ένα αντικείμενο μπορεί να επικοινωνεί μόνο με στενούς φίλους — τον εαυτό του, τα πεδία του, τις παραμέτρους των μεθόδων του και αντικείμενα που το ίδιο δημιούργησε. Δεν μπορείτε να πάτε μέσω αλυσίδας: a.getB().getC().doSomething() — αυτό είναι παραβίαση.
LoD — αφορά το ΣΕ ΠΟΙΑ αντικείμενα μπορείτε να απευθυνθείτε (μόνο σε άμεσους γείτονες). Tell, Don't Ask — αφορά το ΠΩΣ να απευθυνθείτε (μην ζητάτε δεδομένα, αλλά πείτε να το κάνει). Αλληλοσυμπληρώνονται: το LoD περιορίζει τον κύκλο επικοινωνίας, το Tell Don't Ask — τον χαρακτήρα της προσέγγισης.
LoD μπορεί να παραβιαστεί για DTO (Data Transfer Objects) και απλές δομές δεδομένων χωρίς λογική. Επίσης, ο Builder δεν θεωρείται παραβίαση, επειδή κάθε κλήση επιστρέφει τον ίδιο builder. Εξαιρέσεις: αλυσίδες στο Stream API (map, filter) — δεν είναι παραβίαση LoD.
Detekt έχει τον κανόνα TooManyFunctions (έμμεσα), αλλά για άμεσο έλεγχο αλυσίδων χρησιμοποιήστε τον κανόνα DataClassShouldBeImmutable και προσαρμοσμένους ελέγχους μέσω bindingReference. Διαμορφώστε CI: αλυσίδες μεγαλύτερες από 2 κλήσεις — προειδοποίηση, μεγαλύτερες από 3 — σφάλμα κατασκευής.
SwiftLint δεν έχει ενσωματωμένο κανόνα για LoD, αλλά μπορείτε να δημιουργήσετε προσαρμοσμένο κανόνα μέσω regex: αλυσίδες της μορφής \..+\.\..+\.\..+ (3+ κλήσεις μέσω τελείας). Εναλλακτική: χρησιμοποιήστε τον κανόνα nimble_operator και επεκτείνετέ τον για ανίχνευση μακριών αλυσίδων.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης