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大类别:

  1. Creational
  2. Structural
  3. Behavioral

本书不会去按个介绍23个模式,这样去介绍的书籍太多了,而且本书也没有那么多篇幅去按个讲,但是这些都是再进行展开的基础,因此我们会介绍一些最近本的概念:

  1. Factory:负责生产对象,类似于语言本省提供的new关键字,在C++中与new的不同就在于,new不能通过string直接实例化对象,但是factory可以
  2. Repository:不是GoF中提出的设计模式,其类似于一个仓库,负责数据的存储和获取
  3. Adapter:适配器,故名思议就是将接口实现转换
  4. 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带来的好处是:

  1. Reusable code.创建的地方都可以调用这段代码,降低了代码的复杂度。
  2. Testable code.由于关注点分离了,方便测试,可以单独进行创建逻辑的测试
  3. 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
  1. The Factory Method pattern:定义了创建对象的方法,但是具体创建哪个对象,由子类决定
  2. 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);
  }
}

更多的设计模式:

  1. 设计模式:可复用面向对象软件的基础
  2. Head First Design Patterns

请期待下一篇SOLID Design Principles

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 🎉 对设计模式的极简说明!🎉 这个话题可以轻易让任何人糊涂。现在我尝试通过用 最简单 的方式说明它们,来让你(和我...
    月球人simon阅读 1,084评论 1 2
  • 文章作者:Tyan博客:noahsnail.com 3.4 Dependencies A typical ente...
    SnailTyan阅读 4,113评论 2 7
  • 1 人和动物情同此理 这几年,老妈在临汾,儿子在长治,这两个地方去得就多了点。去得多了,有时候,在公交车上打个盹,...
    听风阁主人阅读 326评论 3 2
  • “我想跟你说:你不要舍不得我,因为我也舍不得你” 阿风知道自己心里住着一个恶魔,每隔一段时间就会浮现出来。 秀走的...
    浪子峰阅读 239评论 1 0