3 Kasım 2017 Cuma

SOLID - Open-Closed Principle - Mevcut Kodu Değiştirmeden Ekleme Yap. Yani GoF Örüntülerini Kullan

Giriş
Open-Closed kuralının ortaya çıkışı Bertrand Meyer'e atfedilir. 1988 yılında yazdığı kitabında bu kuraldan bahsetmiştir. Kural şu manaya gelir.
the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification".
Mimari Seviyede Open-Closed Principle
Açıklaması şöyle.
At a class level, the Open-Closed Principle (OCP) states that a class is opened for extension but closed to modification, meaning that you should be able to extend a class's behavior without modifying it. This is usually done by extending the class, either using inheritance or composition.

At an architectural level, we are not trying to modify the functionality of a piece of the system (a process, daemon, service, or microservice that best suits your architecture) but, instead, add new pieces leveraging the work you've already done. In order not to modify an existing piece, your system needs to be fully decoupled.
Kod Seviyesinde Open-Closed Principle
Bu kural insanlar tarafından farklı şekillerde anlaşılıyor.

1. Mevcut koda dokunma
Açıklaması şöyle.
"The principle basically says you can't modify the behavior of a component. Instead you have to provide a new variant of the component with the desired behavior, and keep the old version around unchanged for backwards compatibility."
Open-Closed Kuralı extend kelimesini kullanıyor ancak bu mutlaka kalıtım kullanılacak anlamına gelmez! Sadece mevcut koda dokunmamayı ifade eder.

2. Eski kodu deprecate et, yeniden yaz
Mevcut koda dokunma ve geriye uyumluluğu bozma ama yeni bir çatı ile daha temiz olacak şekilde kodu baştan yaz seçeneği. Açıklaması şöyle.
The pragmatic alternative to open/closed is controlled deprecation. Rather than breaking backwards compatibility in a single release, old components are kept around for a release cycle, but clients are informed via compiler warnings that the old approach will be removed in a later release. This gives clients time to modify the code. This seems to be the approach of React in this case.
3.Tasarlarken kodu değiştirilecek şekilde yaz
Açıklaması şöyle.
...the principle does not say "you can't modify the behavior of a component" - it says, one should try to design components in a way they are open for beeing reused (or extended) in several ways, without the need for modification. This can be done by providing the right "extension points", or, as mentioned by "by decomposing a class/function structure to the point where every natural extension point is there by default." IMHO following the OCP has nothing in common with "keeping the old version around unchanged for backwards compatibility
Kodu değiştirecek şekilde yazarken kalıtımın satır ve sınıf sayısının orantısız şekilde artmasına sebep olduğu için kötü olduğunu düşüneneler var. Açıklaması şöyle
In theory the principle solves the problem of backwards compatibility by creating code which is "open for extension but closed for modification". If a class has some new requirements, you never modify the source code of the class itself but instead creates a subclass which overrides just the appropriate members necessary to change the behavior. All code written against the original version of the class is therefore unaffected, so you can be confident your change did not break existing code.

In reality you easily end up with code bloat and a confusing mess of obsolete classes. If it is not possible to modify some behavior of a component through extension, then you have to provide a new variant of the component with the desired behavior, and keep the old version around unchanged for backwards compatibility.

Kodu böyle yazabilmek büyük bir bilgelik gerektiriyor :)


Mevcut Koda Dokunma İçin Seçenekler
Aşağıda bazı seçenekleri yazmaya çalıştım.

1. Mevcut Koda Dokunma- Open-Closed Kuralını Kalıtım İle Gerçekleştirmek
Kalıtım en çok kullanılan yöntem.
The most common manifestation of the Open Closed Principle is using polymorphism to substitute an existing part of the application with a brand new class.
Örnek
PasswordMinLengthRule sınıfı daha sonra dışarıdan konfigüre edilebilir hale gelsin isteniyor. Bu amaç için aynı arayüzden kalıtan PasswordConfigurableMinLengthRule sınıfı geliştiriliyor.

Liskov Kuralı İle İlişkisi Nedir?
Eğer mevcut arayüzden yeni bir sınıf kalıtılacaksa Liskov kuralı devreye girer. Liskov bir arayüzü kullanan sınıfın - yani tüketicinin - kalıtım hiyerarşisinden tamamen bihaber olmasını ister.

Yeni kalıtılan sınıfın, arayüzü kesinlikle bozmaması ve arayüz çağrılarının değişmemesi gerekir.

Kalıtım için Strategy veya Template tasarım örüntüsü kullanılabilir. Bu iki örüntü ise genellikle Factory veya AbstractFactory ile yakından ilişkilidirler.

