Prawo Demeter

Dziś w poście o kolejnej regule pisania dobrego kodu. Prawo Demeter mówi o relacjach między klasami a konkretnie o tym z jakimi obiektami dana klasa może komunikować się. W skrócie, prawo nakazuje odwoływanie się tylko do “bliskich przyjaciół” czyli obiektów, które są bardzo znane danemu obiektowi. Brzmi to trochę abstrakcyjnie dlatego przenieśmy to na  świat programowania obiektowego. Każda metoda obiektu A może wywoływać wyłącznie metody następujących obiektów:

1. obiektów przekazanych jako parametr wejściowy np.: 

private void Method(Employee employee)
{
   int salary = employee.CalculateSalary();
}

2. obiektów, które stanowią bezpośrednie pole\właściwość danej klasy np:  

class MoneyManager
{
    private Employee _employee;
    
    private void Update()
    {
        _employee.GivePayRise(); // OK
    }
}

3. obiektów dostępnych globalnie (np. Singleton):

private void Method(Employee employee)
{
   Employee.Instance.GivePayRise(); // łamie inne zasady ale zgodne z Demeter.
}

4. obiektów stworzonych przez daną metodę:

private void Update()
{
    Employee employee= new Developer();
   int salary = employee.CalculateSalary();
}

Powyższe przykłady pokazują scenariusze zgodne z prawem Demeter. Aby w pełni zrozumieć regułę warto pokazać kilka przykładów łamiących zasadę. Na przykład:

private void Method()
{
    Parent.Parent.Update();
}

Odwołanie się do Parent jest w porządku ponieważ stanowi on składową klasy. Niedozwolone jednak jest drugie odwołanie bo nie jest to bezpośredni “przyjaciel” danej klasy.  W świecie obiektowym, Demeter można uprościć do reguły pojedynczej kropki tzn. jeśli dana linijka kodu zawiera więcej niż jedną kropkę to znaczy, że kod odwołuje się do zbyt dalekich obiektów.

Dzięki przestrzeganiu Demeter, metoda odwołuje się wyłącznie do obiektów, które dobrze zna pod względem wewnętrznej struktury. Pisanie kodu spełniającego prawo Demeter, może polepszyć modyfikowalność i elastyczność projektu poprzez zmieszczenie liczby referencji. W końcu gdy dana klasa odwołuje się poprzez pięć obiektów do szóstego wtedy analiza błędów i testowanie jest utrudnione. Wadą podejścia jest możliwość powstawia wielu metod, które służą wyłącznie jako wrappery tzn. delegują wykonanie danego kodu. Aby to zobrazować rozważmy następujący kod łamiący Demeter:

class EmployeeInfo
{
    public int Salary{get;set;}
}
class Employee
{
    public EmployeeInfo Info{get;set;}
}
class CompanyManager
{
    private Company _company;
    
    public void UpdateSalaries()
    {
        foreach(Employee employee in _company.Employees)
        {
            employee.Info.Salary+=3;
        }
    }

}

Kod nie spełnia reguły bo zachodzi tutaj relacja Company->Employee->EmployeeInfo. W celu naprawienia problemu wystarczy stworzyć dodatkową metodę:

class EmployeeInfo
{
    public int Salary{get;set;}
}
class Employee
{
    public EmployeeInfo Info{get;private set;}
    public GivePayRise(int value)
    {
        Info.Salary+=value;
    }
}
class CompanyManager
{
    private Company _company;
    
    public void UpdateSalaries()
    {
        foreach(Employee employee in _company.Employees)
        {
            employee.GivePayRise(3);
        }
    }

}

W powyższym przypadku, dodanie metody ma sens, ale  w wielu przypadkach jest to po prostu zaśmiecanie kodu. Podsumowując, dzięki Demeter klasa nie musi mieć szerokiej wiedzy o innych obiektach bo odwołuje się wyłącznie do dobrze znanych klas – mniejsza wiedza to mniejsze ryzyko wystąpienia błędu.

6 thoughts on “Prawo Demeter”

  1. No tak, jednak jak zwykle są jakieś ale np. LINQ skutecznie oducza nas tego że “jeśli dana linijka kodu zawiera więcej niż jedną kropkę to znaczy, że kod odwołuje się do zbyt dalekich obiektów.”, przykład enumerable.Select(selector).Where(where).OrderBy(order)

  2. yuhtilo: owszem używasz wielu kropek, ale cały czas działasz na jednym obiekcie tj. IQueryable lub IEnumerable

  3. @yuhtilo:
    Fragment kodu jest OK. Tak jak powiedzial tazos333 odwolujesz sie do jednego obiektu. Przestrzeganie jednej kropki jest tylko uproszczeniem i nie zawsze ma sens.

  4. @tazos333
    wywołania linq nie zwracają tego samego obiektu, ale tylko obiekt o tym samym typie — szczegół, ale w takich szczegółach często tkwi diabeł :]

  5. Prawo Demeter jest ok, ale jest jedno ważne odstępstwo: mapowanie relacyjo-obiektowe (ORM). W bazach relacyjnych możemy zrobić zapytanie po wielu tablicach, w sposób nie przewidziany przez projektanta bazy danych. Po zmapowaniu tego na obiekty mamy to samo. Przykładowo, jeśli mamy obiekty: rachunek, klient, adres, karta, operacja (wpłaty, wypłaty), zestawienie (wyciąg z konta) to poprzez notację kropkową możemy używać: operacja.rachunek_wn.własciciel.adres.ulica, karta.rachunek.zestawienie.data_od, itp. na multum sposobów, a ich ilość rośnie wykładniczo z liczbą obiektów które mogą się łączyć.
    żeby być w zgodzie z prawem Demeter do obiektu rachunek trzeba by dodać właściwości lub metody obiektu bezpośrednio połączonego, np. obiektu klient, tak więc mielibyśmy rachunek.wlasciciel_nazwisko, rachunek.wlasciciel_imie, …
    Nie da się tego zrobić dla wszystkich możliwych połączeń obiektów. Możnaby zrobić dla wszystkich używanych połączeń w programie, ale: i tak będzie tego sporo, a utrudnia się przyszły rozwój oprogramowania (zapotrzebowanie na nowy raport, formatkę, funkcjonalność).
    Tak więc poprzez ORM świat obiektowy zostaje “zainfektowany” podejściem nieobiektowym (łamanie prawa Demeter).

Leave a Reply

Your email address will not be published.