面试官问你如何进行程序设计?——设计模式之七大原则——单一职责、里氏代换、开闭原则、依赖倒转以及C++简单实现

七大原则

设计原则名称 作用
单一职责原则 类的职责要单一,不能将太多的职责放在一个类中
开闭原则 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能
里氏代换原则 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象
依赖倒转原则 要针对抽象层编程,而不要针对具体类编程(抽象不应该依赖细节,细节应该依赖抽象)
接口隔离原则 使用多个专门的接口来取代一个统一的接口
合成复用原则 在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
迪米特法则 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互

1、单一原则(Single Responsibility Principle,SRP)

1.1、背景

作为一个Android工程师,如果不仅让我开发Android,还要从需求调研、到原型设计、再到测试、发布都一个人干。就好像一个“牛”类一般,身兼多职,可是工资不变又不给多发福利。而且“牛”类还有一个弊端就是越“牛”就越“牛”,领导有事第一个想到让你解决,你承担的工作也越来越多。当然相对的,做得越多错得越多,被领导批的也越多。多多少少这个“牛”类都会有所怨言,然后工作积极性下降,导致项目越做越糙。最后不是自己受不了离职滚蛋了,就是公司忍不了开了这个“牛”类。或者双方不离不弃,最后一起堕入地狱。
在这里插入图片描述
把整个项目想想成为一个公司,然后我们每一个职员在工作中则负责各司其职。回到前面所说的工作量:

  • 需求分析:需求调研
  • 产品经理:原型设计
  • 开发人员:程序开发
  • 测试人员:测试
  • 运营人员:发布

单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

1.2、定义

一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

  • 将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
  • 单一职责原则是实现高内聚、低耦合的指导方针,需要设计人员发现类的不同职责并将其分离

1.3、特征

单一职责的优点:

  1. 类的复杂性降低,实现什么职责都有清晰明确的定义。

  2. 可读性提高,复杂性降低。可维护性提高

  3. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,

1.4、应用

以软件开发流程简单说明:
1、需求分析:需求调研
2、开发人员:程序开发
3、测试人员:测试

#include <iostream>
using namespace std;

class softwareDevelop
{
public:
	void DemandResearch() {
		cout << "DemandResearch complete......" << endl;
	}

	void Programing() {
		cout << "Programing complete......." << endl;
	}

	void Testing() {
		cout << "Testing complete........" << endl;
	}

};

int main(void)
{
	
	softwareDevelop pro;
	pro.DemandResearch();
	
	pro.Programing();

	pro.Testing();
	return 0;
}

在这里插入图片描述
依据单一职责原则,这里可以使用一种更加清晰的封装方式:

扫描二维码关注公众号,回复: 11470596 查看本文章
#include <iostream>
using namespace std;

class Demand
{
public:
	void DemandResearch() {
		cout << "DemandResearch complete......" << endl;
	}

};

class  Program
{
public:
	void Programing() {
		cout << "Programing complete......." << endl;
	}

};

class Test
{
public:
	void Testing() {
		cout << "Testing complete........" << endl;
	}

};

int main(void)
{
	
	Demand d;
	d.DemandResearch();
	
	Program p;
	p.Programing();

	Test t;
	t.Testing();
	return 0;
}

在这里插入图片描述

2、里氏替换原则(Liskov Substitution Principle,LSP )

2.1、背景

在现实生活中,什么是父子?就是生你的那个男人和你的关系就是父子(父女)。而这里定义的就是假如 A 能胜任 B 干的所有事情,那 B 就是 A 的父亲,也就是儿子要会父亲的所有能活,儿子活得再烂也要有父亲的水平。

价值观 :很显然,比较传统,严父出孝子。儿子必须要有父亲的能耐,最好青出于蓝胜于蓝。里氏替换原则定义了什么是父子,还有一点要注意的,就是儿子不能在父亲会的技能上搞“创新”。

2.2、定义

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

  • 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。

  • 由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

  • 尽量把父类设计成抽象类或接口,让子类继承父类或实现父接口。增加一个新功能时,通过增加一个新的子类来实现。

  • 子类中可以增加自己特有的方法。

  • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。

2.3、特征

优点

  • 1、代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 2、提高代码的重用性;
  • 3、子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  • 4、提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  • 5、提高产品或项目的开放性。

缺点

  • 1、继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  • 2、降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  • 3、增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。

2.4、应用

参考

3、开闭原则(open closed principle)

(“抽象约束、封装变化”)

3.1、背景

