嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

周梦康 发表于 2014-08-01 4540 次浏览 标签 : 设计模式

设计模式,听起来似乎很高大上,实际也的确很高大上,毕竟都是非常有经验的开发人员在实战的开发中总结出来的套路。只要留心,你会发现其实我们天天在用,在享受它给我们带来的好处和便利。对设计模式的理解,需要日益积累的工作经验,只有走了弯路看到直路后才恍然大悟。哦,原来这里如果使用某某模式就更好了!

面向对象的六大设计原则是这样写的:

1、单一职责原则:避免职责分散,避免承担太多(SRP)

2、开闭原则:模块应对扩展开放,而对修改关闭(OCP)。

3、里氏代换原则:子类必须能替换掉父类(LSP)。

4、依赖倒转原则:父类不依赖子类,抽象不依赖具体(DIP)

5、接口隔离原则:职业单一,承诺最简(ISP)

6、组合复用原则:尽量使用组合,避免滥用继承(CRP)

感觉在自己实际工作,在实现第一个原则“开闭原则”的时候就不小心把下面的原则也都实现了。所谓开闭原则,即对扩展开放对修改关闭。我个人简单的理解,新增的功能需要,就写到扩展里面,不要在原来的代码里去修改了。这样说,可能比较隐晦,等会看代码就一目了然了。

谈设计模式,第一个,不解释肯定是工厂模式了,别问我为什么,就是酱紫,就像 hello world 一样。以一个用户类为例:

//会员系统
class Member{
	private $type;
	public function __construct( $type ){ 
		$this->type = $type;
	}

	//获取会员可以用积分兑换的商品
	function getFreeProduct(){
		switch ($this->type){
			case 'iron':
				return '铁牌会员的积分兑换商品';//实际中读库或者其它操作
			case 'copper':
				return '铜牌会员的积分兑换商品';
			/*
				其它银牌,金牌,一钻,二钻会员
			*/
			default:
				return '铁牌会员的积分兑换商品';
		}
	}

	//获取会员的权限
	function getPermission(){
		switch ($this->type){
			case 'iron':
				return '铁牌会员的权限';
			case 'copper':
				return '铜牌会员的权限';
			/*
				其它银牌,金牌,一钻,二钻会员
			*/
			default:
				return '铁牌会员的权限';
		}
	}
}

如果后面我们再新增一种会员类型,如果Member类里不仅仅只有getFreeProductgetPermission这两个方法,那么就需要在每个方法的switch case里面增加新的分支case,这样的代码就违背咱们开发的第一个原则(开闭原则)了。

打个比方,如果5种会员的功能要更新,任务量实在太大,项目却又急着更新,所以我们会把5种类型会员的功能交给团队里的5个人来实现。比如现在A负责金牌会员,B负责银牌会员...A需要修改金牌会员的优惠规则,他需要改Member类,B也要修改银牌会员的优惠规则,他也需要马上修改Member类。这样的情况就很多了,大家都动这个代码,就很不好管理了,代码覆盖,代码冲突等等。

那么怎解决呢?工厂模式就很好的解决了这个问题:

class MemberFactory{
	private static $memberArr = array('iron', 'copper');//实际从数据库或者其他地方读取
	public static function Create( $type ){
		$type = strtolower(trim($type));
		if (!in_array($type, self::$memberArr)){
			throw new Exception('error member type'); 
		}
		$classMember = ucfirst($type)."Member";
		//这里我们将各个类型的用户类比如ironMember,copperMember都统一放到./Mmeber/下, 以ironMember.class.php, copperMember.class.php为文件名. 这样也保证了各个人的代码不冲突,后期也易扩展。
		require 'Member/'.$classMember.'class.php';
		return new $classMember();
	}
}

//现在实例化:
//$usertype = 'iron';
//假设这里是通过在Thinkphp里自己写的一个M()的查询方法得到的结果,返回的是一个字符串,类似于"iron","copper"
$usertype = M("Member")->getUserType($where);
$member = MemberFactory::Create($usertype);
$member->getFreeProduct();

正如我上面的代码注释中说的,把各个类型的用户类(比如ironMember,copperMember)都统一放到./Mmeber/目录下, 以ironMember.class.php, copperMember.class.php为文件名。需要实例化的时候就包含该文件,这样也保证了各个开发人员的代码不冲突。后期也易扩展,新增一种类型的用户,只需新增一个类型的文件即可。

