详解PHP中的外观模式facade pattern


Posted in PHP onFebruary 05, 2018

关于facade这个词的翻译

facade这个词,原意指的是一个建筑物的表面、外观,在建筑学中被翻译为“立面”这个术语,国内对facade这个词的关注,可能更多要依赖于laravel的流行,似乎都一致把laravel里的facade翻译作“门面”。说实在的,当第一次看到翻译文档里提什么“门面”的时候,我想你跟我的内心一样:“这是在说什么玩意呢?你是在讲商店、店铺的门面吗?”直到现在,如果非得用中文说facade,非得用“门面”这个词,我的心里还是不自觉地会“咯噔”那么一下,我知道这里是有问题的。

facade到底翻译作啥好呢?倒是也有的人群干脆提倡不翻译,遇到它就直接英文单词拿过来,这也不是个长远办法,终归是要为了新入门的人铺平理解的道路才好。后来偶然看到台湾的学者,确切说是台湾的维基百科,将facade pattern译作“外观模式”,考虑到该模式的实际作用,方才感觉瞬间释然。即使laravel里的facade,严格上并不是facade pattern,很多人到现在依然在批评laravel在facade这个词语上的滥用和误导,但它终归也是在借用或模仿facade pattern,所以laravel里的facade,本文也认为同样翻译成“外观”比较好,当然,为了更好理解,可以是“服务外观”。即使如此,从私人角度,我更希望将其直呼为“服务定位器”、“服务代理”或者“服务别名”,实际上国外的很多人也是建议如此更名,只是Taylor在这件事上态度一反往常地强硬,所以也暂且不必强求。

通过下文,待实际了解了facade pattern具体是啥后,我想你会更好地理解为什么翻译为“外观模式”更贴切。

什么是facade pattern(“外观模式”的定义)

不论在现实世界还是编程世界,facade(外观)的目的就是给一个可能原本丑的、杂乱的东西,“披上”一个优美的、吸引人的外观、或者说面具,用中国的俗话就是:什么是外观?“人靠衣装马靠鞍”。基于此,facade pattern就是将一个或多个杂乱的、复杂的、不容易重构的class,添加上(或转换成)一个漂亮优雅的对接入口(interface),这样呢好让你更乐意、更方便地去操作它,从而间接地操作了背后的实际逻辑。

什么时候需要用facade pattern

facade pattern(“外观模式”)经常是用来给一个或多个子系统,来提供统一的入口界面(interface),或者说操作界面。
当你需要操作别人遗留下来的项目,或者说第三方的代码的时候。尤其是通常情况下,这些代码你不容易去重构它们,也没有提供测试(tests)。这个时候,你就可以创建一个facade(“外观”),去将原来的代码“包裹”起来,以此来简化或优化其使用场景。

说得再多,不如来几个例子直观:

示例一:在java中,通过facade操作计算机内部复杂的系统信息

假设我们有这么一些复杂的子系统逻辑:

class CPU {
 public void freeze() { ... }
 public void jump(long position) { ... }
 public void execute() { ... }
}
class Memory {
 public void load(long position, byte[] data) {
  ...
 }
}
class HardDrive {
 public byte[] read(long lba, int size) {
  ...
 }
}

为了更方便地操作它们,我们可以来创建一个外观类(facade):

class Computer {
 public void startComputer() {
  cpu.freeze();
  memory.load(BOOT_ADDRESS, hardDrive.read(BOOT_SECTOR, SECTOR_SIZE));
  cpu.jump(BOOT_ADDRESS);
  cpu.execute();
 }
}

然后我们的客户,就可以很方便地来这样调用了:

class You {
 public static void main(String[] args) {
  Computer facade = new Computer();
  facade.startComputer();
 }
}

示例二:一个糟糕的第三方邮件类

假设你不得不用下面这个看上去很糟糕的第三方邮件类,尤其是里面每个方法名你都得停留个好几秒才能看懂:

interface SendMailInterface
{
 public function setSendToEmailAddress($emailAddress);
 public function setSubjectName($subject);
 public function setTheEmailContents($body);
 public function setTheHeaders($headers);
 public function getTheHeaders();
 public function getTheHeadersText();
 public function sendTheEmailNow();
}
class SendMail implements SendMailInterface
{
 public $to, $subject, $body;
 public $headers = array();
 
 public function setSendToEmailAddress($emailAddress)
 {
  $this->to = $emailAddress;
 }
 public function setSubjectName($subject)
 {
  $this->subject = $subject;
 }
 public function setTheEmailContents($body)
 {
  $this->body = $body;
 }
 public function setTheHeaders($headers)
 {
  $this->headers = $headers;
 }
 public function getTheHeaders()
 {
  return $this->headers;
 }
 public function getTheHeadersText()
 {
  $headers = "";
  foreach ($this->getTheHeaders() as $header) {
   $headers .= $header . "\r\n";
  }
 }
 
 public function sendTheEmailNow()
 {
  mail($this->to, $this->subject, $this->body, $this->getTheHeadersText());
 }
}

