软件设计的“SOLID”五大原则

本文详细介绍了面向对象设计中的五个重要原则:单一职责原则(SRP),确保每个类只有一个明确的职责;开放封闭原则(OCP),提倡对扩展开放,对修改封闭;里氏替换原则(LSP),保证子类可以替换基类而不影响程序行为;接口隔离原则(ISP),避免强迫客户端依赖未使用的接口;以及依赖倒置原则(DIP),强调依赖于抽象而非具体实现。这些原则有助于创建更稳定、可维护和可扩展的代码。
摘要由CSDN通过智能技术生成

一、单一职责

1、定义

单一职责原则(single Responsibility Principle,SRP)规定,每一个软件单元,其中包括组件、类和函数,应该只有一个单一且明确定义的职责。

2、特点

一个类应该仅有一个引起它变化的原因;

变化的方向隐含着类的责任;

遵循SRP的类通常很小而且具有很少的依赖性。它们清晰、易于理解,并且非常容易测试。

职责是一个比类的代码行数更好的标准。一个类可以有100、200甚至500行,如果这些类没有违反单一职责原则,那就完全没有问题。尽管如此,高LOC还是可以作为一个指标,它暗示:你应该注意这些类!也许一切都很好,但也许太大了,因为它们有更多的职责。

二、开放封闭原则

1、定义

所有的系统在其生命周期内都会发生变化。在开发预期比第一个版本持续时间更长的系统时,必须注意这一点。

——Ivar Jaocbson,Swedish computer scientist,1992    OCP(Open Close Principle)

2、特点

对扩展开放,对更改封闭;

类模块应该是可扩展,但是不可修改;

软件系统将随着时间的推移而发展,这是一个简单的事实。必须满足不断增加的新需求,并且现有的需求一定会随着客户的需求或技术的进度而不断改变。这些扩展不仅应该以优雅的方式实现,而且应该以尽可能小的代价完成,它们最好是在不需要更改现有代码的基础上被实现。如果任何新的需求都会导致软件现有且已经经过充分测试的部分发生一连串的变化和调整,那将是致命的。

在面向对象中,支持这一原则的一种方法就是继承。通过继承,可以在不修改类的情况下向类添加新功能。此外,还有许多面向对象的设计模式也支持OCP,例如策略模式或装饰器模式

三、里氏替换原则

1、定义