这样看我们工作中是不是天天在用工厂模式呢?

不过有没有发现,如果我们在一个进程里面很多地方调用MemberFactory::Create()就会每次都重新在内存中开辟一块空间,这是不是太浪费资源了呢?这个最常见的就是我们项目里面的数据库连接了,原来我们写面向过程的代码的时候也知道把数据库连接写在最上面,保证一个进程里面只连接一次,然后在页面底部释放连接。那么现如今,我们以面向对象的方式来开发,如何保证只建立一个连接呢?这样就要说到单例模式(又是套路,说完工厂说单例)。接着上面用户类的实例化的内容继续说。最简单的办法呢就是把已经实例化过的对象放在一个工厂方法里面的静态成员里面,而这个成员是一个数组,把用户类型作为下标,如果在这个进程中之前实例化过就直接返回,没有则实例化并且存入这个静态成员里面,这样说呢,也不是完全的单例模式,但是是单例模式的一个思维方式,也是缓存的一个思维方式。

class MemberFactoryNew{
	private static $memberArr = array('iron', 'copper');//.....
	private static $_instance = array();
	public static function Create( $type ){
		$type = strtolower(trim($type));
		if (!in_array($type, self::$memberArr)){
			throw new Exception('error member type'); 
		}
		
		if (!self::$_instance[$type]){
			$classMember = ucfirst($type)."Member";
                        self::$_instance[$type] = new $classMember();
                }
		return self::$_instance[$type];
	}
}

拿之前我写的那篇ThinkPHP的缓存体系分析就是一个很好的例子http://zhoumengkang.com/287.html

ThinkPHP里有很多缓存扩展个人小结 - 说说自己工作中天天使用的设计模式


//文件地址:D:\code\ThinkPHP3.1.3\Lib\Core\Cache.class.php
class Cache {
 
    protected $handler;
 
    protected $options = array();
 
    public function connect($type='',$options=array()) {
        if(empty($type))  $type = C('DATA_CACHE_TYPE');
        $type  = strtolower(trim($type));
        $class = 'Cache'.ucwords($type);
        if(class_exists($class))
            $cache = new $class($options);
        else
            throw_exception(L('_CACHE_TYPE_INVALID_').':'.$type);
        return $cache;
    }
 
    public function __get($name) {
        return $this->get($name);
    }
 
    public function __set($name,$value) {
        return $this->set($name,$value);
    }
 
    public function __unset($name) {
        $this->rm($name);
    }
    public function setOptions($name,$value) {
        $this->options[$name]   =   $value;
    }
 
    public function getOptions($name) {
        return $this->options[$name];
    }
 
    static function getInstance() {
       $param = func_get_args();
        return get_instance_of(__CLASS__,'connect',$param);
    }
 
    //队列缓存,下篇笔记
    protected function queue($key) {
        
    }
    
    public function __call($method,$args){
        //调用缓存类型自己的方法
        if(method_exists($this->handler, $method)){
           return call_user_func_array(array($this->handler,$method), $args);
        }else{
            throw_exception(__CLASS__.':'.$method.L('_METHOD_NOT_EXIST_'));
            return;
        }
    }
}

