OOD沉思录2 类和对象的关系--包含关系

4.5 如果类包含另一个类的对象,那么包含类应当向被包含的对象发送消息(调用方法)。
    也就是说,所有的包含关系都应当是使用关系
    如果不是这样,那么包含的类有什么用处呢?当然,面向过程的开发人员会想到可能有一个Get方法供其它类使用这个包含的对象,那么按照“数据隐藏原则”,为什么
不让使用包含类的类直接包含被包含的这个对象呢?包含一个对象一定是需要使用它才包含
    比如说汽车包含了发动机,如果违背这条原则的话则定义如下:

  1. class 汽车  
  2. {  
  3.    发动机 m_发动机;  
  4.    发动机 Get发动机(){return m_发动机;}  
  5. }  
  6. //对于使用驾驶员来说,汽车的操作如下:  
  7. 发动机 a=汽车A.Get发动机();  
  8. a.启动();  

    对驾驶员来说,就知道了“汽车里有发动机”的内部细节(),这肯定是不合适的。
    那么我们应当将发动机的启动操作由汽车类来调用,而不是驾驶员,那么定义如下:

  1. class 汽车  
  2. {  
  3.    发动机 m_发动机;  
  4.    启动()  
  5.    {             
  6.        m_发动机.启动();  
  7.    }  
  8. }  
  9. //对于使用驾驶员来说,汽车的操作如下:  
  10. 汽车A.启动();  


    这样对驾驶员来说,就不需要知道汽车细节了,也减少了与发动机的耦合关系。(默念一遍:低耦合,高内聚
    有一个特殊点的情况,对于容器类来说,它的责任就是提供对象给使用者,所以违背这个原则是正常的,其它情况请遵守这条原则。

4.6 尽量让类中定义的每个方法尽可能多地使用包含的对象(即数据成员)
    这其实就是高内聚的翻版强调。如果每个类的情况并非如此,那很可能是这一个类表示了两个或更多的概念,记住一个类只应该表示一个概念。
    最明显的情况就是类的一半方法使用了一半的数据成员,而另一半方法使用了另一半的数据成员,那么这个类就应该一分为二。
    我们假设一个澡堂,有VIP客户和普通客户,各自有不同的服务(普通客户享受中专生服务,VIP客户享受大学生服务),则定义如下:

  1. class 澡堂  
  2. {  
  3.      stack<中专生> 普通服务员;  
  4.      stack<大学生> VIP服务员;  
  5.        
  6.      普通接待()  
  7.      {  
  8.           普通服务员.Pop().侍候();                 
  9.      }  
  10.      普通结帐()  
  11.      {  
  12.           普通服务员.Pop().结帐();  
  13.      }  
  14.   
  15.      Vip接待()  
  16.      {  
  17.           VIP服务员.Pop().侍候();  
  18.      }  
  19.      VIP结帐()  
  20.      {  
  21.           VIP服务员.Pop().结帐();  
  22.      }  
  23. }  

    这是一个我经常看到的类似结构,这种结构不可避免地会在使用者的代码里进行很多条件判断,来确定到底调用那个版本的方法,而这种判断最好避免。
    原因在于这一个类包含了两个概念,一个是针对普通客户,一个是针对VIP客户,违反了本条原则,我们应当将其分开,这里可以考虑再次抽象

  1. class 澡堂  
  2. {          
  3.     abstract 接待();  
  4.     abstract 结帐();  
  5. }  
  6. class 普通澡堂:澡堂  
  7. {  
  8.      stack<中专生> 服务员;           
  9.      接待()  
  10.      {  
  11.           服务员.Pop().侍候();                 
  12.      }  
  13.      结帐()  
  14.      {  
  15.           服务员.Pop().结帐();  
  16.      }  
  17. }  
  18. class VIP澡堂:澡堂  
  19. {  
  20.      stack<大学生> 服务员;  
  21.   
  22.      Vip接待()  
  23.      {  
  24.           服务员.Pop().侍候();  
  25.      }  
  26.      VIP结帐()  
  27.      {  
  28.           服务员.Pop().结帐();  
  29.      }  
  30. }  

    这样这个类的使用者可以使用如下方法:

  1. 澡堂 tmp=null;  
  2. if(顾客 is 普通客户)  
  3.      tmp=new 普通澡堂();         
  4. if(顾客 is VIP客户)  
  5.      tmp=new VIP澡堂();  
  6. tmp.接待();  
  7. //......  
  8. tmp.结帐();  

    眼神好的可能马上就会提出,这里也进行了判断,但是这里的判断我们可以通过两种手段来处理
    一,字典
        在外部保存一个字典:Dictionary<顾客类型,澡堂> dic;
        那么上面的代码就成为下面这样:

  1. 澡堂 tmp=dic[顾客类型];  
  2. tmp.接待();  
  3. //......  
  4. tmp.结帐();  

    二,简单工厂
        实现一个简单工厂,澡堂Factory,则使用代码如下:

  1. 澡堂 tmp=澡堂Factory.Create(顾客类型);  
  2. tmp.接待();  
  3. //......  
  4. tmp.结帐(); 

     这两种方式都可以在程序配置的时候进行调整,将类型的依赖延迟到配置细节中(而这正是类型注入的要旨,别被那些专有的很玄的框架名次忽悠,其实就是这么简单)。

4.7 类包含的对象数目不应当超过开发者短期记忆数量,这个数目通常应该是6左右

4.8 让系统在窄而深的包含体系中垂直分布
    假设有如下两份菜单:
    正餐--->甜瓜
        --->牛排
        --->土豆
        --->豌豆
        --->玉米
        --->馅饼
    或者
    正餐--->甜瓜
        --->牛排套餐--->牛排
                    --->配菜--->豌豆
                            --->土豆
                            --->玉米
        --->馅饼
    对使用者来说,哪种更科学呢?
    回答1或者回答2都是错的,面向对象的使用者从不关心菜单的具体实现,只关心其公共接口(价格,份量,味道等)
    那么对于实现者来说,哪种更科学呢?
    面向过程的程序员可能会选择1,因为他不希望计算正餐价格的时候出现: 价格= ...+正餐.牛排套餐.配菜.豌豆.Get价格()+正餐.牛排套餐.配菜.土豆.Get价格()+...
    而更喜欢:价格=甜瓜.Get价格()+牛排.Get价格()+...+馅饼.Get价格().
    但是在面向对象的世界里,并不存在前者担忧的状况,出现他们所担忧的状况的原因只有一个原因,就是违反了
      经验原则5,参考http://blog.csdn.net/heguodong/article/details/7375974
    其实这里,模式大师已经作出了最完美的解决方案,那就是组合模式.
    考虑我们现在讨论的问题都是关于的菜单上的菜肴,那么我们可以定义一个抽象的菜肴类,其中只关心价格属性

  1. class 菜肴  
  2. {  
  3.      abstract double Get价格();  
  4.      virtual void Add(菜肴 para){}  
  5. }  

    那么我们可以按照套餐的定义进行各个菜肴的定义

  1. class 甜瓜:菜肴  
  2.     {  
  3.           int count;//甜瓜是以个为单位计价  
  4.           readonly int 单价=10;//假设单价为常数  
  5.           double Get价格(){return 单价*count;}  
  6.     }  
  7.     class 牛排:菜肴  
  8.     {  
  9.           double weight;//牛排是以重量为单位计价  
  10.           readonly int 单价=20;//假设单价为常数  
  11.           double Get价格(){ return 单价*weight; }  
  12.     }  
  13.     class 豌豆:菜肴  
  14.     {  
  15.           double Get价格(){ return 5; }//豌豆包吃饱,5块钱  
  16.     }  
  17.     class 土豆:菜肴  
  18.     {  
  19.           double Get价格(){ return 5; }//土豆包吃饱,5块钱  
  20.     }  
  21.     class 玉米:菜肴  
  22.     {  
  23.           double Get价格(){ return 5; }//玉米包吃饱,5块钱  
  24.     }  
  25.     class 馅饼:菜肴  
  26.     {  
  27.           double piece;//馅饼按块计价  
  28.           readonly int 单价=5;//假设单价为常数  
  29.           double Get价格(){ return 单价*piece; }  
  30.     }  
  31.       

        那么配菜,牛排套餐,正餐的概念呢?他们是由多份菜肴组合起来的复合体,专门针对计算价格来说,并不需要区分他们的区别,所以不需要针对每项建立一个类模型,我们只
定义一个组合菜肴类就可以满足需求:

  1. class 组合菜肴:菜肴  
  2. {  
  3.      list<菜肴> lst;  
  4.      double Get价格()  
  5.      {  
  6.          double sum=0;  
  7.          foreach(菜肴 enu in lst)  
  8.              sum+=enu.Get价格();  
  9.          return sum;    
  10.      }  
  11.      override void Add(菜肴 para)  
  12.      {  
  13.           lst.Add(para);  
  14.      }  
  15. }

    那么我们可以通过外部配置的方式建立 配菜,牛排套餐,正餐 的概念,即

  1. 组合菜肴 正餐=new 组合菜肴();  
  2. 正餐.Add(new 甜瓜);  
  3. 正餐.Add(new 馅饼);  
  4.   
  5.      组合菜肴 牛排套餐=new 组合菜肴();  
  6.      牛排套餐.Add(new 牛排);  
  7.                 
  8.               组合菜肴 配菜=new 组合菜肴();  
  9.                        配菜.Add(new 豌豆);  
  10.                        配菜.Add(new 土豆);  
  11.                        配菜.Add(new 玉米);  
  12.   
  13.      牛排套餐.Add(配菜);  
  14.  正餐.Add(牛排套餐);      

     顾客使用完正餐后结帐的调用很简单:

  1. 正餐.Get价格();  

          这里从头到尾都没有出现 正餐.牛排套餐.配菜.豌豆.Get价格() 形式的调用,而且将菜肴的组合需求放到了最后配置时,我们可以使用更灵活的方式配置各种套餐。
     在这里,生成组合的代码就非常灵活了,工厂模式,生成器模式等等都可以根据你的需要进行套用了

 

4.9 在实现语义约束时,最好根据类定义来实现。但是这经常会导致泛滥成灾的类,在这种情况下约束应当在类的行为中实现,通常在类的构造函数中实现,但不是必须如此。
    还是以汽车为例,我们看汽车的定义,为了集中注意力,先只关心汽车的发动机

 

  1. class 汽车  
  2. {  
  3.     汽车(发动机 para)  
  4.     {  
  5.         m_发动机=para;  
  6.     }  
  7.     发动机 m_发动机;  
  8. }  
  9. class 发动机{...}  

    我们可以定义奥迪A6,凯梅瑞等等汽车

 

  1. class 奥迪A6:汽车{......}  
  2. class 凯梅瑞:汽车{......}  



    同样我们可以定义丰田发动机,三菱发动机等等

 

  1. class 丰田发动机:发动机{......}  
  2. class 三菱发动机:发动机{......}  

    问题,假设奥迪A6只能使用丰田发动机,凯梅瑞只能使用三菱发动机,问题是汽车只包含了抽象的发动机,对抽象的汽车类来说,所有发动机没有任何区别,那么需要我们把这个
约束条件放到合适的位置。
    首先我们考虑在构造函数中加以约束,那么奥迪A6,凯梅瑞汽车的构造函数就分别如下:

 

  1. 奥迪A6(丰田发动机 para):base(para){}  
  2. 凯梅瑞(三菱发动机 para):base(para){}  

    这种方式很可靠,能保证系统里不会出现状态非法的汽车(奥迪A6使用了三菱发动机,就是非法状态)
    这种方式的问题在于,如果汽车和发动机的种类繁多,会导致出现泛滥成灾的类。
    第二种方式,我们不保证汽车的状态合法,而是在汽车的行为里检查,状态是不是合法。
    我们看看汽车的启动方法,可以如下定义

 

  1. class 汽车  
  2. {  
  3.      。。。。。。  
  4.      void 启动()  
  5.      {  
  6.           //先检查汽车的状态,如果状态不合法,则告诉用户汽车无法启动,因为什么原因;如果合法,则开启发动机  
  7.           if(检查状态())  
  8.             m_发动机.启动();  
  9.           else  
  10.             throw "汽车使用了不配套的发动机!";                
  11.      }  
  12. }  

    检查状态()这个该怎么实现呢?答案是必须有一个地方存储了汽车可以使用的发动机列表(当然也可以是发动机能匹配的汽车),我们可以实现一个字典,来保存
这个信息,然后根据这个字典来检查汽车状态。
    这个字典的样子和下面差不多
    Key                     Value
    奥迪A6                  丰田发动机
    凯梅瑞                  三菱发动机
    。。。。。。            。。。。。。
   

4.10 在类的构造函数中实现语义约束时,把约束测试放到构造函数领域所允许的尽量深的包含层次中。
    在构造函数中实现约束时,应该遵循这条原则,当然我还没想到如果没有遵循这条原则,构造函数会是什么样子?
    在汽车类里怎么实现这个约束?汽车类并不知道有奥迪A6或者凯梅瑞,这是继承的一个原则(派生类知道基类,但是基类不应该知道派生类),我能想到的实现方式就是如下

 

  1. class 汽车  
  2. {  
  3.      汽车(发动机 para)  
  4.      {  
  5.            if(!检查状态(para))//这个检查状态的实现和上面类似,从一个字典里查询  
  6.                throw new "发动机不匹配";                
  7.      }  
  8. }  



    这种方式确实不会产生非法状态的汽车,但是对使用者来说很不友好。

4.11 约束所依赖的语义信息如果经常改变,那么最好放在一个集中式的第三方对象中。
    4.9里的字典内容如果经常变化,那么最好存放到外部文件里,否则可以作为汽车类的静态属性来实现。

4.12 约束所依赖的语义信息如果很少改变,那么最好分布在约束所涉及的各个类中。

     类包含的数据成员分为两种,一种是描述性的,如长宽高颜色等,另一种是具有行为的子对象,如汽车包含方向盘,而方向盘本身就是一个具有"有意义的行为"的对象。
     前者我们不去仔细讨论,只讨论后者。

4.9 类必须知道它包含什么,但是不能知道谁包含它。
    如汽车必须知道包含了方向盘(否则汽车的左转,右转行为都无法实现),但是方向盘不能知道包含它的汽车,否则方向盘就无法重用到其它的汽车上。

4.10 同一个类包含的对象之间不应当有使用关系。
    从复用性和复杂性角度考虑。

posted @ 2012-10-23 15:57  怡馨  阅读(665)  评论(0编辑  收藏  举报