İçeriğe geç

SOLID Programlama Prensipleri

SOLID prensipleri OOP nesne tabanlı yazılım geliştirirken kullanılan standartlaştırılmış kurallardır. 5 önemli tasarım ilkesi vardır.

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov’s Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion/Injection Principle

Şimdi bu 5 maddeyi açıklayacak örnekler vererek devam edelim.

1.(S)ingle Responsibility Principle(Tek Sorumluluk Prensibi): Her method ve class’ın tek bir sorumluluğu olur örnek olarak database işlemleri yapan bir class içerisinde log’lama işlemlerini yürüten işlemler konulmamalı yada loglama class’ımız var ise bu class içerisinde loglama haricinde başka bir şeylerin dahil edilmemesi gerekiyor ve metod’lardada aynı şekilde switch/case ile yönlendirmemek gerekiyor her metodun tek bir sorumluluğu olmalıdır.

Aşağıdaki örnek bu kuralı ihlal ediyor;

class Ticket {
const SEVERITY_LOW = 'low';
const SEVERITY_HIGH = 'high';
// ...
protected $title;
protected $severity;
protected $status;
protected $conn;
public function __construct(\PDO $conn) {
$this->conn = $conn;
}
public function setTitle($title) {
$this->title = $title;
}
public function setSeverity($severity) {
$this->severity = $severity;
}
public function setStatus($status) {
$this->status = $status;
}

private function validate() {
// Implementation...
}
public function save() {
if ($this->validate()) {
// Implementation...
}
}
}
// Client
$conn = new PDO(/* ... */);
$ticket = new Ticket($conn);
$ticket->setTitle('Checkout not working!');
$ticket->setStatus(Ticket::STATUS_OPEN);
$ticket->setSeverity(Ticket::SEVERITY_HIGH);
$ticket->save();

Ticket class’ı validation(doğrulama) ve kaydetme işlemleri yaptığı için bu kuralı çiğnemektedir. Bu sorunu gidermek için;

interface KeyValuePersistentMembers {
public function toArray();
}
class Ticket implements KeyValuePersistentMembers {
const STATUS_OPEN = 'open';
const SEVERITY_HIGH = 'high';
//...
protected $title;
protected $severity;
protected $status;
public function setTitle($title) {
$this->title = $title;
}
public function setSeverity($severity) {
$this->severity = $severity;
}
public function setStatus($status) {
$this->status = $status;
}
public function toArray() {
// Implementation...
}
}
class EntityManager {
protected $conn;
public function __construct(\PDO $conn) {
$this->conn = $conn;
}
public function save(KeyValuePersistentMembers $entity)
{
// Implementation...
}
}
class Validator {
public function validate(KeyValuePersistentMembers $entity) {
// Implementation...
}
}
// Client
$conn = new PDO(/* ... */);
$ticket = new Ticket();
$ticket->setTitle('Payment not working!');
$ticket->setStatus(Ticket::STATUS_OPEN);
$ticket->setSeverity(Ticket::SEVERITY_HIGH);
$validator = new Validator();

if ($validator->validate($ticket)) {
$entityManager = new EntityManager($conn);
$entityManager->save($ticket);
}

KeyValuePersistentMembers interface’ı oluşturarak toArray() metodunu tanımladık. Bu metod EntityManager ve Validator sınıfları tarafından kullanılıyor. instantiation, validation ve save işlemlerini farklı sınıflara atayarak görev dağılımını yapmış olduk.

2.(O)pen/Closed Principle(Açık/Kapalı Prensibi): Değişime kapalı ama geliştirmeye açık şekilde kodlamanın kurgulanması gerekiyor mesela bugün log’lamaları xml’de yapıyorsunuz ama bir karar ile loglamanın artık xml’de değilde database’de yapılması istendi, varolan kodlarınızda değişiklik değil güncelleme yapılması gerekiyor misal olarak xml log’laması yapan Single Reponsibility kuralına uymuş olan bir class’ınız var ve bu class ILogger arayüzünden türetilmiş Database loglaması yapacak class’ımızda bu arayüzden türetilip metod içerikleri ona göre yazılır böylelik bu değişiklik değil geliştirme olmuş olur. Kısacası Uygulama gelişime açık, değişime kapalı olmalıdır. Yani yeni eklenen bir modül için kodda gereksiz if blokları gibi değişiklikler olmamalıdır. Bunun yerine kalıtım yoluyla sorun çözülmelidir.

Bu kurala uymayan bir örnek verelim;

class CsvExporter {
public function export($data) {
// Implementation...
}
}
class XmlExporter {
public function export($data) {
// Implementation...
}
}

class GenericExporter {
public function exportToFormat($data, $format) {
if ('csv' === $format) {
$exporter = new CsvExporter();
} elseif ('xml' === $format) {
$exporter = new XmlExporter();
} else {
throw new \Exception('Unknown export format!');
}
return $exporter->export($data);
}
}

Bu örnekte yeni bir exporter tanımlamak için GenericExporter sınıfını düzenlemek ve bu exporter için yeni bir class tanımlamak gerekiyor. Gördüğünüz gibi birçok kısımda değişiklik yapmamız gerekiyor. Gelin şimdi bu örneğimizi uygun hale getirelim.

interface ExporterFactoryInterface {
public function buildForFormat($format);
}
interface ExporterInterface {
public function export($data);
}
class CsvExporter implements ExporterInterface {
public function export($data) {
// Implementation...
}
}
class XmlExporter implements ExporterInterface {
public function export($data) {
// Implementation...
}
}
class ExporterFactory implements ExporterFactoryInterface {
private $factories = array();
public function addExporterFactory($format, callable $factory)
{
$this->factories[$format] = $factory;
}
public function buildForFormat($format) {
$factory = $this->factories[$format];
$exporter = $factory(); // the factory is a callable
return $exporter;
}
}
class GenericExporter {
private $exporterFactory;
public function __construct
(ExporterFactoryInterface $exporterFactory) {
$this->exporterFactory = $exporterFactory;
}
public function exportToFormat($data, $format) {
$exporter = $this->exporterFactory->
buildForFormat($format);
return $exporter->export($data);
}
}
// Client
$exporterFactory = new ExporterFactory();
$exporterFactory->addExporterFactory(
'xml',
function () {
return new XmlExporter();
}
);
$exporterFactory->addExporterFactory(
'csv',
function () {
return new CsvExporter();
}
);
$data = array(/* ... some export data ... */);
$genericExporter = new GenericExporter($exporterFactory);
$csvEncodedData = $genericExporter->exportToFormat($data, 'csv');

Öncelikle ExporterFactoryInterface ve ExporterInterface isimli 2 interface oluşturduk. CsvExporter ve XmlExporter sınıflarına bu interface’leri implement ettik. ExporterFactoryInterface interface’inden implement edilen ExporterFactory sınıfı oluşturuldu. Ana amacı buildForFormat metoduyla tanımlandı. Bu metodla exporter callback function olarak döndürülüyor. Son olarak ExporterFactoryInterface interface’ini implement eden GenericExporter sınıfı düzenlendi. Artık kullanıcı rahatlıkla GenericExporter’a yeni formatlar ekleyebilir.

3.(L)iskov ‘s Substitution Principle(Liskov’un yerine geçme prensibi): Liskov’un yerine geçme prensibi alt sınıflardan oluşturulan nesnelerin üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışı göstermek zorunda olduklarını söyler. Yani;türetilen sınıflar, türeyen sınıfların tüm özelliklerini kullanmak zorundadır. Eğer kullanmaz ise ortaya işlevsiz, dummy kodlar çıkacaktır. Bu durumda üst sınıfta if else blokları kullanarak tip kontrolü yapmamız gerekebilir ve böylelikle Açık Kapalı prensibine de ters düşmüş oluruz. Interface Segregation Prinsiple kısmında interface’le üzerinden bu konu anlatılırken burada abstract class’lar üzerindeki kullanımdan bahsediliyor.

Örneğimizde 2 adet yazımız olacak. Bunlardan bir tanesi hem yazıcı hemde tarayıcı özelliklerine sahip olsun. Diğer yazıcıda ise sadece yazma özelliği olsun ve bunları ana bir sınıftan türetelim.

abstract class BasePrinter {
  abstract public function print($value);
  abstract public function scan($value);
}

class HpPrinter extends BasePrinter  {
  public function print($value) {
    return $value;
  }
  public function scan($value) {
    throw new \Exception();
  }
}

class CanonPrinter extends BasePrinter  {
  public function print($value) {
    return $value;
  }
  public function scan($value) {
    return $value;
  }
}

Yukarıdaki örnekte gördüğümüz gibi BasePrinter sınıfından 2 adet yazıcı türettik . Hp ve Canon yazıcıları yazdırma özelliğine sahip fakat Hp’de tarama özelliği yok. O yüzden exception fırlatıyoruz ve Liskov’s Substitution prensibini ihlal etmiş oluyoruz. Türetildiği sınıfın özelliklerini tam anlamı ile kullanmıyoruz. Eğer yazıcının tarama özelliği yoksa ona göre bir sınıftan türetip yazma özelliği içinde başka bir interface oluşturabiliriz. Gelin şimdi bu örneği Liskov’s Substitution prensibine uygun hale getirelim.

abstract class BasePrinter {
  abstract public function print($value);
}

interface IScan{
   public funtion scan($value);
}

class HpPrinter extends BasePrinter  {
  public function print($value) {
    return $value;
  }
}

class CanonPrinter extends BasePrinter implements IScan {
  public function print($value) {
    return $value;
  }
  public function scan($value) {
    return $value;
  }
}

Artık HP sınıfı sadece yazma özelliğine sahip. Böyle kullanıldığında iki sınıfta miras aldıkları sınıfların tüm özelliklerini barındırıyor.

(I)nterface Segregation Principle(Arayüz ayrımı prensibi): Bir arayüze gereğinden fazla kullanılmayacak özellik eklenmemelidir. Kullanılabilecek özellikler, metodlar eklenerek kullanılmalıdır.

interface WorkerInterface
{
    public function code();
    public function test();
}

class Developer implements WorkerInterface
{
    public function code()
    {
        return 'coding';
    }
    public function test()
    {
        return 'basic testing';
    }
}

class Tester implements WorkerInterface
{
    //Client should not be forced to implement interface they don't use. 
    public function code()
    {
        return null;
    }

    public function test()
    {
        return 'advanced testing';
    }
}

Yukarıdaki örneğimizde WorkerInterface adında bir arayüzü var. Tester sınıfımızı bu arayüzden türettiğimizde code yazması için zorlamış oluyoruz. Hadi sen test yaptın birazda kod yaz demiş oluyoruz ve akabinde bu prensibi ihlal etmiş oluyoruz. Zamanla arayüzler büyüdüğünde bu durum ile karşılaşabiliriz. Arayüzlerin bu kadar büyümesi normal bir durum değildir ve endişelenmeniz gerekir. İşte tam bu noktada arayüzleri ayırma işlemi yapabiliriz.

class ProjectManager
{
    public function manage(ManagableInterface $worker)
    {
        $worker->work();
    }
}

interface ManagableInterface
{
    public function work();
}

interface CodeableInterface
{
    public function code();
}

interface TestableInterface
{
    public function test();
}

class Developer implements CodeableInterface, TestableInterface, ManagableInterface
{
    public function work()
    {
        $this->code();
        $this->test();
    }

    public function code()
    {
        return 'coding';
    }

    public function test()
    {
        return 'basic testing';
    }
}

class Tester implements TestableInterface, ManagableInterface
{
    public function work()
    {
        $this->test();
    }

    public function test()
    {
        return 'advanced testing';
    }
}

5.(D)ependency Inversion Principle(Bağımlılığın ters çevrilmesi prensibi): Bu prensibe göre de bir sınıf diğer bir sınıfa doğrudan bağımlı olmamalıdır. Aralarındaki bağ soyut bir kavram üzerinden sağlanmalıdır. Bu soyut kavram interface de olabilir abstract class da olabilir.

class ElasticSearchLog{
   public function log($data){
      //save data in elastic search index.
      return true;
   }
}

class DatabaseLog{
   public function log($data){
      //save data in database.
      return true;
   }
}

class Logger{
   private $elasticSearchLog;
   private $databaseLog;

   public function __construct(ElasticSearchLog $elasticSearchLog, DatabaseLog $databaseLog){
      $this->elasticSearchLog = $elasticSearchLog;
      $this->databaseLog = $databaseLog;
   }

   public function log($type, $data){
      switch($type){
         case 'elastic':
               $this->elasticSearchLog->log($data);
               break;
         case 'database':
               $this->databaseLog->log($data);
               break;
      }
   }
}

$logger = new Logger();
$logger->log('elastic', ['test log']);

Bu örneğimizde Logger adında üst seviye diyebileceğimiz bir sınıfımız var. log fonksiyonunu kontrol ettiğimizde alt sınıflar ile doğrudan bir bağlantı görüyoruz. Yani Logger sınıfı hem ElasticSearchLog sınıfını hemde DatabaseLog sınıfını çağırmış ve artık bu 2 sınıfıda bir bağımlılık söz konusu. İşte tam bu noktada çözümü yine interface aracılığı ile uygulayabiliriz ve diğer sınıflardaki bağımlılığı tamamen kaldırabiliriz. Gelin şimdi örneğimizi inceleyelim.

interface ILogger{
   public function log($data);
}

class ElasticSearchLog implements ILogger {
   public function log($data){
      //save data in elastic search index.
      return true;
   }
}

class DatabaseLog implements ILogger{
   public function log($data){
      //save data in database.
      return true;
   }
}

//New mongodb log
class MongoDbLog implements ILogger{
   public function log($data){
      //save data in mongodb.
      return true;
   }
}

class Logger{
   private $logger;

   public function __construct(ILogger $logger){
      $this->logger= $logger;
   }

   public function log($data){
      $this->logger->log($data)
   }
}

$mongoDbLog = new MongoDbLog();

$logger = new Logger($mongoDbLog);
$logger->log(['test_data']);

Şimdi dikkatinizi Logger sınıfına çekmek istiyorum.__construct’da artık sadece interface’i kullanıyoruz yani diğer hiç bir sınıf ile bağlantı yok . Ne iş yaptıkları Logger sınıfını ilgilendirmiyor. Gördüğünüz gibi ILogger adında bir interface oluşturduk ve bağımlılıkları oradan yönetiyoruz.

Kategori:GenelPhp

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir