Single responsibility principle

什麼 SOLID?

其實他是包含五個原則

  • 單一功能原則 SRP
  • 開閉原則 OCP
  • 里氏替換原則 LSP
  • 接口隔離原則 ISP
  • 依賴反轉原則 DIP

今天就專心把 SRP 學好就行了,其他的以後再說。

每個類只能有一種改變的理由

不用背名詞解釋,因為要會應用才是重點

情境

購物車系列選擇

需求描述

胖仔,我要一個購物車功能用來買系列書用的,例如說哈利波特全套,但是我要有個按了系列選擇後把所有書都放到購物車內,這有沒有辦法做到?喔對了順帶一提,這個回傳的資料有可能之後需要因為第三方要接會修改喔,記得寫的“彈性”一點,還有後台也要能看到系列書的列表。

需求描述拆解(最重要的一個步驟)

  • 他要有個接口可以打,沒有牽扯到寫入問題所以可以用 GET 的方式做
  • 需要返回指定的格式跟系列書
  • 這個指定的格式也許之後會修改
  • 跟資料庫拉資料後台也有一個類似的表單可以看

所以我們已經有一個概念。

 大概具象化就是長這個樣子

動手做做看

今天來點不一樣的

SLIM FRAMEWORK

官方有個建議的 skeleton

$ composer create-project slim/slim-skeleton slim-skeleton

弄完應該要看到這樣

塞點假資料

DROP TABLE IF EXISTS `products`;

CREATE TABLE `products` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `series` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

LOCK TABLES `products` WRITE;
/*!40000 ALTER TABLE `products` DISABLE KEYS */;

INSERT INTO `products` (`id`, `series`, `name`, `price`)
VALUES
	(1,'哈利波特','神秘的魔法石',500),
	(2,'哈利波特','消失的密室',400),
	(3,'哈利波特','阿茲卡班的逃犯',450),
	(4,'哈利波特','火盃的考驗',450),
	(5,'哈利波特','鳳凰會的密令',650),
	(6,'哈利波特','混血王子的背叛',550),
	(7,'哈利波特','死神的聖物',600);

/*!40000 ALTER TABLE `products` ENABLE KEYS */;
UNLOCK TABLES;

他要有個接口可以打,沒有牽扯到寫入問題所以可以用 GET 的方式做

步驟一

<?php

//src/routes.php


$app->get('/cart/items/[{series}]', function ($request, $response, $args) {
    //do something.
});

我要拆分功能面到 Controller

新建 app, composer autoload by psr-4.

$ Composer dump

$ Generating autoload files

新建 Controller

跟 dependencies 說你要註冊 ProductController

<?php

// controllers
$container[App\Controllers\ProductController::class] = function($container) {
  return new App\Controllers\ProductController();  
};

修改 Route 的位置

<?php

//routes.php

$app->get('/cart/items/[{series}]', App\Controllers\ProductController::class);

確認一下 controller 有沒有 call 到,__invoke

public function __invoke()
{
    echo 1234;
}

步驟二 

需要返回指定的格式跟系列書

要有 database eloquent

$ composer require illuminate/database

settings

// Slim Settings
'determineRouteBeforeAppMiddleware' => false,
'db' => [
    'driver' => 'mysql',
    'host' => 'localhost',
    'database' => 'database',
    'username' => 'user',
    'password' => 'password',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
]

dependencies

<?php

// Service factory for the ORM
$container['db'] = function ($container) {
    $capsule = new \Illuminate\Database\Capsule\Manager;
    $capsule->addConnection($container['settings']['db']);

    $capsule->setAsGlobal();
    $capsule->bootEloquent();

    return $capsule;
};

Inject to controller

<?php


// controllers
$container[App\Controllers\ProductController::class] = function ($container) {
    $db = $container->get('db');
    return new App\Controllers\ProductController($db);
};

ProductController

<?php

namespace App\Controllers;

use Illuminate\Database\Capsule\Manager;

class ProductController
{
    /**
     * @var Manager
     */
    private $db;

    public function __construct(Manager $db)
    {
        $this->db = $db;
    }

    public function __invoke()
    {
        $products = $this->db->table('products')->get();
        dd($products);
    }
}

改變 controller 接口名稱

<?php

$app->get('/cart/items/[{series}]', App\Controllers\ProductController::class . ':series');

帶入 Response, Request

public function series(RequestInterface $request, ResponseInterface $response, $args)
{
    $products = $this->db->table('products')->where('series', $args['series'])->get();

    return $response->withJson($products->toArray());
}

相依於抽象不是實體

/cart/items/哈利波特

步驟三

這個指定的格式也許之後會修改

transform pattern

我們只需要書名跟價格就可以了,其他都不需要。

Transformable

新建一個 contract 來實現 transform