为什么要“开”和“闭”
一般情况,我们接到需求变更的通知,通常方式可能就是修改模块的源代码,然而修改已经存在的源代码是存在很大风险的,尤其是项目上线运行一段时间后,开发人员发生变化,这种风险可能就更大。所以,为了避免这种风险,在面对需求变更时,我们一般不修改源代码,即所谓的对修改关闭。不允许修改源代码,我们如何应对需求变更呢?答案就是我们下面要说的对扩展开放。

通过扩展去应对需求变化,就要求我们必须要面向接口编程,或者说面向抽象编程。所有参数类型、引用传递的对象必须使用抽象(接口或者抽象类)的方式定义,不能使用实现类的方式定义;通过抽象去界定扩展,比如我们定义了一个接口A的参数,那么我们的扩展只能是接口A的实现类。总的来说,开闭原则提高系统的可维护性和代码的重用性。

3.2、定义

一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭.即一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化.

  • 1、对扩展开放。模块对扩展开放,就意味着需求变化时,可以对模块扩展,使其具有满足那些改变的新行为。换句话说,模块通过扩展的方式去应对需求的变化。
  • 2、对修改关闭。模块对修改关闭,表示当需求变化时,关闭对模块源代码的修改,当然这里的“关闭”应该是尽可能不修改的意思,也就是说,应该尽量在不修改源代码的基础上面扩展组件。
  • 将相同的变化封装到一个接口或抽象类中
  • 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

3.3、特征

  1. 对软件测试的影响
    软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 可以提高软件的可维护性
    遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

3.4、应用

在这里插入图片描述
对于繁忙的业务员来说:

#include <iostream>
using namespace std;

/*
如果需要增加新的功能,需要再次添加新的成员函数,会导致类越来越复杂
*/

class BankWorker {

public:
	void save() {
		cout << "save money." << endl;
	}

	void transter() {
		cout << "transfer money." << endl;
	}

	void pay() {
		cout << "pay money." << endl;
	}
	/*
	如果在后期需要增加网银开通、贷款等业务,则需要在此处继续添加函数。
	*/
};


int main()
{
	BankWorker *bw = new BankWorker;

	bw->pay();
	bw->transter();
	bw->save();

	delete bw;
	bw = NULL;
	system("pause");
	return 0;
}

在这里插入图片描述

问题来了:对于银行业务员类(BankWorker)的设计就违背了开闭原则。因为如果后期需要添加新的功能,就不得不修改类的源代码。

符合开闭原则的思路设计代码

#include <iostream>
using namespace std;

class BankWorker {
public:/*纯虚函数的设计用来抽象银行业务员的业务*/
	virtual void doBusiness() = 0;
};

/*创建存钱的银行员*/
class saveBankWorker : public BankWorker {
public:
	virtual void doBusiness() {
		cout << "save money." << endl;
	}
};

/*创建转账的银行员*/
class transferBankWorker : public BankWorker {
public:
	virtual void doBusiness() {
		cout << "transfer money." << endl;
	}
};

/*创建取钱的银行员*/
class payBankWorker :public BankWorker {
public:
	virtual void doBusiness() {
		cout << "pay money." << endl;
	}
};

/*后期如果需要增加新的功能,只需要再次集成一个新类实现业务函数即可*/
/*新增办理基金的银行员*/
class fundationBankWorker :public BankWorker {
	virtual void doBusiness() {
		cout << "fundation money." << endl;
	}
};

int main()
{
	/*
	C++产生多态的3个必要条件
	1、有继承,如saveBankWorker继承了BankWorker
	2、要有重写,这里的BankWorker类的doBusiness()函数是纯虚函数,
	就会被重写,这个函数也称之为接口函数
	3、父类指针指向子类对象
	*/

	BankWorker *bw = NULL;    //实例化一个父类指针

	bw = new saveBankWorker;  //将父类指针指向子类对象
	bw->doBusiness();         //调用业务函数
	delete bw;                //释放空间
	bw = NULL;                //将指针指向空,更加安全

	bw = new transferBankWorker;
	bw->doBusiness();
	delete bw;
	bw = NULL;

	bw = new payBankWorker;
	bw->doBusiness();
	delete bw;
	bw = NULL;

	system("pause");
	return 0;
}

在这里插入图片描述

4、依赖倒置原则(Dependence Inversion Principle,DIP)

4.1、背景

举个例子,现在你需要实现一个比萨店,你第一件想到的事情是什么?我想到的是一个比萨店,里面有很多具体的比萨,如:芝士比萨、素食比萨、海鲜比萨……
比萨店是上层模块,比萨是下层模块,如果把比萨店和它依赖的对象画成一张图,看起来是这样:

在这里插入图片描述
没错!先从顶端开始,然后往下到具体类,但是,正如你看到的你不想让比萨店理会这些具体类,要不然比萨店将全都依赖这些具体类。现在“倒置”你的想法……别从上层模块比萨店开始思考,而是从下层模块比萨开始,然后想想看能抽象化些什么。你可能会想到,芝士比萨、素食比萨、海鲜比萨都是比萨,所以它们应该共享一个Pizza接口。对了,你想要抽象化一个Pizza。好,现在回头重新思考如何设计比萨店。

在这里插入图片描述
图一的依赖箭头都是从上往下的,图二的箭头出现了从下往上,依赖关系确实“倒置”

另外,此例子也很好的解释了“上层模块不应该依赖底层模块,它们都应该依赖于抽象。”,在最开始的设计中,高层模块PizzaStroe直接依赖低层模块(各种具体的Pizaa),调整设计后,高层模块和低层模块都依赖于抽象(Pizza)

4.2、定义

上层模块不应该依赖底层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

  • 要针对抽象(接口)编程,而不是针对实现细节编程。
  • 细节具有多变性,而抽象层则相对稳定
  • 任何类都不应该从具体类派生。
  • 使用继承时尽量遵循里氏替换原则。

4.3、特征

  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

4.4、应用

假设现在组装一台电脑需要cpu,硬盘,内存,这三种器件可以相互对接(我的意思是硬件设备可以直接连接使用),然后电脑厂商可以根据不同的cpu,硬盘,内存进行搭配不同样式的电脑,在后期的升级和维护中,可能会有新的cpu、硬盘、内存品牌增加进行相互组合。
在这里插入图片描述

#include "iostream"
using namespace std;


/*
    抽象层(中间层)
*/
class HardDisk{
public:
    virtual void work()=0;
};

class Memory{
public:
    virtual void work()=0;
};

class Cpu{
public:
    virtual void work()=0;
};


/*
    让Computer 框架和具体的电脑产商 解耦合
*/
/*
    高层架构层,依赖于抽象层(中间层)
*/
class Computer{
public:
    Computer(Cpu *mycpu, Memory *mem, HardDisk *hard){
        m_cpu = mycpu;
        m_mem = mem;
        m_hard = hard;
    }

    //高层业务函数,只关心每个硬件的业务(是否工作等),并不关心硬件是那些个产商生产的
    void work(){
        m_cpu->work();
        m_mem->work();
        m_hard->work();
    }


private:
    Cpu *m_cpu=NULL;
    Memory *m_mem=NULL;
    HardDisk *m_hard=NULL;
};

/*
    实现层(底层),只需要依赖于中间抽象层,实现抽象层的方法
*/

class XiJieHardDisk :public HardDisk{
public:
    virtual void work(){
        cout << "XiJie HardDisk working..." << endl;
    }
};

class InterCpu :public Cpu{
public:
    virtual void work()
    {
        cout << "Inter Cpu working..." << endl;
    }
};

class JSDMemory :public Memory{
public:
    virtual void work()
    {
        cout << "JSD Memory working" << endl;
    }
};

int main()
{
    XiJieHardDisk *xjdisk = new XiJieHardDisk;
    InterCpu *intercpu = new InterCpu;
    JSDMemory *jsdmemory = new JSDMemory;

    Computer *myComputer = new Computer(intercpu, jsdmemory, xjdisk);
    myComputer->work();

    delete xjdisk;
    delete intercpu;
    delete jsdmemory;
    delete myComputer;

    system("pause");
    return 0;
}

在这里插入图片描述

利用此种方法可以完美的解决后期新的品牌加入问题,同时将底层和高层进行分离,可以更好的管理自己的代码。

参考

1、https://www.cnblogs.com/WindSun/p/10223080.html
2、https://www.cnblogs.com/dolphin0520/p/3919839.html
3、https://www.jianshu.com/p/02926f3a5c1d
4、https://www.cnblogs.com/liebrother/p/10193334.html
5、https://zhuanlan.zhihu.com/p/24269134
6、https://blog.csdn.net/qq_21078557/article/details/78257558
7、https://www.cnblogs.com/lsgxeva/p/7773009.html
8、https://www.jianshu.com/p/c3ce6762257c
9、《大话设计模式》

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/107307394