以上层次符合一般面向对象设计的规则,其中CContainer表示控件容器类的基类。
那么如何根据XML创建这些对象呢?
我们想到了抽象工厂(Abstract Factory), 定义如下:
Class CControlFactory
{
Public:
CButton* CreateButton() { return new CButton;}
CLabel* CreateLabel() { return new CLabel;}
CPanel* CreatePanel() { return new CPanel;}
}
显然,以上设计每次加入一个新控件都要重新加一个Create方法,不符合面向对象设计的开放封闭原则(OCP,Open Closed Principle), 因此我们把它改成下面的接口:
Class CControlFactory
{
Public:
CControlBase* CreateControl(const string& strTypeName)
{
If(strTypeName==”button”)
return new Button;
Else if(strTypeName==”label”)
return new Label;
Else if(strTypeName==”panel”)
return new Panel;
Else
Return null;
}
};
显然上面的设计尽管比前一个好多了, 但是仍然有硬编码(hard code)的味道, 每次新加一个控件,都要在这里改代码。
如何才能新加控件,又不影响这里的代码呢?
我们想到了注册机制, 设计如下:
Class CControlFactory
{
Public:
Bool RegisterControl(const string& strTypeName, void* lpfnCreateFun);
CControlBase* CreateControl(const string& strTypeName) ;
Protected:
map<string, void*> m_controlMap;
};
我们看到我们上面的设计是通过注册控件和它的创建方法,保存在一个Map里,然后在CreateControl时通过查询这个Map,调用控件注册的创建方法, 来生成新控件。
可以看到上面的设计已经完全符合开放封闭原则,我们新加控件完全不会影响现有的代码。
但是考虑这样一个需求,要求新生成控件要有默认风格,并且这些风格是可变的,比如我们要生成的Button 默认风格是要求以某个图片为背景。
显然上面注册创建函数的方法满足不了我们这里的需求,因为我们不可能通过在我们的创建函数里硬编码来指定初始控件风格。
怎么样才能让我们新创建的控件有默认风格,并且该默认风格是可变的?
我们想到了设计模式里创建型模式中的原型(Prototype)模式, 给我们的基本控件增加一个Clone方法, 代码如下:
Class CControlBase
{
Public:
Virtual CControlBase* Clone() { return new CControlBase(*this);}
};
Class CButton: public CControlBase
{
Public:
Virtual CControlBase* Clone() { return new CButton (*this);}
};
Class CLabel: public CControlBase
{
Public:
Virtual CControlBase* Clone() { return new CLabel (*this);}
};
Class CContainer: ControlBase
{
Public:
Virtual CControlBase* Clone() { return new CContainer (*this);}
};
Class CPanel: public CContainer
{
Public:
Virtual CControlBase* Clone() { return new CPanel (*this);}
};
而我们ControlFactory的设计如下:
Class CControlFactory
{
Public:
Bool RegisterControl(const string& strTypeName, CControlBase* pPrototypeControl);
CControlBase* CreateControl(const string& strTypeName) ;
Protected:
map<string, CControlBase*> m_controlMap;
};
可以看到在注册时我们把控件原型保存起来,然后在CreateControl时通过该控件原型的Clone方法,创建我们的新控件。这种设计下,我们只要在注册时提供不同的原型控件,后面创建时就有不同的默认风格,非常方便。
至此,我们基本完成了ControlFactory的工作。
这里还可以优化的是这里的ControlFactory我们可以把它定义成单例(Singleton):
Class CControlFactory
{
Public:
Static CControlFactory* GetInstacne();
Public:
Bool RegisterControl(const string& strTypeName, CControlBase* pPrototypeControl);
CControlBase* CreateControl(const string& strTypeName) ;
Protected:
map<string, CControlBase*> m_controlMap;
};
有了Control层次和Control Factory,我们接下来考虑如何解析XML来生成Control Tree?
一般的设计会写一个如下的类:
Class CControlBuilder
{
Public:
CControlBase* BuildControlTree(const string& strXML);
};
上面的设计直接通过传入XML,生成Control Tree,看起来很完美。
但是显然这里有2件事情要做,一件是XML的解析,另一件是Control Tree的生成。我们把这2个东西柔在一个类里面,对我们以后维护很不方便。
比如说我们本来用TinyXML来解析XML的,但是后来发现它是基于DOM的,效率太低,想改用别的XML解析器,这时你就会发现修改CControlBuilder这个类是多么痛苦了。
显然,我们比较好的设计是我们应该把XML解析和Build Control Tree这2个功能分离开来,这也符合面向对象设计时的单一职责原则(Single Responsibility Principle)。
我们抽象出一个XML的解析接口:
Class IXMLParser
{
Public:
Virtual bool SetDoc(const cstring& strXML);
Virtual string GetTagName();
Virtual bool FindElem(const string& strName );
Virtual bool FindChildElem(const string& strName );
Virtual bool IntoElem();
Virtual bool OutOfElem();
Virtual string GetAttrib(const string& strName) const;
Virtual string GetChildAttrib(const string& strName) const;
};
Class CTinyXMLParser: public IXMLParser
{
};
可以看到通过这种方式,我们把XML的解析过程抽象出来,我们本身不用关心解析器的类型(说明:上面XML解析的设计不一定合理,只是一个示例)。
现在我们可以开始写我们的Control Tree Builder了,
Class CControlBuilder:
{
Public:
CControlBase* BuildControlTree(const string& strXML)
{
CTinyXMLParser parser;
parser.SetDoc(strXML);
CControlFactory* pFactory = CControlFactory::Instance();
String strControl = parser.GetTagName();
CControlBase* pRoot = pFactory->CreateContorl(strControl);
….
Return pRoot;
}
};
显然上面的方式, 我们修改XML解析器的类型很不方便,我们可以通过工厂方法(Factory Method)来方便以后扩展。
Class CControlBuilder
{
Public:
CControlBase* BuildControlTree(const string& strXML)
{
Auto_ptr< IXMLParser > parser = CreateXMLParser() ;
Parser->SetDoc(strXML);
CControlFactory* pFactory = CControlFactory::Instance();
String strControl = parser->GetTagName();
CControlBase* pRoot = pFactory->CreateContorl(strControl);
….
Return pRoot;
}
Protected:
Virtual IXMLParser* CreateXMLParser()
{
Return new CTinyXMLParser;
}
};
好了,到这里我们所有的设计都全部完成了,基本类图如下:
PS, 在设计CControlBuilder时,本来考虑是不是该用设计模式中的生成器(builder)模式, 但是Builder模式强调的是同样的创建过程,生成不同的产品。显然我们这里无论XML如何解析,最终只有Root Control这一种产品。 所以这里如果用这个模式的话,就有点过度设计了。
总结一下,我们上面用了哪些设计模式?
Composite, Abstract Factory, Factory Method, Singleton, Prototype.
你看出来了吗?
你们有更好的设计思路吗?
说明: (1)上面的代码都只是设计时的伪代码,拿来编译肯定过不了
(2)考虑Windows的窗口类注册和创建机制,会发现尽管Windows的API是C语言方式,但是设计思想是类似的。
(3) 对于DirectUI来说,上面控件类的这种层次设计其实很滥, 有兴趣的话可以学下WPFJ