OOP设计模式 - 结构型

Posted by 昆山吴彦祖 on 2021.12.28

创建型的几个模式,其实都很好理解。从结构型开始就变得比较抽象和晦涩难懂了。 但其中有部分其实我们日常使用laravel或者其他框架的时候,都已经反复使用过了,比如Opencart里的注册器、laravel里的门面、数据映射、流接口 等等。 主流都框架都试图通过各种设计模式来实现不同类与对象之间的分层、解耦,以提升拓展性。

  1. 注册器模式
  2. 适配器模式
  3. 代理模式
  4. 门面模式
  5. 组合模式
  6. 装饰器模式
  7. 桥梁模式
  8. 数据映射模式
  9. 依赖注入模式
  10. 流接口模式
  11. 享元模式

注册器模式

一个注册器,把一些常用的类实例化后存放在注册器里,需要的时候直接取出来用。通常是作为一个单例使用。

有点像 laravel的 绑定接口到实现,直接通过服务容器取出使用。

class Register(){
	private static $instance;
	private static $tree;
	// 固定为 私有静态
	private static function getInstance(){
		if(static::instance === null){
			static::instance = new self();
		}
		return static::instance;
	}
	public static function set($key,$value){
		static::tree[$key] = $value;
	}
	public static function get($key){
		return static::tree[$key];
	}
	
	private function __construct(){
		              
	}
	private function __clone(){
		
	}
	private function __wakeup(){
		
	}
}

//应用
$register = Register::getInstance();
$register->set('db_connect',new Pdo(...));
$connect = $register->get('db_connect');zzxz

适配器模式

适配器模式顾名思义,将本不适用的接口通过适配转化为可用,让原本由于方法差异无法一起使用的类可以一起使用。

代理模式主要的使用场景之一,是作为集成新系统时引入新系统接口(比如引入第三方库)

细分的话 还可以分为接口适配器(为interface提供适配),类适配器(为class提供适配)等等

//客服消息接口
interface message(){
	public function send($user,$message);
}
class sms implements message(){
	public $sms;
	public function __construct(){
		$this->sms = new Sms;
	}
	public function send($user,$message){
		$this->sms->sendTo($user->mobile,$message);
	}
}

//需要做适配的对象
class wechat_template_message(){
	public $wechat;
	public function send($openid,$template,...$messages){
		$this->wechat->sendTemplateMessage($openid,$template,...$messages);
	}
}

//模板消息适配器- 将微信模板消息类适配成符合message接口的类
class wechat_template_message_adapter{
	private $wechat_template_message;
	private $template;

	public function __construct(wechat_template_message $wechat_template_message,$template){
		$this->wechat_template_message = new wechat_template_message();
		$this->template= $template;
	}
	public function send($user,$message){
		$this->wechat_template_message->send($user->openid,$this->template,$message);
	}
}

// 应用
$sms = new sms();
$user = new User("张三");
$sms->send($user,"客服信息");

$wechat_template_message = new wechat_template_message_adapter(new wechat_template_message(),"XW1233");
$user = new User("李四");
$wechat_template_message->send($user,"客服信息");

上面这个例子可能本身不是很合适,理论上把 sms服务类/微信模板消息服务类 继承统一信息接口,然后在客服类中注入 接口实现 貌似更合理。

代理模式

代理模式某种程度上和适配器感觉蛮像的,都是一种提供类间接访问的模式。

个人理解的区别在于:

  • 适配器做的是将某个不适用的接口转化为适用。
  • 代理是需要为类提供一种访问控制机制 或者 为某个类的访问提供附加操作(这点优点类似laravel 请求中间件的概念,把用户请求先通过中间件过滤一遍)。
class db(){
	private $pdo;
	private $mail;
	public function __construct(Mail $mail){
		$this->pdo = new Pdo(...);
		$this->mail = $mail;
	}
	public function query($sql){
		$this->pdo->query($sql);
		return $this->pdo->fetchAll();
	}
}

class dbProxy(){
	$db;
	public function __construct(){
		$this->db= new db();
	}

	public function query($sql){
		try{
			return $this->db->query($sql);
		}catch(\\PdoException $e){
			$this->mail->notice_to_admin("数据库查询错误:".$e->getMessage());
			throw $e;
		}
	}
}

// 我用了一个 需要附加操作所有使用代理的案例
// 当然也可以通过代理实现功能访问的控制授权等等,而不需要把访问控制耦合进类自身(单一职责)
$db = new dbProxy();
$res = $db->query($sql);

