Week11 Notes
“对象性能模式”
单间模式
面向对象很好地解决了抽象的问题,但是不可避免要付出一定的代价,在某些情况下,抽象带来的代价需要谨慎处理,比如虚函数和继承
虚函数带来的代价还是很多的,某些倍乘效应
典型的模式有Singleton和Flyweight
在某些特殊的场景下,面向对象带来的成本有问题
经常有一些特殊的类,必须保证在系统中只存在一个实例,才能确保逻辑的正确,以及具有良好的效率,如何绕过常规的构造器使一个类只有一个实例呢,这是设计者的责任,不是使用者的责任
怎么实现呢?
Class Singleton{
Private:
Singleton();
Singleton(constSigleton& other);
Public:
StaticSingleton* getInstance();
StaticSingleton* m_instance;
};
//线程非安全版本
Singleton* Singleton::m_instance = nullptr;
Singleton* Singleton::getInstace(){
If(m_instace== nullptr){
M_instance = new Singleton();
}
returnm_instance;
}
//线程安全版本。但锁的代价过高
Singleton* Singleton::getInstance(){
Locklock;
If(m_instance== nullptr){
M_instance= new Singleton();
}
returnm_instance;
}
这是线程安全版本,锁的代价在哪里?假设对象已经被创建出来,m_instance已经不是null了,这是后两个线程都是在读,此时不需要加锁。
当高并发的时候(外部服务器)十万人同时在线,在这种场景下,这个锁的代价可能也是很高的。
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getIncetance(){
If(m_instance== nullptr){
Locklock;
If(m_instance == nullptr)
M_instance = new Singleton();
}
returnm_instance;
}
锁前检查,正确但代价过高
锁后检查,为了保证正确
内存读写reorder的情况会导致双检查锁失效
在指令层的时候和我们的假设有可能不一样
new的这一步先分配内存,再调用构造器,一般对分配的那块内存初始化,第三步是做完的这些东西的指针值给m_instance,但这三步是我们假想的顺序,在指令层,这三步有可能被reorder,先分配内存,然后把内存地址给m_instance,最后再调用构造器。这是编译器优化的时候有可能会这样干,每一种语言都会有这种情况。怎么解决这个问题?
//C++ 11版本以后的跨平台实现(volatile)
std::atomicSingleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance(){
Singleton*tmp = m_instance.load(std::memory_order_relaxed);
Std::atomic_thread_fence(std::memory_order_acquire);
If(tmp== nullptr){
Std::lock_guardlock(m_mutex);
Tmp= m_instance.load(std::memory_order_relaxed);
If(tmp== nullptr){
Tmp= new Singleton;
Std::atomic_thread_fence(std::memory_order_relaxed);
M_instance.store(tmp,std::memory_order_relaxed);
}
}
return tmp;
}
作用是保证一个类仅有一个实例,实例构造器可以设置为protected以允许子类派生,一般不要支持拷贝构造函数和clone接口,因为这样有可能导致多个对象实例。
享元模式
Flyweight
在软件系统中采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价。使用共享的技术可以有效地支持大量细粒度的对象
class Font{
private:
//uniqueobject key
stringkey;
public:
Font(conststring& key){…}
};
class FontFactory{
private:
map fontPool;
public:
Font*GetFont(const string& key){
Map::iterator item = fontPool.find(key);
If(item!=footPool.end()){
ReturnfontPool[key];
}
else{
Font* font = new Font(key);
fontPool[key] = font;
return font;
}
}
void clear(){…}
}
对象池的一种设计方式,有就返回,没有就创建一个。实际中数据结构也不同,接口也可能不同。通过一个共享的方案来支持大量细粒度的对象。出去的对象最好是只读的,否则出去以后被更改了。Flyweight一般只解决对象的代价问题。对象的数量太大导致对象的开销太大。对象上10万,大概是5M的水平
几十个对象不要用flyweight,里面使用的数据结构也一样需要开销
状态模式
“状态变化模式”某些对象的状态经常面临变化,如何对这些变化进行有效的管理,同时又维持高层模块的稳定?某些对象的状态如果改变,其行为也会发生变化,比如文档处于只读状态。
Enum NetworkState{
Network_Open
Network_Close,
NetworkConnect,
};
//类似于状态机
class NetworkProcessor{
NetworkStatestate;
Public:
VoidOperation1(){
If(state== Network_Open){
State= Network_Close;
}
else_if …
}
voidoperation2(){…}
}
那我们这样写有什么问题?
这和之前的strategy很像,if_else在你的代码中出现很多,以后如果添加了一种新的状态wait,那之前的代码要怎么更改,需求的变化让你的代码不断地在变化,按照strategy给我们的经验,先提抽象基类
class NetworkState{
public:
Network*pNext;
Virtualvoid Operation1() = 0;
Virtualvoid Operation2() = 0;
Virtual~NetworkState(){}
};
class OpenState:public NetworkState{
staticNetworkState* m_instance;
public:
staticNetworkState* getInstance(){
if(m_instance== nulptr){
m_instance= new OpenState();
}
return m_instance;
}
voidoperation1(){
//…
pNext = CloseState::getInstance;
}
};
class NetworkProcessor{
NetworkState*pState;
Public:
NetworkProcessor(NetworkState*pState){
This->pState= pState;
}
voidOperation1(){
pState->Operation1();
pState= pState->pNext;
}
};
允许一个对象在其内部状态改变的时候改变它的行为,从而是对象看起来似乎修改了其行为。和strategy非常像。为不同的状态引入不同的对象使得状态转换变得更加明确,如果state对象没有实例变量,那么各个上下文可以共享一个state对象,从而节省对象开销。(Singleton设计模式)
备忘录
Memento
属于“状态变化”模式,某些对象在状态转换过程中,要求程序能够回溯到对象之前处于某个点时的状态,如果使用一些公共接口来让其他对象得到对象的状态,会暴露对象细节实现
在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象外部保存这个状态。
Class Memento{
Stringstate;
Public;
Memento(conststring& s): state(s){}
StringgetState() const{return state;}
VoidsetState(const string& s) {state = s;}
}
class Originator{
stringstate;
public:
Originator(){}
MementocreateMomento(){
Mementom(state);
Returnm;
}
voidsetMomento(const Memento& m){
state = m.getState();
}
};
int main(){
Originatororiginator;
//存储到备忘录
Mementomem = originator.reateMomento();//捕获对象状态
//改变originator状态
//从备忘录中恢复
originator.setMomento(memento);
}
不破坏originator的封装性,
备忘录存储原发器对象的内部状态,在需要时恢复原发器的状态。
Memento模式的核心是信息隐藏,即Originator需要向外界隐藏信息,保持其封装性,但同时又需要将状态保持到外界。
由于现代语言运行时都具有相当的对象序列化支持,因此往往采用效率较高又容易正确实现的序列化方案来实现momento模式
实现的是一个类似于深拷贝的事情,内存快照的方式,复杂对象不好实现,一个对象里面没有指针,很简单,但如果指针里面再有指针,指针里面再有指针,这样具体实现的时候不太好实现,所以我们一般用序列化,
组合模式
Composite
数据结构模式
iterator,chain ofresponsibility
组件在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,会破坏组件的复用。
需要将客户代码和复杂的对象容器结构解耦。
将对象组合成树性结构以表示部分和张体的层次结构,composite使得用户对单个对象和组合对象的使用具有一致性。
Class component{
Public:
Virtualvoid process() = 0;
Virtual~component(){}
};
class Composite: public component{
stringname;
listelements;
public:
composite(conststring& s): name(s){}
voidadd(component* element){
elements.push_back(element);
}
voidremove(component* element){
elements.remove(element);
}
void process(){
//1.process current node
//2.process leaf nodes
for(auto &e:elements)
e->process();//多态调用
}
};
class Leaf:public component{
stringname;
public:
Leaf(strings): name(s) {}
Voidprocess(){
//processcurrent node
}
};
void Invoke(component& c){
c.process();
}
int main(){
compositeroot(“root”);
compositetreeNode1(“treeNode1”);
compositetreeNode2(“treeNode2”)
…
Leafleaf1(“left1”);
Leafleaf2(“left2”);
Root.add(&treeNode1);
treeNode1.add(&treeNode2);
treeNode2.add(&leaf1);
root.add(&treeNode2);
}
composite模式采用树形结构来实现普遍存在的对象容器,将一对多的关系转化为一对一的关系,使得客户代码可以一致地处理对象和容器,不需要关心处理的是单个的兑现还是组合的对象容器,将客户代码与复杂的对象容器结构解耦是composite的核心思想,解耦以后客户代码将与纯粹的抽象接口而不是对象容器的内部实现结构发生以来,更能应对变化。
迭代器模式
iterator
集合对象内部结构常常变化各异,但对于这些集合对象希望不暴露内部结构的同事,可以让外部客户代码透明地访问其中包含的元素,这种透明遍历也为同一种算法在多种集合对象上进行操作提供了可能。
提供一种方法顺序访问一个聚合对象中的各个元素,不暴露该对象的内部表示。
职责链
chain of responsibility
一个请求可能被多个对象处理,但每个请求在运行的时候只能有一个接受者2,如果显示指定会有紧耦合
让请求的需求者自动来判断
命令模式
command
“行为变化”模式
组件行为的变化经常导致组件本身剧烈的变化
运行时绑定,一串虚函数的调用
如何将行为请求者与行为实现者解耦?
将请求封装成一个对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志。
一旦把行为作为对象后可能获得的好处有redo, undo, stack入栈出栈,将行为抽象为对象
实现command接口的具体命令对象ConcreteCommand有时候需要可能会保存一些额外的状态信息。使用composite模式封装为一个复合指令
使用接口和实现来定义行为接口规范。
访问器
类层次结构中要增加新的行为方法,在基类中更改会给子类带来负担,所以要透明的为类层次结构上动态地增加。(运行时增加)
class element{
virtualaccept(Visitor &visitor)
}
class visitot{
public:
voidvisitElementA(ElementA& element)
}
访问器的缺点:
有几个子类visit里就有几个visitconcreteelemntA
所以需要知道有几个子类
所以visitor模式需要在类的层次结构需要稳定
但其中的操作经常面临频繁改动
解析器
问题比较复杂,如果使用
a+b-c+d把规则提取出来,
class Expression{
public:
virtualint interpreter(map var) = 0;
virtual~Expression(){}
};
变量表达式:
VarExpression:public Expression{
Char key
Public:
VarExpression(constchar& key){
This->key= key;
}
int interpreter(map var) override{
return var[key];
}
};
符号表达式:
class SymbolExpression:public Expression{
protected:
Expression*left;
Expression*right;
Public:
SymbolExpression(Expression*left, Expression* right):
Left(left),right(right){}
};
class AddExpression: public SymbolExpression{
public:
AddExpression(Expression*left, Expression* right):
SymbolExpression(left,right){}
Intinterpreter(map var) override{
Returnleft->interpreter(var) + right->interpreter(var);
}
};
Expression* analyse(string expStr){
StackexpStack;
Expression*left = nullptr;
Expression*right = nullptr;
For(int I = 0; I< expStr.size(); i++){
Switch(expStr[i]){
Case’+’:
Case’-’:
Default:
}
}
}
给定一个语言,定义它的文法的一种表示,并定义一个解释器,
应用的场合是一个难点,只有满足“业务规则频繁变化,且类似的结构不断重复出现,并且容易抽象为语法规则的问题”
使用interpreter模式来表示文法规则,从而可以使用面向对象技巧来方便扩展
interpreter模式适合简单的文法表示,复杂的表示方法会产生比较大的类层次结构,需要求助于语法分析生成器这样的标准工具
总结
管理变化,提高复用!
分解和抽象
八大原则
重构技巧:
静态转为动态,早绑定转为晚绑定,编译时依赖转为运行时依赖,继承转为组合
直接组合一个对象和继承在C++内存上是一样的,这样B就不能变化了,但是如果A中组合B的指针,B就具有灵活性,可以改变,指向B的子类
关注抽象类和接口,理清变化点和稳定点,审视依赖关系,要有framework和application的区隔思维,良好的设计是演化的结果。