2. Mevcut Koda Dokunma - Open-Closed Kuralını Decorator İle Gerçekleştirmek
Decorator örüntüsü mevcut koda dokunmadan ve yeni bir arayüz tanımlamadan kodu değiştirebilmeyi sağlar. Şöyle bir arayüz olsun
public interface ITask
{
  void Execute();
} 
Bu arayüzü gerçekleştiren bir sınıfımız olsun
public class SendToEmailTask : ITask
{
  void Execute() {...}
} 
Bir gün loglama yeteneği olan bir sınıf eklemek istersek kalıtmak yerine Decorator örüntüsü ile bu işlevi sağlayan yeni bir sınıfı yazarız.
public class LoggingTask : ITask
{
  private readonly ITask task;

  public LoggingTask(ITask task)
  {
    //guard clause
    this.task = task;
  }

  public void Execute() 
  { 
    Logger.Log("task...");
    this.task.Execute();
  }
} 
3.Mevcut Koda Dokunma-  Open-Closed Kuralını Strategy Ya Da Dependency Injection İle Gerçekleştirmek
Elimizde şöyle bir kod olsun. "Point for extension" için strategy kullanılıyor. Context sınıfı değişime kapalı, ancak IBehavior ile davranışı değiştirilebilir.
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

  // Context is however open for extension through
  // this private field
  private IBehavior behavior;

  // The context calls the behavior in this public 
  // method. If you want to change this you need
  // to implement it in the IBehavior object
  public void doStuff() {
    if (this.behavior != null)
      this.behavior.doStuff();
  }

  // You can dynamically set a new behavior at will
  public void setBehavior(IBehavior behavior) {
    this.behavior = behavior;
  }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
  public void doStuff();
}
4. Mevcut Koda Dokunma - Open-Closed Kuralını Template Tasarım Örüntüsü İle Gerçekleştirmek
Örnek
Elimizde şöyle bir kod olsun. LiskovBase sınıfı template örüntüsünü kullanıyor. LiskovSub template örüntüsüne sadece girdi sağlıyor. Ana algoritma halen template içinde çalışıyor. Böylece LiskovBase değişmeden kalır ama algoritma girdisi değiştirilebilir.
public class LiskovBase {
  // this is now a template method
  // the code that was duplicated
  public final void doStuff() {
    System.out.println(getStuffString());
  }

  // extension point, the code that "varies"
  // in LiskovBase and it's subclasses
  // called by the template method above
  // we expect it to be virtual and overridden
  public string getStuffString() {
    return "My name is Liskov";
  }
}

public class LiskovSub extends LiskovBase {
  // the extension overridden
  // the actual code that varied
  public string getStuffString() {
    return "I'm sub Liskov!";
  }
}
Örnek
Template örüntüsü kullanarak PasswordValidator kodlamak için şöyle yaparız.

5. Mevcut Koda Dokunma - Open-Closed Kuralını Adapter Tasarım Örüntüsü İle Gerçekleştirmek
Örnek
Şöyle yaparız. Mevcut kod yani draw_point() metodu Point nesnesi alıyor. Ancak biz Line nesnesi kullanmak istiyoruz. LineAdapter ile bu iş halledilebilir.
struct Point {
  int32_t     m_x;
  virtual void draw(){ cout<<"Point\n"; }
};


struct Point2D : Point {
  int32_t     m_y;
  void draw(){ cout<<"Point2D\n"; }
};


void draw_point(Point &p) {
  p.draw();
}


struct Line {
  Point2D     m_start;
  Point2D     m_end;
  void draw(){ cout<<"Line\n"; }
};

struct LineAdapter : Point {
  Line&       m_line;
  LineAdapter(Line &line) : m_line(line) {}
  void draw(){ m_line.draw(); }
};

int main() {
  Line l;
  LineAdapter lineAdapter(l);
  draw_point(lineAdapter);
  return EXIT_SUCCESS;
}
Open-Closed Kuralını İhlal Ettiğimizi Nasıl Anlarız? Bunu anlamanın en kolay yolu closed olması gereken koda dokunmak zorunda kaldığımızı andır.
Örnek
Bu örneğin adı "Replace Conditional With Polymorphism Refactoring". Klasik bir refactoring örneğidir.

Elimizde bir şekil hiyerarşimiz olsun. Her şekil için alan hesaplamak isteyelim. Eğer CalculateArea() metodu ana sınıfta ise, yeni bir şekil tipi eklemek istediğimizde, ana sınıfı da değiştirmemiz gerekir. Bu durumda Closed kuralını ihlal ederiz.
public double CalculateArea()
{
  switch(BoxType)
  {
    case BoxType.Rectangle:
      return Width * Height;
    case BoxType.Square:
      return Width * Height; 
    case BoxType.Circle:
      return Radius * Math.PI;
    default: return 0;
  }
}

Hiç yorum yok:

Yorum Gönder