18 Aralık 2019 Çarşamba

SOLID - Interface Segregation Kuralı - Küçük Arayüzler İyidir

Giriş
Açıklaması şöyle. Yani büyük arayüzler de küçük parçalara bölünmeli.
The Interface segregation principle (ISP) says that smaller, lightweight interfaces are preferable to larger more generalized interfaces.
Interface Segregation Nedir?
Elimizde şöyle bir kod olsun. Bu kodda UserRegistrationClient sınıfı UserService sınıfının sadece register() metoduna ihtiyaç duyuyor, ancak tüm metodlarını da görebiliyor.
class UserService {
  User register(String username) { … }
  User find(String username) { … }
  void lock(User user) { … }
}

class UserRegistrationClient {
  private UserService userService;

  void registerUser() {
    String username = …
    User user = userService.register(username);
    …
  }
}
Interface Seggregation Kuralı bu kodu şu hale getirebilirsiniz diyor. Yeni kodda bir RegisterUser arayüzü tanımlandı ve UserRegistrationClient sadece bu daha küçük arayüzü biliyor.
interface RegisterUser {
  User register(String username);
}

class UserService implements RegisterUser {
  //as above
}

class UserRegistrationClient {
  private RegisterUser registerUser;

  void registerUser() {
    String username = …
    User user = registerUser.register(username);
    …
  }
}
Interface Segregation Kuralı artık yeni Functional Programming yöntemleriyle rahatça yer değiştirebiliyor. Bu kodu şu hale de getirebiliriz. Arayüzün ortadan kalması, iyi bir şey de olabilir, anlamayı da zorlaştırabilir.
class UserRegistrationClient {
  private Function<String,User> registerUser;

  void registerUser() {
    String username = …
    User user = registerUser.apply(username);
    …
  }
}

class UserService {
//as above
}

new UserRegistrationClient(userService::register);
1. Separation Of Mechanism And Policy İle Farkı
Separation of mechanism and policy tasarım kuralının açıklaması şöyle.
The separation of mechanism and policy is a design principle in computer science. It states that mechanisms (those parts of a system implementation that control the authorization of operations and the allocation of resources) should not dictate (or overly restrict) the policies according to which decisions are made about which operations to authorize, and which resources to allocate.
Bu kurala göre bir nesnenin çalışması (mechanism) ile hangi zamanda hangi kurallara göre çalışması (policy) farklı şeyler.

Örnek
Kural açıklayan bir örnek şöyle.
Separation of mechanism from policy is that the car is built to go up to 100 miles per hour in any direction it is pointed, and the driver is responsible for going the correct way down the highway in the correct lane at the correct speed, not the car. The policies about what the speed limit is and when you are allowed to turn left are not embedded in the engine except insofar as the engines were designed by people who knew what likely policies would be enacted.
2. Interface Segregation Kuralı İçin Bazı Örnekler

2.1 Örnek - Kalıtımın En Tepesini Değiştirmek
Kalıtım hiyerarşisinde bir nesnenin yeni bir metoda ihtiyacı varsa, her şeyin türediği arayüze yeni bir metod ekleyip tüm hiyerarşiye boş metodlar eklemek yerine, yeni bir arayüz oluşturulur ve nesnenin bu yeni arayüzden kalıtması sağlanır. Açıklaması şöyle.
Clients should not be forced to depend upon interfaces that they don't use.
Not : Yeni bir arayüz eklemek yerine bir diğer seçenek olan Composition Over Inheritance tercih edilebilir.

2.2 Örnek - Yeni Bir Arayüz Tanımlamak
Örneğin Door isimli bir arayüz olsun. Eğer TimedDoor diye bir sınıfımız varsa ve bu sınıfa setTimeout() metodu eklememiz gerekiyorsa, Door arayüzünü değiştirmek yerine, TimedObject isimli yeni bir arayüz tanımlar ve TimedDoor nesnesini bu arayüzden de kalıtırız.

