The Clean Architecture in PHP 读书笔记(二)
本文为系列文章的第二篇,第一篇地址是
The Clean Architecture in PHP 读书笔记(一)
你的解耦工具
在下面的文章中,主要会围绕去耦给出5大工具,让你轻松去耦。
- Design Patterns,A Primer
- SOLID Design Principles
- Depedency Injection
- Defining a Contract with Interfaces
- Abstracting with Adapters
先介绍第一个工具:设计模式。
Design Patterns,A Primer
设计模式是对软件中通用问题的总结,有了设计模式,方便我们进行交流,譬如一说MVC,我们就知道是怎么回事了,不然我们必须巴拉巴拉一大堆话去描述,不易于传播、交流,一个好的设计模式必然有一个简单易懂的名字,因为只有简单的东西,才能被大家广泛传播。
如今,一谈到设计模式,被大家广泛传播的就是1994年四人帮提出的23种模式,分为了3大类别:
- Creational
- Structural
- Behavioral
本书不会去按个介绍23个模式,这样去介绍的书籍太多了,而且本书也没有那么多篇幅去按个讲,但是这些都是再进行展开的基础,因此我们会介绍一些最近本的概念:
- Factory:负责生产对象,类似于语言本省提供的new关键字,在C++中与new的不同就在于,new不能通过string直接实例化对象,但是factory可以
- Repository:不是GoF中提出的设计模式,其类似于一个仓库,负责数据的存储和获取
- Adapter:适配器,故名思议就是将接口实现转换
- Strategy:目的是灵活性,将一个或一组行为封装起来,方便的进行替换
对上面的概念具体展开
The Factory Patterns
简单代码:
$customer = new Customer();
如果customer足够简单,上面的创建没有问题,但是如果customer创建的时候,必须进行一堆初始化,那我们就疯了,一个简单的想法,当然是将这些创建逻辑抽离出来,变为一个函数,进行复用,因此就有下面的版本,
复杂代码:
class CustomerFactory {
protected $accountManagerRepo;
public function __construct(AccountManagerRepository $repo) {
$this->accountManagerRepo = $repo;
}
public function createCustomer($name) {
$customer = new Customer();
$customer->setName($name);
$customer->setCreditLimit(0);
$customer->setStatus('pending');
$customer->setAccountManager(
$this->accountManagerRepo->getRandom()
);
return $customer;
}
}
总结下出现上面代码的原因:软件复杂度的提升,可以这么说,软件中各种问题的出现,都是因为软件的复杂度,不同的复杂度有不同的应对方法,总的目标都是为了降低复杂度
那上面出现的CustomerFactory
带来的好处是:
- Reusable code.创建的地方都可以调用这段代码,降低了代码的复杂度。
- Testable code.由于关注点分离了,方便测试,可以单独进行创建逻辑的测试
- Easy to change.创建逻辑只在一处,方便修改
Static Factories
class CustomerFactory {
public static function createCustomer($name) {
$customer = new Customer();
$customer->setName($name);
$customer->setCreditLimit(0);
$customer->setStatus('pending');
return $customer;
}
}
$customer = CustomerFactory::createCustomer('ACME Corp');
静态工厂,通过一个static方法访问,那怎么评判是静态工厂好,还是上面一个工厂好呢?答案是:
It depends
没有最好的,只有更合适的,如果静态方法能满足需求,那就是用静态工厂,如果你是数据源会经常变化,那就使用实例化工厂,并且将需要的依赖注入进来。
Types of Factories
- The Factory Method pattern:定义了创建对象的方法,但是具体创建哪个对象,由子类决定
- The Abstract Factory pattern:抽象工厂模式的粒度更粗一点,不仅管一个对象的创建,而是管着一组相关对象的创建
talk is cheap,show me the code
让我们上代码的。
class Document {
public function createPage() {
return new Page();
}
}
如果我们又好几种page,怎么办?
class Document {
public function createPage($type) {
switch $type {
case "ResumeDocument":
return new ResumePage();
case "PortfolioDocument":
return new PortfolioPage();
}
}
}
上面代码的问题是:我们每次新曾一个类型的page,必须要修改createPage
方法,不满足开放封闭原则(OCP),那PHP有个好处是,直接传递给new字符串就能创建对象,看代码:
class Document {
public function createPage($type) {
return new $type;
}
}
问题:我们无法验证传入$type
的有效性,而且对createPage
返回的类型,我们也无法检查,那怎么办呢?
思路是:将创建逻辑按照关注点分离的逻辑,每个类型的document创建自己的page,这就解决了$type
的有效性问题,那对于返回值,我们定义一个公共接口,只有实现这个接口的才是符合预期的。
abstract class AbstractDocument {
abstract public function createPage():PageInterface;
}
class ResumeDocument extends AbstractDocument {
public function createPage():PageInterface {
return new ResumePage();
}
}
class PortfolioDocument extends AbstractDocument {
public function createPage():PageInterface {
return new PortfolioPage();
}
}
interface PageInterface {}
class ResumePage implements PageInterface {}
class PortfolioPage implements PageInterface {}
介绍第一个概念The Abstract Factory pattern
抽象工厂就是对于工厂方法的1+1=2的叠加。
如果我们的Document不止创建一个page,还要创建cover,那就会有两个create方法,此时每个子类就多实现一个方法的。
Repository Pattern
仓储模式:该模式在Eric Evan的神书:Domain-Driven Design: Tackling Complexity in the Heart of Software中详细的进行了描述
A REPOSITORY represents all objects of a certain type as a conceptual set (usuallyemulated). It acts like a collection, except with more elaborate querying capability. Domain-Driven Design, Eric Evans, p. 151
仓储类似于一个数据集合,相比较集合有更多精细设计的query,当我们谈论Repository的时候,我们关注的不再是“querying the database”,而是更纯粹的目的:存取数据
常用的读取数据的方法
class MyRepository {
public function getById($id);
public function findById($id);
public function find($id);
public function retrieve($id);
}
常用的写方法:
class MyRepository {
public function persist($object);
public function save($object);
}
每个Repository对应一个类的存取,有多个类,就会有多个Repository。
一个Repository怎么工作?
Objects of the appropriate type are added and removed, and the machinery behindthe REPOSITORY inserts them or deletes them from the database. Domain-Driven Design, Eric Evans, p. 151
主要给出一些资源:
Dotrine ORM:实现了Date Mapper
Eloquent ORM,Propel:实现了Active Record
Zend Framework 2:提供了Table Data Gateway模式
如果你想要自己实现一个Repository,强烈推荐Patterns of Enterprise Application Architecture
关于数据库的各种模式,推荐一个ppt
Adapter Pattern
直接看代码:
class GoogleMapsApi {
public function getWalkingDirections($from, $to) {}
}
interface DistanceInterface {
public function getDistance($from, $to);
}
class WalkingDistance implements DistanceInterface {
public function getDistance($from, $to) {
$api = new GoogleMapsApi();
$directions = $api->getWalkingDirections($from, $to);
return $directions->getTotalDistance();
}
}
适配器,非常明确的表名了意图,通过WalkingDistance
将GoogleMapsApi适配为了DistanceInterface
Strategy Pattern
也是看代码:
public function invoiceCustomers(array $customers) {
foreach ($customers as $customer) {
$invoice = $this->invoiceFactory->create(
$customer,
$this->orderRepository->getByCustomer($customer)
);
// send invoice...
}
}
// 此处我们根据用仓库里获取到的用户账单,通知用户
interface InvoiceDeliveryInterface {
public function send(Invoice $invoice);
}
class EmailDeliveryStrategy implements InvoiceDeliveryInterface {
public function send(Invoice $invoice) {
// Use an email library to send it
}
}
class PrintDeliveryStrategy implements InvoiceDeliveryInterface {
public function send(Invoice $invoice) {
// Send it to the printer
}
}
// 账单有两种通知方式,一种是发邮件,一种是print
public function invoiceCustomers(array $customers) {
foreach ($customers as $customer) {
$invoice = $this->invoiceFactory->create(
$customer,
$this->orderRepository->getByCustomer($customer));
switch ($customer->getDeliveryMethod()) {
case 'email':
$strategy = new EmailDeliveryStrategy();
break;
case 'print':
default:
$strategy = new PrintDeliveryStrategy();
break;
}
$strategy->send($invoice);
}
}
// 通过策略模式:我们可以在runtime,根据不同的账单,选择而不同的发送策略
// 问题:发送策略的选择不应该在外部直接暴露,改选择什么通知策略,应该是用户自己知道的,因此就有了下面的代码:
class InvoiceDeliveryStrategyFactory {
public function create(Customer $customer) {
switch ($customer->getDeliveryMethod()) {
case 'email':
return new EmailDeliveryStrategy();
break;
case 'print':
default:
return new PrintDeliveryStrategy();
break;
}
}
}
// 此时的客户端,根据最少职责原则,此时invoiceCustomers值负责创建账单,并且发送,并不负责选择什么方式寄送
public function invoiceCustomers(array $customers) {
foreach ($customers as $customer) {
$invoice = $this->invoiceFactory->create(
$customer,
$this->orderRepository->getByCustomer($customer));
$strategy = $this->deliveryMethodFactory->create(
$customer);
$strategy->send($invoice);
}
}
更多的设计模式:
请期待下一篇SOLID Design Principles
- 如何把Photoshop改造成远程控制工具(RAT)来利用
- Office高级威胁漏洞在野利用分析
- 10行代码告诉你,为什么说Python数据可视化是一件艺术品
- 没想到你是这样的Linux | 终端下有趣的命令合集
- PhEmail:基于Python的开源网络钓鱼测试工具
- 数据库中间件mysql-proxy细节【mysql官方的中间件】
- Office CVE-2017-8570远程代码执行漏洞复现
- Java 并发包中的读写锁及其实现分析
- 深入理解 Spring 事务原理
- Chrome开发者工具的小技巧
- Java Web中JSP中6种动作概况知识点总结——每日一语法学习
- 从Flash到Silverlight进阶教程-用代码来创建动画
- 从Flash到Silverlight进阶教程-Tweener
- silverlight设置浏览器Cookies
- php概述
- php教程
- php环境搭建
- PHP书写格式
- php变量
- php常量
- PHP注释
- php数组
- php字符串 string
- PHP整型 integer
- PHP浮点型 float
- php布尔型
- php数据类型之数组
- php数据类型之对象
- php数据类型之null
- php数据类型之间的转换
- php运算符
- php表达式
- PHP循环控制
- PHP流程控制
- php函数
- php全局变量
- PHP魔术变量
- php命名空间
- php 日期
- PHP包含文件
- php文件
- PHP 文件上传
- php Cookies
- php Sessions
- php email
- php安全email
- php错误处理
- PHP异常处理
- php过滤器
- PHP 高级过滤器
- php json
- php 表单
- PHP MySQL 简介
- PHP 连接 MySQL
- php创建数据库
- php 创建表
- php mysq 插入数据
- PHP MySQL 插入多条数据
- PHP MySQL 预处理语句
- php mysql 读取数据
- php mysql where
- PHP MySQL Order By
- PHP MySQL Update
- PHP MySQL Delete
- php ODBC
- 第13期:表统计信息的计算
- 2019.8.15乘兴打Codeforces Round #569 (Div. 2)小记A题A. Alex and a Rhombus
- 2019.8.15乘兴打Codeforces Round #569 (Div. 2)小记B. Nick and Array
- 《hdu 免费馅饼》
- 技术分享 | 使用 pt-query-digest 分析慢日志
- 2019.8.15乘兴打Codeforces Round #569 (Div. 2)小记
- Codeforces Beta Round #14 (Div. 2)A. Letter
- Vue 改变数据,页面不刷新的问题
- R语言再保险合同定价案例研究
- 开发一个简单的 Vue 弹窗组件
- R语言模拟人类生活预期寿命动态可视化动画图gif
- Vue 动态添加路由及生成菜单
- R语言泊松回归对保险定价建模中的应用:风险敞口作为可能的解释变量
- Vue 页面权限控制和登陆验证
- Codeforces Round #502 (in memory of Leopoldo Taravilse, Div. 1 + Div. 2)C. The Phone Number