什么是GRASP

  通用责任分配软件模式GRASP为英文General Responsibility Assessment Software Patterns各单词首字母的组合,分别解释为:

    General:通用的、抽象的、广泛应用的;

    Responsibility:责任、义务、职责;

    Assessment:分配责任于一个模块或类;

    Software:计算机代码、软件;

    Patterns:规律、模板、抽象或模式;

  GRASP对于面向对象系统分析和设计有着重大的指导意义,能让我们做好对象责任分配。要做好对象责任分配,首先,我们要明确对象责任的含义。

  责任是类间的一种合约或是义务。责任可以包括行为(方法)、数据、对象创建等。它们可以细分为两部分:知道责任——表示知道什么;行为责任——表示做什么。

  所以,得出的公式是:责任 = 知道责任 + 行为责任

  知道责任包括:

    (1) 了解私有的封装数据;

    (2) 了解相关联的对象;

    (3) 了解能够派生或者计算的事务;

  行为责任包括:

    (1) 自己执行一些行为,如创建一个对象或进行一次计算等;

    (2) 在其他对象中的初始化操作;

    (3) 在其他对象中控制或协调各项活动。

  面向对象设计过程就是将责任分配给对象的过程,注意,责任不是类的方法,类的方法是用来实现责任的。责任的分配可以反应在协作图或者顺序图中。

  例如在销售业务中,存在一个交费行为,它属于一个责任。它的行为责任表示了交费的行为,它需要创建一个付费记录的对象。它的知道责任必须知道付款记录类Payment,知道如何记录及计算Payment类中的数据。而具有这个责任的对象,应该是销售类Saled,如图所示:

  

ResponseEntity 支持中文 responsibility的中文_数据库

Information Expert (信息专家)

  信息专家模式是面向对象设计的最基本的原则。就是说,我们在设计对象(类)时,如果某个类能够在某方面具有完整的信息,足以实现某个责任,就将这个责任分配给这个类,这个即所谓的信息专家。

  在购物车系统的例子中,我们要让每个商品(Item)在购物车内只出现一次,如果相同的商品加入车中,我们只需要增加商品(Item)的数量,在原数量基础上加1,而不是在商品列(ItemArray)增加一个相同的商品(Item),如图:

  

ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_02

  如果是这样,程序就要检测两个商品是不是一样,问题是哪个类应该具有“检测”这个责任?根据业务规则,两个商品是依赖商品编码来标识的,商品编码在建模中描述为ProductID。哪个类会具有ProductID?这里当然是Item类。

  根据信息专家原则,Item类应该包含检测责任。所以,我们在Item类中重写了Equals方法,用于检测两个商品是否是同一商品。代码如下:

  




ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_03

ResponseEntity 支持中文 responsibility的中文_数据库_04

代码

public 
      
    class 
     Item : IFormattable
 { 
     // 
    重新Equal方法 
    
       
    public 
      
    override 
      
    bool 
     Equals( 
    object 
     obj)
 {
     if 
     (obj  
    == 
      
    null 
    )
     return 
      
    false 
    ;
     // 
    引用同一个对象返回真 
    
       
    if 
     (Object.ReferenceEquals( 
    this 
    , obj))
     return 
      
    true 
    ;
     // 
    不同对象类型,返回假 
    
       
    if 
     ( 
    this 
    .GetType()  
    != 
     obj.GetType())
     return 
      
    false 
    ;
 Item objItem      = 
     (Item)obj;
     // 
    对象产品ID相同,返回真 
    
       
    if 
     (_productID  
    == 
     objItem._productID)
     return 
      
    true 
    ;
     return 
      
    false 
    ;
 }
 }


  对于ShoppingCar类,它具有将相同商品的数量加1的责任,所以应该具有检测商品是否存在的责任。它可以在增加商品的方法AddItem中调用ArrayList的IndexOf方法来检测。代码如下:

  


ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_03

ResponseEntity 支持中文 responsibility的中文_数据库_04

代码

public       
    class 
     ArrayListCart : ShoppingCart {     private      ArrayList _item;     public       
    override 
      
    void 
     AddItme(Item item) {     //     取列表相同的商品 
               int 
     index  
    = 
     _item.IndexOf(item);     //     存在商品判断 
               if 
     (index  
    >= 
      
    0 
    ) UpdateItem(item);     else      _item.Add(item); } }


 

Creator (创造者)

  应用情况符合以下条件之一,类A应该具有创建类B的责任:

  (1) A是B的聚合。

  (2) A是B的容器。

  (3) A有初始化B的数据。

  (4) A记录B的实例。

  (5) A频繁使用B。

  当一个类有责任去创建其他类的实例时,这两个类就连接(又称为耦合)起来了。连接本身没有错,但我们需要出去某些不好的链接。类间耦合用于测量一个类对另一个类的依赖程度大小,当两个类耦合,就会有一个类需要另一个类才能正确的工作。在程序设计中,以creator(创造者)模式为原则,凡不符合以上条件的应用,不要设计类的耦合。

  