interface Transformable
{
    public function transform($attributes);
}

implement

<?php

namespace App\Transformers;

use App\Contracts\Transformable;

class ProductTransformer implements Transformable
{
    public function transform($attributes)
    {
        // TODO: Implement transform() method.
    }
}

Inject Transformer

// transformer
$container[App\Transformers\ProductTransformer::class] = function($container) {
  return new App\Transformers\ProductTransformer;  
};

// controllers
$container[App\Controllers\ProductController::class] = function ($container) {
    $db = $container->get('db');
    $transform = $container->get(App\Transformers\ProductTransformer::class);
    return new App\Controllers\ProductController($db, $transform);
};

Controller

class ProductController
{
    /**
     * @var Manager
     */
    private $db;
    /**
     * @var Transformable
     */
    private $transformer;

    public function __construct(Manager $db, Transformable $transformer)
    {
        $this->db = $db;
        $this->transformer = $transformer;
    }


    public function series(RequestInterface $request, ResponseInterface $response, $args)
    {
        $products = $this->db->table('products')->where('series', $args['series'])->get();

        return $response->withJson($this->transformer->transform($products));
    }

Implement transform

public function transform($attributes)
{
    $result[] = $attributes->map(function ($product) {
        return [
            'name' => $product->name,
            'price' => $product->price,
        ];
    });

    return $result;
}

抽離 Eloquent 為 Repository

還記得剛剛說過"每個類只能有一種理由改變
"嗎?

 

當你要改變 query 條件時卻改變了 controller 這就不太合理了,controller 不就是負責呼叫和 render or response?

Repositories

<?php

namespace App\Repositories;


class ProductRepository
{

}
//dependencies

// repositories
$container[App\Repositories\ProductRepository::class] = function($container) {
    return new App\Repositories\ProductRepository($container);
};

這邊注意我是 inject container

實作組件

use Slim\Container;

class ProductRepository
{
    protected $builder;
    /**
     * @var Container
     */
    private $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->newQueryBuilder();
    }

    public function newQueryBuilder()
    {
        $this->builder = $this->container->get('db');

        return $this;
    }
}

dependencies

// controllers
$container[App\Controllers\ProductController::class] = function ($container) {
    $repository = $container->get(App\Repositories\ProductRepository::class);
    $transform = $container->get(App\Transformers\ProductTransformer::class);
    return new App\Controllers\ProductController($repository, $transform);
};

Controller

class ProductController
{
    /**
     * @var ProductRepository
     */
    private $repository;
    /**
     * @var Transformable
     */
    private $transformer;

    public function __construct(ProductRepository $repository, Transformable $transformer)
    {
        $this->repository = $repository;
        $this->transformer = $transformer;
    }

我們好像忘記了 Eloquent。 來蓋一個 Product Model/ViewModel

Repository

public function getBySeries($series)
{
    return Product::where('series', $series)->get();
}

隱藏細節

Model

public function scopeSeries($query, $value)
{
    return $query->where('series', $value);
}

Repository

public function getBySeries($series)
{
    return Product::series($series)->get();
}

Repository

class ProductRepository extends Repository
{
    public function __construct($container)
    {
        parent::__construct($container);
    }

    public function getBySeries($series)
    {
        return Product::series($series)->get();
    }
}

abstract Repository

abstract class Repository
{
    protected $builder;
    /**
     * @var Container
     */
    private $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->newQueryBuilder();
    }

    public function newQueryBuilder()
    {
        $this->builder = $this->container->get('db');

        return $this;
    }
}

修改的情境

我要在回傳資料內加入系列書的價格總和

Transformer

public function transform($attributes)
{
    $result = $attributes->map(function ($product) {
        return [
            'name' => $product->name,
            'price' => $product->price,
        ];
    });

    $result = array_add($result, 'total', $attributes->sum('price'));

    return $result;
}

情境2

取系列要模糊比對:例如輸入“哈利”就要拿到資料

Eloquent

public function scopeSeries($query, $value)
{
    return $query->where('series', 'like', "%{$value}%");
}

Make transform more better

abstract controller

use Prophecy\Exception\Doubler\ClassNotFoundException;

abstract class Controller
{
    public function transform($data)
    {
        $class = str_replace('Controller', 'Transformer', static::class);

        if (! class_exists($class)) {
            throw new ClassNotFoundException('Class ' . $class .' does NOT exist!', $class);
        }

        $transformer = new $class;

        return $transformer->transform($data);
    }
}

減少 dependencies DI 一層

// controllers
$container[App\Controllers\ProductController::class] = function ($container) {
    $repository = $container->get(App\Repositories\ProductRepository::class);
    return new App\Controllers\ProductController($repository);
};

每個類只能有一種改變的理由

SRP + slim

By Yi-hsuan Lai

SRP + slim

  • 593