门面模式

门面模式为复杂的子系统提供一个统一的访问界面,他定义的是一个高层接口,该接口使得子系统更加容易使用,避免外部模块深入到子系统内部而产生与子系统内部细节耦合的问题。

门面模式是laravel中常用的模式之一,感觉和代理模式很像,提供类的一个专门的访问层。

但是目的性不太一样:

  • 门面在用户和子系统中加入一个中间层,从而让子系统更易用,且一个门面可以为多个子系统或子系统中多个类提供外部接口,这进一步优化了部分比较复杂的子系统的客户使用体验。

class wechat_template_message(){
	public function send($openid,...$message){
	}
	public function sendAll(Array $openids,...$message){
	}
}
class wechat_pay(){
	public function pay(){
		echo "微信支付方法";
	}
}
class wechat_offiaccount(){
	public function menu(){
		echo "微信公众号菜单方法";
	}
}

//门面
class wechat_facade(){
	private $template_message;
	private $pay;
	private $offiaccount;
	
	public function __construct(){
		$this->template_message = new wechat_template_message();
		$this->pay= new wechat_pay();
		$this->offiaccount= new wechat_offiaccount();
	}
	public function template_message_send($openid,...$message){
		$this->template_message->send($openid,...$message);
	}
	public function pay(){
		$this->pay->pay();
	}
}

//应用
$wechat = new wechat_facade();
$wechat->template_message_send($openid,$message);

组合模式

组合模式(Composite Pattern)有时候又叫做部分-整体模式,用于将对象组合成树形结构以表示“部分-整体”的层次关系。组合模式使得用户对单个对象和组合对象的使用具有一致性。

这段描述是复制过来的,比较抽象。举个例子可能会比较明白,这也是最常见的符合组合模式的案例:文件夹遍历,文件夹下包含多个文件,其中也可能包含文件夹,我们需要遍历某个文件夹内所有文件。 典型的树形结构 (整体-部分)

//组合模式一个基本功能是 让部分和整体使用具有一致性,所以我们定义个接口
interface readable(){
	public function read();
}

class floder implements readable(){
	public $files=[];
	public $name;
	public function __construct($name){
		$this->name = $name;
	}
	public function add(readable $readable){
		$this->files[] = $readable;
	}
	public function read(){
		foreach($this->files as $file){
			$file->read();
		}
	}
}
class image implements readable(){
	public function read(){
		echo $image->url;
	}
}
class text implements readable(){
	public function read(){
		echo $text->content;
	}
}

//应用
$floder = new floder("farther");
$floder->add($image1);
$floder->add($text1);

$floder_son = new floder("son");
$floder_son ->add($image2);
$floder->add($floder_son);

$floder->read();

装饰器模式

当我们需要为一个类增加新的功能,通常我们只能更改实现类(说明这个类功能实现不符合开闭原则,当然在小型项目中无可厚非),如果我们希望代码符合开闭原则,用拓展来代替修改。

这时候我们就可以用装饰器模式来实现了,当然这仅限于某些经常需要增加功能的类。

//原始汽车类只提供启动、停止两个功能
interface car(){
	public function run();
	public function stop();
}
class car(){
	public function run(){
		echo "run";
	}
	public function stop(){
		echo "stop";
	}
}

abstract class Decorator(){
	protected $car;
	public function __construct(car $car){
		$this->car = $car;
	}
	abstract public function job();
}
//福特公司出品的最新款汽车 还能飞行(话说这还算汽车吗?) 来个飞行装饰器
class fly_decorator(){
	public function job(){
		$this->add_wing();
		$this->fly();
		return $this->car();
	}
	private function add_wing(){
		$this->car->wing = $wing;
	}
	private function fly(){
		$this->car->fly = function(){
			echo "fly";
		}
	}
}

//总统座驾还能防弹  来个防弹专属装饰器
class safe_decorator(){
	public function job(){
		$this->change_glass();
		$this->change_wheel();
		return $this->car();
	}
	private function change_glass(){
		$this->car->glass = "防弹玻璃";
	}
	private function change_wheel(){
		$this->car->wheel = "实心防爆轮胎";
	}
}

//应用
$fly_car = new car();
$fly_decorator = new fly_decorator($fly_car);
//装饰完成以后这辆车就能飞了
$fly_car = $fly_decorator->job();

