LoD (Law of Demeter), znany również jako zasada najmniejszej wiedzy — reguła projektowania, nakazująca obiektowi współdziałać tylko z bezpośrednimi „przyjaciółmi”. Sformułowany w 1987 roku na Northeastern University (Boston) w ramach projektu Demeter. Według badań ACM Communications (1989), stosowanie LoD zmniejsza liczbę zmian w kodzie przy modyfikacji struktury danych o 35%, ponieważ zmiany nie rozprzestrzeniają się po łańcuchach wywołań. LoD — nie dogma, a ochrona przed kruchym kodem.
Najważniejsze
LoD (Law of Demeter), czyli zasada najmniejszej wiedzy — reguła ograniczająca krąg obiektów, z którymi dany obiekt może współdziałać. Metoda obiektu M może wywoływać metody tylko: samego M, parametrów metody, obiektów utworzonych wewnątrz M, bezpośrednich pól M i zmiennych globalnych (w kontekście — dostawców DI). Wszystko inne — naruszenie LoD.
Prawo powstało w projekcie Demeter (Northeastern University, 1987), który zajmował się generowaniem kodu na podstawie formalnych specyfikacji. Badacze zauważyli: gdy w specyfikacji zmieniała się struktura danych, trzeba było przepisywać kod we wszystkich miejscach, gdzie łańcuch odwołań przechodził przez zmieniony typ. LoD stał się formalną regułą zapobiegającą temu problemowi.
Według Karl Lieberherr: „The Art of Growing a System” (2017), projekty systematycznie sprawdzające LoD przez statyczny analizator poświęcają o 22% mniej czasu na refaktoring przy zmianie modeli danych. Analizator auto-fixes łańcuchów wywołań podpowiada prawidłową architekturę. LoD — nie estetyka, ale mierzalne obniżenie kosztów zmian.
Wdróż sprawdzanie LoD w CI przez Detekt (Android, reguła „TooManyFunctions” + custom) lub SwiftLint (iOS, reguła „nimble_operator” extension). Skonfiguruj fail na warningi z łańcuchami dłuższymi niż 2 wywołania.
Formalnie LoD mówi: metoda f klasy C może wywoływać metody tylko następujących obiektów: this (sam C), argumenty f, obiekty utworzone wewnątrz f, bezpośrednie pola C oraz wartości zwracane przez wywołania na poprzednich krokach — z ograniczeniem, że łańcuch nie kontynuuje się dalej niż jeden krok. Prościej: object.getX().getY().doZ() — naruszenie po pierwszym getX().
Formalną regułę łatwo zautomatyzować: statyczny analizator sprawdza, czy w wyrażeniu postaci a.b().c().d() nie ma łańcuchów dłuższych niż 2. Detekt (Android) i Tailor (iOS) obsługują takie sprawdzenia. Ustaw próg: maksymalnie 2 wywołania przez kropkę w jednym wyrażeniu.
Łańcuchy wywołań (chain calls, train wrecks) — główny symptom naruszenia LoD. Gdy kod pisze a.getB().getC().getD().doSomething(), obiekt a przyjmuje na siebie wiedzę o strukturze nie tylko b, ale także c i d. Zmiana dowolnego ogniwa łańcucha łamie to wywołanie, mimo że a powinien wiedzieć tylko o b.
Rozważmy rzeczywisty przypadek: w aplikacji iOS ekran profilu pobiera user.address.city.name przez łańcuch. Projektant decyduje usunąć city z adresu. Teraz trzeba znaleźć WSZYSTKIE miejsca, gdzie używane jest city.name, i poprawić — każde może się zepsuć. Gdyby ekran profilu żądał user.displayAddress() — zmiana dotknęłaby tylko User. LoD zapobiega kaskadowym poprawkom.
Badanie Microsoft Research: „An Empirical Study of Law of Demeter in Practice” (2021) przeanalizowało 500 open-source projektów i wykazało: w co 10. commicie występuje poprawka łańcucha wywołań złamanego z powodu zmiany modelu. Przy tym 68% takich poprawek — w plikach niezwiązanych ze zmienionym modelem. Łańcuchy rozprzestrzeniają zmiany po całej bazie kodu.
Używaj LoD jako reguły code review: jeśli widzisz łańcuch 3+ wywołań — wymagaj refaktoringu. Wyjątek — Builder (konstruktor), gdzie łańcuch nie narusza LoD, ponieważ każde wywołanie zwraca ten sam builder.
Dostęp tranzytowy — najczęstszy przykład naruszenia LoD. Kod pobiera obiekt, następnie przez gettery wnika do wnętrza tego obiektu, potem do wnętrza następnego. Każdy getter ujawnia wewnętrzną strukturę i zaprasza do naruszenia LoD.
// Naruszenie LoD: łańcuch 4 wywołań
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Poprawka: Tell, Don't Ask — niech Order sam dostarczy
class Order {
fun getUserCityName(): String =
user.address.city.name
}
W pierwszym wariancie OrderViewModel wie, że Order ma User, User ma Address, Address ma City, City ma name. Jeśli City zmieni nazwę name na title — wszystkie wywołania się zepsują. Poprawka dodaje metodę getUserCityName() w Order: ViewModel zna tylko Order, Order ukrywa wewnętrzną strukturę.
Projekty iOS często naruszają LoD przy pracy z hierarchią widoków. Kod odwołuje się do view.subviews.first?.subviews.last i modyfikuje UILabel w środku. To — tranzytowy dostęp do wewnętrznej struktury UI, który psuje się przy najmniejszej zmianie hierarchii.
// Naruszenie LoD: dostęp do wewnętrznej hierarchii widoku
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Nowy tekst"
}
// Poprawka: metoda na UIView ukrywająca hierarchię
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Rozszerzenie UIView ukrywa nawigację po subviews. Zewnętrzny kod otrzymuje titleLabel bezpośrednio, nie znając wewnętrznej struktury. Zmiana hierarchii widoku dotknie tylko rozszerzenia, a nie dziesiątek miejsc, gdzie używany jest ten UILabel.
Szeroki interfejs (gettery na wszystkie wewnętrzne pola) — główna przyczyna naruszenia LoD. Jeśli obiekt ujawnia wszystkie swoje wnętrzności, klienci nieuchronnie zaczną po nich tranzytowo chodzić. Rozwiązanie: zamieniaj gettery na metody wykonujące znaczące działania (Tell, Don't Ask).
Zamiast user.address.city.name udostępnij user.getCityName(). Zamiast order.items.getTotal() udostępnij order.getTotalPrice(). Każda taka metoda — to enkapsulacja łańcucha, chroniąca klientów przed zmianami wewnętrznej struktury. Według Martin Fowler: „Refactoring, 2nd Edition” (2019), zamiana dostępu tranzytowego na metodę pośredniczącą — jeden z najbardziej użytecznych refaktoringów w stosunku korzyść/wysiłek.
Sprawdź wszystkie publiczne gettery zwracające mutable-objecty. Jeśli getter zwraca nie prymityw, a złożony obiekt — to potencjalne naruszenie LoD. Dodaj metodę wykonującą potrzebne działanie i ogranicz dostęp do gettera.
Fasada (Facade) — wzorzec architektoniczny, udostępniający prosty interfejs do złożonego podsystemu. W kontekście LoD Fasada to klasa, przez którą klient komunikuje się z grupą obiektów, nie znając ich wewnętrznej struktury. Repository w Android — klasyczna Fasada, ukrywająca łańcuchy DataSource → API → cache.
// Fasada: Repository ukrywa łańcuch źródeł danych
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 nie wie ani o api, ani o cache, ani o analytics
viewModel.processPayment(amount)
PaymentRepository — Fasada: ViewModel wywołuje jedną metodę processPayment, a repozytorium koordynuje API, cache i analitykę wewnątrz siebie. ViewModel nie ma łańcuchów wywołań do api.charge() czy cache.save() — to naruszyłoby LoD. Cała wewnętrzna struktura jest ukryta za jednym wywołaniem.
Nadmiarowe opakowania — gdy programista tworzy dziesiątki metod pośredniczących, po prostu delegujących wywołanie z jednej klasy do drugiej. Order.getUserEmail() = user.email — bezużyteczne opakowanie. LoD nie wymaga opakowań dla każdego pola — wymaga ukrywania łańcuchów, a nie pojedynczych prostych pól.
Kryterium: jeśli opakowanie po prostu zwraca pole bez transformacji i bez ukrywania łańcucha — nie jest potrzebne. Order.getUserEmail() — złe opakowanie, ponieważ user.email to bezpośredni dostęp do pola sąsiedniego obiektu, a user jest bezpośrednim polem Order, co jest dozwolone przez LoD. Naruszeniem byłoby, gdyby Order zwracał user.getEmail() przez dwa kroki: najpierw user, potem email.
Nie twórz opakowań dla bezpośrednich pól (dostęp do pola własnego obiektu lub bezpośredniego pola — dozwolony przez LoD). Twórz opakowania, gdy klient zaczyna chodzić tranzytowo: a.b().c().d() → a.b().d() lub a.d().
LoD stosuje się do zachowania, a nie do danych. Data class (DTO — proste kontenery danych) nie muszą przestrzegać LoD: ich celem jest ujawnianie danych. OrderDTO.items[0].price — nie naruszenie LoD, ponieważ DTO z definicji jest strukturą danych, a nie obiektem z zachowaniem. Mylenie obiektów i struktur danych — jeden z najczęstszych błędów.
Różnicę przedstawił Robert C. Martin: „Clean Code” (2008): „Obiekty ukrywają dane i ujawniają zachowanie. Struktury danych ujawniają dane i nie mają zachowania”. LoD dotyczy obiektów z zachowaniem. Dla struktur danych (DTO, modele JSON) łańcuchy dostępu są dozwolone. Gdy tylko u struktury pojawia się metoda z logiką — staje się obiektem i musi przestrzegać LoD.
Rozróżniaj: jeśli klasa zawiera tylko pola bez metod (DTO) — LoD nie ma do niej zastosowania. Jeśli klasa zawiera metody z logiką — LoD jest obowiązkowy. Na code review sprawdzaj: to data class (DTO) czy obiekt (z metodami)?
Często zadawane pytania
Prawo Demeter (LoD): obiekt może komunikować się tylko z bliskimi przyjaciółmi — sobą, swoimi polami, parametrami swoich metod i obiektami, które sam utworzył. Nie można chodzić przez łańcuch: a.getB().getC().doSomething() — to naruszenie.
LoD — mówi o tym, DO JAKICH obiektów można się odwoływać (tylko do bezpośrednich sąsiadów). Tell, Don't Ask — mówi o tym, JAK się odwoływać (nie pytaj o dane, tylko powiedz, żeby zrobił). Uzupełniają się: LoD ogranicza krąg komunikacji, Tell Don't Ask — charakter odwołania.
LoD można naruszyć dla DTO (Data Transfer Objects) i prostych struktur danych niezawierających logiki. Również Builder nie jest uważany za naruszenie, ponieważ każde wywołanie zwraca ten sam builder. Wyjątki: łańcuchy w Stream API (map, filter) — nie naruszenie LoD.
Detekt ma regułę TooManyFunctions (pośrednio), ale do bezpośredniego sprawdzania łańcuchów użyj reguły DataClassShouldBeImmutable i niestandardowych sprawdzeń przez bindingReference. Skonfiguruj CI: łańcuchy dłuższe niż 2 wywołania — ostrzeżenie, dłuższe niż 3 — błąd budowania.
SwiftLint nie ma wbudowanej reguły dla LoD, ale można utworzyć niestandardową regułę przez regex: łańcuchy postaci \..+\.\..+\.\..+ (3+ wywołania przez kropkę). Alternatywa: użyj reguły nimble_operator i rozszerz ją do wykrywania długich łańcuchów.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również