2.3 Örnek - Yeni Bir Arayüz Tanımlamak
Bir diğer örnekte IRepository arayüzüne Generate() metodu eklemek yerine, IReport arayüzü IRepository'den kalıtır ve rapor üretmeye gereksinim duyan nesneler bu yeni IReport arayüzünü kullanırlar.

2.4 Örnek - Yeni Bir Arayüz Tanımlamak
Şöyle yaparız.
class Payment implements Expirable, Limited {
 /* ... */
}

class PaymentProcessor {
    // Using payment here because i'm working with payments.
    public void process(Payment payment) {
        boolean expired = expirationChecker.check(payment);
        boolean pastLimit = limitChecker.check(payment);

        if (!expired && !pastLimit) {
          acceptPayment(payment);
        }
    }
}

class ExpirationChecker {
    // This the `Expirable` world, so i'm  using Expirable here
    public boolean check(Expirable expirable) {
        // code
    }
}

class LimitChecker {
    // This class is about checking limits, thats why im using `Limited` here
    public boolean check(Limited limited) {
        // code
    }
}

3. Kötü Örnekler

3.1.Örnek
Java'da bu kuralın ihlal edildiği bir çok yer var. En bariz örnekler Collections arayüzleri. Bu arayüzleri gerçekleştiren bir çok sınıf UnsupportedOperationException atar.

3.2. dynamic_cast Yapmak
Elimizde şöyle bir kod olsun
class Example implements Interface1, Interface2 {
    ...
}
Bu nesneyi Interface1 olarak kullanalım.
Interface1 example = new Example();
Eğer Interface1 nesnesi Interface2'ye çevrilmek zorunda kalıyorsa bir sıkıntı vardır.
if (example instanceof Interface2) {
    ((Interface2) example).someInterface2Method();
}
Açıklaması şöyle.
I suspect that if you're finding that lots of your classes implement different combinations of interfaces then either: your concrete classes are doing too much; or (less likely) your interfaces are too small and too specialised, to the point of being useless individually.

If you have good reason for some code to require something that is both a Interface1 and a Interface2 then absolutely go ahead and make a combined version that extends both. If you struggle to think of an appropriate name for this (no, not FooAndBar) then that's an indicator that your design is wrong.
Bu durumu düzeltmek için yeni bir interface tanımlanabilir. Şöyle yaparız
interface Interface3 extends Interface1, Interface2 {}
ve bundan sonran bu arayüz kullanılır. Şöyle yaparız
public void doSomething(Interface3 interface3){
    ...
}
Eğer yeni arayüz tanımlamak mümkün değilse şöyle yaparız.
public <T extends Interface1 & Interface2> void doSomething(T t){
    ...
}
3.3. Aşırıya Kaçmak
Interface Segregation Kuralı uç noktaya kadar izlenirse, nihayetinden tek metoda sahip bir sürü arayüz sınıflarının olacağı iddia ediliyor.

Örnek
Elimizde şöyle bir kod olsun. Sınıfımız belki de tek metod gerektiren 3 ayrı arayüzden kalıtmak zorunda kalırsa bence aşırıya kaçılıyor.
// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}
Örnek
Elimizde şöyle bir kod olsun.
IGeometryManager
{
   Shape CreateTriangle();
   Shape CreateCircle();
   Shape CreateSquare();
   Shape CreateEllipse();
   Shape CreateCurve();
   void RemoveAll();
}
şeklinde olan bir sınıfın gittikçe parçalanarak bir sürü arayüze dönüşeceği iddia edilmiş.
ITriangleCreator
{
   Shape CreateTriangle();
}

ICircleCreator
{
   Shape CreateCircle();
}

ISquareCreator
{
   Shape CreateSquare();
}

IEllipseCreator
{
   Shape CreateEllipse();
}

ICurveCreator
{
   Shape CreateCurve();
}

IShapeRemover
{
   void RemoveAll();
}
Ve hatta bu kadar arayüzün aslında functional programming'e giriş olacağı da iddia edilmiş.

Hiç yorum yok:

Yorum Gönder