LSP(Liskov Substitution Principle

> 子类必须能够替换它们的基类(is-a)。

> 继承表达类型抽象。

2、规则

里氏替换原则分别为类层次结构制定了一下规则:

> 基类的前置条件不能在派生类中增强。

> 基类的后置条件不能在派生类中被削弱,也就是说派生类方法的后置条件(即方法的返回值)要比父类更严格。

> 基类的所有不变量(包括数据成员和函数成员)都不能通过派生子类更改或违反。

> 历史约束(即“历史规则”):对象的(内部)状态只能通过公共接口(封装)中方法调用来改变。由于派生类可能引入基类中不存在的新属性和方法,因此这些方法可能运行派生类的对象更改基类中那些不允许被改变的状态。所谓的历史约束就是禁止这一点。

3、需求

3.1、需求1

开发一个具有基本形状类型的类库,例如,Circle、Rectangle、Triangle和TextLabel。

UML类图设计如下:

 代码实现如下:

#include<memory>
#include<vector>
#include<string>
#include<iostream>
using namespace std;

class Point final
{
public:
	Point() :x{ 5 }, y{ 10 }{}
	Point(const uint16_t int_x, const uint16_t int_y) {
		x = int_x;
		y = int_y;
	}
private:
	uint16_t x;
	uint16_t y;
};

class Shape
{
public:
	Shape() :isVisible{ false } { }
	virtual ~Shape() = default;
	void moveTo(const Point& newCenterPoint) {
		hide();
		centerPoint = newCenterPoint;
		show();
	}
	virtual void show() { 
		isVisible = true; 
		std::cout << "--------show Shape---------" << std::endl;
	}
	virtual void hide() { 
		isVisible = false; 
		std::cout << "--------hide Shape---------" << std::endl;
	}
private:
	Point centerPoint;
	bool isVisible;
};

class Rectangle : public Shape 
{
public:
	Rectangle() : width{ 2 }, height{ 3 }{ }
	Rectangle(const uint16_t newWidth, const uint16_t newHeight) {
		width = newWidth;
		height = newHeight;
	}
	virtual void show() override {
		Shape::show();
		std::cout << "--------show Rectangle---------" << std::endl;
	}
	virtual void hide() override {
		Shape::hide();
		std::cout << "--------hide Rectangle---------" << std::endl;
	}
	virtual void setWidth(const uint16_t newWidth) {
		width = newWidth;
	}
	virtual void setHeight(const uint16_t newHeight) {
		height = newHeight;
	}
	void setEdges(const uint16_t newWidth, const uint16_t newHeight) {
		width = newWidth;
		height = newHeight;
	}
	uint64_t getArea() const {
		return static_cast<uint64_t>(width) * height;
	}
private:
	uint16_t width;
	uint16_t height;
};

class Circle : public Shape
{
public:
	void show() override {
		std::cout << "--------show Circle---------" << std::endl;
	}
	void hide() override{
		std::cout << "--------hide Circle---------" << std::endl;
	}
private:
	uint16_t redius;
};

class TextLabel : public Shape
{
public:
	void show() override {
		std::cout << "--------show TextLabel---------" << std::endl;
	}
	void hide() override {
		std::cout << "--------hide TextLabel---------" << std::endl;
	}
	void setText(std::string newText) {
		text = newText;
	}
private:
	std::string text;
};

using ShapePtr = std::shared_ptr<Shape>;
using shapeCollection = std::vector<ShapePtr>;

void showAllShape(const shapeCollection& shapes)
{
	for (auto& t : shapes)
	{
		t->show();
	}
}

int main()
{
	shapeCollection shapes;
	shapes.push_back(std::make_shared<Rectangle>());
	shapes.push_back(std::make_shared<Circle>());
	shapes.push_back(std::make_shared<TextLabel>());
	showAllShape(shapes);

	system("pause");
	return 0;
}

运行结果如下:

 3.2、需求2

在上述设计中,新增正方形。

方案1

从Rectangle派生一个新类Squre。

UML类如下:

但是方案1不是一个好的解决方案,Square提供一个带有两个参数的接口setEdges(违反了最少惊讶原则)会令人非常费解。如果使用两个相同的值,setWidth和setHeight接口将无法进行赋值。

方案2

从需求分析,正方形并不是矩形的子类型,因此使用组合而不是继承。为了不违反DRY原则,使用Rectangle类的实例作为Square的内部实现。

UML类图如下:

 代码实现:

class Square : public Shape
{
public:
	Square() {
		impl.setEdges(5, 5);
	}
	explicit Square(const uint16_t edgeLength) {
		impl.setEdges(edgeLength, edgeLength);
	}
	void setEdge(const uint16_t length) {
		impl.setEdges(length, length);
	}
	virtual void moveTo(const Point& newCenterPoint) override {
		impl.moveTo(newCenterPoint);
	}
	virtual void show() override {
		impl.show();
	}
	virtual void hide() override {
		impl.hide();
	}
	uint64_t getArea() const {
		return impl.getArea();
	}
private:
	Rectangle impl;
};

四、接口隔离原则

1、定义

接口隔离原则ISP(Interface Segregation Principle):

> 不应该强迫客户程序依赖它们不用的方法。

> 接口应该小而完备。

代码示例1:

class Bird
{
public:
	virtual ~Bird() = default;

	virtual void fly() = 0;
	virtual void eat() = 0;
	virtual void run() = 0;
	virtual void tweet() = 0;
};

// 麻雀
class Sparrow : public Bird
{
public:
	virtual void fly() override {}
	virtual void eat() override {}
	virtual void run() override {}
	virtual void tweet() override {}
};

// 企鹅
class Penguin : public Bird
{
public:
	virtual void fly() override {}
};

上述示例中虽然企鹅也是一只鸟,但是它无法飞翔。虽然接口相对较小,但声明的这些函数显然不能适用每一个具体的鸟类。 

接口隔离原则指出,我们应该将“宽接口”分离成更小且高度内聚的接口。生成的小接口也成为角色接口。

代码示例2:

class Lifeform
{
public:

	virtual void eat() = 0;
	virtual void move() = 0;
};
class Flyable
{
public:

	virtual void fly() = 0;
};
class Audible
{
public:

	virtual void makeSound() = 0;
};

// 麻雀
class Sparrow : public Lifeform, public Flyable, public Audible
{
public:
	//todo
};

// 企鹅
class Penguin : public Lifeform, public Audible
{
public:
	//todo
};

五、依赖倒置原则

1、定义

依赖倒置原则DIP(Dependence Inversion Principle):

> 高层模块(稳定)不应该依赖于低层模块(变化),二者都不应该依赖于抽象(稳定)。

> 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。

代码示例1:

#include<iostream>
using namespace std;

class Apple
{
public:
	void eaten()
	{
		std::cout << "正在吃苹果..." << std::endl;
	}
};
class Person
{
public:
	void eat(Apple* apple)
	{
		apple->eaten();
	}
};

int main()
{
	Person* jamin = new Person();
	Apple* apple = new Apple();

	jamin->eat(apple);
	delete apple;
	delete jamin;

	system("pause");
	return 0;
}

代码示例2:

#include<iostream>
using namespace std;

class Fruit
{
public:
	virtual void eaten() = 0;
};

class Apple : public Fruit
{
public: 
	void eaten() override
	{
		std::cout << "正在吃苹果..." << std::endl;
	}
};
class Banana : public Fruit
{
public:
	void eaten() override
	{
		std::cout << "正在吃香蕉..." << std::endl;
	}
};
class Person
{
public:
	void eat(Fruit* fruit)
	{
		fruit->eaten();
	}
};

int main()
{
	Person* jamin = new Person();
	Fruit* apple = new Apple();
	Fruit* banana = new Banana();

	jamin->eat(apple);
	jamin->eat(banana);
	delete apple;
	delete banana;
	delete jamin;

	system("pause");
	return 0;
}

解释:

要尽可能使用接口或抽象类。也就是“面向接口编程”或者说“面向抽象编程”,也就是说程序中要尽可能使用抽象类或是接口。

> 依赖倒置原则,主要是函数传参参数要尽可能使用抽象类或接口,这就是“高层模块不应该依赖底层模块,两者都应该依赖其抽象”的解释。就要求每个实现类都应该尽可能从抽象中派生,这就是上面“细节应该依赖抽象”。

> 每个类都尽量要有接口或抽象类,或者两者都有。

> 变量的表面类型尽量是接口或者抽象类(比如Fruit* apple=new Apple(); Fruit是表面类型,Apple是实际类型)。

> 任何类都不应该从具体类中派生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值