编程的耦合与解耦

在项目的开发过程中,我们经常强调代码模块尽量做到 高内聚低耦合,那么到底是什么是耦合?怎样做到低耦合?
很多同学感到疑惑,下面我深入探讨这个问题,如何在程序设计中进行解耦合实现低耦合。

大家所听说的依赖注入,控制反转,AOP(面向切面编程)等等概念其最终的目的都是为了解耦,提高代码复用和一些其他的功能,熟练掌握这些概念是高手的必经之路,这里对这些概念都做了总结,详细请参阅文档中的相关章节

什么是耦合?

在程序设计中我们通常谈到的耦合,是指一个功能模块和其他模块之间的依赖关系

这是一个典型的耦合示例,显然能看出来类 BB 要正常工作,依赖类 AA,因为在里面实例化了类 AA,这意味着如果我想把类 BB 拿出来,放到其他项目用,将无法工作,那么此时我们就可以说类 AA 和类 BB 耦合度高,或者说有耦合。
如果你想不通为什么不同时把类 AA 也一起拿过去的话,想想如果你在项目自己封装一个工具类,但是类里面使用了thinkphp框架里自带的助手函数,那要你把工具类拿到laravel框架里使用,意味着什么?把整个thinkphp框架一起复制过去?参考一下 thinkphp里helper.php里的函数,几乎都是在函数里调用系统的各个类库,你想仅仅把一个函数复制过去,毫无意义
一个典型的示例是大家在一些论坛看到的某些帖子称 比Rbac权限验证更灵活,更强大的Auth类验证方式。 参考http://www.thinkphp.cn/topic/4029.html
Auth类里的部分代码

		protected function getUserInfo($uid)
		{
			static $user_info = [];
			$user = Db::name($this->config['auth_user']);
			// 获取用户表主键
			$_pk = is_string($user->getPk()) ? $user->getPk() : 'uid';
			if(!isset($user_info[$uid])) {
				$user_info[$uid] = $user->where($_pk , $uid)->find();
			}
			return $user_info[$uid];
		}

这个是获取一个用户信息的方法,明显的看到里面用到了 Db::name() 方法,意味着如果这个类拿到其他项目,就可能无法使用,因为你无法确定其他项目里是否有这个方法,事实上从tp3.2版本迁移到5.0版本都无法使用,需要改代码

所以耦合的弊端十分明显,当然了或许作者在设计的时候就没有考虑这个问题,这不是错
但是我们在设计项目的时候,应该需要考虑这个问题,这也是为什么提倡高类聚,低耦合的原因

如何降低、解除耦合?

实现低耦合就是对两类之间进行解耦,解除类之间的直接关系,将直接关系转换成间接关系,下面来提供几种解耦思路。

1.通过传入耦合对象的方式



这里我们把对象在外面实例化好以后,以参数的形式传入,这样就可以实现解耦,因为无需不关心对象是什么对象,只要对象有test方法即可
典型的参考示例是thinkphp官方提到过的依赖注入机制,实际上做的就是这个事情,只是他使用的自动传入参数
https://www.kancloud.cn/manual/thinkphp5/215849


构造函数里强制指定了要传入的类型为 Request对象
在实例化的时候,就可以自动把这个对象的实例传进来,关于如何传进来请阅读本手册的反射类依赖注入章节
同时这里又有一个问题,那就是在我们的示例代码里类 BB 里指定了只能调用传入对象的test方法,如果我想执行更多更负责的逻辑,甚至是调用好几个类里指定的方法,这里的调用方式就有点力不从心
于是我们可以参考下面一种解耦幅度更大的方式

2.通过传入执行逻辑方式

这种方式看起来相对有点隐晦难懂,需要一定的基础
这里是直接把一个执行逻辑以闭包的形式传进去执行
里面的方式1比较简单
方式2涉及 call_user_func_array 函数,他接受两个参数,第一个参数为需要执行的函数,第二个参数为执行第一个参数时候传进去的参数
方式3为建立函数的反射再调用 invokeArgs 方式执行这个函数,可以参考 反射类 章节
他们做的事情是一样的,利用这个思想,可以在比起之前的解耦方式上更大幅度的扩展

典型的应用可以参考 iThink里extend文件夹下的 namespace auth 中的 Auth 类 其中有个execClosureList 方法

这个方法的作用是把多个需要放在事务里执行的数据库操作逻辑写到多个闭包里,此方法是 iThink 里十分重要的方法,所有CURD方法都会经过这个方法需要大家十分熟悉 参考 闭包事物构造器章节


		/**
		 * 闭包事物构造器
		 *
		 * @param callable $startTrans     启动事物注册方法
		 * @param callable $commit         提交事物注册方法
		 * @param callable $rollback       回滚事物注册方法
		 * @param array    $list           事物列表
		 * @param string   $err            当事物执行失败时返回的错误信息
		 * @param null     $globalVariable 每个事物都可以作用到的共享变量,会push到每个事物元素的参数上
		 *
		 * @return bool
		 */
		public static function execClosureList(callable $startTrans , callable $commit , callable $rollback , $list = [] , &$err = null , &$globalVariable = null)
		{
			/*	参数和错误信息可不传
			 * [
					[
							function($a, $b) {
								//执行成功返回真
							} ,
							array(
								1 ,
								2 ,
							) ,
							'error massage'
						] ,
						[
							function($a, $b) {
								//执行成功返回真
							} ,
							array(
								1 ,
								2 ,
							) ,
					] ,
				];

			 * */
			call_user_func($startTrans);
			try
			{
				$flag = true;

				while (($flag !== false) && ($closure = array_shift($list)))
				{
					//没传参数设置参数为空数组
					!is_array($closure[1]) && $closure[1] = [];
					//传全局全局变量吧全局变量push到参数列表
					!is_null($globalVariable) && $closure[1][] = &$globalVariable;

					//执行闭包
					$flag = call_user_func_array($closure[0] , $closure[1]);

					($flag === false) && is_string($closure[2]) && ($err = $closure[2]);
				}

				($flag !== false) ? call_user_func($commit) : call_user_func($rollback);

				return $flag;
			} catch (\Exception $e)
			{
				call_user_func($rollback);
				$err = $e->getMessage();

				return false;
			}
		}