个人理解
定义:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高级接口,这个接口使得这个子系统更加容易使用。
外观模式的构造:一个子系统的若干类(可以负责相同的事务,也可以负责不同的事务;可以继承同一父类,也可以内部方法尽不相同)、一个外观类Facade。
使用方法:
-
在设计初期阶段,应该要有意识的将不同的两个层分离,层与层之间建立外观Facade
-
在开发阶段,子系统往往因为不断地重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖
-
在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求必须依赖于它,为新系统增加一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的代码的接口,让新系统与Facade交互,Facade与遗留代码交互所有复杂的工作。
案例分析
外观模式在MVC框架中有具体的提现,所以不写代码,然后把MVC的源码进行简单的分析,我使用的是thinkphp5.1.x。
众所周知,MVC框架主要分为三个层数据访问层(Model)、业务逻辑层(Controller)、展示层(View)。
Model层代码片段(类过大,没有全部复制,想要详细了解可以自己去看):
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <[email protected]>
// +----------------------------------------------------------------------
namespace think;
use InvalidArgumentException;
use think\db\Query;
/**
* Class Model
* @package think
* @mixin Query
*/
abstract class Model implements \JsonSerializable, \ArrayAccess
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;
/**
* 架构函数
* @access public
* @param array|object $data 数据
*/
public function __construct($data = [])
{
if (is_object($data)) {
$this->data = get_object_vars($data);
} else {
$this->data = $data;
}
if ($this->disuse) {
// 废弃字段
foreach ((array) $this->disuse as $key) {
if (array_key_exists($key, $this->data)) {
unset($this->data[$key]);
}
}
}
// 记录原始数据
$this->origin = $this->data;
$config = Db::getConfig();
if (empty($this->name)) {
// 当前模型名
$name = str_replace('\\', '/', static::class);
$this->name = basename($name);
if (Container::get('config')->get('class_suffix')) {
$suffix = basename(dirname($name));
$this->name = substr($this->name, 0, -strlen($suffix));
}
}
if (is_null($this->autoWriteTimestamp)) {
// 自动写入时间戳
$this->autoWriteTimestamp = $config['auto_timestamp'];
}
if (is_null($this->dateFormat)) {
// 设置时间戳格式
$this->dateFormat = $config['datetime_format'];
}
if (is_null($this->resultSetType)) {
$this->resultSetType = $config['resultset_type'];
}
if (!empty($this->connection) && is_array($this->connection)) {
// 设置模型的数据库连接
$this->connection = array_merge($config, $this->connection);
}
if ($this->observerClass) {
// 注册模型观察者
static::observe($this->observerClass);
}
// 执行初始化操作
$this->initialize();
}
/**
* 保存当前数据对象
* @access public
* @param array $data 数据
* @param array $where 更新条件
* @param string $sequence 自增序列名
* @return bool
*/
public function save($data = [], $where = [], $sequence = null)
{
if (is_string($data)) {
$sequence = $data;
$data = [];
}
if (!$this->checkBeforeSave($data, $where)) {
return false;
}
$result = $this->exists ? $this->updateData($where) : $this->insertData($sequence);
if (false === $result) {
return false;
}
// 写入回调
$this->trigger('after_write');
// 重新记录原始数据
$this->origin = $this->data;
return true;
}
}
Model通过实现两个接口\JsonSerializable(JSON序列化接口), \ArrayAccess,如果需要了解这两个接口,可以点击链接:JsonSerializable、ArrayAccess。Model其实使用的是DB中的链接数据库方法(这里往上就不说了,有点跑题了,需要了解如何实现的请看\think\db\Query类)。在Model中定义了常用的方法,例如代码中已经复制了的save()方法,还包括很多isUpdate()、allowField()等等。
Controller层代码:
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <[email protected]>
// +----------------------------------------------------------------------
namespace think;
use think\exception\ValidateException;
use traits\controller\Jump;
class Controller
{
use Jump;
/**
* 视图类实例
* @var \think\View
*/
protected $view;
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 验证失败是否抛出异常
* @var bool
*/
protected $failException = false;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 前置操作方法列表(即将废弃)
* @var array $beforeActionList
*/
protected $beforeActionList = [];
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 构造方法
* @access public
*/
public function __construct(App $app = null)
{
$this->app = $app ?: Container::get('app');
$this->request = $this->app['request'];
$this->view = $this->app['view'];
// 控制器初始化
$this->initialize();
// 控制器中间件
if ($this->middleware) {
foreach ($this->middleware as $key => $val) {
if (!is_int($key)) {
if (isset($val['only']) && !in_array($this->request->action(), $val['only'])) {
continue;
} elseif (isset($val['except']) && in_array($this->request->action(), $val['except'])) {
continue;
} else {
$val = $key;
}
}
$this->app['middleware']->controller($val);
}
}
// 前置操作方法 即将废弃
foreach ((array) $this->beforeActionList as $method => $options) {
is_numeric($method) ?
$this->beforeAction($options) :
$this->beforeAction($method, $options);
}
}
// 初始化
protected function initialize()
{}
/**
* 前置操作
* @access protected
* @param string $method 前置操作方法名
* @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]]
*/
protected function beforeAction($method, $options = [])
{
if (isset($options['only'])) {
if (is_string($options['only'])) {
$options['only'] = explode(',', $options['only']);
}
if (!in_array($this->request->action(), $options['only'])) {
return;
}
} elseif (isset($options['except'])) {
if (is_string($options['except'])) {
$options['except'] = explode(',', $options['except']);
}
if (in_array($this->request->action(), $options['except'])) {
return;
}
}
call_user_func([$this, $method]);
}
/**
* 加载模板输出
* @access protected
* @param string $template 模板文件名
* @param array $vars 模板输出变量
* @param array $config 模板参数
* @return mixed
*/
protected function fetch($template = '', $vars = [], $config = [])
{
return $this->view->fetch($template, $vars, $config);
}
/**
* 渲染内容输出
* @access protected
* @param string $content 模板内容
* @param array $vars 模板输出变量
* @param array $config 模板参数
* @return mixed
*/
protected function display($content = '', $vars = [], $config = [])
{
return $this->view->display($content, $vars, $config);
}
/**
* 模板变量赋值
* @access protected
* @param mixed $name 要显示的模板变量
* @param mixed $value 变量的值
* @return $this
*/
protected function assign($name, $value = '')
{
$this->view->assign($name, $value);
return $this;
}
/**
* 视图过滤
* @access protected
* @param Callable $filter 过滤方法或闭包
* @return $this
*/
protected function filter($filter)
{
$this->view->filter($filter);
return $this;
}
/**
* 初始化模板引擎
* @access protected
* @param array|string $engine 引擎参数
* @return $this
*/
protected function engine($engine)
{
$this->view->engine($engine);
return $this;
}
/**
* 设置验证失败后是否抛出异常
* @access protected
* @param bool $fail 是否抛出异常
* @return $this
*/
protected function validateFailException($fail = true)
{
$this->failException = $fail;
return $this;
}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @param mixed $callback 回调方法(闭包)
* @return array|string|true
* @throws ValidateException
*/
protected function validate($data, $validate, $message = [], $batch = false, $callback = null)
{
if (is_array($validate)) {
$v = $this->app->validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
list($validate, $scene) = explode('.', $validate);
}
$v = $this->app->validate($validate);
if (!empty($scene)) {
$v->scene($scene);
}
}
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
if (is_array($message)) {
$v->message($message);
}
if ($callback && is_callable($callback)) {
call_user_func_array($callback, [$v, &$data]);
}
if (!$v->check($data)) {
if ($this->failException) {
throw new ValidateException($v->getError());
}
return $v->getError();
}
return true;
}
}
Controller定义了一部分常用的方法fetch()、display()。我们经常用到的success()、error()、redirect()则在Jump类里面,下一面截取代码。
<?php
/**
* 用法:
* class index
* {
* use \traits\controller\Jump;
* public function index(){
* $this->error();
* $this->redirect();
* }
* }
*/
namespace traits\controller;
use think\Container;
use think\exception\HttpResponseException;
use think\Response;
use think\response\Redirect;
trait Jump
{
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 操作成功跳转的快捷方法
* @access protected
* @param mixed $msg 提示信息
* @param string $url 跳转的URL地址
* @param mixed $data 返回的数据
* @param integer $wait 跳转等待时间
* @param array $header 发送的Header信息
* @return void
*/
protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) {
$url = $_SERVER["HTTP_REFERER"];
} elseif ('' !== $url) {
$url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Container::get('url')->build($url);
}
$result = [
'code' => 1,
'msg' => $msg,
'data' => $data,
'url' => $url,
'wait' => $wait,
];
$type = $this->getResponseType();
// 把跳转模板的渲染下沉,这样在 response_send 行为里通过getData()获得的数据是一致性的格式
if ('html' == strtolower($type)) {
$type = 'jump';
}
$response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_success_tmpl')]);
throw new HttpResponseException($response);
}
/**
* 操作错误跳转的快捷方法
* @access protected
* @param mixed $msg 提示信息
* @param string $url 跳转的URL地址
* @param mixed $data 返回的数据
* @param integer $wait 跳转等待时间
* @param array $header 发送的Header信息
* @return void
*/
protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
$type = $this->getResponseType();
if (is_null($url)) {
$url = $this->app['request']->isAjax() ? '' : 'javascript:history.back(-1);';
} elseif ('' !== $url) {
$url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : $this->app['url']->build($url);
}
$result = [
'code' => 0,
'msg' => $msg,
'data' => $data,
'url' => $url,
'wait' => $wait,
];
if ('html' == strtolower($type)) {
$type = 'jump';
}
$response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_error_tmpl')]);
throw new HttpResponseException($response);
}
/**
* 返回封装后的API数据到客户端
* @access protected
* @param mixed $data 要返回的数据
* @param integer $code 返回的code
* @param mixed $msg 提示信息
* @param string $type 返回数据格式
* @param array $header 发送的Header信息
* @return void
*/
protected function result($data, $code = 0, $msg = '', $type = '', array $header = [])
{
$result = [
'code' => $code,
'msg' => $msg,
'time' => time(),
'data' => $data,
];
$type = $type ?: $this->getResponseType();
$response = Response::create($result, $type)->header($header);
throw new HttpResponseException($response);
}
/**
* URL重定向
* @access protected
* @param string $url 跳转的URL表达式
* @param array|integer $params 其它URL参数
* @param integer $code http code
* @param array $with 隐式传参
* @return void
*/
protected function redirect($url, $params = [], $code = 302, $with = [])
{
$response = new Redirect($url);
if (is_integer($params)) {
$code = $params;
$params = [];
}
$response->code($code)->params($params)->with($with);
throw new HttpResponseException($response);
}
/**
* 获取当前的response 输出类型
* @access protected
* @return string
*/
protected function getResponseType()
{
if (!$this->app) {
$this->app = Container::get('app');
}
$isAjax = $this->app['request']->isAjax();
$config = $this->app['config'];
return $isAjax
? $config->get('default_ajax_return')
: $config->get('default_return_type');
}
}
View层代码:
View层其实不存在代码,因为View层是html页面,开发者使用thinkphp官方定义好的活自定义的模板引擎直接写html代码即可。但是程序在运行时,框架都会将html文件转为php文件,行程一个View层的php原生文件,这时就可以实现逻辑控制了。以下是View编译后的一段代码。
<?php /*a:1:{s:58:"E:\wamp\www\farmshopWechat\app\ddgy\view\access\index.html";i:1538289954;}*/ ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>XXX的果园</title>
<link rel="shortcut icon" href="">
<meta name="Keywords" content=""/>
<meta name="Description" content=""/>
<meta name="author" content="RenLing"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="renderer" content="webkit"/>
<link rel="stylesheet" href="/farmshopWechat/public/static/ddgy/css/public.css">
<link rel="stylesheet" href="/farmshopWechat/public/static/ddgy/css/index.css">
<link rel="stylesheet" href="/farmshopWechat/public/static/weui/css/jquery-weui.css">
<link rel="stylesheet" href="/farmshopWechat/public/static/weui/lib/weui.css">
</head>
<body>
<div class="back">
<div class="pot <?php echo htmlentities($data['pot']); ?>" id="pot"></div>
<div class="tree tree_<?php echo htmlentities($data['tree']['status']); ?>" id="tree"></div>
<div class="tree_floating floating_helpWatering">
<div class="title"></div>
<div class="img"></div>
</div>
<div class="progress">
<div class="progress_public progress_middle" id="progress_middle"></div>
<div class="progress_public progress_end"></div>
</div>
<div class="progress_title" id="progress_title"></div>
<div class="operate">
<div class="grid"></div>
<div class="grid_space"></div>
<div class="grid friends" id="friends"></div>
</div>
<div class="watering_cooling_progress hide"></div>
<div class="watering watering_default" id="watering" worker="">
<img src="/farmshopWechat/public/static/ddgy/image/kettle/2.png" class="watering_kettle watering_kettle_wait">
<div id="water">浇水</div>
</div>
<div class="watering_kettle_watering hide"></div>
<a id="home" href="<?php echo url('ddgy/Index/index'); ?>"></a>
<div class="friendsList" id="friendsList">
<div class="upPull_title">
<span>好友列表</span>
<i class="iconfont icon-cha close"></i>
</div>
<div id="friends_content"></div>
</div>
</div>
</body>
</html>
<script type="text/javascript" src="/farmshopWechat/public/static/ddgy/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/farmshopWechat/public/static/weui/js/jquery-weui.min.js"></script>
<script>
window.farm = {};
window.farm.water = <?php echo json_encode($data['water']); ?>;
window.farm.master = "<?php echo htmlentities($data['master']); ?>";
</script>
<script type="text/javascript" src="/farmshopWechat/public/static/ddgy/js/public.js"></script>
<script type="text/javascript" src="/farmshopWechat/public/static/ddgy/js/access.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
这是我正式项目中的View层代码编译后的结果。
剩下的我们就需要知道浏览器访问的使用是怎样依次调用的。
同一入口(public\index.php)代码:
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <[email protected]>
// +----------------------------------------------------------------------
// [ 应用入口文件 ]
namespace think;
// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';
// 支持事先使用静态方法设置Request对象和Config对象
// 执行应用并响应
Container::get('app')->run()->send();
App类中的run()方法:
public function run()
{
try {
// 初始化应用
$this->initialize();
// 监听app_init
$this->hook->listen('app_init');
if ($this->bindModule) {
// 模块/控制器绑定
$this->route->bind($this->bindModule);
} elseif ($this->config('app.auto_bind_module')) {
// 入口自动绑定
$name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME);
if ($name && 'index' != $name && is_dir($this->appPath . $name)) {
$this->route->bind($name);
}
}
// 监听app_dispatch
$this->hook->listen('app_dispatch');
$dispatch = $this->dispatch;
if (empty($dispatch)) {
// 路由检测
$dispatch = $this->routeCheck()->init();
}
// 记录当前调度信息
$this->request->dispatch($dispatch);
// 记录路由和请求信息
if ($this->appDebug) {
$this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true));
$this->log('[ HEADER ] ' . var_export($this->request->header(), true));
$this->log('[ PARAM ] ' . var_export($this->request->param(), true));
}
// 监听app_begin
$this->hook->listen('app_begin');
// 请求缓存检查
$this->checkRequestCache(
$this->config('request_cache'),
$this->config('request_cache_expire'),
$this->config('request_cache_except')
);
$data = null;
} catch (HttpResponseException $exception) {
$dispatch = null;
$data = $exception->getResponse();
}
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
});
$response = $this->middleware->dispatch($this->request);
// 监听app_end
$this->hook->listen('app_end', $response);
return $response;
}
这个方法中进行了控制器的绑定操作(监听app_init下面),经过一系列的处理,最终返回Response,然后到public/index.php中执行send()方法。
简化UML图
在这里可以把Index类看做外观类,外观类负责将多个Model类进行逻辑处理,然后App可以看做客户端,客户端直接调用外观类即可(注意,这里面的每个子类User/Orders/Pay都是继承于Model,在外观模式中,这一项不是必须的,每个字类可以是继承与同一父类,也可以是不同功能的类)。
下一篇
初识设计模式——建造者模式