ResponseEntity 支持中文 responsibility的中文_测试_07

Low Coupling (低耦合)

  低耦合是指我们的设计有责任减少(降低)类间的连接。低耦合的作用很重要:

  (1) 低耦合使得一个类的修改对其他类的影响范围有所降低。

  (2) 低耦合使得系统变得更容易维护。

  (3) 低耦合使得类更容易理解,因为类会变得简单、专业、高内聚。

  下列情况让A,B两个类产生耦合:

  (1) A具有一个B的属性。

  (2) A调用B对象的方法。

  (3) A的方法包括对B的作用,例如返回的是B类型或参数是B类型

  (4) A是B的子类,或者A是B的实现类。

  不对陌生人说话(Don't Talk To Strangers)原则:

  不要相连不需要通信的两个对象,不要作无为的耦合。

  拇指规则二条:

  (1) 如果A已经与B有连接,如分配责任A给B不适合(违反信息专家模式),那么分配责任B给A。

  (2) 两个模块中的内部类间连接是一个大错误。

  例如上面Creator(创造者)模式的例子——出货单(Invoice)与物品(InvoiceItem),需要计算整个出货单的价格(总价),业务规则是由出货人清点计算出货单,但由于出货单与物品的的耦合已经存在,所以应分配这个责任给出货单类Invoice,以此减少类间的连接。在Invoice类中增加totalPrice方法,用于计算出货单的总价。

  

ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_08

High Cohesion (高内聚)

  高内聚是指分配职责时使内聚保持为最高,目的是提高设计类的重用性,并且控制类设计的复杂程度。容易理解的解释是:高内聚是指,我们要努力分解类,使得分解出来的类具有独立的责任。

  非常低的内聚是指,一个类单独处理很多不同模块的事务。如果有一个类,它既处理数据的存储功能,又处理用户接口操作或图形处理,这种跨模块的设计就是非常低的内聚。

  比较低的内聚是指,单独处理一个模块的所有事务,它却具有太多的责任,也就是说类中有太多的属性及方法。比如在数据存取系统中,不分SQL及Oracle,或文件存取,全由一个类完成,要实现高内聚就应该分解这个类,使之变成更多轻量级的类。

  高内聚腔调的宗旨是,类只处理与模块相关的功能,它与其他类合作共同完成任务。

  低内聚的缺点如下:

   (1) 很难被理解和维护,这种类可能包括成千上万行代码,近百个方法或者属性。

   (2) 难于实现类的重用。

   (3) 系统变得脆弱,不断地需要修改。

  当你准备分配新的责任时,先问问自己类中的其他功能是不是与这个责任有一定的关联?如果不是,最好把这个责任分配给别的类。如果新增加的责任与别的类也没有一定的相关性,你可以新建一个类。例如有一个订货类,现在要增加用于支持存取数据到Excel表中以支持不同数据来源的责任。这个责任应该假如Order类中吗?订货的详情与数据存取是不是一个概念呢?当然不是,它们甚至不相近,所以我们应该新建一个类OrderDAO来处理数据存取。如下图:

  

ResponseEntity 支持中文 responsibility的中文_测试_09

  高内聚的优点由上的例子就可以体现出来:

  (1) 高内聚可以表现关联责任的一个抽象,易于实现类的重用。上面的类设计可扩展为如下设计:由OracleDAO抽线出Save()及Restore()方法,再由子类OrderDAOExcel实现存取Excel数据的功能,由类OrderDAOSQL实现SQL数据库存取功能,如图:

  

ResponseEntity 支持中文 responsibility的中文_数据库_10

  (2) 高内聚使得维护工作变得简单。假设存取Excel表出错,问题当然是在OrderDAOExcel类中。

  (3) 高内聚使得系统模块化,支持团队工作。可以由一人实现Excel存取,另一个实现SQL数据库存取。

Controller (控制器)

  人们通常将接收和处理系统事件的职责分配给以下类:

  (1) 能全面代表系统、设备或者子系统的类。

  (2) 可以代表系统事件发生时用例发生情景的类。

  (3) 代表某些卷入真实世界应用中的活动的类(例如人物角色控制器类)。

  上面这些类就是控制器类。不要将这些责任分配给用户界面类(如窗口window、对话框dialog、页面page等) ,因为控制器类不会是用户界面类。GRASP有以下共识:

  (1) 系统事件的接收与处理通常由一个高级类来代替。

  (2) 一个子系统会有很多控制器类,分别处理不同的事务。

  例如,在银行系统中,我们会有一个控制器类用于处理所有银行事务,即类TransactionController

  

ResponseEntity 支持中文 responsibility的中文_游戏_11

  当出纳点击用户界面中用于执行支付账单的按钮时,这个事务控制类TransactionController就会取得并且处理这个事件。大家常说的MVC三层架构强调了控制模式的应用。当然,在一些特殊情况下,例如,一个用户界面产生了其他用户界面的事件,那么这个用户界面也会有些控制的逻辑,但最终还是交由控制器类来处理。

  如果系统只设计一个控制器类并由一个视图(View)类来处理系统事务,会违反高内聚的原则,是不好的设计。

Polymorphism (多态)

  当相关的行为只是由于种类不同时,可以分配相关的责任给指定的种类。多态允许你设计出组件插入式的系统。下面考虑一个绘画系统,它要画各种形状,如图:

  

ResponseEntity 支持中文 responsibility的中文_数据库_12

  首先定义一个抽象形状类shape,矩形、圆形、椭圆形都重写shape类的draw方法来画出自己。使用这种多态机制,绘画的责任就分配给了指定的类。这样,新的形状加入系统后,也不会对其他类有任何影响。多态可以使设计的内聚(cohesion)程序提高。

  多态的这种特性已经荣誉了面向对象设计,成为面向对象的三大特征之一。面向对象的三大特征是:多态、继承、封装。

Pure Fabrication (纯虚构)

  实现高内聚和低耦合都是系统设计的目标,也是软件设计人员的责任。但是高内聚与低耦合是互相矛盾的,因为高内聚意味类的数量增多,对象间要合作完成任务,它们之间的链接就要增加,使得耦合提高。要解决这个矛盾,我们可以应用纯虚构模式,而应用这种模式又增加了高内聚的特征。如上节的绘画例子,如果我们的系统要考虑在不同的系统,如Linux及Windows下绘画,该如何设计?如何使我们设计的类因系统的不同而独立?我们应该设计一个高层次的类,用于分配高内聚的责任,如图:

  

ResponseEntity 支持中文 responsibility的中文_数据库_13

  上面的entity类就是纯虚构的设计,无论哪一个系统(Windows或Linux),都需要实体来完成绘画。所以纯虚构来表现为一种概念。又例如系统要对数据进行存取,我们会设计另一种纯虚构类,它包括增加数据及更新数据的方法,如图:

  

ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_14

  至于实质性的数据以何种形式存取,可以通过用子类继承persistentStorage来实现。GoF的抽象工厂模式就是GRASP纯虚构模式的一种实际体现。

Indirection (间接)

  要避免对象间直接耦合,可以将协调组件或服务的责任分配给中间对象,这个中间对象称为间接或中介对象。我们在前面提过,直接耦合两个子系统是错误的,会引起维护困难,间接模式就是一个解决方案。另外,引入中间对象可以使得两个子系统的重用性得到提高。例如人事系统中岗位及员工之间的关系,员工是分配在岗位上,它们有连接。但是,潜在的,员工及岗位都是独立的,因此我们需要设计一个中间类——分配,用于连接员工及岗位,如图:

  

ResponseEntity 支持中文 responsibility的中文_ResponseEntity 支持中文_15

  当两个子系统不能独立应用时,我们可使用抽象概念来耦合,就是采用抽象类来连接相关的抽象类,那么它们的子类都可以独立应用而不用考虑这种连接。

  GoF的Facade模式也是间接(Indirection)的例子。Facade避免了两个子系统间直接相连,各子系统的类只直接与Facade类相连,使得某个子系统类的改变不会影响其他子系统的类。另外,GoF中介模式也是间接的一种表现。

Protected Variations (受保护变化)

  找出预计有变化或不稳定的点,我们有责任为这些变化的点创建稳定的接口。例如,游戏开发商致力于开发可通用的游戏引擎,它是个外层包装模块,开发商使用相同的游戏引擎,可以开发出不同的游戏。这种类型包装实现的细节,而支持不同客户的调用。

  受保护变化也可理解为开闭原则(the Open Closed Principle,OCP),意思是说,一个软件实体应当对扩展开放,对修改关闭。换句话说就是,我们在设计一个模块的时候,应当使这个模块在不被修改的前提下可以被扩展。

  这种好处是通过扩展已有的软件系统,可以提供新的行为,以满足软件的新需求,使变化中的软件系统有一定的适应性和灵活性。已有的软件模块,特别是最重要的抽象层模块,不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。

  受保护变化的一个很普遍的例子是GoF的适配器(Adapter)模式。采用OpenGL接口开发的游戏,同时也需要用到Direct3D,这样,就要应用一个OpenGL到Direct3D的适配器,如图:

  

ResponseEntity 支持中文 responsibility的中文_测试_16

  其他受保护变化的例子还有JDBC和ODBC,它们是链接数据库的公共接口,允许用户书写运行于来自不同厂商的数据库服务器上的应用。同一应用用来访问不同数据库的接口是一样的,这是因为实现了受保护变化的模式。