软件设计复杂性的根本原因是变化
1 客户需求
2 技术平台
3 开发团队
4市场环境
…
如何解决软件变化的复杂性
1分解:分而治之,常用思维模型
2抽象,更高层次,人们处理复杂性有一个通用的技术,就是抽象;
由于不能掌握全部的复杂对象,我们选择忽略它的非本质细节,
而去处理泛化和理想化了的对象模型。
变化是复用的天敌,而面向对象设计最大的优势是抵御变化!
重新认识面向对象
封装、继承、多态
- 隔离变化
从宏观层面来看,面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减到最小; - 各司其职
从微观层面来看,面向对象的方式更强调各个类的“责任”,由于需求变化导致的新增类型不应该影响原来类型的实现; - 对象是什么
1从语言实现层面看,对象封装了代码和数据;
2从规格层面讲,对象是一系列可被使用的公用接口;
3从概念层面讲,对象是某种责任的抽象;
面向对象设计原则
- 依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖与低层模块(变化),二者都应该依赖与抽象(稳定);
- 抽象(稳定)不应该依赖与实现细节(变化),实现细节应该依赖与抽象(稳定);
可实现隔离;
- 开放封闭原则(OCP)
- 对扩展开放,对更改封闭;
- 类模块应该可以扩展,但是不可以修改;
- 单一职责原则(SRP)
功能尽量单一; - Liskov 替换原则(LSP)
- 子类必须能够替换她们的基类(IS-A)
- 继承表达类型抽象
- 接口隔离原则(ISP)
- 不应该强迫客户程序依赖他们不用的方法;
- 接口应该小而完备;
-
优先使用对象组合,而不是类继承
-
封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层一侧进行修改,而不会对另一侧产生不良影响,从而实现层次间的松耦合;
- 针对接口编程,而不是针对实现编程
- 不将变量声明为某个特定的具体类,而是声明为某个接口;
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口;
- 减少系统中部分依赖关系,从而实现“高内聚、松耦合”的类型设计方案;
接口标准化!
活字印刷的设计
将设计原则提升为设计经验
- 设计习语
- 设计模式
- 架构模式
设计模式分类
从目的看:创建型、结构型、行为型
从范围看:类模式、对象模式
从封装变化角度对模式分类
- 组件协作:
- Template Method
- Strategy
- Observer/Event
- 单一职责:
- Decorator
- Bridge
- 对象创建:
- Factory Method
- Abstract Factory
- Prototype
- Builder
- 对象性能:
- Singleton
- Flyweight
- 隔离:
- Facade
- Proxy
- Mediator
- Adapter
- 状态变化:
- Memento
- State
- 数据结构:
- Composite
- Iterator
- Chain of
- Resposibility
- 行为变化:
- Command
- Visitor
- 领域问题:
- Interpreter
设计模式应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用,没有一步到位的设计模式;敏捷开发提倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设计模式的方法;
重构关键技法
静态->动态
早绑定->玩绑定
继承->组合
编译时依赖->运行时依赖
紧耦合-松耦合
组件协助模式
现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定实现框架与应用程序之间的松耦合,是二者之间协作时常用模式;
典型模式:模板方法、策略模式、事件模式(观察者)
Template Method
模式定义
定义一个操作中的算法骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method 使得子类可以不改变(复用)一个算法的结构,即可重定义(override 重写)该算法的某些特定步骤。
动机(Motivation)
- 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但是各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现;
- 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
实例
方式一://早绑定,无设计模式应用
//程序库开发人员
class Library{
public:
void Step1(){/*...*/}
void Step3(){/*...*/}
void Step5(){/*...*/}
}
//
class Application{
public:
bool Step2(){/*...*/}
void Step4(){/*...*/}
}
int main(){
Library lib();
Application app();
//业务逻辑
lib.Step1();
if(app.Step2()){
lib.Step3();
}
for(int i=0;i<4;i++){
app.Step4();
}
lib.Step5();
}
方式二://晚绑定,Library调用Application,Run为相对稳定的程序骨架
//程序库开发人员
class Library{
public:
void Run(){//**稳定,模板方法**稳定中有变化
//业务逻辑
Step1();
if(Step2()){//支持变化==>虚函数的多态调用
Step3();
}
for(int i=0;i<4;i++){
Step4();//支持变化==>虚函数的多态调用
}
Step5();
}
virtual ~Library(){}
protected:
void Step1(){/*...*/}//稳定
void Step3(){/*...*/}//稳定
void Step5(){/*...*/}//稳定
virtual bool Step2()=0;//变化
virtual void Step4()=0;//变化
}
class Application:public Library{
public:
virtual bool Step2(){/*子类重新实现*/}
virtual void Step4(){/*子类重新实现*/}
}
int main(){
Library *pLib=new Application();
pLib->Run();
delete pLib;
}
类图
红色为相对稳定的功能
蓝色为待子类实现的内容
小结
- Template Method 是一种非常基础性的设计模式,面向对象系统中有着大量的应用。它用最简洁的机制(虚函数)为很多应用程序框架提供了灵活的扩展点,是代码复用的常用实现结构;
- 除了可以灵活应对子步骤变化外,“不要调用我,让我调用你”的反向控制结构是模板方法模式的典型应用;
- 在具体实现方面,被模板方法调用的虚方法可以有实现,也可以没有任何实现(抽象方法,纯虚方法),但是一般把他们设置为protected方法;(不会供外界调用)
Strategy 策略
模式定义
定义一系列算法,把他们一个一个封装起来,并且使他们可相互替换(变化)。该模式使得算法可独立于使用它们的客户程序(稳定)而变化(扩展,子类化);
动机(Motivation)
- 在软件构建过程中,某些对象使用的算法可能是多种多样,经常改变,如果将这些算法都编码到对象中,将会变得异常复杂;而且有时候支持不使用算法也是一种性能负担;
- 如何在运行时根据需要透明的更改算法?将算法与对象本身解耦,从而避免上述问题?
类图
实例
/普通实现,若需求变更添加其它国家税收则需要修改代码,违反开闭原则/
enum TaxBase{
CN_Tax,
US_Tax,
DE_Tax
}
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
/*...*/
if(tax==CN_Tax){}
else if(tax==US_Tax){}
else if(tax==DE_Tax){}
/*...*/
}
}
模式实现
class TaxStrategy{
public:
virtual double Calculate(const Context& context)=0;
virtual ~TaxStrategy(){}//必须
}
class CNTax:public TaxStrategy{
public:
virtual double Calculate(const Context& context){
/****************/
}
}
class USTax:public TaxStrategy{
public:
virtual double Calculate(const Context& context){
/****************/
}
}
class DETax:public TaxStrategy{
public:
virtual double Calculate(const Context& context){
/****************/
}
}
//扩展代码,对扩展开放,对修改关闭
class FRTax:public TaxStrategy{
public:
virtual double Calculate(const Context& context){
/****************/
}
}
class SalesOrder{
private:
TaxStrategy * strategy;//
public:
SalesOrder(StrategyFactory* strategyFactory){
this->strategy=strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
double CalculateTax(){
/*...*/
Context context();
double tax=strategy->Calculate(context);
/*...*/
}
}
小结
- Strategy 及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
- Strategy模式提供了用条件语句之外的另一种选择,消除了条件语句,就是在解耦。含有许多条件判断语句的代码通常都需要Strategy模式。
- 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省开销。
Strategy 策略
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都能得到通知并自动更新。
动机(Motivation)
- 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象(目标对象)的状态发生变化,所有依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好的抵御变化。
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
类图
实例
/普通实现,/
class FileSplitter{
string m_filePath;
int m_fileNumber;
//加进度
ProgressBar* m_progressBar;//通知
public:
FileSplitter(const string& filePath,int fileNumber,ProgressBar* progressBar):
m_filePath(filePath),
m_fileNumber(fileNumer),
m_progressBar(progressBar){
}
void split(){
//1.读取大文件
//分批次向小文件中写入
for(int i=0;i<m_fileNumber;i++){
//....
//加进度
if(m_progressBar!=nullptr){
m_progressBar->setValue((i+1)/m_fileNumber);
}
}
}
}
class MainForm:public Form{
TextBox* txtFilePath;
TextBox* txtFileNumber;
//加进度
ProgressBar* m_progressBar;
public:
void Button1_click(){
string filePath=txtFilePath->getText();
int number=atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath,number,m_progressBar);
splitter.split();
}
}
模式实现 1只支持一个观察者(伪观察者)
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IPorgress(){}
}
class FileSplitter{
string m_filePath;
int m_fileNumber;
//ProgressBar* m_progressBar;//具体通知控件
IProgress* m_iprogress;//抽象通知机制
public:
FileSplitter(const string& filePath,int fileNumber,IProgress* iprogress):
m_filePath(filePath),
m_fileNumber(fileNumer),
/*m_progressBar(progressBar)*/
m_iprogress(iprogress){
}
void split(){
//1.读取大文件
//分批次向小文件中写入
for(int i=0;i<m_fileNumber;i++){
//加进度
float progressValue =m_fileNumber;
progressValue=(i+1)/m_fileNumber;
onProgress(progressValue);
}
}
protected:
void onProgress(float value){
if(m_iprogress!=nullptr){
m_iprogress->DoProgress(progressValue);//
}
}
}
class MainForm:public Form,public IProgress{//尽量不要使用多继承,除非仅接口
TextBox* txtFilePath;
TextBox* txtFileNumber;
//加进度
ProgressBar* m_progressBar;
public:
void Button1_click(){
string filePath=txtFilePath->getText();
int number=atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath,number,this);
splitter.split();
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
}
模式实现 2 观察者
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IPorgress(){}
}
//按照类图,add、remove、onProgress三个函数可以放到再上一级类里,此类继承即可
class FileSplitter{
string m_filePath;
int m_fileNumber;
//ProgressBar* m_progressBar;//具体通知控件
//IProgress* m_iprogress;//抽象通知机制
List<IProgress*>m_iprogressList;//抽象通知机制,支持多个观察者
public:
FileSplitter(const string& filePath,int fileNumber,IProgress* iprogress):
m_filePath(filePath),
m_fileNumber(fileNumer),
/*m_progressBar(progressBar)*/
/*m_iprogress(iprogress)*/{
}
void split(){
//1.读取大文件
//分批次向小文件中写入
for(int i=0;i<m_fileNumber;i++){
//加进度
float progressValue =m_fileNumber;
progressValue=(i+1)/m_fileNumber;
onProgress(progressValue);
}
}
protected:
void addIProgress(IProgress* iprogress){
m_iprogressList.add(iprogress);
}
void removeIProgress(IProgress* iprogress){
m_iprogressList.remove(iprogress);
}
void onProgress(float value){
List<IProgress*>::Iterator itor=m_iprogressList.begin();
while(itor!=m_iprogressList.end()){
(*itor)->DoProgress(progressValue);//
}
}
}
class MainForm:public Form,public IProgress{//尽量不要使用多继承,除非仅接口
TextBox* txtFilePath;
TextBox* txtFileNumber;
//加进度
ProgressBar* m_progressBar;
public:
void Button1_click(){
string filePath=txtFilePath->getText();
int number=atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath,number);
splitter.addIProgress(this);
splitter.addIProgress(&cn);
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
}
小结
- 使用面向对象的抽象,Observer 模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合。
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分;
“单一职责”模式
在软件组件的设计中,如果责任划分不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时的关键是划分职责;
典型模式:Decorator、Bridge
Decorator
模式定义
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator 模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数);
动机(Motivation)
- 在某些情况下我们可能会“过度的使用继承来扩展对象功能”,由与继承为类引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增加),多种子类的组合(扩展功能的组合)会导致更多子类的膨胀;
- 如何使“对象功能的扩展”能够根据需要来动态实现?同时避免“扩展功能增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响降到最低?
实例
纯粹使用继承
使用装饰器模式
类图
红色为相对稳定的功能
蓝色为待子类实现的内容
小结
“对象创建” 模式
通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式 :1. Factory Method 2. Abstract Factory 3. Prototype 4. Builder
工厂方法模式
模式定义
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。
动机(Motivation)
- 在软件系统中,经常面临创建对象的工作,由于需求变化,需要创建的对象的具体类型经常变化;
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合;
类图
实例
/普通实现,若需求变更添加其它国家税收则需要修改代码,违反开闭原则/
class MainWindow{
public:
void Button_Click(){
ISplitter* splitter=
new BinarySplitter(filePath,number);//等号后依赖了具体类
splitter->split();
}
}
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
}
class BinarySplitter{
public:
void split(){
...
}
}
class TxtSplitter{
public:
void split(){
...
}
}
进阶实现
class MainWindow{
public:
void Button_Click(){
ISplitter* splitter=
factory.CreateSplitter();//等号后间接依赖了具体类,依然无法实现解耦
splitter->split();
}
}
class SplitterFactor{
public:
ISplitter* CreateSplitter(){
return new BinarySplitter();
}
}
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
}
模式实现
小结
- Factory Method 模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱;
- Factory Method 模式通过面向对象的手法,将所要创建的具体对象的工作延迟到子类,从而实现一种扩展(而非改变)的策略,较好的解决了这种紧耦合关系;
- Factory Method 模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同;
d