14 Aralık 2017 Perşembe

SOLID - Liskov Kuralı - Kalıtım Varsa A ve B Yer Değiştirebilmelidir

Giriş
SOLID kurallarından olan Liskov kuralı anlaması kolay ancak uygulaması kafa karıştıran bir kavram.

Liskov Kuralı
Liskov Substitution Principle (LSP) şu manaya gelir. Subtyping ve subclassing Java,C# gibi dillerde geçerli olma da bazı programlama dillerinde farklı kavramlar.
The LSP is concerned about subtyping and polymorphism.
Alt sınıfların hepsi gerçekleştirdikleri üst sınıfın yerine kullanılabilmelidir. Bu kural tamamen polymorphsim ile alakalıdırBazıları "program to an interface" kuralını belli bir web api/protokole göre yap şeklinde algılasalar da bence kast edilen şey o değil.

Kalıtımın LPS Olmadığı Durumlar
Kalıtım her programlama dilinde polymorphisim anlamına gelmez. Bu tür kullanımlarda LPS kuralı gerçerli değildir. Açıklaması şöyle
Two common use cases of inheritance language constructs that are not a case of subtyping are:

- Inheritance used to inherit the implementation of a base class, but not its interface. In nearly all cases composition should be preferred. Languages like Java can't separate inheritance of implementation and interface, but e.g. C++ has private inheritance.

- Inheritance used to model a sum type/union, e.g.: a Base is either CaseA or CaseB. The base type does not declare any relevant interface. To use its instances, you must cast them to the correct concrete type. The casting can be done safely and is not the issue. Unfortunately, many OOP languages are not able to restrict the base class subtypes to only the intended subtypes. If external code can create a CaseC, then code assuming that a Base can only be a  CaseA or CaseB is incorrect. Scala can do this safely with its case class concept. In Java, this can be modelled when the Base is an abstract class with a private constructor, and nested static classes then inherit from the base.
Yanlış Hiyerarşi
Bazen kalıtım olmaması yanlış hierarşiden daha iyi olabiliyor. Açıklaması şöyle
Finally, it is sometimes sensible to avoid any subclasses at all, but model all objects through a single type. This is particularly relevant in games because many game objects do not fit nicely into any hierarchy, and may have many different capabilities. E.g. a role playing game may have an item that is both a quest item, buffs your stats with +2 strength when equipped, has a 20% chance on ignoring any damage received, and provides a melee attack. Or maybe a reloadable sword because it is *magic*. Who knows what the story requires.
Yanlış Kullanım
Tüm bu istisnaları geçtikten sonra halen kalıtım hiyerarşimiz varsa ve şu kuralları ihlal ediyorsak yine problem var anlamına gelir. Liskov'un yanlış kullanılması ile ilgili bir açıklama şöyle
Developers who are new to the concept of programming to interfaces often have difficulty letting go of what is behind the interface.
At compile time, any client of an interface should have no idea which implementation of the interface it is using. Such knowledge can lead to incorrect assumptions that couple the client to a specific implementation of the interface.
Imagine the common example in which a class needs to save a record in persistent storage. To do so, it rightly delegates to an interface, which hides the details of the persistent storage mechanism used. However, it would not be right to make any assumptions about which implementation of the interface is being used at run time. For example, casting the interface reference to any implementation is always a bad idea.
Örnek
Yani şöyle olmamalı
void someMethod(ISomeClass interface){
  SomeClass cast = (SomeClass)interface;
}
Örnek
Hiyerarşide bazı alt sınıfların bazı özellikleri desteklememesi yüzünden algoritma için kontroller olmamalı. Yani şöyle olmamalı.
if (canReload(weapon)) {
  reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

1. Ön ve Son Koşullar
Bazen kod gerçek isteği tam ifade edemeyebilir. Örneğin Java'daki şöyle bir arayüze eksik sayılar girilemesin demenin imkanı yok.
public interface Roots_Squares {
    public double square_root( double value );
}
Bu durumda kod isteği tam belirtemese bile kalıtım hiyerarşisindeki davranışta tutarlılık olmalıdır.
Liskov kuralının tam bilinmediği bir çok eski kodda arayüz tarafından tanımlanan, ancak kalıtan sınıflarda boş bırakılan veya UnsupportedOperationException, NotSupportedException atan metodlar var. Bu tür kodlar günümüzde makbul değil.

1. Strengthed Pre-Condition
Eğer alt sınıf üst sınıfın sağladığı arayüzden farklı bir arayüz gerektiriyorsa ve bu uygulanırsa Liskov kuralı ihlal edilir. Örneğin üst sınıflar int ile çalışıyorsa ancak alt sınıf unsigned int gerektiriyorsa alt sınıfın talep ettiği ön koşul güçlendirilmiş olur.Bir başka örnek olarak Immutable arayüzler verilebilir. Örnekte Map arayüzü gerçekleştirilmesine rağmen nesne Immutable olduğu için ekleme çıkarma işlemlerinde hata verir.
Map<String, Integer> myMap = ImmutableMap.copyOf(justSomeMap);
myMap.put(key, val); // bang. runtime error.

2. Eased Out Post-Condition
Eğer alt sınıf üst sınıfın sağladığı arayüzden farklı bir tip döndürüyorsa Liskov kuralı ihlal edilir. Örneğin üst sınıf unsigned int döndürüyorsa ancak alt sınıf int döndürülmesini gerektiriyorsa son koşul gevşetilmiş olur.

2. Liskov ve Constructor
Liskov sınıfların constructor metoduna uygulanmaz. Ata sınıfın constructor metodu ile alt sınıfın constructor metodu farklı imzalar kullanabilirler.
The Liskov Substitution Principle does not apply to constructors; it only applies post-construction. You can change parameter lists all you want, and you can even make matching parameters exhibit very different behaviors.

An exception to this rule is that if a callback is passed in to the constructor of both the base class and the superclass, and if it cannot be set at a later time, then the subclass's version, in normal situations, must be backwards-compatible with superclass's version. The reason for this is that, in normal situations, outside code cannot otherwise enter into a state in which it would behave in the same way.
Liskov Kural Nasıl Düzeltilebilir
Eğer bir kalıtım hiyerarşisinde alt türlere mutlaka bakmak gerekiyorsa ve bunu düzeltemiyorsak, Visitor örüntüsü ile durumu kurtarabiliriz.

Hiç yorum yok:

Yorum Gönder