文章目录
- 面向对象编程的八大原则
- 1 单一职责原则
- 2 开放-关闭原则
- 3 里氏替换原则
- 4 接口隔离原则
- 5 依赖倒置原则
- 6 迪米特法则/ 最少知识原则
- 7 合成复用原则
- 8 针对接口编程而不是针对实现编程
面向对象编程的八大原则
面向对象编程有一系列的设计准则来保证软件的质量,包括:单一职责原则,开放-关闭原则,里氏替换原则,接口隔离原则,依赖倒置原则,迪米特法则/ 最少知识原则,合成复用原则,针对接口编程而不是针对实现编程原则
1 单一职责原则
单一职责原则强调一个类只负责一个功能,仅有一个引起它变化的原因。这样在修改一个功能时,不会显著影响其他功能。
例如,图书管理员的职责是管理书籍的借还,而不是同时负责打扫卫生和修理设备。
正例:
class Librarian {
public:void manageBooks() {// 管理书籍借还}
};class Cleaner {
public:void cleanLibrary() {// 打扫卫生}
};class Technician {
public:void repairEquipment() {// 修理设备}
};
在这个例子中,每个类都只有一个职责:Librarian
负责管理书籍借还,Cleaner
负责打扫卫生,Technician
负责修理设备。这样职责明确,维护也更加方便。
反例:
class Librarian {
public:void manageBooks() {// 管理书籍借还}void cleanLibrary() {// 打扫卫生}void repairEquipment() {// 修理设备}
};
在这个例子中,Librarian
类不仅负责管理书籍借还,还负责打扫卫生和修理设备,这违反了单一职责原则。这样的类变得复杂且难以维护,更改书籍管理的代码可能会同时影响到清洁和修理设备的功能。
2 开放-关闭原则
开放-关闭原则强调软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,即在有新需求或变化时,通过扩展现有代码来实现新功能,而不是修改原有代码。
例如,插座设计时应该支持不同电器的插头(如电视、冰箱、洗衣机等),我们可以通过增加新的插头适配器来支持新电器,而不需要更改插座本身的设计。
正例:
// 电器接口
class Appliance {
public:virtual void plugIn() = 0; // 虚函数,表示插入电源virtual ~Appliance() {}
};// 电视类
class TV : public Appliance {
public:void plugIn() override {// 电视的插入电源逻辑std::cout << "TV is plugged in." << std::endl;}
};// 冰箱类
class Fridge : public Appliance {
public:void plugIn() override {// 冰箱的插入电源逻辑std::cout << "Fridge is plugged in." << std::endl;}
};// 插座类
class Socket {
public:void plugInAppliance(Appliance* appliance) {appliance->plugIn();}
};int main() {TV tv;Fridge fridge;Socket socket;socket.plugInAppliance(&tv);socket.plugInAppliance(&fridge);return 0;
}
在这个例子中,Appliance
是一个接口(抽象类),TV
和 Fridge
是具体实现。Socket
类通过接口来插入不同的电器,从而对扩展开放,对修改关闭。我们可以增加新的电器类,而不需要修改 Socket
类的代码。
反例:
// 插座类直接管理不同电器
class Socket {
public:void plugInTV() {// 电视的插入电源逻辑std::cout << "TV is plugged in." << std::endl;}void plugInFridge() {// 冰箱的插入电源逻辑std::cout << "Fridge is plugged in." << std::endl;}
};int main() {Socket socket;socket.plugInTV();socket.plugInFridge();return 0;
}
在这个例子中,Socket
类直接管理不同电器的插入逻辑。如果要增加新的电器类型,就需要修改 Socket
类的代码,违反了开放-关闭原则。
3 里氏替换原则
里氏替换原则要求子类能够替换其父类并出现在父类能够出现的任何地方,而不引起任何错误或异常。
正例
假设我们有一个基类 Shape
,它代表一个形状,并且有一个虚函数 draw()
来绘制形状。然后,我们有两个派生类 Circle
和 Rectangle
,分别代表圆形和矩形。
#include <iostream>class Shape {
public:virtual void draw() const {std::cout << "Drawing a generic shape." << std::endl;}virtual ~Shape() {}
};class Circle : public Shape {
public:void draw() const override {std::cout << "Drawing a circle." << std::endl;}
};class Rectangle : public Shape {
public:void draw() const override {std::cout << "Drawing a rectangle." << std::endl;}
};void drawShape(const Shape& shape) {shape.draw();
}int main() {Circle circle;Rectangle rectangle;drawShape(circle); // 调用 Circle 的 drawdrawShape(rectangle); // 调用 Rectangle 的 drawreturn 0;
}
在这个例子中,Circle
和 Rectangle
类都重写了 Shape
类的 draw()
方法,且它们的实现都是合理的,没有违反基类 Shape
的任何假设。因此,它们可以被 Shape
类型的引用或指针所替代,并且程序可以正常工作,符合里氏替换原则。
反例
假设我们修改了 Rectangle
类,增加了一个 setHeight()
方法来设置矩形的高度,但在 Shape
基类中并没有这个方法。现在,如果我们尝试在一个只接受 Shape
类型对象的函数中调用 setHeight()
,就会出现问题。
class Rectangle : public Shape {
public:void draw() const override {std::cout << "Drawing a rectangle." << std::endl;}// 添加了只在 Rectangle 类中存在的方法void setHeight(int height) {// 设置高度的逻辑}
};void modifyShape(Shape& shape) {// 尝试调用 setHeight(),但 Shape 没有这个方法,因此这里会编译失败shape.setHeight(10);
}int main() {// 尝试使用 modifyShape 将会失败,因为 Shape 没有 setHeight 方法Rectangle rectangle;modifyShape(rectangle);return 0;
}
这里 Rectangle
对象不能被当作 Shape
对象来安全地使用: Shape
类不应该有 setHeight()
方法,那么尝试在 Shape
类型的对象上调用 setHeight()
都是不合理的,这违反了里氏替换原则。
4 接口隔离原则
接口隔离原则要求客户端不应该依赖于它不需要的接口,即将大接口拆分为多个小接口,客户端只依赖于它需要的接口。
例如,手机上的应用程序可以只请求访问特定的功能模块(如相机、电话、短信),而不需要整合所有功能模块的超级接口。
正例:
// 功能接口
class Camera {
public:virtual void takePhoto() = 0;virtual ~Camera() {}
};class Phone {
public:virtual void call() = 0;virtual ~Phone() {}
};// 智能手机类,同时继承两个抽象类
class Smartphone : public Phone, public Camera {
public:void call() override {// 手机打电话的逻辑}void takePhoto() override {// 拍照的逻辑}
};int main() {Smartphone phone;Phone* phoneInterface = ☎Camera* cameraInterface = ☎phoneInterface->call(); // 正常使用电话功能cameraInterface->takePhoto(); // 正常使用相机功能return 0;
}
在这个例子中,Smartphone
类实现了 Phone
和 Camera
接口,但客户端可以根据需要选择依赖于 Phone
接口或 Camera
接口,符合接口隔离原则。
反例:
class SuperInterface {
public:virtual void call() = 0;virtual void takePhoto() = 0;
};// 智能手机类
class Smartphone : public SuperInterface {
public:void call() override {// 手机打电话的逻辑}void takePhoto() override {// 拍照的逻辑}
};int main() {Smartphone phone;SuperInterface* superInterface = ☎superInterface->call(); // 虽然只需要电话功能,但是依然必须实现所有接口中的方法,不符合接口隔离原则。return 0;
}
在这个负面例子中,Smartphone
类实现了一个大接口 SuperInterface
,包含了所有功能,尽管客户端只需要电话功能,仍需要实现拍照功能,违反了接口隔离原则。
5 依赖倒置原则
依赖倒置原则 要求高层模块不应依赖于低层模块,而是两者都应该依赖于抽象接口。
例如,USB接口能够连接不同类型的设备(如打印机、键盘、鼠标),这些设备通过共同的接口(USB接口)与电脑通信,而不需要知道每个设备的具体实现细节。
正例:
// 抽象设备接口
class Device {
public:virtual void operate() = 0; // 设备操作的抽象方法virtual ~Device() {}
};// 具体设备类:打印机
class Printer : public Device {
public:void operate() override {// 打印机操作的具体实现std::cout << "Printer is printing." << std::endl;}
};// 电脑类,依赖于设备接口
class Computer {
private:Device* device; // 电脑依赖于抽象的设备接口
public:Computer(Device* dev) : device(dev) {}void operateDevice() {device->operate(); // 通过抽象接口操作设备}
};int main() {Printer printer;Computer computer(&printer);computer.operateDevice(); // 电脑操作打印机return 0;
}
在这个例子中,Computer
类通过抽象的 Device
接口依赖于具体的 Printer
类,符合依赖倒置原则。
反例:
class Printer {
public:void operate() {// 打印机操作的具体实现std::cout << "Printer is printing." << std::endl;}
};// 电脑类,直接依赖于具体的打印机类
class Computer {
private:Printer printer; // 电脑直接依赖于具体的打印机类
public:void operateDevice() {printer.operate(); // 直接操作打印机}
};int main() {Computer computer;computer.operateDevice(); // 电脑直接操作打印机,违反了依赖倒置原则。return 0;
}
在这个负面例子中,Computer
类直接依赖于具体的 Printer
类,如果需要改变打印机为其他设备,就需要修改 Computer
类的代码,违反了依赖倒置原则。
6 迪米特法则/ 最少知识原则
最少知识原则要求一个对象应该尽可能少地了解其他对象,只和与之直接交互的对象(中介)通信,减少对象之间的耦合度。
例如,公司老板只通过秘书与外部供应商进行沟通,而不直接与供应商交流。
正例:
#include <iostream>
#include <string>// 供应商类
class Supplier {
public:void supply(const std::string& item) {std::cout << "Supplying " << item << "." << std::endl;}
};// 秘书类
class Secretary {
private:Supplier* supplier; // 秘书知道供应商的存在,但不直接与供应商交互
public:Secretary(Supplier* s) : supplier(s) {}void orderItem(const std::string& item) {supplier->supply(item); // 通过供应商供货}
};// 老板类
class Boss {
private:Secretary* secretary; // 老板只通过秘书与供应商交互
public:Boss(Secretary* sec) : secretary(sec) {}void placeOrder(const std::string& item) {secretary->orderItem(item); // 老板通过秘书订购物品}
};int main() {Supplier supplier;Secretary secretary(&supplier);Boss boss(&secretary);boss.placeOrder("500 units of paper");return 0;
}
在这个例子中,Boss
类只通过 Secretary
类与 Supplier
类进行交互,符合最少知识原则。
反例:
class Boss {
public:void placeOrder(Supplier* supplier, const std::string& item) {supplier->supply(item); // 老板直接与供应商交互,违反了最少知识原则}
};int main() {Supplier supplier;Boss boss;boss.placeOrder(&supplier, "500 units of paper");return 0;
}
在这个负面例子中,Boss
类直接与 Supplier
类交互,违反了最少知识原则。如果 Supplier
类的实现发生变化,那么 Boss
类也可能需要进行相应的修改,这增加了系统的维护成本和复杂度。
7 合成复用原则
合成复用原则强调尽量使用对象组合而不是继承来实现复用,通过将已有的对象纳入新对象中,作为新对象的成员变量来实现新功能。
如果子类和父类之间存在明显的“是一个(IS-A)”关系,即子类是父类的一种类型,可以使用继承。例如,Dog
继承自Animal
,因为狗是一种动物。但是,如果类之间存在明显的“有一个(HAS-A)”关系,即一个类拥有另一个类的实例,则应该使用合成。例如,Car
有一个Engine
,所以Car
类可以包含一个Engine
类的实例。
正例:
#include <iostream>class Engine {
public:void start() {std::cout << "Engine started" << std::endl;}
};class Car {
private:Engine engine; // Car拥有一个Engine对象public:void start() {engine.start(); // 调用Engine的start方法std::cout << "Car is now running" << std::endl;}
};int main() {Car myCar;myCar.start(); // 输出:Engine started 和 Car is now runningreturn 0;
}
在这个例子中,Car
类没有继承自Engine
类,而是将Engine
类的实例作为自己的成员变量。这样,Car
类就复用了Engine
类的功能,同时保持了类的独立性和封装性。
反例:
#include <iostream>class Engine {
protected:void start() { // 注意这里改为protected,以便子类可以访问std::cout << "Engine started" << std::endl;}
};// 错误地通过继承复用Engine的功能
class Car : public Engine {
public:void startCar() {start(); // 调用从Engine继承来的start方法std::cout << "Car is now running" << std::endl;}
};int main() {Car myCar;myCar.startCar(); // 输出:Engine started 和 Car is now runningreturn 0;
}
虽然这个反例在技术上可行,但它破坏了类的封装性和独立性。Car
类现在“是一个”Engine
,这在现实中显然是不合理的。此外,如果Engine
类有其他与汽车无关的功能或属性,那么这些都会被Car类继承,从而导致不必要的复杂性和潜在的错误。
8 针对接口编程而不是针对实现编程
针对接口编程,而不是针对实现编程,指的是在编程时应依赖于抽象接口,而不是具体实现。
假设你正在开发一个游戏,其中有一个角色系统。游戏中的角色可以有不同的类型,比如战士、法师和盗贼,每种角色都有自己独特的技能。
正例
// Character 接口
class Character {
public:virtual ~Character() {} // 虚析构函数virtual void attack() = 0; // 纯虚函数,要求子类必须实现virtual void defend() = 0; // 纯虚函数,要求子类必须实现
};// 战士类实现 Character 接口
class Warrior : public Character {
public:void attack() override {std::cout << "Warrior attacks fiercely!" << std::endl;}void defend() override {std::cout << "Warrior defends with shield." << std::endl;}
};// 角色系统使用接口编程
void battle(Character& character) {character.attack();character.defend();
}int main() {Warrior warrior;battle(warrior); // 传入 Warrior 对象,展示如何使用接口编程return 0;
}
如果我们针对一个接口(或基类)来编程,比如定义一个Character
接口,然后让战士、法师、盗贼等角色类都实现这个接口,那么我们就可以在不修改已有代码的情况下,通过添加新的角色类或者修改接口的实现来扩展或修改游戏的行为。
反例
// 直接使用具体类,没有接口
class Warrior {
public:void warriorAttack() {std::cout << "Warrior attacks fiercely!" << std::endl;}void warriorDefend() {std::cout << "Warrior defends with shield." << std::endl;}
};// 角色系统直接使用 Warrior 类
void warriorBattle(Warrior& warrior) {warrior.warriorAttack();warrior.warriorDefend();
}int main() {Warrior warrior;warriorBattle(warrior); // 如果添加新的角色类型,比如法师,需要修改 warriorBattle 或添加新的函数return 0;
}
如果我们直接针对每种角色的具体实现(即战士类、法师类、盗贼类)来编程,那么当我们需要添加一个新的角色类型或者修改某个角色的行为时,可能需要修改大量已经存在的代码。例如,如果我们需要添加一个新的角色类型(如法师),我们就需要修改warriorBattle
函数或者创建一个新的函数,这增加了代码的复杂性和维护成本。