这个时候你又不好直接改源码,没办法,来一个facade吧

class SendMailFacade
{
 private $sendMail;
 public function __construct(SendMailInterface $sendMail)
 {
  $this->sendMail = $sendMail;
 }
 public function setTo($to)
 {
  $this->sendMail->setSendToEmailAddress($to);
  return $this;
 }
 public function setSubject($subject)
 {
  $this->sendMail->setSubjectName($subject);
  return $this;
 }
 public function setBody($body)
 {
  $this->sendMail->setTheEmailContents($body);
  return $this;
 }
 public function setHeaders($headers)
 {
  $this->sendMail->setTheHeaders($headers);
  return $this;
 }
 public function send()
 {
  $this->sendMail->sendTheEmailNow();
 }
}

然后原来不加优化的终端调用可能是这样的:

$sendMail = new SendMail();
$sendMail->setSendToEmailAddress($to);
$sendMail->setSubjectName($subject);
$sendMail->setTheEmailContents($body);
$sendMail->setTheHeaders($headers);
$sendMail->sendTheEmailNow();

现在有了外观类,就可以这样了:

$sendMail  = new SendMail();
$sendMailFacade = new sendMailFacade($sendMail);
$sendMailFacade->setTo($to)->setSubject($subject)->setBody($body)->setHeaders($headers)->send();

示例三:完成一个商品交易的复杂流程

假设呢,一个商品交易环节需要有这么几步:

$productID = $_GET['productId']; 
$qtyCheck = new productQty();

 // 检查库存
if($qtyCheck->checkQty($productID) > 0) {
  
 // 添加商品到购物车
 $addToCart = new addToCart($productID);
  
 // 计算运费
 $shipping = new shippingCharge();
 $shipping->updateCharge();
  
 // 计算打折
 $discount = new discount();
 $discount->applyDiscount();
  
 $order = new order();
 $order->generateOrder();
}

可以看到,一个流程呢包含了很多步骤,涉及到了很多Object,一旦类似环节要用在多个地方,可能就会导致问题,所以可以先创建一个外观类:

class productOrderFacade {
 public $productID = '';  
 public function __construct($pID) {
  $this->productID = $pID;
 }
 public function generateOrder() {   
  if($this->qtyCheck()) {
   $this->addToCart();
   $this->calulateShipping();
   $this->applyDiscount();
   $this->placeOrder();
  }   
 }
 private function addToCart () {
  /* .. add product to cart .. */
 } 
 private function qtyCheck() {
  $qty = 'get product quantity from database';
  if($qty > 0) {
   return true;
  } else {
   return true;
  }
 }
  private function calulateShipping() {
  $shipping = new shippingCharge();
  $shipping->calculateCharge();
 }
 private function applyDiscount() {
  $discount = new discount();
  $discount->applyDiscount();
 }
 private function placeOrder() {
  $order = new order();
  $order->generateOrder();
 }
}

这样呢,我们的终端调用就可以两行解决:

$order = new productOrderFacade($productID);
$order->generateOrder();

示例四:往多个社交媒体同步消息的流程

// 发Twitter消息
class CodeTwit {
 function tweet($status, $url)
 {
 var_dump('Tweeted:'.$status.' from:'.$url);
 }
}
// 分享到Google plus上
class Googlize {
 function share($url)
 {
 var_dump('Shared on Google plus:'.$url);
 }
}
//分享到Reddit上
class Reddiator {
 function reddit($url, $title)
 {
 var_dump('Reddit! url:'.$url.' title:'.$title);
 }
}

如果每次我们写了一篇文章,想着转发到其他平台,都得分别去调用相应方法,这工作量就太大了,后期平台数量往往只增不减呢。这个时候借助于facade class:

class shareFacade {
 
 protected $twitter; 
 protected $google; 
 protected $reddit; 
 function __construct($twitterObj,$gooleObj,$redditObj)
 {
 $this->twitter = $twitterObj;
 $this->google = $gooleObj;
 $this->reddit = $redditObj;
 } 
 function share($url,$title,$status)
 {
 $this->twitter->tweet($status, $url);
 $this->google->share($url);
 $this->reddit->reddit($url, $title);
 }
}

这样终端调用就可以:

$shareObj = new shareFacade($twitterObj,$gooleObj,$redditObj);
$shareObj->share('//myBlog.com/post-awsome','My greatest post','Read my greatest post ever.');

facade pattern的优劣势

优势

能够使你的终端调用与背后的子系统逻辑解耦,这往往发生在你的controller里,就意味着你的controller可以有更少的依赖,controller关注的更少了,从而责任和逻辑也更明确了,同时也意味着你子系统里的逻辑更改,并不会影响到你的controller里终端调用。

劣势

虽然特别有用,但是一个常见的陷阱就是,过度使用这个模式,明明可能那个时候你并不需要,这个往往注意即可。当然也有人争论说,明明我原来的代码都能用,干嘛费这个劲,那么同样是房子,你是喜欢住在精致的屋子里呢,还是说有四面墙就行了呢?

感觉facade pattern与其他的设计模式似曾相识?

认真学过我们《Laravel底层核心技术实战揭秘》这一课程的同学,可能到这里就会尤其觉得这个facade pattern好像在哪里见过?可能你会脱口而出:“这货跟之前咱们学的decorator pattern有啥区别呢?为啥不直接说成修饰者模式呢?”

确实,在“包装”逻辑方面,它们确实类似,但是:

修饰者模式(Decorator)——用来给一个Object添加、包裹上新的行为、逻辑,而不需要改动原来的代码

外观模式(facade pattern)——用来给一个或多个复杂的子系统、或者第三方库,提供统一的入口,或者说统一的终端调用方式

还是有一定差别的~

PHP 相关文章推荐
PHP连接SQLServer2005 的问题解决方法
Jul 19 PHP
php 广告调用类代码(支持Flash调用)
Aug 11 PHP
php常用ODBC函数集(详细)
Jun 24 PHP
那些年我们错过的魔术方法(Magic Methods)
Jan 14 PHP
PHP FATAL ERROR: CALL TO UNDEFINED FUNCTION BCMUL()解决办法
May 04 PHP
CodeIgniter输出中文乱码的两种解决办法
Jun 12 PHP
PHP两种去掉数组重复值的方法比较
Jun 19 PHP
php字符串操作针对负值的判断分析
Jul 28 PHP
PHP版微信第三方实现一键登录及获取用户信息的方法
Oct 14 PHP
tp5(thinkPHP5)操作mongoDB数据库的方法
Jan 20 PHP
laravel实现Auth认证,登录、注册后的页面回跳方法
Sep 30 PHP
tp5使用layui实现多个图片上传(带附件选择)的方法实例
Nov 17 PHP
浅析PHP开发规范
Feb 05 #PHP
实例讲解PHP页面静态化
Feb 05 #PHP
PHP使用ActiveMQ实例
Feb 05 #PHP
详解PHP中mb_strpos的使用
Feb 04 #PHP
详解PHP文件的自动加载(autoloading)
Feb 04 #PHP
PHP实现QQ登录的开原理和实现过程
Feb 04 #PHP
PHP实现正则表达式分组捕获操作示例
Feb 03 #PHP
You might like
PHP MSSQL 存储过程的方法
2008/12/24 PHP
PHP中error_reporting()函数的用法(修改PHP屏蔽错误)
2011/07/01 PHP
PHP 字符串正则替换函数preg_replace使用说明
2011/07/15 PHP
php判断手机访问还是电脑访问示例分享
2014/01/20 PHP
php实现mysql数据库操作类分享
2014/02/14 PHP
利用PHP命令行模式采集股票趋势信息
2016/08/09 PHP
PHP批量获取网页中所有固定种子链接的方法
2016/11/18 PHP
那些年,我还在学习jquery 学习笔记
2012/03/05 Javascript
jquery方法+js一般方法+js面向对象方法实现拖拽效果
2012/08/30 Javascript
javascript 禁用IE工具栏,导航栏等等实现代码
2013/04/01 Javascript
深入解读JavaScript中的Hoisting机制
2015/08/12 Javascript
javascript中for/in循环及使用技巧
2015/09/01 Javascript
AngularJS教程 ng-style 指令简单示例
2016/08/03 Javascript
Angular 5.0 来了! 有这些大变化
2017/11/15 Javascript
详解webpack打包后如何调试的方法步骤
2018/11/07 Javascript
NodeJs入门教程之定时器和队列
2019/03/08 NodeJs
解决layui数据表格Date日期格式的回显Object的问题
2019/09/19 Javascript
微信小程序自定义导航栏(模板化)
2019/11/15 Javascript
vue实现在进行增删改操作后刷新页面
2020/08/05 Javascript
简单介绍Ruby中的CGI编程
2015/04/10 Python
python中如何正确使用正则表达式的详细模式(Verbose mode expression)
2017/11/08 Python
python 切换root 执行命令的方法
2019/01/19 Python
Python实现随机生成任意数量车牌号
2020/01/21 Python
Python中使用filter过滤列表的一个小技巧分享
2020/05/02 Python
基于FME使用Python过程图解
2020/05/13 Python
Python基于argparse与ConfigParser库进行入参解析与ini parser
2021/02/02 Python
Helly Hansen工作服美国官方网上商店:为最恶劣的环境
2019/09/04 全球购物
软件工程师面试题
2012/06/25 面试题
出国签证在职证明
2014/01/16 职场文书
教师师德演讲稿
2014/05/06 职场文书
2014坚持党风廉政建设思想汇报
2014/09/18 职场文书
公司车辆维修管理制度
2015/08/05 职场文书
化验室安全管理制度
2015/08/06 职场文书
忘记Grafana不要紧2种Grafana重置admin密码方法详细步骤
2022/04/07 Servers
redis 解决库存并发问题实现数量控制
2022/04/08 Redis
MySQL 原理与优化之Limit 查询优化
2022/08/14 MySQL