设计模式的设计原则之2.0
七大原则
设计原则名称 | 作用 |
---|---|
单一职责原则 | 类的职责要单一,不能将太多的职责放在一个类中 |
开闭原则 | 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能 |
里氏代换原则 | 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象 |
依赖倒转原则 | 要针对抽象层编程,而不要针对具体类编程(抽象不应该依赖细节,细节应该依赖抽象) |
接口隔离原则 | 使用多个专门的接口来取代一个统一的接口 |
合成复用原则 | 在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系 |
迪米特法则 | 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互 |
前四种原则——》》》单一职责、里氏代换、开闭原则、依赖倒转
5、接口隔离(InterfaceSegregation Principle,ISP)
5.1、背景
我们生活中用到的智能手机和智能手表,它们有类似的接口功能,也有不同的功能,比如
- 手机(phone):看时间、看天气、听音乐、看视频
- 智能手表(smartwatch):看时间、看天气、测心率
-
一个接口中封装了太对的方法,导致
phone
和Smartwatch
这两个类中必须实现一些无用的方法。 -
这样的接口稳定性较差,如果
phone
需要增加一个方法的话,那么`Smartwatch这个实现类中也要相应的实现这个方法(当然方法体内是空的,但是必须要实现的)。 -
编码混乱,导致修改时难度增加(需要自己去区分开哪些是这个类中的方法,哪些是另外的一个类中的方法,这样额外增加了工作量)。
应该怎么设计?
将公共的部分抽取出来单独放在一个接口中,自己独有的行为放在相应的接口中,通过独有的这个接口去继承公共的接口,这样的话,就能很好的起到接口的隔离的作用。这个地方我只是举了这样的一个例子,公共的部分是show
,那么在实际的使用中,可能是别的相关的功能等。那样的话需要自己去对他们进行抽取。
5.2、定义
客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。
- 不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。
- 接口属于客户,不属于它所在的类层次结构
- 不要在一个接口里面放很多的方法,这样会显得这个类很臃肿。
- 接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加灵活轻便
- 何为最小的接口,即能够满足项目需求的相似功能作为一个接口,这样设计主要就是为了“高内聚”
注意:接口隔离和单一职责的区分
从功能上来看,接口隔离和单一职责两个原则具有一定的相似性。其实如果我们仔细想想还是有区别的。
(1)从原则约束的侧重点来说,接口隔离原则更关注的是接口依赖程度的隔离,更加关注接口的“高内聚”;而单一职责原则更加注重的是接口职责的划分。
(2)从接口的细化程度来说,单一职责原则对接口的划分更加精细,而接口隔离原则注重的是相同功能的接口的隔离。接口隔离里面的最小接口有时可以是多个单一职责的公共接口。
(3)单一职责原则更加偏向对业务的约束,接口隔离原则更加偏向设计架构的约束。这个应该好理解,职责是根据业务功能来划分的,所以单一原则更加偏向业务;而接口隔离更多是为了“高内聚”,偏向架构的设计。
5.3、特征
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点:
- 1、将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 2、接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 3、如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 4、使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 5、能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
5.4、应用
#include <iostream>
using namespace std;
class CommonInter {
public:
CommonInter() {}
virtual void showTime() = 0;
virtual void showWeather() = 0;
};
class phoneInter :public CommonInter
{
public:
phoneInter() {}
virtual void showTime()
{
cout << "showTime......" << endl;
}
virtual void showWeather()
{
cout << "showWeather......." << endl;
}
void listenMusic()
{
cout << "listenMusic......" << endl;
}
void watchVideo()
{
cout << "watchVideo......" << endl;
}
};
class watchInter :public CommonInter
{
public:
watchInter() {}
virtual void showTime()
{
cout << "showTime......" << endl;
}
virtual void showWeather()
{
cout << "showWeather......." << endl;
}
void checkHeartbeat()
{
cout << "checkHeartbeat......" << endl;
}
};
class phone
{
private:
phoneInter inter;
public:
void use1()
{
inter.showTime();
}
void use2()
{
inter.listenMusic();
}
};
class watch
{
private:
watchInter inter;
public:
void use1()
{
inter.showTime();
}
void use2()
{
inter.checkHeartbeat();
}
};
int main(int argc, char *argv[])
{
phone p;
p.use2();
watch w;
w.use2();
return 0;
}
6、迪米特原则(Law of Demeter,LoD)
也被称为最少知识原则(Least knowledge Principle,LKP)
6.1、背景
只与你的直接朋友交谈,不跟“陌生人”说话。(Talk only to your immediate friends and not to
strangers)
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
在迪米特法则中,对于一个对象,其朋友包括以下几类:
- (1) 当前对象本身(this);
- (2) 以参数形式传入到当前对象方法中的对象;
- (3) 当前对象的成员对象;
- (4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
- (5) 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
6.2、定义
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
- 不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达
- 那么当其中某一个模块发生修改时,就会尽量少地影响其他模块
6.3、特征
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
6.4、应用
在运用迪米特法则时要注意以下 6 点。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
- 举个例子来说,如果你使用 RMI 的方式传递一个对象 VO( Value Object),这个对象就必须使用 Serializable 接口,也就是把你的这个对象进行序列化,然后进行网络传输。突然有一天,客户端的 VO 对象修改了一个 属性的访问权限,从 private 变更为 public 了,如果服务器上没有做出响应的变更的话,就会报序列化失败。 这个应该属于项目管理范畴,一个类或接口客户端变更了,而服务端没有变更,那像话吗?!
#include <iostream>
#include<string>
using namespace std;
//明星
class Star
{
private:
string _name;
public:
Star(string name):_name(name) {}
string getName()
{
return _name;
}
};
//粉丝
class Fans
{
private:
string _name;
public:
Fans(string name) :_name(name) {}
string getName()
{
return _name;
}
};
//媒体公司
class Company
{
private:
string _name;
public:
Company(string name) :_name(name) {}
string getName()
{
return _name;
}
};
//经纪人
class Agent
{
private:
Star _myStar;
Fans _myFans;
Company _myCompany;
public:
Agent(Star myStar, Fans myFans, Company myCompany) :_myStar(myStar), _myFans(myFans), _myCompany(myCompany)
{ }
void setStar(Star myStar)
{
_myStar = myStar;
}
void setFans(Fans myFans)
{
_myFans = myFans;
}
void setCompany(Company myCompany)
{
_myCompany = myCompany;
}
void meeting()
{
cout << _myFans.getName() << "与明星" << _myStar.getName() << "见面了。" << endl;;
}
void business()
{
cout << _myCompany.getName() << "与明星" << _myStar.getName() << "洽淡业务。" << endl;
}
};
int main(int argc, char *argv[])
{
Star s ("KOBE");
Fans f ("KOBE_FANS");
Company c("CBA");
Agent agent(s,f,c);
agent.meeting();
agent.business();
Star s1("Lebron James");
agent.setStar(s1);
agent.business();
return 0;
}
7、合成复用原则(Composite Reuse Principle, CRP)
C++类与类之间的存在的几种关系以及UML类图简单说明(依赖、关联、聚合、组合、泛化(继承)、实现)
7.1、背景
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。下图是用继承关系
实现的汽车分类的类图。
从上图 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。
但如果改用组合关系实现就能很好地解决以上问题,其类图如下图所示。
1、聚合关系(Aggregation)
UML:带空心菱形的实线来表示,菱形指向整体
-
聚合(Aggregation)关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,是 has-a 的关系。
-
整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。
- 例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
2、组合关系(Composition)
UML :组合关系用带实心菱形的实线来表示,菱形指向整体
- 组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的。
- 在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。
- 例如,头和嘴的关系,没有了头,嘴也就不存在了。
7.2、定义
合成复用原则又叫组合/聚合复用原则。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
- 合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
7.3、特征(为什么使用合成/聚合复用,而不使用继承复用?)
1、采用组合或聚合复用时
可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能
- 1、优点
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
- 2、缺点:
- 就是系统中会有较多的对象需要管理。
2、继承复用
继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。继承是类型的复用。
- 1、优点
- (1) 新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。
- (2) 修改或扩展继承而来的实现较为容易。
- 2、缺点
- (1) 继承复用破坏包装,因为继承将超类的实现细节暴露给了子类。因为超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又叫“白箱”复用。
- (2) 如果超类的实现改变了,那么子类的实现也不得不发生改变。因此,当一个基类发生了改变时,这种改变会传导到一级又一级的子类,使得设计师不得不相应的改变这些子类,以适应超类的变化。
- (3) 从超类继承而来的实现是静态的,不可能在运行时间内发生变化,因此没有足够的灵活性。
7.4、应用
#include<iostream>
#include<vector>
#include<string>
#include<iterator>
using namespace std;
/*
合成复用原则:
继承和组合优先使用组合,避免继承带来的麻烦
人开不同的车,不必人去继承车类,而使用组合,
把车组合进人里面进行调用
*/
class AbstractCar
{
public:
virtual void run()=0;
};
class BMW : public AbstractCar
{
public:
virtual void run()
{
cout << "BMW is run."<<endl;
}
};
class Fort : public AbstractCar
{
public:
virtual void run()
{
cout << "Fort is run."<<endl;
}
};
class People
{
public:
void setCar(AbstractCar* car)
{
this->car=car;
}
void Drive()
{
car->run();
delete car;
}
private:
AbstractCar * car;
};
void test()
{
People* p=new People();
p->setCar(new BMW());
p->Drive();
p->setCar(new Fort());
p->Drive();
}
int main()
{
test();
return 0;
}
参考
1、https://zhuanlan.zhihu.com/p/24246822
2、http://c.biancheng.net/view/1330.html