现在的位置: 首页 > 综合 > 正文

一次有意义的面向对象设计尝试

2012年12月17日 ⁄ 综合 ⁄ 共 5971字 ⁄ 字号 评论关闭
前言
  上一周由于工作的需要,我尝试运用面向对象的设计思想来解决实际工作中遇到的一个设计问题。整个设计过程主要涉及到C++语言,OO编程思想,设计模式这三个方面的知识,是对我软件设计能力的一次综合考验。虽然最后由于种种原因,我的设计方案并没有获得采纳,但是这个并不重要,重要的是在整个设计过程中我自己思考过,并提出了我的方案,也就是说,我在这次设计过程中学到了很多。我决定把这次设计记录下来,一方面是为自己留个”纪念",更重要的是希望能够和更多的人在软件设计方面进行深入的探讨和交流。

介绍
  首先,我就简单介绍一下我所遇到的问题。由于商业机密的关系,这里讨论的问题经过我的"加工",但是问题的本质并没有变,并不影响我们对问题的分析:
  "我可能会面对很多的Symbol,每个Symbol都会或多或少有用一些Property,不同的属性具有不同的显示方式和修  改方式,我们需要做的就是为每个Symbol提供显示和修改roperty的能力,同时要处理同一个Symbol下的Property 之间的交互。“
对于这个问题,我感觉我所面临的最大的两个挑战是:

  • 由于每个Property可能拥有独有的需求,可能需要为每个Property抽象成一个类,当然这些Property类可以从共有的一个基类派生出来。但是由于Property的数目可能比较多,这样我们就需要实现大量的类。我总觉得这个并不是一个好的"兆头"。
  • 如果实现同一Symbol下不同Property之间的交互也是一个重点考虑的问题。

经过一段时间的思考,我的头脑中就慢慢浮现出一个大概的框架,我努力尝试使用我所学到的相关知识去"细化"这个框架,慢慢的我的思路开始清晰起来。

面向接口编程(Interface Oriented Programming)
  面向接口编程的思想已经得到了广泛的应用,在COM技术,Java和C#语言中已经大放异彩。它的优势非常明显,极大降低了类与类之间,模块和模块之间的依赖程度,为"无缝"和"无限"的扩展提供了可能。具体到本文讨论的问题,我抽象出以下个接口:

  • IProperty。这个接口从具体的Propety中抽象出来。这个接口声明了一些方法用来描述Property应该具有的功能。

      interface IProperty {
          
    virtual void GetPropertyValue() = 0;
          
    virtual void SetPropertyValue() = 0;
          
          
    virtual void Connect(IPropNotify*= 0;
          
    virtual void Disconnect() = 0;
      }

  • IPropertyMgr。我抽象出一个Property管理类(Manager class),用来管理属于某个Symbol所有的Property。
      interface IPropertyMgr {
          
    virtual void AddProperty(IProperty*= 0;
          
    virtual void RemoveProperty(IProperty*= 0;
          
    virtual void GetProperty(long,IProperty**= 0;
          
    virtual long GetPropertyCount() = 0;
      }

  • IPropNotify。我抽象出一个事件通知类(Event Notify class)。当Property发生变化时,Property可以向其他的对象发出消息通知他们。
      interface IPropNotify {
          
    virtual void OnChanged(IProperty*= 0;
      }

事件处理(Event Handling)
  由前面的分析我们知道,我们需要一个"角色"来响应Property变化,那具体是谁呢?这里涉及到面向对象设计中的一个重要问题,就是确定对象的"职责"的问题。一个对象最好是一个相对独立的实体,它只需要关系属于他自己的职责,而它和其他对象之间的交流必须通过一些良好的"渠道"来完成,并且这样的"渠道"越少越好。这样的设计就保证对象之间的依赖最少,并且得到有效的控制。在这次讨论的问题中,我们应该努力使Property成为一个独立的实体,它没有权利也没有能力去通知其他Property来完成交互,而最有理由来处理Property之间交互的应该是熟悉这些Property的"角色",这样很自然的就会想到那个Property管理类。为了让Property管理类能够捕捉这个事件,就需要让它继承IPropNotify接口:

    interface ISymPropMgr : public IPropertyMgr, public IPropNotify {
    
public:
      
virtual void AddProperty(IProperty*);
      
virtual void RemoveProperty(IProperty*);
      
virtual void GetProperty(long,IProperty**);
      
virtual long GetPropertyCount();
      
      
virtual void OnChanged(IProperty*= 0;
   
private:
      std::vector
<IProperty*> m_props;
    }


(注:事后想想这里实际上应用的是"观察者(Observer)模式”,其中Property扮演的是"Abstract Subject"的角色,而Symbol Manager扮演的是"Abstract Observer"的角色。)

对象标识(Object Identify)
  有了前面的接口类,接下来我们就需要从这些接口类派生出具有一个意义的类。首先是属性类:

    class CSymProperty : public IProperty {
    
public:
      
virtual void GetPropertyValue();
      
virtual void SetPropertyValue();
      
      
virtual void Connect(IPropNotify*);
      
virtual void Disconnect();
   
private:
      IPropNotify
* m_propNotify;
    }

前面我们也讨论过,给每一个具体的Property派生出一个Property类并不是一个很好的主意,这里我想用一个Property类来抽象所有的具体Property。这样就带来一个新的问题:对于每个Property类的对象我们如何判断这个对象代表的是那一个具体的Property?这样我们需要添加一些成员变量来标识具体的Property并提供接口来初始化类对象,这里我就选用最简单的Property ID来标识具体的属性:

    class CSymProperty : public IProperty {
    
public:
        
void SetPropertyID(UINT id);
    
private:
        UINT m_propID;
    }

委托(Delegate)
  由于我们使用一个Property类抽象许多不同的具体Property,当我们在现实接口的方法的时候,我们会遇到以下两种可能:

  • 对于所有的具体Property,某个方法的实现是一样的。对于这样的方法,我们可以直接实现。例如IPropNotify的Connect和方法:

        void CSymProperty::Connect(IPropNotify* pNotify) {
            
    if (pNotify) {
                m_propNotify 
    = pNotify;
            }
        }
        
    void CSymProperty::Disconnect() {
            
    if (m_propNotify) {
                m_propNotify
    ->Release();
                m_propNotify 
    = NULL;
            }
        }

  • 对于所有的具体Property, 某个方式的实现是不一样的,实现的细节依赖于具体的Property。这样的方法实现起来就相对来说比较麻烦,在具体的实现中,我们需要根据不用的Property来做出相应的处理。可以想象在具体的实现代码中,可能会存在庞大的"if..else if.."语句或者是"switch..case.."语句。这里我嗅到了"坏代码"的味道,应该采取措施优化一下这里的设计。这里我就想到了,为什么不把不同的实现细节提取出来抽象成一个概念:
        interface IProperyHelper {
                
    virtual void GetPropertyValue(IProperty*);
                
    virtual void SetPropertyValue(Property*);
        }

  • 有了这个实现类的概念后,我们就可以根据不同的实现细节,派生出对应的具体实现类。这样对于Property类的某些方法的实现,就可以转到具体实现类的对象来完成:

        class CSymProperty : public IProperty {
        
    public:
            
    void SetImpl(IPropertyHelper*);
        
    private:
            IPropertyHelper
    * m_propHelper;
        }
        
    void CSymProperty::SetImpl(IPropertyHelper* pHelper) {
            
    if (pHelper) {
                m_propHelper 
    = pHelper;
            }
        }
        
    void CSymProperty::GetPropertyValue {
            
    if (m_propHelper) {
                m_propHelper
    ->GetPropertyValue(static_cast<IProperty*>(this));
            }
        }

  • 可以看得出来,CSymProperty保持了绝对的稳定,我们就可以从维护CSymProperty类的繁琐中解脱出来,转而将精力放在具体的实现类上,这一点对代码的维护和扩展非常重要。
  • (注:事后查阅一些相关的资料,发现这样的设计思路实际上就是"委托(Delegate)"的概念。)

组合和聚合(Containment/Aggregate)
  从设计思想的角度来看,CSymProperty类将具体的实现细节委托给实现类来完成;从实现的角度来看,CSymProperty类包含一个指向实现 类对象的指针。对于包含关系,如果再仔细区分的话,又可以划分成两个更具体的关系:"组合"和"聚合"。对于"组合"和"聚合"之间的区别和判断,如果站 在不用的角度去思考这个问题,就很可能会得到不同的判别方法,所以很多书中并没有给出一个确定的定义。我个人觉得从"生命周期"的角度可能会好一点:如果 两个对象同时创建,同时销毁,他们具有相同的生命周期,则他们的关系应该是"组合";反之则是"聚合"。本文中的Property类和实现类,并不需要为 每一个Property对象都创建一个实现类对象,具有相同实现的Property对象完全可以共享一个实现类对象,所以Property类和实现类应该 是"聚合"的关系。

引用计数(Reference Count)
  从前面的分析我们已经知道Property类对象和实现类对象之间是一种"聚合" 的关系,多个Property对象可以共享一个实现类对象。由于实现类对象的生命周期不受Property类对象的控制,那么我们该如何销毁实现类对象呢?这里我采用的引用计数的方法,并使用COM中采用的引用计数策略:

  • 当实现类对象创建的时候,初始化引用计数为0;
  • 任何对实现类对象的引用都会使引用计数加1;
  • 任何失去对实现类对象的引用都会使引用计数减1,当引用计数减到0时,实现类对象删除自己(自杀)。

由于Property类包含了一个指向实现类对象的指针,也就是说它保存了一次对实现类对象的引用,所以Property类有责任正确维护引用计数:

    void CSymProperty::SetImpl(IPropertyHelper* pHelper) {
        
if (pHelper) {
            m_propHelper 
= pHelper;
            m_propHelper
->AddRef();
        }
    }   
    CSymProperty::
~CSymProperty() {
        
if (m_propHelper) {
            m_propHelper
->Release();
        }
    }

在CSymProperty类初始化的时候增加实现类对象的引用计数,在析构的时候减小实现类对象的引用计数。

管理者(Manager Class)
  剩下的部分就是管理者类。设计一个管理者类的思路应该是一个很自然的想法,特别是考虑到对象职责的情况下。管理者类使纷繁的不同对象之间的"纠葛"变得容易控制和扩展。在这次讨论的问题中,具体的管理者类按照不同的Symbol进行派生:

    CSym1PropMgr : public ISymPropMgr;
    CSym2PropMgr : 
public ISymPropMgr;

(注:仔细想想,这里实际上应用的是"Mediator"模式。)

总结
  从本次设计过程我们可以总结出一些面向对象设计的技巧和原则:

  • 注意对象的职责。对象的职责最好是单一和相对独立的。
  • 将变化的部分提取出来抽象成类。
  • 优先使用组合而不是继承。
  • 使用引用计数关系对象的销毁。
  • 使用管理者类管理不同对象之间的交互。

历史记录
07/21/2007   v1.0
原文的第一版
07/28/2007   v1.1
原文的第二版,添加了组合/聚合以及引用计数的内容。

抱歉!评论已关闭.