zoukankan      html  css  js  c++  java
  • 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 同一个类包含的对象之间不应当有使用关系。
        从复用性和复杂性角度考虑。

  • 相关阅读:
    数组和字符串//反转字符串
    数组和字符串//实现strStr()
    数组和字符串//二进制求和
    数组和字符串//加一
    数组和字符串//至少是其他数字两倍的最大数
    LeetCode111_求二叉树最小深度(二叉树问题)
    数据结构6.8_树的计数
    数据结构6.7_回溯法和树的遍历
    数据结构6.6_赫夫曼树及其应用
    数据结构6.5_树与等价问题
  • 原文地址:https://www.cnblogs.com/xyqCreator/p/2735670.html
Copyright © 2011-2022 走看看