特色技术
专业从事预应力结构体系的设计、施工一体化解决方案
原创 | 设计模式之单一职责原则
- 分类:程序技术
- 作者:
- 来源:
- 发布时间:2020-03-14 15:01
- 访问量:
【概要描述】单一职责原则为我们提供了一个编写程序的准则,要求我们在编写类、抽象类和接口、方法(函数)时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。
原创 | 设计模式之单一职责原则
【概要描述】单一职责原则为我们提供了一个编写程序的准则,要求我们在编写类、抽象类和接口、方法(函数)时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。
- 分类:程序技术
- 作者:
- 来源:
- 发布时间:2020-03-14 15:01
- 访问量:
一、定义
应该有且仅有一个原因引起类的变更。(There should never be more than one reason for a class to change)
单一职责原则为我们提供了一个编写程序的准则,要求我们在编写类、抽象类和接口、方法(函数)时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。
如果一个类承担的职责过多,就等于把这些职责耦合在一起。一个职责的变化可能会影响或损坏其他职责的功能。而且职责越多,这个类变化的几率就会越大,类的稳定性就会越低。
在软件开发中,经常会遇到一个功能类T负责两个不同的职责:职责P1,职责P2。现因需求变更需要更改职责P1来满足新的业务需求,当我们实现完成后,发现因更改职责P1竟导致原本能够正常运行的职责P2发生故障。而修复职责P2又不得不更改职责P1的逻辑,这便是因为功能类T的职责不够单一,职责P1与职责P2耦合在一起导致的。
二、例子一
例如下面的工厂类Mill1,负责将原料进行预处理然后加工成产品X和产品Y。
#include <string>
using namespace std;
class Mill1
{
private:
string preProcess(const string&material);
public:
Mill1();
virtual ~Mill1();
string processx(const string&material);
string processy(const string&material);
};
Mill1::Mill1()
{
}
Mill1::~Mill1()
{
}
string Mill1::preProcess(const string&material)
{
return "*"+material+"*";
}
string Mill1::processx(const string&material)
{
return preProcess(material)+"加工成产品x";
}
string Mill1::processy(const string&material)
{
return preProcess(material)+"加工成产品y";
}
运行后结果:
三、根据需求修改后的例子二
现因市场需求,优化产品X的生产方案,需要改变原料预处理的方式,将预处理方法preprocess()修改为:
string Mill1::preProcess(const string&material)
{
return "#"+material+"#";
}
运行结果如下:
从运行结果中可以发现,在使产品X可以达到预期生产要求的同时,也导致了产品Y的变化,但是产品Y的变化并不在预期当中,这便导致程序运行错误甚至崩溃。
四、修改例子二
为了避免这种问题的发生,我们在软件开发中,应当注重各职责间的解耦和增强功能类的内聚性,来实现类的职责的单一性。
切断职责P1与职责P2之间的关联(解耦),然后将职责P1封装到功能类T1中,将职责P2封装到功能类T2中,使职责P1与职责P2分别由各自的独立的类来完成(内聚)。
按照单一职责原则可以将上面的工厂类按照以下方式进行分解。
首先,定义一个抽象工厂类AMill,有预加工preProcess和加工process方法,如下:
class AMill
{
public:
AMill();
virtual ~AMill();
virtual string preprocess(const string&material)=0;
virtual string process(const string&material)=0;
};
由结果可知,优化产品X的生产方案,不会再影响产品Y。
五、根据市场的扩散需求进行调整
1、尽管我们在开发设计程序时,总想着要使类的职责单一,保证类的高内聚低耦合,但是很多耦合往往发生在不经意间,其原因为:类的职责扩散。
2、由于软件的迭代升级,类的某一职责会被分化为颗粒度更细的多个职责。这种分化分为横向细分和纵向细分两种形式。
3、职责细化后的代码:
class Mill3
{
public:
Mill3();
virtual ~Mill3();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
string packing(const string&material);//包装工序
public:
string processx(const string&material);//加工成品完成
};
string Mill3::preprocess(const string&material)
{
return "*"+material+"---->";
}
string Mill3::handling(const string&material)
{
return preprocess(material)+"处理---->";
}
string Mill3::packing(const string&material)
{
return handling(material)+"包装---->";
}
string Mill3::processx(const string&material)
{
return packing(material)+"产品X";
}
执行后结果如下:
4、横向细分
现因业务拓展,工厂增加生产产品Y,产品Y与产品X除了包装不同之外,其它都一样,换汤不换药。
秉着代码复用,少敲键盘,高效完成工作的原则,对工厂类Mill3进行拓展,如下:
class Mill3
{
public:
Mill3();
virtual ~Mill3();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
string packing(const string&material);//包装工序
public:
string processx(const string&material);//加工成品x完成
string processy(const string&material);//加工成品y完成
};
string Mill3::preprocess(const string&material)
{
return "*"+material+"---->";
}
string Mill3::handling(const string&material)
{
return preprocess(material)+"处理---->";
}
string Mill3::packing(const string&material)
{
return handling(material)+"包装---->";
}
string Mill3::processx(const string&material)
{
return packing(material)+"产品X";
}
string Mill3::processy(const string&material)
{
return packing(material)+"产品Y";
}
执行后结果如下:
5、纵向细分
因业务拓展,工厂除了生产产品X,还生产半成品,简单包装一下就可以了,不需要贴上产品X的商标。
因为有现有的材料预处理、处理、包装方法,为了方便起见,直接将类Mill3中的packaging方法,由private改为public,然后修改main类调用packaging方法,代码如下:
class Mill3
{
public:
Mill3();
virtual ~Mill3();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
public:
string packing(const string&material);//包装工序
string processx(const string&material);//加工成品x完成
string processy(const string&material);//加工成品y完成
};
int main(int argc, char* argv[])
{
Mill3* pMill3=new Mill3();
cout<<pMill3->processx("原料")<<endl;
cout<<pMill3->packing("原料")<<endl;
printf("Hello World!\n");
return 0;
}
运行结果如下图所示:
这样之前的问题又出现了,如果优化产品X的生产方案,需要改变原料预处理的方式,同样也会牵连到其他生产过程的变化,无论是纵向的还是横向的。所以,如果更改了原料预处理的方法,牵一发动全身的问题依然存在。
对于这个问题该怎么解决呢?
有很多人为了省事方便可能会采用下面的方法。
对于横向细分的情况,可以用Copy大法将类Mill3进行拆分,分成Mill3X和Mill3Y两个类,如下:
class Mill3x
{
public:
Mill3x();
virtual ~Mill3x();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
string packing(const string&material);//包装工序
public:
string processx(const string&material);//加工成品x完成
};
class Mill3y
{
public:
Mill3y();
virtual ~Mill3y();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
string packing(const string&material);//包装工序
public:
string processy(const string&material);//加工成品y完成
};
纵向细分的情况可以再拆出一个类
class subMill3
{
public:
subMill3();
virtual ~subMill3();
private:
string preprocess(const string&material);//材料预处理
string handling(const string&material);//处理工序
public:
string packing(const string&material);//包装工序
};
这样虽然解决了因职责横向细分或纵向细分导致的牵一发动全身的问题,但是这样就使代码完全失去复用性了,而且Mill3X,Mill3Y的职责真正单一了吗?从横向上看,Mill3X只生产产品X,Mill3Y只生产产品Y来看,类的职责是单一了,但是从纵向上看,两个类都有颗粒度更小的四个职责,原料预处理,加工,包装,产出成品,SubMill3也有颗粒度更小的三个职责,原料预处理,加工,包装。
六、符合设计模式的解决方案
单一职责要求我们在编写类,抽象类,接口时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。
按照这个原则对于工厂类Mill我们重新调整一下实现方案。
1、首先,将四个职责抽取成以下四个接口
class IPreProcess
{
public:
virtual string preProcess(const string&material)=0;
};
class IProcess
{
public:
virtual string Process(const string&material)=0;
};
class IPacking
{
public:
virtual string Packing(const string&material)=0;
};
class Imill
{
public:
virtual string Mill(const string&material)=0;
};
2、然后,有四个职责类分别实现这四个接口,如下:
class CPreprocess:public IPreProcess
{
public:
CPreprocess();
virtual ~CPreprocess();
string preProcess(const string&material);
};
class CProcess:public IProcess
{
private:
IPreProcess* preprocess;
public:
CProcess(IPreProcess* p_preprocess);
virtual ~CProcess();
string Process(const string&material);
};
class CPacking:public IPacking
{
private:
IProcess* process;
public:
CPacking(IProcess* p_process);
virtual ~CPacking();
string Packing(const string&material);
};
class CMillx:public Imill
{
private:
IPacking* packing;
public:
CMillx(IPacking* p_packing);
virtual ~CMillx();
string Mill(const string&material);
};
3、需求变动1:
如果增加产品Y的生产(产品Y与产品X除了包装不同之外,其它都一样),可以增加一个IMill的实现类CMillY。
class CMilly:public Imill
{
private:
IPacking* packing;
public:
CMilly(IPacking* p_packing);
virtual ~CMilly();
string Mill(const string&material);
};
Main函数如下:
int main(int argc, char* argv[])
{
CMillx* pMillx=new CMillx(new CPacking(new CProcess(new CPreprocess())));
CMilly* pMilly=new CMilly(new CPacking(new CProcess(new CPreprocess())));
cout<<pMillx->Mill("原料")<<endl;
cout<<pMilly->Mill("原料")<<endl;
printf("Hello World!\n");
return 0;
}
运行结果如下:
4、需求变动2:
如果因市场需求,优化产品X的生产方案,改变原料预处理的方式,需要将预处理工序的”*”改成”#”,则可以可以增加一个原料预处理接口IPreProcess的实现类CPreprocessA,如下:
class CPreprocessA:public IPreProcess
{
public:
CPreprocessA();
virtual ~CPreprocessA();
string preProcess(const string&material);
};
string CPreprocessA::preProcess(const string&material)
{
return "#"+material+"--->";
}
Main函数修改为:
int main(int argc, char* argv[])
{
CMillx* pMillx=new CMillx(new CPacking(new CProcess(new CPreprocessA())));
CMilly* pMilly=new CMilly(new CPacking(new CProcess(new CPreprocess())));
cout<<pMillx->Mill("原料")<<endl;
cout<<pMilly->Mill("原料")<<endl;
printf("Hello World!\n");
return 0;
}
运行结果如下:
可以看到,在优化产品X的生产方案,改变原料预处理的方式的过程中,并没有改变产品Y的正常生产。
5、需求变动3:
如果想生产产品X的半成品的话,不需更改生产代码,只需在main函数中直接调用即可,如下:
int main(int argc, char* argv[])
{
CMillx* pMillx=new CMillx(new CPacking(new CProcess(new CPreprocessA())));
CMilly* pMilly=new CMilly(new CPacking(new CProcess(new CPreprocess())));
CPacking *packing=new CPacking(new CProcess(new CPreprocess));
cout<<pMillx->Mill("原料")<<endl;
cout<<pMilly->Mill("原料")<<endl;
cout<<packing->Packing("原料")<<endl;
printf("Hello World!\n");
return 0;
}
运行结果如下:
6、需求变动4:
即使哪天市场需求再变化,再优化产品X的生产方案,改变原料预处理的方式,只需再添加个类,实现预处理接口IPreProcess即可,如下:
class CPreprocessB:public IPreProcess
{
public:
CPreprocessB();
virtual ~CPreprocessB();
string preProcess(const string&material);
};
string CPreprocessB::preProcess(const string&material)
{
return "&"+material+"--->";
}
运行结果如下:
依然不会对其他逻辑造成影响。
七、结论
单一职责原则不是单单的将类的功能进行颗粒化拆分,拆分的越细越好,这样虽然可以保证类的功能职责的单一性,但是也会导致类的数量暴增,功能实现复杂,一个功能需要多个功能细分后的类来完成,会造成类的调用繁琐,类间关系交织混乱,后期维护困难。所以单一职责原则并不是要求类的功能拆分的越细越好,对类的功能细分需要有个度,细分到什么程度才是最合适呢,细分到在应对未来的拓展时,有且仅有一个因素导致其变更。
在一个项目中,类不是孤立存在的,是需要与其他类相互配合实现复杂功能的。所以类的职责和变更因素都是难以衡量的,会因所属项目的不同而不同。需要在长期的开发经验中慢慢摸索。
扫二维码用手机看