Bu gece de çirkin kod yazmaktan bıkmış haldeyken kendime bir süredir temiz kod yazmak ile ilgili ahkam kesmediğimi hatırlattım. Hepimiz inheritance, polymorphism, abstraction, encapsulation gibi nesne tabanlı programlama konseptlerini geliştirdiğimiz projelerde bolca kullanıyoruz. Belki bir iş mülakatında sorsalar bunlar ne diye bülbül gibi anlatırız da. Peki yazılımın kalite çıtasını Allahuekber dağlarına kadar çıkartabilecek olan bu güçlü araçları yeterince doğru kullanabiliyor muyuz?

Misal, bir arayüzün farklı implementasyonlarının nesnelerinden oluşan bir listenin elemanları üzerinde çeşitli işlemler yapıyorsunuz ve her sınıf için yapacağınız işlemler farklılık gösteriyor. Örneğin aşağıdaki Shape arayüzü ve onun altındaki Circle, Rectangle ve Square sınıflarını hayalimize yükleyelim.

// Shape
public interface Shape {
}

// Circle
public class Circle implements Shape {
    private int centerX;
    private int centerY;
    private int radius;

    public Circle(int centerX, int centerY, int radius) {
        this.centerX = centerX;
        this.centerY = centerY;
        this.radius = radius;
    }

    public int getCenterX() {
        return centerX;
    }

    public int getCenterY() {
        return centerY;
    }

    public int getRadius() {
        return radius;
    }
}

// Rectangle
public class Rectangle implements Shape {
    private int x;
    private int y;
    private int width;
    private int height;

    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

// Square
public class Square extends Rectangle {
    public Square(int x, int y, int length) {
        super(x, y, length, length);
    }
}

Şimdi aşağıdaki gibi bir listeye bu şekilleri doldurduğumuzu düşünelim:

List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(30, 40, 20));
shapes.add(new Square(50, 60, 30));
shapes.add(new Rectangle(10, 10, 20, 30));

Bu şekilleri belirli bir formatta konsola yazdırmak isteseydiniz (toString formatının dışında), aşağıdaki gibi bir kod yazar mıydınız?

private static void printShapes(List<Shape> shapes, PrintStream out) {
  for (Shape shape : shapes) {
    if (shape instanceof Circle) {
      Circle circle = (Circle) shape;
      out.printf("Circle Center = (%d, %d) and Radius = %d\n", 
        circle.getCenterX(), circle.getCenterY(), circle.getRadius());
    } else if (shape instanceof Square) {
      Square square = (Square) shape;
      out.printf("Square TopLeftCorner = (%d, %d) and SideLength = %d\n", 
        square.getX(), square.getY(), square.getWidth());
    } else if (shape instanceof Rectangle) {
      Rectangle rectangle = (Rectangle) shape;
      out.printf("Rectangle TopLeftCorner = (%d, %d), Width = %d and Height = %d\n", 
        rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
    }
  }
}

Çok acil bir durumda bunu yazmamız istenirse belki böyle yazardık. Bir diğer yöntem de her Shape arayüzüne String explainYourself(); gibi bir method ekleyip, bu açıklama metinlerini nesnelerin kendilerinin dönmesini sağlayabilirdik. Böylece if/else ve instanceof kullanımlarından kurtulurduk. Aşağıdaki gibi bir kod ile konsola yazdırma işlemi tamamlanırdı:

private static void printShapes(List&lt;Shape&gt; shapes, PrintStream out) {
  for (Shape shape : shapes) {
    out.println(shape.explainYourself());
  }
}

Peki sonra sizden java.awt.Graphics kullanarak tüm bu şekilleri ekrana çizmeniz istenirse? O zaman da Shape arayüzünün üzerine paintYourself(Graphics g) methodunu mu ekleyecektiniz? Yoksa bu sefer eski usül aşağıdaki gibi if/else ve instanceof kullanarak mı çözmeyi uygun bulacaktınız?

private static class ShapeDrawerPanel extends JPanel {
  private List<Shape> shapes;

  public ShapeDrawerPanel(List<Shape> shapes) {
    this.shapes = shapes;
  }

  @Override
  public void paint(Graphics g) {
    g.setColor(Color.WHITE);
    g.fillRect(0, 0, getWidth(), getHeight());

    for (Shape shape : shapes) {
      if (shape instanceof Circle) {
        g.setColor(Color.RED);
        Circle circle = (Circle) shape;
        g.drawOval(circle.getCenterX() - circle.getRadius(), circle.getCenterY() - circle.getRadius(), 
          circle.getRadius() * 2, circle.getRadius() * 2);
      } else if (shape instanceof Square) {
        g.setColor(Color.GREEN);
        Square square = (Square) shape;
        g.drawRect(square.getX(), square.getY(), square.getWidth(), square.getHeight());
      } else if (shape instanceof Rectangle) {
        g.setColor(Color.BLUE);
        Rectangle rectangle = (Rectangle) shape;
        g.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
      }
    }
  }
}

Shape arayüzüne sahip sınıflar ile yapacağımız her farklı işlem için tekrar tekrar bu if/else bloklarını yazmak oldukça kötü. Bunu zaten tartışmaya açık görmüyorum.

Her yeni işlem için tüm sınıflara yeni birer method eklemek ise bu sınıfları bir hayli kirletiyor. Her yeni operasyonda sisteminizdeki tüm data sınıflarınızı modifiye etmek zorunda kalıyorsunuz. Bu da data sınıflarınız ile bu sınıfları kullanan diğer sınıflar arasında yanlış bir bağımlılık oluşturuyor. Bir sınıfın kendisini kullanan sınıflara ait detayları bilmemesi gerekiyor. Shape sınıfına Graphics kullanarak kendisini ekrana nasıl çizmesi gerektiği detayını eklersek, ona geometrik bir şeklin yerini ve boyutlarını tanımlama amacının dışında farklı bir özellik kazandırmış oluyoruz ve sistemi bu şekilde tasarlamaya başlarsak ileride birisi tüm şekilleri XML dosyasına çevir dediğinde yazılımın tutarlı olması için bu fonksiyonaliteyi de Shape üzerine eklemek zorunda kalırız.

Ben biraz düşünerek bu şekilde bir yazılım tasarladığımızda SOLID kurallarının 3 tanesini çiğneyebileceğimizi gördüm. Diğer 2 kuralı da bir şekilde bozuyorsak yorum olarak iletebilirsiniz.

  • Single Responsibility: Shape arayüzüne bu kadar fazla sorumluluk ekleyerek ilk önce bu kuralı çiğnemiş oluruz.
  • Open-Closed Principle: Her yeni talepte Shape arayüzüne ve ondan türeyen sınıflara doğrudan müdahale etmek zorunda kaldığımız için bu kuralı da ezip geçmiş oluyoruz.
  • Interface segregation principle: Bu kural bir sınıfın kullanmayacağı bir arayüzü implement etmek zorunda bırakılmaması gerektiğini söylüyor. Bizim örneğimizde de belki bazı Shape türü sınıfların ekranda çizilmemesi veya konsola yazılmaması gerekiyordur fakat ilgili methodları Shape arayüzüne koyduğumuz için tüm geometrik şekiller bu methodları barındırmak zorunda kalıyor. Bu şekilde zorlarsak bu kuralı da ezmiş oluyoruz.

Visitor Design Pattern

Yeteri kadar kötü kod gördük diyip asıl amacıma geçiyorum. Visitor pattern’ın en önemli avantajı üzerinde çeşitli operasyonlar yapacağımız sınıfları hiç modifiye etmeden yeni operasyonlar tanımlayabiliyor olmamız. Diğer avantajı ise arayüz kullanarak kaybettiğimiz nesne tiplerini (Örneğin Shape türündeki değişkende aslında hangi nesne tipi olduğunu bilmiyoruz) instanceof kontrolü yapmadan geri kazanabiliyor olmamız. Böylece nesnenin tipine uygun işlemin yapılmasını sağlıyor.

Bu design pattern’ı uygulayabilmek için öncelikle tüm şekilleri ziyaret edebilecek bir Visitor arayüzüne ihtiyacımız var. Bunu aşağıdaki şekilde tanımlıyoruz:

public interface ShapeVisitor {
    void visit(Circle circle);

    void visit(Rectangle rectangle);

    void visit(Square square);
}

ShapeVisitor arayüzünün Shape türü nesneleri ziyaret edebilmesi için, Shape sınıfına aşağıdaki gibi accept methodunu ekliyoruz.

public interface Shape {
    void accept(ShapeVisitor visitor);
}

Bu method Shape alt sınıflarının hepsinde aynı şekilde implement ediliyor. O nedenle sadece Circle sınıfını burada örnek olarak gösteriyorum.

public class Circle implements Shape {
    private int centerX;
    private int centerY;
    private int radius;

    public Circle(int centerX, int centerY, int radius) {
        this.centerX = centerX;
        this.centerY = centerY;
        this.radius = radius;
    }

    @Override
    public void accept(ShapeVisitor visitor) {
        // I accept the visitor and introduce myself
        visitor.visit(this);
    }

    public int getCenterX() {
        return centerX;
    }

    public int getCenterY() {
        return centerY;
    }

    public int getRadius() {
        return radius;
    }
}

Ve bitti. Bundan sonra Shape ve ondan türeyen tüm sınıflara bir daha müdahale etmiyoruz. Ne zaman Shape nesneleri ile nesne tipine bağımlı bir işlem yapmak gerekirse o zaman ShapeVisitor arayüzünden yeni bir sınıf yaratıp işlemleri onun içinde hallediyoruz. Böylece ne Shape nesnelerinin kendilerini kullanan koda bağımlılığı kalıyor, ne de ana kodun nesne tiplerini bilmeye ihtiyacı kalıyor.

Örneğin aşağıda şekillerin açıklamalarını konsola yazan kodu ShapeVisitor kullanarak yeniden yazalım:

public class PrinterShapeVisitor implements ShapeVisitor {
  private PrintStream out;

  public PrinterShapeVisitor(PrintStream out) {
    this.out = out;
  }

  @Override
  public void visit(Circle circle) {
    out.printf("Circle Center = (%d, %d) and Radius = %d\n", 
      circle.getCenterX(), circle.getCenterY(), circle.getRadius());
  }

  @Override
  public void visit(Rectangle rectangle) {
    out.printf("Rectangle TopLeftCorner = (%d, %d), Width = %d and Height = %d\n", 
      rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
  }

  @Override
  public void visit(Square square) {
    out.printf("Square TopLeftCorner = (%d, %d) and SideLength = %d\n", 
      square.getX(), square.getY(), square.getWidth());
  }
}

Bunun kullanımı ana kodda aşağıdaki şekilde yapılıyor ve tüm if/else ve instanceof kontrollerinden bizi kurtarıyor.

private static void printShapes(List<Shape> shapes, PrintStream out) {
    PrinterShapeVisitor visitor = new PrinterShapeVisitor(out);
    shapes.forEach(shape -> shape.accept(visitor));
}

Verdiğim bir diğer örnek ise Graphics sınıfına çizim yapmaktı. Bunu da aşağıdaki sınıfı yaratarak halledebiliriz:

public class GraphicsShapeVisitor implements ShapeVisitor {
  private Graphics g;

  public GraphicsShapeVisitor(Graphics graphics) {
    this.g = graphics;
  }

  @Override
  public void visit(Circle circle) {
    g.setColor(Color.RED);
    g.drawOval(circle.getCenterX() - circle.getRadius(), circle.getCenterY() - circle.getRadius(), 
      circle.getRadius() * 2, circle.getRadius() * 2);
  }

  @Override
  public void visit(Rectangle rectangle) {
    g.setColor(Color.BLUE);
    g.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
  }

  @Override
  public void visit(Square square) {
    g.setColor(Color.GREEN);
    g.drawRect(square.getX(), square.getY(), square.getWidth(), square.getHeight());
  }
}

Ve bunun kullanımı da aşağıdaki şekilde tüm if/else ve instanceof kontrollerinden kurtulmuş halde yapılabiliyor:

private static class ShapeDrawerPanel extends JPanel {
  private List<Shape> shapes;

  public ShapeDrawerPanel(List<Shape> shapes) {
    this.shapes = shapes;
  }

  @Override
  public void paint(Graphics g) {
    g.setColor(Color.WHITE);
    g.fillRect(0, 0, getWidth(), getHeight());

    GraphicsShapeVisitor visitor = new GraphicsShapeVisitor(g);
    shapes.forEach(shape -> shape.accept(visitor));
  }
}

Visitor pattern’ın en sevdiğim yönü beni bir çok defa type-check ve type-cast yapmaktan kurtarıyor olması. Geliştirdiğim sınıfların birbirine bağlılıklarını düşürerek onları tekrar kullanılabilir parçalar (resusable components) haline dönüştürmesi kodun kalitesi için inanılmaz bir artı.

Kaliteli kod yazımında design pattern kullanmanın önemi çok büyük. Kullanınız, kullandırtınız.

Ben boş vaktim kaldıkça çeşitli bloglardan farklı design pattern’ları inceliyorum. Bunu biliyorum dediklerimi dahi tekrar tekrar açıp farklı kaynaklardan okuyorum. Hatta geçen gün bu camiada herkesin parmakla gösterdiği aşağıdaki kitabı sipariş ettim, elime geçmesini sabırsızlıkla bekliyorum.

Ben kıçımı kaldırıp yeni bir yazı yazıncaya kadar eliniz temiz koddan kirli koda değmesin.

Design Patterns: Elements of Reusable Object-Oriented Software

Design Patterns: Elements of Reusable Object-Oriented Software