耦合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、系统划分

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

代码之前的工作比较多,也比较广,需要的不仅仅是开发技能,还包括行业知识,分析能力,设计能力。以及更深远的商业预见能力,预见系统之上的商业前景,是否值得我们开发这么一个系统,以及系统的大方向,服务的客户群,需要达到的目标,盈利的点。

  一旦上面的商业目标确定之后,就需要行业专家以及架构师的介入。这两者会配合工作,行业专家的主要职责是构建满足商业目标的业务模型,将商业目标分解为很多小的业务模型,梳理其中的业务流程。

  架构师的主要职责是建立业务模型对应的软、硬件系统模型,验证业务模型的可行性,确定业务流程可以用系统来表达。其中哪些可以用系统实现,哪些需要人工辅助来实现,又或者哪些可能不合理,需要修改业务模型或者流程。

  同时架构师还需要根据现有人员的知识结构进行技术选型。包括:使用那种平台?哪种语言?什么工具?模块如何划分?如何分层?层或者是模块之间如何交互?交互的标准是什么?需要那些基础类库?这些前期的工作,可以由架构师团队来做,或者是在架构师的指导下由高级程序员完成。

  这时候开发人员也不是没有事情可做,应该在高级程序员的带领下,熟悉架构师确定的技术平台,以及语言特性。或者是进行一些技术的探索,对后面肯定会用到的技术难点进行一些积累,事先解决一些可能遇到的技术问题,做一些技术积累。也可以叫做热身。

  架构师将系统划分为很多的子系统,做一些横向的划分。确定子系统之间的交互格式。

  之后架构师会将架构的结果,技术选型方案交给CTO或者是高层去确定一下,确定使用哪种技术。CTO和高层根据成本来最终决定使用哪种技术方案。不过一般架构师给出的技术方案都会被通过,因为也是根据公司目前的技术掌握情况制定的方案,所以通过的可能性还是很大的。

  待技术方案确定之后,架构师会将包括:系统架构图,划分好的子系统,确定好的子系统交互方式,基础类库这些东西交给不同的项目组。由项目组的leader带领项目组的高级程序员对于分配的模块进行分析,提出自己的异议。一般来说没有太大的异议,主要是因为拿到的模块已经分析的比较透彻了。一般就是一些理解上面的异议,也就是概念问题,理解概念有助于加快开发的进度,和进行开发进度的估算。

  估算进度之后,上报项目经理开发进度,项目经理根据上报情况,衡量一些其他因素,经过几次的反复,最终确定一个开发的进度安排。

  leader会根据人员和任务的情况,将任务分解为小任务,分配到合适的开发者手中。然后大家开始做详细设计,一边理解业务和模型,一边进行详细设计。整理用例,写用例的实现,也就是详细设计。leader会和高级程序员审核设计是否合理,以及用到技术是否合理。等这些都确定之后,形成文档之后,才进行开发。

  当然了,如果应用TDD的话。就需要先写一些测试用例,还要进行测试用例的设计。

  今天说到这里,不知道大家有什么意见,欢迎大家一起讨论。

  

【Blog】http://virusswb.cnblogs.com/

【MSN】jorden008@hotmail.com

【说明】转载请标明出处,谢谢

原文地址:https://www.cnblogs.com/Leo_wl/p/1801864.html