再读《重构》和《架构整洁之道》
目录
开放封闭原则 --- OCP
依赖倒置原则 --- DIP
单一职责原则 --- SRP
Liskov替换原则 --- LSP
接口隔离原则 -- ISP
迪米特法则(最小知识原则) -- LOD
软件中的基础结构(函数、类或模块)对于功能扩展是开放的,但是对于修改是封闭的。
可实施的具体行为
面向接口编程,不要面向实现编程
依赖倒置原则
Liskov替换原则
eg.
//Method One
int DoSomeFunction(code, param1, param2)
{
int rtnCode = ...
switch(code)
{
case CODE_A:
{
code block A
break;
}
case CODE_B:
{
code block B
break;
}
....
default:
rtnCode = not support error code
return rtnCode;
}
//Method Two
int ProcessForCodeA(param1, param2)
{
code block A
}
int ProcessForCodeB(param1, param2)
{
code block B
}
struct
{
int code;
int (*Processor)(param1, param2);
}ITEM;
//Table-Driven Methods
static ITEM processItems[] =
{
{CODE_A, ProcessForCodeA},
{CODE_B, ProcessForCodeB},
......
};
int DoSomeFunction(code, param1, param2)
{
for_each(item in processItems)
{
if(item.code == code)
{
return item.Processor(param1, param2);
}
}
return not supoort error code;
}
#define MAX_PROCESS_ITEMS 32
ITEM processItems[MAX_PROCESS_ITEMS] = { 0 };
int DoSomeFunction(code, param1, param2)
{
......
}
bool RegisterProcessor(int code, Processor func)
{
add {code, func} to processItems[]
}
void DeregisterProcessor(int code)
{
remove code process item from processItems[]
}
Strategy 策略模式
Template Method 模板方法模式
Visitor 访问者模式
意图:定义一些列算法,把它们一个个封装起来,并且使它们可以互相替换。本模式使得算法可独立于使用它的客户而变化
适用性:
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。本模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
适用性:
意图:表示一个作用于某对象结构中的各个元素的操作。本模式使得你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
适用性:
class Client
{
Element *element;
void SomeOperation()
{
生成一个ContreteVisitor1的实例 v
element->Accept(&v);
v.ShowSomething(); //用ContreteVisitor1定义的方式输出结果
}
void AnotherOperation()
{
生成一个ContreteVisitor2的实例 v
element->Accept(&v);
v.ShowSomething(); //用ContreteVisitor2定义的方式输出结果
}
};
传统的层次化设计模型,上层和下层业务分离,上层依赖下层提供的功能,下层不能反向依赖上层。
依赖倒置不是简单的依赖方向翻转,它的核心仍然是抽象接口。
1、高层模块不应该依赖于底层模块(二者都应该依赖于抽象)
2、抽象不应该依赖于实现,实现应该依赖于抽象
关键是抽象,面向接口编程
Liskov替换原则
取款业务逻辑流程是稳定的
提款机的实现是变化的
A: bool Withdraw(账户验证参数,金额)
B: int GeiQian(其他参数,账户验证参数,金额)
将取款业务流程中对提款机的操作接口抽象出来,定义一组提款的逻辑接口。
抽象接口的提出,解除了取款业务和提款机的耦合关系,在满足Liskov替换原则的基础上,替换不同厂家的提款机变的非常容易。
class Teller
{
public:
...
bool IsConnected(环境参数) = 0;
bool GetBalance(位置信息, 余额信息) = 0;
bool Pay(上下文信息,支付金额) = 0;
...
};
//具体不同厂家的提款机,就是这个抽象接口的实现者
class IntelTeller : public Teller
{
IntelTeller(TellerMgmt& tm)
{ tm.Register(this); }
...
bool IsConnected(环境参数)
{ Intel 的实现 }
bool GetBalance(位置信息, 余额信息)
{ Intel 的实现 }
bool Pay(上下文信息,支付金额)
{ Intel 的实现 }
...
};
class MoftTeller : public Teller
{
...
bool IsConnected(环境参数)
{ Moft 的实现 }
bool GetBalance(位置信息, 余额信息)
{ Moft 的实现 }
bool Pay(上下文信息,支付金额)
{ Moft 的实现 }
...
};
//提供注册接口,由能提供者注册自己提供的功能
class TellerMgmt
{
void Register(Teller *teller)
{ ...... }
Teller *GetTeller(...)
{ ...... }
protected:
//管理注册的Teller们;
}
TellerMgmt& GetTellerMgmtObj()
{
static TellerMgmt tm;
if(tm 没有初始化)
{
对tm初始化,并设置初始化标志
}
return tm;
}
//取款业务模块
Teller *teller = tm.GetTeller();
if(teller->IsConnected(...))
{
......
}
意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
适用性:
当类只有一个实例,并且客户可以从一个众所周知的访问点访问它时
重要:
不要把单实例模式当全局变量用
描述:对一个类而言,应该只有一个引起它变化原因。
现在的描述:任何一个软件模块都应该只对某一类行为者负责
ConnectionMgmt::AddModem(Connection *modem)
{
Add modem to modems list
}
ConnectionMgmt::Connect(...user param...)
{
for_each(modem in modems list)
{
modem->Dial(...);
}
}
ModemImplemention modem1;
ModemImplemention modem2;
......
ConnectionMgmt cm;
cm.AddModem(&modem1);
cm.AddModem(&modem2);
......
cm.Connect(...);
可实施的具体行为:
高内聚原则
分离变化的部分和不变的部分
TDD 测试驱动开发
最小知识原则 LOD
可实施的设计模式:
Facade 外观模式
Proxy 代理模式
Adapter 适配器模式
单一职责原则 --- Facade 模式
意图:为子系统中的一组接口提供一个一致的界面。本模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
意图:为其他对象提供一种代理,以控制对这个对象的访问。本模式定义了一个代理对象,通过代理对象屏蔽原对象的一些接口
意图:将一个类的接口转换成客户希望的另一个接口。本模式使得原本由于接口不兼容而不能在一起工作的那些类可以在一起工作
AdapterA::OperationA(...)
{
...
opl->Function1(...);
}
AdapterB::OperationB(...)
{
...
opl->Function3(...);
}
描述:子类型(subtype)必需能够替换掉它们的基类型
OO背后的主要机制是抽象和多态,在C++和Java这样的静态语言中,支持抽象和多态的关键机制之一就是继承。如果继承体系中某个类的实现不满足LSP原则,那么这个体系就会变的很脆弱,失去健壮性。
违反LSP原则,将导致对OCP原则的违反。
class A
{
}
class B : public A
{
bool Operation();
};
class C : public A
{
bool Operation();
};
void TestFunc(const A& ta)
{
if(ta is a B)
{
static_cast(ta).Operation();
}
else if(ta is a C)
{
static_cast(ta).Operation();
}
else if(...)
{
...
}
...
}
//new design
class A
{
virtual bool Operation();
};
void TestFunc(const A& ta)
{
...
ta.Operation();
...
}
/*
正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。
比如增加一个新类D:
*/
class D : public A
{
bool Operation();
};
//使用基类类型的函数TestFunc()不需要做任何修改就可以支持D:
D d;
TestFunc(d)
类的继承体系设计要遵循 IS-A 原则,并且这个 IS-A 是关于行为的,不是关于数据的。
IS-A 原则只能作为子类型定义的含义过于宽容,应该将子类型的“可替换性”作为子类型定义的必要条件。
潜在的违反LSP的情况:
没有 IS-A 关系的继承
派生类中退化了某个函数
还有一些语言层面上的错误,会导致违反LSP原则,比如某个子类的Operation()内部抛出了异常(而不是按照约定返回错误值),这会使得TestFunc的行为发生不可控的变化。
描述:不应该强迫客户依赖于它们不用的方法。换句话说,一个类对另一个类的依赖应该是建立在最小的接口范围上的。
强迫客户依赖它们不使用的方法,那么客户就要面临着这些未使用的方法的改变所带来的变更,无形中增加了不必要的耦合关系,潜在地违反SRP原则。
可实施的具体行为:
通过拆分职责分离接口
使用委托分离接口
分离变化的部分和不变的部分
使用多继承分离接口
使用委托分离接口
class TimedDoor: public Door
{
void DoorTimeOut(...)
}
class TimerClient
{
virtual void TimeOut() = 0;
}
class DoorTimedAdapter: public TimerClient
{
DoorTimedAdapter(TimedDoor& door) { aTimedDoor = door; }
virtual void TimeOut() { aTimedDoor.DoorTimeOut(); }
TimedDoor aTimedDoor;
}
class Timer
{
RegisterTimeClient(TimerClient *tc)
{
//add tc to tcs 列表
}
//事件发生时:
for_each( tc in tcs)
tc->TimeOut(...);
}
TimedDoor door;
Timer timer;
timer.RegisterTimeClient(new DoorTimedAdapter(door));
door.Open();
class Timer
{
friend static Timer& GetTimer()
public:
RegisterTimeClient(TimerClient *tc) { add tc to tcs 列表}
protected:
Timer() {}
//事件发生时:
for_each( tc in tcs)
tc->TimeOut(...);
}
Timer& Timer::GetTimer()
{
static Timer timer;
if(....)
{
}
return timer;
}
class TimedDoor: public Door
{
TimedDoor()
{
Timer& timer = Timer::GetTimer();
timer.RegisterTimeClient(new DoorTimedAdapter(*this));
}
virtual void DoorTimeOut(...);
}
TimedDoor door;
door.Open();
使用多重继承分离接口
class TimerClient
{
virtual void TimeOut() = 0;
}
class TimedDoor: public Door, public TimerClient
{
TimedDoor()
{
Timer& timer = Timer::GetTimer();
timer.RegisterTimeClient(this);
}
virtual void TimeOut()
{
do alarm report
}
// other interface inherit from Door
}
TimedDoor door;
door.Open();
可实施的设计模式:
Facade 外观模式
Proxy 代理模式
ISP原则和SRP原则
SRP原则强调的是“只有一个原因能造成对象的改变”,这就潜在地对一个类的接口个数和接口内方法的个数提出了要求。一般来说,接口个数越多,接口内的方法个数越多,越容易违反SRP原则。所以具体实施的时候,都要求一个类只实现一个接口。
ISP原则强调的是“隔离”,对一个类实现的接口数量和各个接口内方法的数量都没有要求,只要求这些接口之间相互隔离,并且没有多余的接口。ISP在具体实施的时候,可以使用多继承的方式使用那些相互隔离的接口。
虽然ISP所采用多继承的时候会潜在地造成一个类的接口的增加,但是这两个原则本质上是不矛盾的,因为每一个被分离出来的接口都应该是满足SRP原则的。
描述:迪米特法则(Law of Demeter)又叫作最少知识原则,一个对象应当对其他对象有尽量少的了解。
“不要和陌生人说话”,一个软件实体应该尽量少的与其他软件实体发生相互作用,换句话说,对其他软件实体有尽量少的知识(了解)。
迪米特法则的初衷是降低类之间的耦合,减少对其他类的依赖。
可实施的设计模式:
Facade 外观模式
Mediator 中介者模式
意图:用一个中介对象封装一些列的对象交互。本模式使得对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
三个对象的协作:TextField,Button 和 StaticText
class WelcomeDialog
{
TextField aUserNameText;
Button aNextButton;
StaticText aStaticWelcome;
WelcomeDialogMediator mediator;
......
};
//WelcomeDialog类的初始化部分:
......
aUserNameText.SetMediator(mediator);
aNextButton.SetMediator(mediator);
aStaticWelcome.SetMediator(mediator);
mediator.btnNext = aNextButton;
mediator.staticWelcome = aStaticWelcome;
......
};
void TextField::TextChange(String text)
{
......
mediator.OnTextChange(this->self_ID, text);
......
};
void WelcomeDialogMediator::OnTextChange(int id, String text)
{
//检查 id 是否是 aUserNameText,不同的控件触发的TextChange可以有不同的响应处理
boolean isEnable = text.IsEmpty() ? false : true;
...
btnNext.SetState(isEnable);
staticWelcome.SetText(text);
...
}
适用性: