Pasjonuję się IT, ekonomią i muzyką. Moim ulubionym językiem jest Java i w niej pracuję na codzień.
Przy projektowaniu obiektowym możemy się zastanowić, w jaki sposób uprościć relacje między naszymi obiektami, tak aby zmiana w jednej klasie nie propagowała konieczności zmian w wielu innych. Z pomocą przychodzi nam prawo Demeter, , które w skrócie ogranicza nas do interakcji jedynie z najbliższymi sąsiednimi obiektami. Dzięki temu, nasz obiekt jest zależny tylko od swoich sąsiadów. Kiedy implementacja sąsiada naszego sąsiada się zmieni - nie interesuje nas to. Jest to tak zwany luźny związek (ang. loose coupling). Nie łamiemy w ten sposób zasady enkapsulacji obiektu i nie tworzymy dużej ilości powodów, dla których mielibyśmy modyfikować naszą klasę. W ten sposób możemy sprawić, że nasz projekt jest zgodny z zasadami SOLID. Poniżej, na przykładzie logiki aplikacji quizowej, możemy przeanalizować przypadek tight coupled (1) i loose coupled (2).
//// przykład tight coupling (1)
class QuizWhichBreaksLawOfDemeter {
private questions: Question[]
// ...
isAnswerCorrect(question: Question) {
// poniżej łamiemy Prawo Demeter
// wnikając w implementację answer
return question.getAnswer().getId() ==
question.getUserAnswer().getId()
}
countCorrectAnswers(): number {
return this.questions
.filter(q => this.isAnswerCorrect(q))
.length
}
}
// anemiczna klasa question, nic prócz getterów i setterów
class Question {
private answer: QuizOption
private userAnswer: QuizOption
// ...
getUserAnswer(): QuizOption {
return this.userAnswer
}
getAnswer(): QuizOption {
return this.answer
}
}
W powyższym przykładzie z poziomu quizu dostajemy się do wnętrza answer, który nie jest naszym bezpośrednim sąsiadem (quiz <-> question <-> answer). Metoda isAnswerCorrect()
wywołuje łańcuch question.getUserAnswer().getId()
, dostając się głęboko do struktury, której szczegółów implementacyjnych nie powinniśmy znać. Łamiemy Prawo Demeter.
Zastanówmy się, jak możemy rozwiązać ten problem. Musimy ograniczyć splątanie naszych klas i ich wspólną zależność od siebie.
Poniżej przykład, który jest nieco lepszy. Metoda isAnswerCorrect()
została przeniesiona z klasy Quiz, do klasy Question. Manipuluje ona już swoimi własnymi sąsiadami, wystawiając Quizowi tylko metodę do sprawdzenia ( isAnswerCorrect()
).
// przykład loose coupling (2)
class QuizLooseCoupled {
questions: Question[]
// nie interesuje nas jak jest sprawdzana odpowiedź
// po prostu wywołujemy metodę naszego sąsiedniego obiektu
countCorrectAnswers(): number {
return this.questions
.filter(q => q.isAnswerCorrect())
.length
}
}
class Question {
// ...
private answer: QuizOption
private userAnswer: QuizOption
// tutaj przeniesiona jest odpowiedzialność funkcji
isAnswerCorrect() {
return this.answer.getId() === this.userAnswer.getId()
}
}
Podany przykład jest prosty. Popełnienie błędu z pierwszej sekcji nie jest fatalne w skutkach, refaktoryzacja zajmie pół minuty. W przypadku większych aplikacji z wieloma tight coupled obiektami może powstać problem, który poskutkuje efektem domina - naprawiając jeden obiekt, musimy naprawić 2,3,4 następnych…
Na ile posiadamy czasu, warto zastanowić się nad rozluznieniem wiezi naszych obiektów póki nie ma dramatu - nie żyjemy w starożytnej Grecji, ale Prawo Demeter warto znać :)
Całość kodu można znaleźć na moim githubie.