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