10 Aralık 2019 Salı

Law Of Demeter

Giriş
Low Of Demeter aynı zamanda "The Principle of Least Knowledge" olarak ta adlandırılır. 1987 yılında ortaya konmuştur.
The Law of Demeter (LoD) or principle of least knowledge is a design guideline for developing software, particularly object-oriented programs. In its general form, the LoD is a specific case of loose coupling. The guideline was proposed by Ian Holland at Northeastern University towards the end of 1987, and can be succinctly summarized in each of the following ways:[1]
  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.
The fundamental notion is that a given object should assume as little as possible about the structure or properties of anything else (including its subcomponents), in accordance with the principle of "information hiding".
It may be viewed as a corollary to the principle of least privilege, which dictates that a module possess only the information and resources necessary for its legitimate purpose.
Wikipedia'daki açıklaması şöyle.
More formally, the Law of Demeter for functions requires that a method m of an object O may only invoke the methods of the following kinds of objects:[2]
  • O itself
  • m's parameters
  • Any objects created/instantiated within m
  • O's direct component objects
  • A global variable, accessible by O, in the scope of m
Bu kural Bridge ve Facade örüntülerine de çok benziyor.

Bu yöntemde, A nesnesi kendisine parametre olarak geçilen B nesnesini kullanır, ancak B'nin sahibi olduğu C nesnesine "The Principal of Least Knowledge" gereğince C nesnesini bilmesi gerekmiyorsaerişmez!

The Principal of Least Knowledge Örnekleri
Örnek
Elimizde şöyle bir kod olsun
# better
dog.walk()

# worse
dog.legs().front().left().move()
dog.legs().back().right().move()
# etc.
Birinci kullanımın neden kötü olduğunun açıklaması şöyle. Burada Dog sınıfından başka Leg sınıfını da bilmek gerekiyor. Hatta walk() logic tamamen kayboluyor.
There are two reasons why the second is worse. The first, not really directly LOD-related, is that your walk logic isn't reusable, which is a problem in the case where there are multiple places in your codebase where a dog must walk.

The LOD-related reason why this code is bad is because it forces the current consumer to know that DogLeg exists and how to operate it. The unspoken expectation here is that your consumer only knows about a Dog, and that it can be made to move around, but how that Dog moves around isn't something the consumer cares about (that's up to the Dog to manage for themselves).
Bu sefer elimizde şöyle bir kod olsun
account.user().fullName()
Burada Law Of Demeter ihlal ediliyor gibi görünüyor ancak aslında edilmiyor. Açıklaması şöyle. Sebebi ise hem Account hem de User nesnelerinin Domain Object yani açıktan bilinen nesneler olması.
If Account and User are both domain objects of which your consumer has public knowledge, then there's no issue with asking them to handle a User object directly.
The expectation here is that "the account refers to its owner" is part of the Account interface, and therefore returning the owner (represented by a User object) is fair game.

Comparatively, your Dog interface is not expected to include "the dog has legs", but rather "the dog is able to move around", and the legs are just an implementation detail so the dog is able to fulfill its contract (i.e. moving around). The interface itself doesn't specify the existence of legs, and therefore the consumer of Dog shouldn't be relying on the existence of legs.

In essence, a DogLeg is considered a private implementation detail, whereas a User (class) is publically known. This means that there's significantly less issue with expecting your consumer to handle a User than there is with expecting them to handle a DogLeg.

That being said, if account.user() was actually an AccountUser object which would also be considered a private implementation detail, then the same principle applies as it does for DogLeg.

This is what makes LOD so tricky to pinpoint. It's not something that is objectively true based on your code alone, it hinges on subjective context and expectation of interfaces/contracts. By renaming the code, you change the reader's implicit expectation, which can change whether something is considered an LOD violation.
Eğer Leg nesnesi de açıktan bilinmesi gereken bir nesne olsaydı. Örneğin ön bacaklara erişmek gerekseydi şöyle yapardık
dog.legs().front()
account.user().fullName()
Açıklaması şöyle. Bu durumda Leg sınıfına erişim kabul edilir olurdu
Technically, it's the same code. But what changes is our expectation of how acceptable it is to force a consumer to directly handler a DogLeg vs forcing them to handle a User.

1. C# ve Null-Conditional Operator
C# 6.0'daki null-conditional operator bu kuralı ihlal ediyor diyenler var. Örneğin şöyle bir kod gerçekten çok derinlere erişiliyor hissi veriyor. Aslında yine yukarıdaki gibi A,B,C vs. nesnelerinden hangisinin Domain Object olduğu önemli.
var x = A?.B?.C?.D?.E?.F;
Bu kodun kuralı ihlal etmediğini savunan The Law of Demeter Is Not A Dot Counting Exercise yazısı ise ilginç.

2. Law Of Demeter Uygulansın İstiyorsak

2.1 Çözüm 1 - A'nın C'ye Erişimi Yönetmesi
Bu yöntemde çoğunlukla A nesnesine C'ye erişim için yeni metodlar ekleniyor.

Örnek 1
Şu kod yerine
a.getB().getItems ()
Şöyle yaparız.
class A 
{
  private B b;

  void doSomething() {
    b.update();
  }

  Items getItems()
  {
    return b.getItems();
  }
}
Şöyle kodlarız.
a.getItems ()
Örnek 
Reservation->Show->Rows->Seats sınıfların erişmek yerine
int selectedRow =...;
int selectedSeat = ...;
if (show.getRows().get(selectedRow).getSeats().get(selectedSeat)
 .getReservationStatus()) {
{...}
Şöyle yaparız.
int selectedRow = ...;
int selectedSeat = ...;
if (show.isSeatReserved(selectedRow, selectedSeat)) {...}
2.2. Çözüm 2 - A Bir Arayüz İse Virtual Metod Eklenir
Bu aslında Çözüm 1 ile aynı şey.
Örnek
Şöyle yaparız.
// Violating LOD

bool isAlive = player.heart.IsBeating();

// But what if the player is a robot?

public class HumanPlayer : Player {
  public bool IsAlive() {
    return this.heart.IsBeating();
  }
}

public class RobotPlayer : Player {
  public bool IsAlive() {
    return this.IsSwitchedOn();
  }
}

// This code works for both human and robot players, and thus wouldn't need to be
// changed when new (sub)types of players are developed.
bool isAlive = player.IsAlive();
2.3 Çözüm 3 - A'nın C'yi Bir Handler Sınıfına Vermesi
Örnek
Tam bir çözüm mü bilmiyorum ama bazen şöyle yapılıyor. A nesnesi B'den C nesnesini alınca C'yi başka bir sınıfa geçiyor ve bir iş yapmasını istiyor. Yani aslında ortaya bir D sınıfı daha çıkıyor ancak her sınıf sadece tek bir friend bildiği için arkadaşımın arkadaşını bilmek zorunda kalmıyorum. Böylece A ve C birbirlerinden ayrılıyorlar. MyStream A nesnesi, encoder B nesnesi, frame ise C nesnesi. A yani MyStream B'den frame'i alınca FrameHandler'a geçiyor.
MyStream::processFrame () {
  frame = encoder->WaitEncoderFrame()
  FrameHandler::DoOrGetSomethingForFrame(frame); 
    ...
}

void FrameHandler::DoOrGetSomethingForFrame(Frame *frame)
{
  frame->DoOrGetSomething();
}  
3. Law Of Demeter'in Getireleri
C nesnesi sıkça değişen bir nesne ise A bu tür değişikliklerden asgari etkilenir.

6. Law Of Demeter'in Götürüleri
Law of Demeter'in problemli noktalarından birisi, A nesnesinin gönderdiği mesaj aslında C nesnesinde sonlanıyorsa, B nesnesine bir sürü metod eklenmek zorunda kalınması. Bu metod genellikle sadece delegation yapar.

Örnek
Sadece delegation yapan Class2 sınıfı şöyledir.
public Class1
{
    public void DoSomething()
    {
        Class2 class2 = new Class2();
        string myValue = class2.GetSomeValuePlease();

       //why would I not do class2.MyClass3.GetActualValue();
    }
}

public Class2
{
    public Class3 MyClass3 = new Class3();
    public string GetSomeValuePlease()
    {
        return this.MyClass3.TheActualValue();
    }
}

public Class3
{
    public string TheActualValue()
    {
        return "This is the value";
    }
}
Örnek
Bu örnekte de A -> B -> C ilişkisini A sınıfı C'ye direkt erişerek bozuyor. Elimizde bir sınıf olsun. Yani B
class Rewriter {
  public List<Mapping> getMappings();
}
Bu arayüzü kullanan bir başka sınıf olsun. Yani A. A sınıfı B'nin içindeki listeye yani C'ye erişiyor olsun
class RewriterSpec {
  private final Rewriter rewriter;

  public RewriterSpec(Rewriter rewriter) {
    this.rewriter = rewriter;
  }

  public addMapping(Mapping m) {
    rewriter.getMappings().add(m);
  }
}
A sınıfı girişte C'yi elde eder ve kendi alanı olarak saklar. Böylece C'ye erişebilir.
class RewriterSpec {

  private final List<Mapping> mappings;

  public RewriterSpec(Rewriter rewriter) {
    this.mappings = rewriter.getMappings();
  }

  public addMapping(Mapping m) {
    mappings.addMapping(m);
  }
}
7. Call Stack ve Debug
Daha da karmaşık örneklerde iç içe geçmiş katmanı sayısı arttıkça, Z'den A'ya ait bir metodu çağıran kodu "step in" şeklinde debug etmek sıkıntılı oluyor.
class A {}
class B {A a;}
class C {B b;}
etc...
class Z {Y y;}



Hiç yorum yok:

Yorum Gönder