Cache类里通过connect方法实际就是一个工厂方法,实现了各个 Cache 子类的实例化,就和上面的各个用户子类的实例化一样。注意这里并没有像我上写那样手动加载,而是ThinkPHP对类的自动加载写在了Think::autoload()方法里了。而在初始化的时候通过spl_autoload_register(array('Think', 'autoload'));注册为spl自动加载方法。需要了解的请查看Think.class.php(https://github.com/liu21st/thinkphp/blob/master/ThinkPHP/Library/Think/Think.class.php)的内容。

具体的ThinkPHP的整个缓存体系是怎么走的,这里大体说下流程:

S->Cache::getInstance($type,$options)->get_instance_of->call_user_func_array(array('Cache', 'connect'), $args)

具体的还请查看http://zhoumengkang.com/287.html

也许你和我一样看到这个Cache:connect方法还在想刚刚不是说只实例化一次么?这里怎么没有那样做呢?这也是ThinkPHP的高明之处,他把这个工厂方法又抽象了一层,都写到了get_instance_of这个函数里,在实例化对象的时候先找这个函数调,静态变量里没有再去找工厂方法。

/**
 * 取得对象实例 支持调用类的静态方法
 * @param string $name 类名
 * @param string $method 方法名,如果为空则返回实例化对象
 * @param array $args 调用参数
 * @return object
 */
function get_instance_of($name, $method='', $args=array()) {
    static $_instance = array();
    $identify = empty($args) ? $name . $method : $name . $method . to_guid_string($args);
    if (!isset($_instance[$identify])) {
        if (class_exists($name)) {
            $o = new $name();
            if (method_exists($o, $method)) {
                if (!empty($args)) {
                    $_instance[$identify] = call_user_func_array(array(&$o, $method), $args);
                } else {
                    $_instance[$identify] = $o->$method();
                }
            }else{
                $_instance[$identify] = $o;
            }
        }else{
            halt(L('_CLASS_NOT_EXIST_') . ':' . $name);        
        }
    }
    return $_instance[$identify];
}

如果没有这个专门用来实例化对象的函数,那么我们在我们的Cache工厂方法,DB工厂方法,Member工厂方法里都要去做单例的处理,这样是不是代码重复得太多了呢?如果把单例处理抽出来,而工厂只负责创造对象而不去负责是否单例判断。

继续说上面的Cache类,其实这里还是运用了"适配器模式"的思想。不管你是memcache还是apc或者是Xcache,在这些子类里面都统一把缓存的CURD封装在了get,set,rm方法里面,每个缓存的子类都严格按照这个要求来,这样我们都可以通过Cache类就可以统一调用这些方法了,而不用管各个不通缓存具体实现时使用的具体函数了。这就是“适配器模式”。这里Cache类不是严格的适配器模式,可以简单参考下下面的代码:

interface CacheInterface
{
	function set($name, $value, $expire = null);

	function get($name);

	function delete($name);
}

//适配器
class CacheApc implements CacheInterface
{
	function get($name) {
		return apc_fetch($name);
	}

	function set($name, $value, $expire = null) {
        return apc_store($name, $value, $expire);
	}

	function delete($name) {
         return apc_delete($name);
	}
}

class CacheMemcache implements CacheInterface
{
	private $memcache_obj;

	function __construct() 
	{
		$this->memcache_obj = new Memcache;
		$this->memcache_obj->connect('localhost', 11211);
	}

	function get($name) {
		return $this->memcache_obj->get($name);
	}

	function set($name, $value, $expire = null) {
        return $this->memcache_obj->set($name, $value, $expire);
	}

	function delete($name) {
         return $this->memcache_obj->delete($name);
	}
}

//把工厂模式应用进来
class Cache
{
	private static $cacheArr = array('apc', 'memcache');
	private static $cacheed = array();
	public static function getInstance( $type )
	{
		$type = strtolower(trim($type));
		if (!in_array($type, self::$cacheArr))
		{
			throw new Exception('error cache type'); 
		}
		//这里也可以用switch case来创建对象,可读性更好
		
		if(!Cache::cached[$type]){
			$class = "Cache".ucfirst($type);
			Cache::cached[$type] = $class;
		}
		return new $class();
	}
}

看完以后是不是想说:“尼玛,我不是天天就这么在用么?”

而所谓的装饰器模式的使用范围则更加广泛,只是我们在工作偷懒了,都用简单的if else完成了,装饰器嘛,就是点缀的意思,比如给某个帖子后面加个精华的按钮,给某个商品的后面添加个促销的提示。这就是装饰,就像现在的美女都喜欢带个兔女郎发卡。为什么要使用装饰模式呢?也就是为了遵循开闭原则,新增的需求都在扩展中写。

而建造者模式,则更加不用说了,面向过程的时候就天天在用,一个函数,一个方法一般都隐性默认为只做一件事,那么很多逻辑复杂的事情就要抽出来重新封装成一个方法或者函数,这就是建造者模式了。。。被自己萌哭了。

后面还有很多模式,比如访问者模式,状态模式,组合模式,桥接模式等,我们实际工作也经常在使用,只是自己没太注意。如果能找到合适的业务场景,也许就会豁然开朗,慢慢学习,等完全熟透了,再更新笔记。

嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

评论列表