耦合Couple
引言 |
本文将到的耦合是指的软件开发中的耦合,而且是代码方面的耦合。包括后面讲到的分层,也只是逻辑分层,不是物理分层。
耦合是我们做软件开发经常会遇到的词汇,尤其是在使用面向对象语言进行开发的时候。看到的相关资料也都在说要低耦合,减少耦合。
尽管我们加入了设计模式,分了层,分了模块,做了等等的工作,还是发现存在耦合,还是有人说耦合高了,导致不能修改,修改、维护的代价太大了。直接导致工期不能固定,不能预估,不知道什么时候才能完成任务。
下面就让我们分析一下耦合从何而来?耦合又是什么呢?如何降低耦合呢?耦合能否不再存在呢?耦合可以解除吗?where is the couple?what is the couple?
正文 |
就像我们分的层一样,分层式为了降低耦合,但是分了层,层之间的耦合是降低了,可是在层的内部的耦合还是需要重新定义的,这里面的耦合不比层之间的耦合要低。通常我们的项目会分为三层:数据访问层、业务逻辑层、表现层,如果需要的话,我们还会加入服务层。还有在这些层之间交互的数据,实体。因此,我觉得耦合包括:实体耦合、数据访问层耦合、业务层耦合、服务耦合(如果存在服务层)。
1、实体耦合
什么叫做实体耦合呢?就是实体的公用,尤其存在于从数据库直接生成的实体。例如一个申请实体,需要申请人填写一些信息,例如:姓名、申请标题、内容。然后由审批的人查看,进行审批,会加入意见,是否同意等信息。同时,申请的时间和处理申请的时间也是关键的信息,也需要在数据库中记录。
生成下面的实体
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BeautyCode.TDD.ConApp
{
public class Application
{
public Guid ApplcaitonID
{
get ;
set ;
}
public Guid ApplyerID
{
get ;
set ;
}
public string ApplyTitle
{
get ;
set ;
}
public string ApplyContent
{
get ;
set ;
}
public DateTime ApplyDate
{
get ;
set ;
}
public Guid Checker
{
get ;
set ;
}
public DateTime CheckDate
{
get ;
set ;
}
public CheckResult CheckResult
{
get ;
set ;
}
public string CheckReason
{
get ;
set ;
}
}
[Flags]
public enum CheckResult
{
Waiting = 1 ,
Agree = 2 ,
Disagree = 4
}
}
其实在添加申请的时候,数据库applicaiton表中的有些字段是不用添加的,例如:处理时间,处理人之类和处理相关的字段。那么这个实体会让做添加功能的程序员很是疑惑,那些字段是必须要赋值的呢?那些不用去管呢?这个类会造成疑惑。就算他通过了解知道了他需要赋值的字段。在后面肯定有一个申请人和审批人查询申请信息的功能。
有一天需求有了变化,申请人多了一个选项,需要他选择,当然了,内容会加入数据库。因为审批人建立了一些申请的类型,可以对申请信息进行分类,方便统计。
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BeautyCode.TDD.ConApp
{
public class Application
{
public Guid ApplcaitonID
{
get ;
set ;
}
public Guid ApplyerID
{
get ;
set ;
}
public string ApplyTitle
{
get ;
set ;
}
public string ApplyContent
{
get ;
set ;
}
public DateTime ApplyDate
{
get ;
set ;
}
public Guid Checker
{
get ;
set ;
}
public DateTime CheckDate
{
get ;
set ;
}
public CheckResult CheckResult
{
get ;
set ;
}
public string CheckReason
{
get ;
set ;
}
public ApplyType ApplyType
{
get ;
set ;
}
}
[Flags]
public enum CheckResult
{
Waiting = 1 ,
Agree = 2 ,
Disagree = 4
}
public enum ApplyType
{
Family=1,
Social=2,
Personal=4
}
}
数据库要添加一个字段,类要添加一个字段。由于实体是公用的,添加、查询、显示都是用一个实体,但是有的字段有的时候是不用的。这个就给后面的维护带来很大的困难,可能会发生错误,这个类背负了过多的责任。如果这个字段只是为了添加功能而设立的,显示和查询功能用这个实体的时候就不应该看见它,要不然他们就需要知道这个字段的意思,是否需要赋值。而且加一个字段,只是为添加功能而设计的字段,需要担心是否显示和查询功能会有问题。就像上面我们的枚举量是从1开始的,没有0。在显示的时候,如果没有对这个字段赋值,那么这个属性就是枚举量的默认值,0,然后就会报错,不存在0的枚举量值。
这样的类应该被分为多个,添加就是一个添加用的实体,每一个字段都是要赋值的,这样就不会给做添加功能的程序员带来麻烦,减少沟通的成本,加快开发的速度。
实体类应该是专用的,每个实体类都有一个场景,都有自己的context,不应该混用。如果混用,就是实体耦合。这个耦合也是我们应该在最初的时候需要注意的。
2、数据访问层耦合
什么叫做数据访问层耦合呢?先让我们看一个例子。
查询,根据界面上的条件查询申请信息。我们的用户有两类,一个是申请者,一个是审批者。申请者应该查询自己的申请,审批者则可以查询所有的申请。于是有了下面的方法。根据申请时间、申请类型、标题进行查询。
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BeautyCode.TDD.ConApp
{
public class ApplicationDataAccess
{
public List < Application > Find(DateTime ? applyDateBegin, DateTime ? applyDateEnd, ApplyType applyType, string title)
{
List < Application > applications = null ;
return applications;
}
}
}
可是有一天,上面说需要添加一个条件,就是处理结果,好吧,方法加一个参数吧。后来又说了,加上一个处理时间的参数吧,好吧,加上两个吧。等等,参数列表会越来越长,好像会没有尽头。我们来小小的改造一下,建立一个查询实体,需要查询的字段都放在里面。
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BeautyCode.TDD.ConApp
{
public class ApplicationDataAccess
{
public List < Application > Find(ApplicationFind find)
{
List < Application > applications = null ;
return applications;
}
}
public class ApplicationFind
{
public DateTime ? ApplyDateBegin
{
get ;
set ;
}
public DateTime ? ApplyDateEnd
{
get ;
set ;
}
public ApplyType ApplyType
{
get ;
set ;
}
public string Title
{
get ;
set ;
}
public CheckResult CheckResult
{
get ;
set ;
}
}
}
这下好了,以后如果需要添加查询条件只需要打开find实体,添加一个属性就可以了。当然了,存储过程和一些代码还是需要修改的。可是毕竟参数不会越来越长了,几个参数看起来也比较舒服。因为在实际的项目中,一个方法,除了这个参数,肯定会有其他的参数,例如方法访问者的信息,用来验证用户合法性,还可能会包括一些异常信息的反馈等等。
过来几天,问题又来了。申请者需要加一个条件,可是审批者还是原来的条件。好吧,在查询方法中,判断一下用户的类型,然后决定使用的存储过程。然后处理查询结果的时候,也需要进行判断。而且写好之后,除了测试申请者用户,还需要测试审批者用户,防止审批者使用这个方法出现问题。
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BeautyCode.TDD.ConApp
{
public class ApplicationDataAccess
{
public List < Application > Find(ApplicationFind find,UserType userType)
{
List < Application > applications = null ;
if (userType == UserType.Applyer)
{
}
else
{
}
return applications;
}
}
public enum UserType
{
Applyer,
Checker
}
public class ApplicationFind
{
public DateTime ? ApplyDateBegin
{
get ;
set ;
}
public DateTime ? ApplyDateEnd
{
get ;
set ;
}
public ApplyType ApplyType
{
get ;
set ;
}
public string Title
{
get ;
set ;
}
public CheckResult CheckResult
{
get ;
set ;
}
}
}
这样每次申请者添加查询条件,都要修改这个方法,还要防止不要破坏审批者使用这个功能。
如果以后加入几种用户类型呢?if。。。else。。。会越写越长,这个方法会越来越难以维护。维护之前要区分那一段是给那一种用户的,修改一种用户的查询,不要影响了别人。如果存储过程用的是一样,那就更是痛苦了,在存储过程中还需要一堆的if。。。else。。。,那个维护起来就更是麻烦了。很是痛苦。
这就是数据访问层的耦合。这时候我们应该给每一种用户写一个查询方法,每个用户都有自己的find实体,对应一个自己的存储过程。做到方法和存储过程的专用。下回再来修改一种用户的查询功能的时候,就不用害怕会影响别人了,不用测试其他的用户类型了。
3、业务逻辑层耦合
什么是业务逻辑层耦合呢?
其实和数据访问层耦合差不多,也是由于方法公用产生的。应该用同样的方法来解决,方法专用,不要大家混用一个。
4、服务层耦合
什么时候服务层耦合呢?
也是基于方法的耦合?解决方法同上。
5、面向对象和耦合
封装、继承、多态是面向对象的三个特征。是不是使用面向对象就可以避免耦合呢?答案是:No。
尤其在使用了其中的继承之后,不仅不是消除耦合,反而是引入了耦合。因为,一个对象继承另外一个对象,如果基类修改了,那么继承类也被迫需要进行修改,这不就是引入了耦合,甚至是加重了耦合吗?
继承就代表加重耦合的程度,估计这也是为什么继承不被推荐使用的原因之一吧。
结论 |
结论就是耦合永远无法消除,我们能做的就是尽量的减少耦合。而且面向对象也不能消除耦合,反而用的不好,还会加重耦合的程度。
1、实体的专用性
1) 尽量的保持实体的专用性,也就是一个功能的方法,虽然和两外一个方法的返回结果类似,可能只需要添加一两个属性,这样的情况,重新建立实体,方便后面可能对这两个方法返回内容的修改不至于相互影响。
2) 尽量保持一个实体中的每一个属性,每一个被赋值的属性,将来都会用到,否则减少实体的属性,或者新建一个实体,使用正好合适的属性个数。
3) 分离添加和显示用的实体,因为添加可能不是每个字段都需要赋值,或者一些值是默认值。
4) 分离不同类型的用户使用的实体,尽管是相同的功能。可以在类名添加ForPlanter之类的后缀来解决。因为不同用户关注的点不同,关注的属性肯定不相同。而且修改也不影响其他类型用户的使用。
2、方法的专用性
保持方法的专用性,分离不同用户的业务方法和数据访问方法。也是为了后面的修改,不至于影响其他用户功能的使用。
3、系统划分
先按照功能模块或者是服务的对象主体来划分系统,划分为子系统。然后再每个子系统中分层,子系统之间的交互使用接口。子系统相关的后台代码独立,方便日后维护升级。