代码改变世界

项目总结-耦合Couple

2010-08-07 16:37  Virus-BeautyCode  阅读(2923)  评论(7编辑  收藏  举报

 

耦合Couple

 

 

 引言

  本文将到的耦合是指的软件开发中的耦合,而且是代码方面的耦合。包括后面讲到的分层,也只是逻辑分层,不是物理分层。

  耦合是我们做软件开发经常会遇到的词汇,尤其是在使用面向对象语言进行开发的时候。看到的相关资料也都在说要低耦合,减少耦合。

  尽管我们加入了设计模式,分了层,分了模块,做了等等的工作,还是发现存在耦合,还是有人说耦合高了,导致不能修改,修改、维护的代价太大了。直接导致工期不能固定,不能预估,不知道什么时候才能完成任务。

  下面就让我们分析一下耦合从何而来?耦合又是什么呢?如何降低耦合呢?耦合能否不再存在呢?耦合可以解除吗?where is the couple?what is the couple?

 

 

 

正文
   在我看来,尽管我们分层了,分了模块,应用了设计模式,耦合永远还是存在的。我们能做的是给耦合定义一个限度,做到什么程度就可以了呢?这需要根据项目的context来决定。context包括:时间,资金,人员,需求等一下客观因素。

  就像我们分的层一样,分层式为了降低耦合,但是分了层,层之间的耦合是降低了,可是在层的内部的耦合还是需要重新定义的,这里面的耦合不比层之间的耦合要低。通常我们的项目会分为三层:数据访问层、业务逻辑层、表现层,如果需要的话,我们还会加入服务层。还有在这些层之间交互的数据,实体。因此,我觉得耦合包括:实体耦合、数据访问层耦合、业务层耦合、服务耦合(如果存在服务层)。

  1、实体耦合

  什么叫做实体耦合呢?就是实体的公用,尤其存在于从数据库直接生成的实体。例如一个申请实体,需要申请人填写一些信息,例如:姓名、申请标题、内容。然后由审批的人查看,进行审批,会加入意见,是否同意等信息。同时,申请的时间和处理申请的时间也是关键的信息,也需要在数据库中记录。

 

  生成下面的实体

 

 

代码
using System;
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;
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
    }
  [Flags]
    
public enum ApplyType
    {
        Family=1,
        Social=2,
        Personal=4
    }
}

 

  数据库要添加一个字段,类要添加一个字段。由于实体是公用的,添加、查询、显示都是用一个实体,但是有的字段有的时候是不用的。这个就给后面的维护带来很大的困难,可能会发生错误,这个类背负了过多的责任。如果这个字段只是为了添加功能而设立的,显示和查询功能用这个实体的时候就不应该看见它,要不然他们就需要知道这个字段的意思,是否需要赋值。而且加一个字段,只是为添加功能而设计的字段,需要担心是否显示和查询功能会有问题。就像上面我们的枚举量是从1开始的,没有0。在显示的时候,如果没有对这个字段赋值,那么这个属性就是枚举量的默认值,0,然后就会报错,不存在0的枚举量值。

  这样的类应该被分为多个,添加就是一个添加用的实体,每一个字段都是要赋值的,这样就不会给做添加功能的程序员带来麻烦,减少沟通的成本,加快开发的速度。

  实体类应该是专用的,每个实体类都有一个场景,都有自己的context,不应该混用。如果混用,就是实体耦合。这个耦合也是我们应该在最初的时候需要注意的。

  2、数据访问层耦合

  什么叫做数据访问层耦合呢?先让我们看一个例子。

  查询,根据界面上的条件查询申请信息。我们的用户有两类,一个是申请者,一个是审批者。申请者应该查询自己的申请,审批者则可以查询所有的申请。于是有了下面的方法。根据申请时间、申请类型、标题进行查询。

  

代码
using System;
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;
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;
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、系统划分

先按照功能模块或者是服务的对象主体来划分系统,划分为子系统。然后再每个子系统中分层,子系统之间的交互使用接口。子系统相关的后台代码独立,方便日后维护升级。