桥梁模式/桥接模式

Decouple an abstraction from its implementation so that the two can vary independently。翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”——GoF 《设计模式》

上面是最官方的桥梁模式定义,看完你完全是一脸萌比啊,什么玩意?

还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合来替代继承关系,避免继承层次的指数级爆炸。

这和前面的装饰器模式的核心思想不谋而合 : **如果需要拓展、复用功能 ,组合是一种更优秀的模式。**继承在多个类之间形成了更强的耦合关系,而面向对象开发的核心思想之一就是低耦合。

下面通过一个案例来解释:超市的商品分类,传统的方式是一个树形结构

生抽

海天牌生抽

500ml装

200ml装

李锦记生抽

600ml装

100ml装

老抽

海天老抽

李锦记老抽

耗油

鲁花耗油

海天耗油

如果用对象来表示 产品接口 - 调味料抽象类 - 生抽抽象类 - 海天生抽具体类 一步步继承下去。

这就是上面说的一个类存在多个维度(产品分类、品牌、容量),且各个维度独立变化(每个品牌都生产各个产品,每种产品都有各种容量装)。我们可以将变化维度抽象出来。通过不同组合形成最后的类(海天 - 耗油 - 200ml 装)。

interface brand(){
	public function setBrand();
	public function packing();
}
class haitian implements brand(){
	public function setBrand(){
		return "海天";
	}
	public function packing(){
		echo "开始用海天包装打包";
	}
}
class lijinji implements brand(){
	public function setBrand(){
		return  "李锦记";		
	}
	public function packing(){
		$this->packing_before();
		echo "开始用李锦记包装打包";
	}
	private function packing_before(){
		echo "李锦记包装前会先进行额外空气清洗";
	}
}

interface size(){
	public function injection();
}
class big implements brand(){
	private $size = 600;
	public function injection(){
		echo  "开始注入$this->size ml的产品";		
	}
}
class small implements brand(){
	private $size = 200;
	public function injection(){
		echo  "开始注入$this->size ml的产品";		
	}
}

abstract product(){
	private $brand;
	private $size;
	public function __construct(brand $brand,size $size){
		$this->brand = $brand;
		$this->size= $size;
	}
	public function getBrand(){
		return $this->brand->setBrand();
	}
	public function packing(){
		$this->brand->packing();
	}
	public function injection(){
		$this->size->injection();
	}
}
class shengchou extends product{
	public $category = "生抽";
	public function tixian(){
		echo "加入生提现";
	}
}
class laochou extends product{
	public $category = "老抽";
	public function shangse(){
		echo "加入生上色";
	}
}

//应用
$product = new shengchou(new lijinji(),new small());
$product->injection();//灌装
$product->packing();//包装

上面这个例子,可能会让人觉得,我直接在 产品中注入一个 new brand(”李锦记”) 品牌类不就好了,事实上如果每个品牌类实现的方法都一样,那的确是可以这样。

但如果每个品牌可能会出现方法不一致的状况(就是brand类里要写各种 switch /if 来判断不同品牌),那就说明这个类不符合单一职责原则了,那我们就可以通过 interface 和 继承类来处理

数据映射模式

主流框架对于数据库的操作现在都基于orm数据映射模式,就以laravel为例。 我们要操作user(实体),我们会创建一个 models\user 类(映射类),用于帮组我们间接的操作数据库(持久化数据存储)。

interface model{
	public function create();
	public function update();
}
class user implements model{
	public function create(array $user){
		$this->pdo->query(...);
	}
	public function update(array $user){
		$this->pdo->query(...);
	}
}

//应用
$user = new user();
$user->create([
	'name'=>"张三",
	'nickname'=>"张三",
]);

依赖注入模式

依赖注入开发中最常用的设计模式之一。laravel的核心之一就是 依赖注入容器。依赖注入本身是非常简单的,并不一定要用依赖注入容器来实现自动注入(当然自动注入的确很方便)。

依赖注入配合 依赖反转思想(类依赖抽象而非实现),可以将原本必须在控制器里实现的 userModel 转化为 在实例化控制器时外部注入的任意userInterface实现类。如果我们需要更换用户查询的查询器,这样就不需要调整控制器本身,只需要修改注入的实现。更符合单一职责原则

class userController (){
	private $user;
	// 通过注入一个可用的userinterface 接口实现类,可以达到很好的解耦效果
	public function __construct(){
		$this->user = new UserModel();
	}
	public function getUser(){
		return $this->user->find($id);
	}
}

//修改后
class userController (){
	private $user;
	// 通过注入一个可用的userinterface 接口实现类,可以达到很好的解耦效果
	public function __construct(UserInterface $user){
		$this->user = $user;
	}
	public function getUser(){
		return $this->user->find($id);
	}
}

流接口模式

流接口模式也是laravel中非常常见的一种模式,特别是 数据模型操作。

class db(){
	private $pdo;
	public function __set($key,$value){
		$this->$key = $value;
	}
	public function table(array $table){
		$this->pdo->table= $table;
		return $this;
	}
	public function where(array $where){
		$this->pdo->where = $where;
		return $this;
	}
	public function sort($sort){
		$this->pdo->sort= $sort;
		return $this;
	}
	public function limit($limit){
		$this->pdo->sort= $limit;
		return $this;
	}
	public function get(){
		$this->pdo->query(...);
	}
}

//应用
$db = new db();
$db->table('user')->where(['id','10'])->get();

享元模式

共享元素 享元模式。当我们要批量创建类,发现这些类中有很多重复的元素时候,可以考虑使用这个模式。

集合了单例模式、数据池模式的优点,核心思想就是同样的类只创建一次,尽可能的节省内存空间。

这次我们用一个游戏角色模式来做案例:

服务器上游戏角色很多,每个角色又存储了非常多的信息,有私有的(每个人都不一样的:游戏角色名、角色技能点)、有共有的(固定几个选项的:角色性别、角色皮肤、角色职业等),这时候我们就可以通过享元模式来优化角色创建。

//享元接口
interface flyweightInterface(){
	public function init();
}

class sexFlyweight implements flyweightInterface(){
	public $sex;
	public function init($name){
		$this->sex = $name;
	}
	public function job(){
		if($this->sex =="男"){
			echo "男的出去打猎";
		}else{
			echo "女的做饭";		
		}
	}
}

class skinFlyweight implements flyweightInterface(){
	public $skin ;
	public function init($name){
		$this->skin = new $name;		
	}
	public function change($id){
		echo "更换皮肤";
	}
}
class occupationFlyweight implements flyweightInterface(){
	public $occupation ;
	public function init($name){
		$this->occupation = new $name;		
	}
	public function showPowers(){
		echo "输出当前角色的能力";
	}
}

//享元工厂
abstract flyweightFactory(){
	private $pool = [];
	****private $flyweight;
	public function get($name){
		if(!isset($this->pool[$name])){
			$this->flyweight->init($name);
			$this->pool[] = $this->flyweight;
		}
		return $this->pool[$name];
		
	}
}
class sexFlyweightFactory extends  flyweightFactory{
	****public function __construct(){
		$this->flyweight = new sexFlyweight();
	}	
} 
class skinFlyweightFactory extends  flyweightFactory{
	****public function __construct(){
		$this->flyweight = new skinFlyweight();
	}	
} 
class occupationFlyweightFactory extends  flyweightFactory{
	****public function __construct(){
		$this->flyweight = new occupationFlyweight();
	}	
} 

//应用
//创建一个男性、默认皮肤、战士、角色名张三的橘色
$sexFlyweightFactory= new sexFlyweightFactory();
$sex = $sexFlyweightFactory->get("男");

$skinFlyweightFactory= new skinFlyweightFactory();
$skin= $skinFlyweightFactory->get("default");

$occupationFlyweightFactory= new occupationFlyweightFactory();
$occupation = $occupationFlyweightFactory->get("战士");

$role = new role($sex,$skin,$occupation,"张三");
$role->occupation->showPowers();//输出当前角色的职业能力

上面的例子中,每个职业、皮肤只会被实例化一次存储在享元工厂内,然后创建不同角色的时候注入到角色中使用。

当然这个栗子比较简单,事实上我们通过多例模式完全可以实现一样的功能(但话说回来了,多例和单例现在被认为是比较反模式的设计模式了,这样说遇到类似的状况用享元模式是更好的)。

享元模式和桥梁模式都属于 用组合进行类拓展的模式。他们的适用场景也很像,包括上面这个例子其实就很适合用桥梁模式来构建。

不过享元模式是为了节约内存而把部分元素抽离出来复用,桥梁模式是为了让类的层次更简单,耦合性更低,两者的使用场景还是有差别的。

享元模式通常来说比较少用到。


设计模式