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”). Подесите fail на упозорења са ланцима дужим од 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 open-source пројеката и открило: у сваком 10. комиту јавља се исправка ланца позива сломљеног због промене модела. Притом 68% таквих исправки — у датотекама које нису повезане са измењеним моделом. Ланци шире промене по целој бази кода.
Користите LoD као правило code review: ако видите ланац од 3+ позива — захтевајте рефакторинг. Изузетак — Builder (конструктор), где ланац не крши LoD, јер сваки позив враћа исти builder.
Транзитивни приступ — најчешћи пример кршења LoD. Код добија објекат, затим кроз гетере продире унутар овог објекта, затим унутар следећег. Сваки гетер открива унутрашњу структуру и позива на кршење 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-ова. Код приступа view.subviews.first?.subviews.last и модификује UILabel унутра. Ово — транзитивни приступ унутрашњој структури UI, који се ломи при најмањој промени хијерархије.
// Кршење LoD: приступ унутрашњој хијерархији view-а
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 директно, не знајући унутрашњу структуру. Промена хијерархије view-а ће утицати само на проширење, а не на десетине места где се користи овај UILabel.
Широки интерфејс (гетери на сва унутрашња поља) — главни узрок кршења LoD. Ако објекат открива све своје унутрашњости, клијенти ће неизбежно почети да транзитивно ходе по њима. Решење: замените гетере методама које извршавају смислене радње (Tell, Don't Ask).
Уместо user.address.city.name пружите user.getCityName(). Уместо order.items.getTotal() пружите order.getTotalPrice(). Свака таква метода — то је инкапсулација ланца која штити клијенте од промена унутрашње структуре. Према Martin Fowler: „Refactoring, 2nd Edition” (2019), замена транзитивног приступа методом посредником — један од најкориснијих рефакторинга по односу корист/напор.
Проверите све јавне гетере који враћају mutable објекте. Ако гетер враћа не примитив, већ сложени објекат — то је потенцијално кршење LoD. Додајте метод који извршава потребну радњу и ограничите приступ гетеру.
Фасада (Facade) — архитектонски образац који пружа једноставан интерфејс ка сложеном подсистему. У контексту LoD, Фасада је класа кроз коју клијент комуницира са групом објеката, не знајући њихову унутрашњу структуру. Repository у Android-у — класична Фасада која скрива ланце DataSource → API → cache.
// Фасада: 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 — Фасада: ViewModel позива једну методу processPayment, а репозиторијум координира API, cache и аналитику унутар себе. ViewModel нема ланаца позива ка api.charge() или cache.save() — то би кршило LoD. Цела унутрашња структура је скривена иза једног позива.
Прекомерни омотачи — када програмер ствара десетине метода посредника које само делегирају позив из једне класе у другу. Order.getUserEmail() = user.email — бескорисни омотач. LoD не захтева омотаче за свако поље — захтева скривање ланаца, а не појединачних једноставних поља.
Критеријум: ако омотач само враћа поље без трансформације и без скривања ланца — није потребан. Order.getUserEmail() — лош омотач, јер user.email представља директан приступ пољу суседног објекта, а user је непосредно поље Order, што је дозвољено LoD. Кршење би било да Order враћа user.getEmail() кроз два корака: прво user, затим емаил.
Не креирајте омотаче за директна поља (приступ пољу свог објекта или непосредном пољу — дозвољено 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) или објекат (са методама)?
Често постављана питања
Деметрин закон (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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође