Monthly Archives: Aralık 2015

Genel Yazılım ve Sistem Mühendisliği

Süzgeç Tasarım Deseni

Belirli bir nesne grubunun bir veya daha fazla kritere göre filtrelenebilmesini sağlayan yapısal tasarım desenidir. Tanımdan da anlaşılacağı üzere filtreleme işlemi tekil veya zincir halinde gerçekleştirilebilir.

Gerçek Hayat Örneği:

Dilerseniz bu örneği Dekoratör Tasarım Deseni yazısında söz ettiğimiz e-ticaret uygulaması üzerinden devam ettirelim.

Kısaca hatırlatmak gerekirse sitemizden aynı kategoriden 3 ürün alan müşteriye 2 fiyatı ödetecek ve 10 TL indirim vereecktik. Bu işlemi yaparken dekoratör ve strateji tasarım desenlerinden yararlanmıştık. Şimdi bu örneği biraz daha özelleştirelim.

Kullanıcımızın, yeni yıl kategorisinden satın alacağı 3 adet 100 TL üzeri ürün için 2 fiyatı ödetelim ve 10 TL indirim uygulayalım.

Uygulama:

Örnekde belirttiğimiz şartlara uygun olan sepet ürünlerini filtrelemek için oluşturacağımız filtrelerin implement edeceği arayüz sınıfını oluşturalım.

<?php
namespace Promotion;

interface FilterInterface
{
    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array());
}

Kullanıcıya sağlayacağımız fırsatın ilk şartı kategori ekseninde olduğuna göre sepet öğelerini kategori bazlı filtreleyecek filtre sınıfını kodlayalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class CategoryFilter implements FilterInterface
{
    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        foreach($items as $item) {
            if( $item->getCategory()->getId() == $args['category_id'] ) {
                yield $item;
            }
        }
    }
}

Bir sonraki ölçütümüz olan tutar bilgisi için yeni bir filtre sınıfı oluşturalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class PriceFilter implements FilterInterface
{
    const CRITERIA_EQUAL = 'eq';
    const CRITERIA_LESS_THAN = 'lt';
    const CRITERIA_GREATER_THAN = 'gt';
    const CRITERIA_LESS_THAN_OR_EQUAL = 'lte';
    const CRITERIA_GREATER_THAN_OR_EQUAL = 'gte';

    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        foreach($items as $item) {
            switch($args['condition']) {
                case self::CRITERIA_EQUAL:
                    if($item->getPrice() == $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_LESS_THAN:
                    if($item->getPrice() < $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_GREATER_THAN:
                    if($item->getPrice() > $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_LESS_THAN_OR_EQUAL:
                    if($item->getPrice() <= $args['price']) {
                        yield $item;
                    }
                    break;
                case self::CRITERIA_GREATER_THAN_OR_EQUAL:
                    if($item->getPrice() >= $args['price']) {
                        yield $item;
                    }
                    break;
            }
        }   
    }
}

Son olarak her iki şartı VE lojiğine tabi tutacak filtre sınıfını oluşturalım.

<?php

namespace Promotion\Filter;

use Promotion\FilterInterface;

class AndFilter implements FilterInterface
{
    /**
     * @var FilterInterface $filter1
     */
    private $filter1;

    /**
     * @var FilterInterface $filter2
     */
    private $filter2;

    /**
     * @param FilterInterface $filter1
     * @param FilterInterface $filter2
     */
    public function AndFilter(FilterInterface $filter1, FilterInterface $filter2)
    {
        $this->filter1 = $filter1;
        $this->filter2 = $filter2;
    }

    /**
     * @param Traversable $items
     * @param array $args
     * @return Generator
     */
    public function apply(Traversable $items, array $args=array())
    {
        $filtered = $this->filter1->apply($items, $args);
        return $this->filter2->apply($filtered, $args);
    }
}

Filtrelerimiz hazır olduğuna göre bir önceki örnekde oluşturduğumuz SpecialOfferCalculator sınıfını yeni şartlara göre düzenleyelim.

<?php
namespace Calculator\Calculator;

use Calculator\Calculator\CalculatorAbstract;
use Promotion\Filter\CategoryFilter;
use Promotion\Filter\PriceFilter;
use Promotion\Filter\AndFilter;

class SpecialOfferCalculator extends CalculatorAbstract
{
    const OFFER_LIMIT = 3;
    const OFFER_CATEGORY_ID = 5; // CATEGORY_ID:5 = Yeni Yıl
    const OFFER_PRICE_LIMIT = 100;
    const OFFER_PRICE_COND = PriceFilter::CRITERIA_GREATER_THAN_OR_EQUAL;

    private prepareItems()
    {
        $filter = new AndFilter(new CategoryFilter(), new PriceFilter());
        $filteredItems = $filter->apply(
            $this->decoratedObject->getItems(), 
            array(
                'category_id' => self::OFFER_CATEGORY_ID,
                'price'       => self::OFFER_PRICE_LIMIT,
                'condition'   => self::OFFER_PRICE_COND
            )
        );

        // Generator tip, iterate edilmedigi surece sonuc donmedigi icin,
        // bir sonraki metodda yapilacak belirli kategorideki urun sayisi
        // kontrolu nedeniyle generator tipini diziye donusturuyoruz.
        $items = array();
        foreach($filteredItems as $item) {
            $items[] = $item;
        }
        return $items;
    }

    public function getTotalPrice()
    {
        $totalPrice = $this->decoratedObject->getTotalPrice();
        $items = $this->prepareItems();

        if(count($items) > self::OFFER_LIMIT) {
            // Urunleri pahalidan ucuza siralayarak 100 TL ve uzeri en ucuz
            // urunun tutarini indirim olarak uyguluyoruz.

            usort($items, function($itemA, $itemB) {
                return $itemA->getPrice() > $itemB->getPrice();
            });

            $totalPrice -= end($items)->getPrice();
        }
        return $totalPrice;
    }
}

Artık sepet tutarımızı hesaplamaya hazırız.

<?php
namespace MyApp;

use Basket\Basket;
use Basket\BasketItem;
use Promotion\Discount;
use Calculator\Calculator\TotalPriceCalculator;
use Calculator\Calculator\SpecialOfferCalculator;
use Calculator\Calculator\DiscountCalculator;

class Main
{
    public function main()
    {
        $basket = new Basket();
        $basket->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 150))
            ->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 180))
            ->add(new BasketItem("001", new Category(5, 'Yeni Yıl'), 'Gömlek', 105))
            ->add(new BasketItem("002", new Category(5, 'Yeni Yıl'), 'Pantolon', 80);
        $basket->setDiscount(new Discount(Discount::DISCOUNT_TYPE_FIXED, 10));
        $basket = new DiscountCalculator(
            new SpecialOfferCalculator(
                new TotalPriceCalculator($basket)
            )
        );
        print $basket->getTotalPrice(); 
        // sub total = 150 + 180 + 105 + 80 = 515
        // ofered total = sub total - 105 = 410
        // Payment total = offered total - discount amount (10) = 405 TL
    }
}
Genel Yazılım ve Sistem Mühendisliği

Dekoratör Tasarım Deseni

Dekoratör tasarım deseni, bir nesne üzerinde yapısal değişiklik gerçekleştirmeden yeni yetenekler kazandırılmak istenildiği durumlarda tercih edilen bir tasarım kalıbıdır. Genellikle tekil sorumluluk ilkesi gereği sorumlulukların müferit sınıflar arasında bölünmek istendiği yerlerde tercih edilir. Belirli bir mal veya hizmetin satışında ara toplam, indirimler, vergiler ve özel durumların hesaplanması istendiği durumlar için son derece elverişli bir yapısal tasarım desenidir.

Gerçek Hayat Örneği:

Bir elektronik ticaret sitesinde aşağıdaki kampsamda hizmetler veirlebilmektedir.

* Kullanıcı sepetine istediği kadar ürün ekleyebilir.
* Sabit tutarlı veya yüzdelik cinsten indirimlerden yararlanabilir.

İstisnalar:

* Kullanıcı A kategorisindeki aynı üründen 3 adet satın alırsa 2 tanesinin parasını öder.

Problem:

Kullanıcı yeni yıl kategorisinden 3 adet gömlek satın almış, hesabına tanımlı olan 10 TL lik indirimden yararlanmıştır.

Uygulama:

Başlamadan önce sepet, ürün ve indirim sınıflarımızı tanımlayalım.

namespace Basket;

class BasketItem
{
	/**
	 * @var string
	 */
	private $code;

	/**
	 * @var Category;
	 */
	private $category;

	/**
	 * @var string
	 */
	private $name;

	/**
	 * @var float
	 */
	private $price;

	public function BasketItem($code, $category, $name, $price)
	{
		$this->code = $code;
		$this->cateogry = $categorY;
		$this->name = $name;
		$this->price = $price;
	}

	/**
	 * @return string
	 */
	public function getCode()
	{
		return $this->code;
	}

	/**
	 * @return Category
	 */
	public function getCategory()
	{
		return $this->category;
	}

	/**
	 * @return string
	 */
	public function getName()
	{
		return $this->name;
	}

	/**
	 * @return float
	 */
	public function getPrice()
	{
		return $this->price;
	}
}
<?php
namespace Basket;

use Basket\BasketItem;
use Promotion\Discount;

class Basket
{
	/**
	 * @var SplObjectStorage
	 */
	private $collection;

	/**
	 * @var float
	 */
	private $totalPrice = 0.0;

	/**
	 * @var \Promotion\Discount
	 */
	private $discount;

	public function Basket()
	{
		$this->collection = new SplObjectStorage();
	}

	/**
	 * @return SplObjectStorage
	 */
	public function getItems()
	{
		return $this->collection;
	}

	/**
	 * @param BasketItem $item
	 * @return Basket
	 */
	public function add(BasketItem $item)
	{
		$this->collection->attach($item);
		return $this;
	}
	
	//...

	public function getTotalPrice()
	{
		return $this->totalPrice;
	}

	public function setTotalPrice($value)
	{
		$this->totalPrice = $value;
	}

	/**
	 * @return \Promotion\Discount
	 */
	public function getDiscount()
	{
		return $this->discount;
	}

	/**
	 * @param \Promotion\Discount $discount
	 */
	public function setDiscount(Discount $discount)
	{
		$this->discount = $value;
	}

	//...
}
<?php
namespace Promotion;

class Discount
{
	DISCOUNT_TYPE_FIXED = 'fixed';
	DISCOUNT_TYPE_PERCENTAGE = 'percentage';

	/**
	 * @var string
	 */
	private $type;
	
	/**
	 * @var float
	 */
	private $amount;

	public function Discount($type, $amount)
	{
		$this->type = $type;
		$this->amount = $amount;
	}

	/**
	 * @return string
	 */	
	public function getType()
	{
		return $this->type;
	}

	/**
	 * @return float
	 */
	public function getAmount()
	{
		return $this->amount;
	}
}

Hesaplama işlemi sırasında kullanıcının sepetini dekore edeceğimize göre sepet ve somut dekoratör sınıfları benzer niteliklere sahip olmalıdır. Bunun için sepet ve dekoratör sınıflarının implement edeceği arayüz sınıfını oluşturalım.

<?php
namespace Calculator;

interface Calculatable
{
	public function getTotalPrice();
}

* Sepet nesnesinin yeni oluşturduğumuz arayüzü implement etmesini sağlıyoruz.

<?php
namespace Basket;

use Basket\BasketItem;
use Calculator\Calculatable;

class Basket implements Calculatable
{
	//...
}

Her dekoratör, kurulum sırasında sepeti veya bir başka dekoratörü argüman olarak alabilir. Birden fazla somut dekoratör sınıfım olacağını varsayarak yeni bir soyut dekoratör sınıfı oluşturuyorum.

<?php
namespace Calculator\Calculator;

use Calculator\Calculatable;

abstract class CalculatorAbstract implements Calculatable
{
	/**
	 * @var Calculatable
	 */
	protected $decoratedObject;

	/**
	 * @var Calculatable $decoratedObject
	 */
	public function CalculatorAbstract(Calculatable $decoratedObject)
	{
		$this->decoratedObject = $decoratedObject;
	}

	public function getItems()
	{
		return $this->decoratedObject->getItems();
	}

	public function getTotalPrice()
	{
		return $this->decoratedObject->getTotalPrice();
	}
}

* Sepet toplamını hesaplayacak yeni bir dekoratör sınıfı oluşturuyorum.

<?php
namespace Calculator\Calculator;

use Calculator\Calculator\CalculatorAbstract;

class TotalPriceCalculator extends CalculatorAbstract
{
	public function getTotalPrice()
	{
		$total = 0;
		foreach($this->decoratedObject->getItems() as $item) {
			$total += $item->getPrice();
		}
		return $toal;
	}
}

* Aynı kategorideki bir üründen 3 adet satın alındığında 2 ürün parası ödemeyi sağlayacak decoratörü geliştirelim.

<?php
namespace Calculator\Calculator;

use Calculator\Calculator\CalculatorAbstract;

class SpecialOfferCalculator extends CalculatorAbstract
{
	const OFFER_LIMIT = 3;

	public function getTotalPrice()
	{
		$totalPrice = $this->decoratedObject->getTotalPrice();
		$groupedProducts = array();

		$isDiscountApplied = false;

		foreach($this->decoratedObject->getItems() as $item) {
			$categoryId = $item->getCategory()->getId();
			
			if(!array_key_exists($groupedProducts, $categoryId)) {
				$groupedProducts[$categoryId] = array();
			}
			
			$groupedProducts[$categoryId][] = $item;

			if(count(groupedProducts[$categoryId]) >= self::OFFER_LIMIT and !$isDiscountApplied) {
				$totalPrice -= $item->getPrice();
				$isDiscountApplied = true;
			}
		}

		return $totalPrice;
	}
}

Yazının başında kullanıcıların, indirimleri yüzde veya sabit tutar cinsinden kullanabileceğinden söz etmiştik. Ancak soyut düşündüğümüzde indirim hesaplamak bizim için nihayi rakama ulaşmaktaki adımlardan biri olup, indirimin yüzde veya sabit tutar cinsinden olması ise indirimin hesaplama metodolojisidir. Bu nedenle indirim hesaplama yöntemini sepet hesaplayıcılardan soyutlamak için strateji tasarım desenini kullanarak yeni bir indirim hesaplayıcı sınıf ailesi oluşturacağız.

<?php
namespace Promotion\Discount;

interface DiscountType
{
	/**
	 * @param float $discountAmount
	 * @param float $price
	 */
	public function calculate($discountAmount, $price);
}
<?php
namespace Promotion\Discount;

use Promotion\Discount\DiscountType;

class FixedDiscount implements DiscountType
{
	/**
	 * @param float $discountAmount
	 * @param float $price
	 */
	public function calculate($discountAmount, $price)
	{
		return $price - $discountAmount;
	}
}
<?php
namespace Promotion\Discount;

use Promotion\Discount\DiscountType;

class PercentageDiscount implements DiscountType
{
	/**
	 * @param float $discountAmount
	 * @param float $price
	 */
	public function calculate($discountAmount, $price)
	{
		return $price - ($price * ($discountAmount / 100));
	}
}
<?php
namespace Promotion;

use Promotion\Discount\DiscountType;

class DiscountContext
{
	/**
	 * @var \Promotion\Discount\DiscountType
	 */
	private $discountType;

	public function DiscountContext(DiscountType $discountType)
	{
		$this->discountType = $discountType;
	}

	public function apply($discountAmount, $price)
	{
		return $this->discountType->calculate($discountAmount, $price);
	}
}

* Şimdi de İndirim tutarını sepet toplamına yansıtacak dekoratör sınıfımızı oluşturalım.

namespace Calculator\Calculator;

use Promotion\Discount;
use Promotion\DiscountContext;

class DiscountCalculator extends CalculatorAbstract
{
	public function getTotalPrice()
	{
		$discount = $this->decoratedObject->getDiscount();
		if($discount->getType() == Discount::DISCOUNT_TYPE_FIXED) {
			$discountType = new \Promotion\Discount\FixedDiscount();
		} elseif($discount->getType() == Discount::DISCOUNT_TYPE_PERCENTAGE) {
			$discountType = new \Promotion\Discount\PercentageDiscount();
		} else {
			throw new Exception('Bad discount defination.');
		}
		$context = new DiscountContext($discountType);
		return = $context->apply($discount->getAmount(), $this->decoratedObject->getTotalPrice());
	}
}

Artık sepet tutarımızı hesaplamaya hazırız.

<?php
namespace MyApp;

use Basket\Basket;
use Basket\BasketItem;
use Promotion\Discount;
use Calculator\Calculator\TotalPriceCalculator;
use Calculator\Calculator\SpecialOfferCalculator;
use Calculator\Calculator\DiscountCalculator;

class Main
{
	public function main()
	{
		$basket = new Basket();
		$basket->add(new BasketItem("001", new Category(1, 'Giyim'), 'Gömlek', 80))
			->add(new BasketItem("001", new Category(1, 'Giyim'), 'Gömlek', 80))
			->add(new BasketItem("001", new Category(1, 'Giyim'), 'Gömlek', 80))
			->add(new BasketItem("002", new Category(1, 'Giyim'), 'Pantolon', 200));
		$basket->setDiscount(new Discount(Discount::DISCOUNT_TYPE_FIXED, 10));
		$basket = new DiscountCalculator(new SpecialOfferCalculator(new TotalPriceCalculator($basket)));
		print $basket->getTotalPrice(); //TotalPrice = 350 TL
	}
}