设计模式里的几大原则:
- 单一职责原则(Single Responsibility Principle)
就一个类而言,应该仅有一个引起它变化的原因,如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
单一职责的好处:当然了拆分合适的细粒度可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性和系统的可维护性;对变更引起的风险降低,当修改一个功能时,可以显著降低对其他功能的影响。
实例:(一个类承担职责过多)
class Animal{
public:
void breathe(string animal){
cout<<animal<<"呼吸空气"<<endl;
}
};
一个动物类,当在测试时比如animal.breathe("牛"); animal.breathe("鱼"); 都是牛/鱼+呼吸空气,当然这违背了客观逻辑,按照单一职责原则我们就要拆分两个类。比如拆成呼吸空气的类和水中鱼类,这样还要修改测试点,可能开销优点大。
(虽然违反了单一职责,但但花销却小)
class Animal{
public:
void breathe(string animal){
if(animal=="鱼"){
cout<<animal<<"呼吸水"<<endl;
}else{
cout<<animal<<"呼吸空气"<<endl;
}
}
};
这样也存在隐患,如果来一个水陆两栖类鱼,我们又要修改类了,这种直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。其他不应该受到影响的也被波及。
class Animal{
public:
void breathe(string animal){
cout<<animal<<"呼吸水"<<endl;
}
void breathe1(string animal){
cout<<animal<<"呼吸空气"<<endl;
}
};
还可以增加一个方法,虽然破环了单一职责原则,因为它并没有动原来方法的代码,所以方法级别上却是符合单一职责原则的。总结下来:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;例子太简单了,它只有一个方法,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。
- 开放封闭原则(Open Close Principle,OCP)
软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。扩展-开放;修改-封闭。也就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则的引入:在软件的生命周期内,因为变化或者升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码引入错误,也可能我们不得不对整个功能进行重构,并且需要原代重新测试。
无论模块是多么封闭,都会存在一些无法对之封闭的变化,既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。在我们最初编写代码时,假设变化不会发生,当变化发生时,我们就创建抽象来隔离以后发生同类的变化。
开闭原则是面向对象设计的核心所在,遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出现频繁变化的那些部分做出抽象,然而对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意,拒绝不成熟的抽象和抽象本身一样重要。
class operation_base
{
public:
virtual ~operation_base() = default;
virtual double GetResult(double operation_a, double operation_b) const = 0;
};
class operation_add : public operation_base
{
virtual double GetResult(double operation_a, double operation_b) const override
{
return operation_a + operation_b;
}
};
需要增加新的计算种类自己扩展,不要修改原有基类。
- 依赖倒转原则(Dependence Inversion Principle)
1. 高层模块不应该依赖低层模块。两个都应该依赖抽象。
2. 抽象不应该依赖细节。细节应该依赖抽象。不管高层模块还是低层模块,它们都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其它受到影响,这就使得无论高层模块还是低层模块都可以很容易地被复用。
相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。那么这个准则带来的好处是:减少类间的耦合性,提高系统的稳定性;降低并行开发引起的风险;提高代码的可读性和可维护性。
实例:妈妈讲故事
class Book
{
public:
Book() = default;
void readbook()
{
cout << "从前有座山..." << endl;
}
};
class Mother
{
public:
void motherReadbook()
{
cout << "妈妈开始讲故事" << endl;
book.readbook();
}
private:
Book book;
};
如果新加一个读报纸的类,我们需要修改妈妈类才能做到读报纸不仅破坏了开闭原则,Mother与Book之间的耦合性太高了。
#include <iostream>
#include <memory>
using namespace std;
class Read
{
public:
virtual ~Read() = default;
virtual void read()const=0;
};
class Book : public Read
{
public:
virtual void read()const override
{
cout << "从前有座山..."<<endl;
}
};
class NewsPaper: public Read
{
virtual void read()const override
{
cout << "俄乌战争即将结束..."<<endl;
}
};
class Mother
{
public:
explicit Mother(Read *reads)
: motherRead(reads)
{
}
~Mother()
{
if (motherRead != nullptr)
{
delete motherRead;
motherRead = nullptr;
}
}
void read()const
{
if (motherRead != nullptr)
{
cout<<"妈妈开始讲故事..."<<endl;
motherRead->read();
}
}
private:
Read *motherRead = nullptr;
};
int main()
{
std::shared_ptr<Mother> mother(nullptr);
mother.reset(new Mother(new Book()));
mother->read();
mother.reset(new Mother(new NewsPaper()));
mother->read();
return 0;
}
无论以后怎样扩展,都不需要再修改 Mother 类了。实际情况中,代表高层模块的 Mother 类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
- 里氏代换原则(Liskov Substitution Principle, LSP)
子类型必须能够替换掉它们的父类型。一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化,简单的说,子类型必须能够替换掉它们的父类型[ASD]。
里氏替换原则是关于继承的一个原则,遵循里氏替换原则能够更好地发挥继承的作用,只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。
由于子类型的可替换性才使得使用父类类型的模块在无需求改的情况下就可以扩展。依赖倒转其实可以说是面相对象设计的标志,用哪种语言来编写程序并不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口,那就是面相对象的设计,反之那就是过程化的设计了[ASD]。
实例:子类必须能够替换掉它们的基类,且程序的行为不会发生任何变化。实现要求
1) 子类可以实现基类的抽象方法,但是必须遵守约定好的协议,即方法的前置条件和后置条件不能被放宽或缩小。
2)子类可以增加自己的方法。
3)当子类覆盖基类的方法时,方法的前置条件(即方法的输入参数)要比基类方法的输入参数更宽松。
4)子类不能重写基类的非抽象和非虚方法(否则做不到无变化的替换)。
#include <iostream>
#include <memory>
using namespace std;
class Animal {
public:
virtual void makeSound() = 0;
// void eat(){
// cout<<"碳水"<<endl;
// }
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Woof!" << std::endl;
}
// void eat(){
// cout<<"碳水化合物"<<endl;
// }
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Meow!" << std::endl;
}
};
void playSound(Animal* animal) {
animal->makeSound();
//animal->eat();
}
int main() {
Dog dog;
Cat cat;
playSound(&dog); // Output: Woof!
playSound(&cat); // Output: Meow!
return 0;
}
- 迪米特法则 (Law of Demeter, LoD)
迪米特法则(LoD,也叫最少知识原则):如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法对话,可以通过第三者转发这个调用。
迪米特法则首先强调对前提是在类对结构设计上,每一个类都应当尽量降低成员对访问权限[J&DP],也就是说,一个类包装好自己对private状态,不需要让别的类知道的字段或行为就不要公开。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在若耦合的类被修改,不会对有关系对类造成波及。
#include <string>
#include <iostream>
using namespace std;
class Course {
public:
std::string name;
Course(std::string name) : name(name) {}
void Info() {
std::cout << "Course Name: " << name << std::endl;
}
};
class Teacher {
private:
std::string name;
Course* course;
public:
Teacher(std::string name, Course* course) : name(name), course(course) {}
void teach() {
std::cout << name << " is teaching ";
course->Info();
}
};
class Student {
private:
std::string name;
Course* course;
public:
Student(std::string name, Course* course) : name(name), course(course) {}
void study() {
std::cout << name << " is studying ";
course->Info();
}
};
int main() {
Course math("english");
Teacher LI("Li", &math);
Student Lei("Lei", &math);
LI.teach();
Lei.study();
return 0;
}
这个实例里student和teacher类都依赖于course类,但它们之间不直接交互。student和teacher都通过 course对象来获取课程信息,这符合迪米特原则,因为它们只与它们直接需要的对象 course交互。通过这种方式减少了类之间的耦合,使得系统更加灵活和易于维护。如果将来需要修改课程信息的显示方式,我们只需要修改 course类,而不需要修改student和teacher类。
- 接口隔离原则(Interface Segregation Principle):
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。这有助于减少不必要的依赖,提高系统的灵活性。
在设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。即,一个类要给多个客户使用,那么可以为每个客户创建一个接口,然后这个类实现所有的接口;而不要只创建一个接口,其中包含所有客户类需要的方法,然后这个类实现这个接口。
在C++中,接口隔离原则可以通过以下方式实现:使用多重继承(继承多个接口);使用虚基类提供默认实现;使用组合(has-a)关系,将一个接口拆分成多个类,然后将这些类组合到一起以提供完整的接口。
#include <iostream>
// 假设有一个庞大的接口
class LargeInterface {
public:
virtual void operation1() = 0;
virtual void operation2() = 0;
virtual void operation3() = 0;
virtual void operation4() = 0;
};
// 拆分接口
class Interface1 {
public:
virtual void operation1() = 0;
virtual void operation2() = 0;
};
class Interface2 {
public:
virtual void operation3() = 0;
virtual void operation4() = 0;
};
// 实现类
class ConcreteClass1 : public Interface1 {
public:
void operation1() override {
std::cout << "operation1 in ConcreteClass1" << std::endl;
}
void operation2() override {
std::cout << "operation2 in ConcreteClass1" << std::endl;
}
};
class ConcreteClass2 : public Interface2 {
public:
void operation3() override {
std::cout << "operation3 in ConcreteClass2" << std::endl;
}
void operation4() override {
std::cout << "operation4 in ConcreteClass2" << std::endl;
}
};
// 组合方式提供完整接口
class ConcreteClass : public ConcreteClass1, public ConcreteClass2 {
// 可以添加额外的操作或者重写操作
};
int main() {
ConcreteClass c;
c.operation1(); // 调用ConcreteClass1的operation1
c.operation2(); // 调用ConcreteClass1的operation2
c.operation3(); // 调用ConcreteClass2的operation3
c.operation4(); // 调用ConcreteClass2的operation4
return 0;
}
LargeInterface 接口被拆分成了Interface1 和Interface2 两个较小的接口。ConcreteClass1和ConcreteClass2 分别实现了这两个较小的接口。ConcreteClass通过多重继承结合了ConcreteClass1 和ConcreteClass2,从而提供了LargeInterface 接口的所有功能。这样做既保持了接口的完整性,也使得不同的客户端可以只依赖它们所需要的接口。
组合/聚合复用原则(Composite/Aggregate Reuse Principle)
尽量使用合成/聚合,不要使用类继承。此原则也叫合成复用原则。
聚合表示一种弱的‘拥有’ 关系,体现的是A对象可以包含B对象,但是B对象不是A对象的一部分;合成则是一种强的’拥有‘ 关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。
class Engine {
public:
void start() {
// 启动发动机的逻辑
}
};
class Car {
public:
void startCar() {
engine.start();
}
Car(Engine engine) {
this->engine = engine;
}
private:
Engine engine;
};
这里car类就可以更灵活地复用其他类的功能,同时也降低了类之间的耦合度。根据组合/聚合复用原则大家需要首选组合,然后才能是继承,使用继承时需要严格的遵守里氏替换原则,务必满足“Is-A”的关系是才可以使用继承,而组合却是一种“Has-A”的关系。
// 继承反例
class Engine {
public:
void start() {
// 启动发动机的逻辑
}
};
// Car类通过继承Engine类来复用start方法
class Car :public Engine {
public:
void startCar() {
start();
}
};
这里继承之后创建的“car是一个发动机”的不合理关系,并引入了不必要的耦合。