初识设计模式——设计原则

cuixiaogang

设计模式中的设计原则是指导软件开发人员进行软件设计的一般性准则,遵循这些原则可以提高软件的可维护性、可扩展性、可复用性等。下面详细介绍常见的几个设计原则及其应用场景。

单一职责原则(Single Responsibility Principle,SRP)

定义

一个类应该有且仅有一个引起它变化的原因,即一个类只负责一项职责。如果一个类承担的职责过多,就会导致职责耦合,一个职责的变化可能会影响到其他职责。

解释说明

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力,当变化发生时,设计会遭到意想不到的破坏。

应用场景举例

在一个电商系统中,有一个 User 类,最初设计时它既负责用户信息的管理(如保存、修改用户的基本信息),又负责用户登录和注册的业务逻辑。按照单一职责原则,我们可以将用户信息管理的功能封装到一个 UserInfoManager 类中,将用户登录和注册的功能封装到一个 UserAuthManager 类中。这样,当用户信息管理的业务规则发生变化时,只需要修改 UserInfoManager 类,而不会影响到用户登录和注册的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
// 用户类,仅用于存储用户的基本信息
class User {
private $id;
private $username;
private $email;

public function __construct($id, $username, $email) {
$this->id = $id;
$this->username = $username;
$this->email = $email;
}

public function getId() {
return $this->id;
}

public function getUsername() {
return $this->username;
}

public function getEmail() {
return $this->email;
}

public function setUsername($username) {
$this->username = $username;
}

public function setEmail($email) {
$this->email = $email;
}
}

// 用户信息管理类,负责用户信息的保存和修改
class UserInfoManager {
public function saveUserInfo(User $user) {
// 这里可以实现将用户信息保存到数据库的逻辑
echo "保存用户信息:ID={$user->getId()}, 用户名={$user->getUsername()}, 邮箱={$user->getEmail()}\n";
}

public function updateUserInfo(User $user) {
// 这里可以实现更新用户信息到数据库的逻辑
echo "更新用户信息:ID={$user->getId()}, 用户名={$user->getUsername()}, 邮箱={$user->getEmail()}\n";
}
}

// 用户认证管理类,负责用户的登录和注册
class UserAuthManager {
public function registerUser($username, $email) {
// 这里可以实现用户注册的逻辑,例如生成用户 ID、保存到数据库等
$userId = rand(1, 1000); // 简单模拟生成用户 ID
$user = new User($userId, $username, $email);
echo "用户注册成功:ID={$user->getId()}, 用户名={$user->getUsername()}, 邮箱={$user->getEmail()}\n";
return $user;
}

public function loginUser($username, $email) {
// 这里可以实现用户登录的逻辑,例如验证用户名和邮箱是否匹配等
echo "用户尝试登录:用户名={$username}, 邮箱={$email}\n";
// 简单模拟登录成功
echo "用户登录成功\n";
}
}

// 使用示例
// 创建用户认证管理对象
$userAuthManager = new UserAuthManager();
// 注册用户
$newUser = $userAuthManager->registerUser("john_doe", "john@example.com");

// 创建用户信息管理对象
$userInfoManager = new UserInfoManager();
// 保存用户信息
$userInfoManager->saveUserInfo($newUser);

// 修改用户信息
$newUser->setUsername("jane_doe");
$newUser->setEmail("jane@example.com");
// 更新用户信息
$userInfoManager->updateUserInfo($newUser);

// 用户登录
$userAuthManager->loginUser("jane_doe", "jane@example.com");
?>

开闭原则(Open Closed Principle,OCP)

定义

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,在不修改现有代码的基础上,通过扩展来实现新的功能。

解释

对一个已完成的类,如果需要修改或增加其中的功能,最好使用继承的方式对其扩展,尽量不要改动类的本身。

应用场景举例

假设我们有一个图形绘制系统,最初只有绘制圆形和矩形的功能。我们可以定义一个抽象的 Shape 类,其中包含一个抽象的 draw 方法,然后让 Circle 类和 Rectangle 类继承自 Shape 类,并实现 draw 方法。当需要添加绘制三角形的功能时,我们不需要修改现有的 Shape、Circle 和 Rectangle 类,只需要创建一个新的 Triangle 类,继承自 Shape 类并实现 draw 方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
// 定义抽象的 Shape 类
abstract class Shape {
// 抽象的 draw 方法,具体实现由子类完成
abstract public function draw();
}

// 定义 Circle 类,继承自 Shape 类
class Circle extends Shape {
private $radius;

public function __construct($radius) {
$this->radius = $radius;
}

// 实现 draw 方法,用于绘制圆形
public function draw() {
echo "绘制一个半径为 {$this->radius} 的圆形。\n";
}
}

// 定义 Rectangle 类,继承自 Shape 类
class Rectangle extends Shape {
private $width;
private $height;

public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}

// 实现 draw 方法,用于绘制矩形
public function draw() {
echo "绘制一个宽为 {$this->width},高为 {$this->height} 的矩形。\n";
}
}

// 定义 Triangle 类,继承自 Shape 类
class Triangle extends Shape {
private $side1;
private $side2;
private $side3;

public function __construct($side1, $side2, $side3) {
$this->side1 = $side1;
$this->side2 = $side2;
$this->side3 = $side3;
}

// 实现 draw 方法,用于绘制三角形
public function draw() {
echo "绘制一个边长分别为 {$this->side1}{$this->side2}{$this->side3} 的三角形。\n";
}
}

// 定义一个绘制图形的函数,接收一个 Shape 对象作为参数
function drawShape(Shape $shape) {
$shape->draw();
}

// 使用示例
// 创建一个圆形对象
$circle = new Circle(5);
// 调用 drawShape 函数绘制圆形
drawShape($circle);

// 创建一个矩形对象
$rectangle = new Rectangle(4, 6);
// 调用 drawShape 函数绘制矩形
drawShape($rectangle);

// 创建一个三角形对象
$triangle = new Triangle(3, 4, 5);
// 调用 drawShape 函数绘制三角形
drawShape($triangle);
?>

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

定义

子类可以替换其父类并且出现在父类能够出现的任何地方,而不会影响程序的正确性。也就是说,子类应该能够完全替代父类,并且不会破坏原有的程序逻辑。

解释

子类型必须能够替换掉他们的父类型。

应用场景举例

在一个几何图形系统中,有一个 Rectangle 类和一个 Square 类。从数学角度看,正方形是一种特殊的长方形,但如果在代码中简单地让 Square 类继承自 Rectangle 类,可能会违反里氏替换原则。因为长方形的长和宽可以独立变化,而正方形的边长是相等的。如果一个方法接受一个 Rectangle 对象作为参数,并对其长和宽进行独立操作,当传入一个 Square 对象时,就可能会导致逻辑错误。为了遵循里氏替换原则,我们可以重新设计类的结构,让 Rectangle 和 Square 都继承自一个更抽象的 Quadrilateral 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
// 定义抽象的四边形类
abstract class Quadrilateral {
// 抽象方法,用于计算面积,具体实现由子类完成
abstract public function getArea();
}

// 定义长方形类,继承自四边形类
class Rectangle extends Quadrilateral {
protected $width;
protected $height;

public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}

// 设置宽度
public function setWidth($width) {
$this->width = $width;
}

// 设置高度
public function setHeight($height) {
$this->height = $height;
}

// 实现计算面积的方法
public function getArea() {
return $this->width * $this->height;
}
}

// 定义正方形类,继承自四边形类
class Square extends Quadrilateral {
protected $side;

public function __construct($side) {
$this->side = $side;
}

// 设置边长
public function setSide($side) {
$this->side = $side;
}

// 实现计算面积的方法
public function getArea() {
return $this->side * $this->side;
}
}

// 一个通用的计算面积的函数,接收一个四边形对象作为参数
function calculateArea(Quadrilateral $quadrilateral) {
return $quadrilateral->getArea();
}

// 使用示例
// 创建一个长方形对象
$rectangle = new Rectangle(5, 10);
// 计算长方形的面积
$rectangleArea = calculateArea($rectangle);
echo "长方形的面积是: ". $rectangleArea. "\n";

// 创建一个正方形对象
$square = new Square(7);
// 计算正方形的面积
$squareArea = calculateArea($square);
echo "正方形的面积是: ". $squareArea. "\n";
?>

依赖倒转原则(DIP)

定义

高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。简单来说,就是要依赖接口或抽象类,而不是具体的实现类。

解释

主要有两个原则:

  • 高层模块不应该依赖底层模块。两个应该依赖抽象
  • 抽象不应该依赖细节,细节应该依赖抽象

应用场景举例

在一个电商系统中,有一个 OrderService 类负责处理订单业务,最初它直接依赖于 MySQLOrderDao 类来实现订单数据的持久化。按照依赖倒置原则,我们可以定义一个抽象的 OrderDao 接口,其中包含订单数据操作的方法,然后让 MySQLOrderDao 类实现这个接口。OrderService 类只依赖于 OrderDao 接口,而不依赖于具体的 MySQLOrderDao 类。这样,当需要更换数据库为 Oracle 时,只需要创建一个实现 OrderDao 接口的 OracleOrderDao 类,并在 OrderService 中注入该类的实例即可,而不需要修改 OrderService 类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
// 定义 OrderDao 接口
interface OrderDao {
public function saveOrder($order);
public function getOrder($orderId);
}

// 实现 MySQLOrderDao 类,实现 OrderDao 接口
class MySQLOrderDao implements OrderDao {
public function saveOrder($order) {
echo "将订单 {$order} 保存到 MySQL 数据库。\n";
}

public function getOrder($orderId) {
echo "从 MySQL 数据库获取订单 ID 为 {$orderId} 的订单。\n";
return "订单详情:ID 为 {$orderId} 的订单信息";
}
}

// 实现 OracleOrderDao 类,实现 OrderDao 接口
class OracleOrderDao implements OrderDao {
public function saveOrder($order) {
echo "将订单 {$order} 保存到 Oracle 数据库。\n";
}

public function getOrder($orderId) {
echo "从 Oracle 数据库获取订单 ID 为 {$orderId} 的订单。\n";
return "订单详情:ID 为 {$orderId} 的订单信息";
}
}

// 定义 OrderService 类,依赖于 OrderDao 接口
class OrderService {
private $orderDao;

public function __construct(OrderDao $orderDao) {
$this->orderDao = $orderDao;
}

public function createOrder($order) {
$this->orderDao->saveOrder($order);
}

public function getOrderDetails($orderId) {
return $this->orderDao->getOrder($orderId);
}
}

// 使用示例
// 使用 MySQLOrderDao
$mysqlOrderDao = new MySQLOrderDao();
$orderServiceWithMySQL = new OrderService($mysqlOrderDao);
$orderServiceWithMySQL->createOrder("订单1");
$orderDetailsFromMySQL = $orderServiceWithMySQL->getOrderDetails(1);
echo $orderDetailsFromMySQL . "\n";

// 更换为 OracleOrderDao
$oracleOrderDao = new OracleOrderDao();
$orderServiceWithOracle = new OrderService($oracleOrderDao);
$orderServiceWithOracle->createOrder("订单2");
$orderDetailsFromOracle = $orderServiceWithOracle->getOrderDetails(2);
echo $orderDetailsFromOracle . "\n";
?>

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

定义

客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。也就是说,要将大的接口拆分成多个小的、具体的接口,让客户端只依赖它需要的接口。

应用场景举例

在一个系统中,有一个 Employee 接口,其中包含了 work、attendMeeting、writeReport 等方法。对于普通员工来说,可能只需要实现 work 方法;而对于管理人员来说,可能需要实现 attendMeeting 和 writeReport 方法。如果让所有员工类都实现这个大的 Employee 接口,会导致一些类实现了它们不需要的方法。按照接口隔离原则,我们可以将 Employee 接口拆分成 Worker 接口(包含 work 方法)、Manager 接口(包含 attendMeeting 和 writeReport 方法),让不同类型的员工类只实现它们需要的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
// 定义 Worker 接口,包含 work 方法
interface Worker {
public function work();
}

// 定义 Manager 接口,包含 attendMeeting 和 writeReport 方法
interface Manager {
public function attendMeeting();
public function writeReport();
}

// 定义普通员工类,实现 Worker 接口
class RegularEmployee implements Worker {
public function work() {
echo "普通员工正在努力工作。\n";
}
}

// 定义管理人员类,实现 Manager 接口
class ManagerEmployee implements Manager {
public function attendMeeting() {
echo "管理人员正在参加会议。\n";
}

public function writeReport() {
echo "管理人员正在撰写报告。\n";
}
}

// 使用示例
// 创建普通员工对象
$regularEmployee = new RegularEmployee();
$regularEmployee->work();

// 创建管理人员对象
$managerEmployee = new ManagerEmployee();
$managerEmployee->attendMeeting();
$managerEmployee->writeReport();
?>

迪米特法则(Law of Demeter,LoD)

也称为最少知识原则

定义

一个对象应该对其他对象有最少的了解。也就是说,一个类应该尽量减少与其他类的直接交互,如果两个类之间不必直接通信,那么这两个类就不应该发生直接的相互作用。如果需要通信,可以通过第三方来转发。

应用场景举例

在一个学校管理系统中,有一个 Student 类和一个 Teacher 类,最初 Student 类需要获取 Teacher 类的一些信息,直接与 Teacher 类进行交互。按照迪米特法则,我们可以引入一个 SchoolManager 类作为第三方,Student 类只与 SchoolManager 类进行交互,由 SchoolManager 类来负责与 Teacher 类进行通信并获取所需信息。这样,Student 类就不需要了解 Teacher 类的具体实现细节,减少了类之间的耦合度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
// 定义 Teacher 类
class Teacher {
private $name;
private $subject;

public function __construct($name, $subject) {
$this->name = $name;
$this->subject = $subject;
}

public function getName() {
return $this->name;
}

public function getSubject() {
return $this->subject;
}
}

// 定义 SchoolManager 类
class SchoolManager {
private $teachers = [];

public function addTeacher(Teacher $teacher) {
$this->teachers[] = $teacher;
}

public function getTeacherInfo($teacherIndex) {
if (isset($this->teachers[$teacherIndex])) {
$teacher = $this->teachers[$teacherIndex];
return [
'name' => $teacher->getName(),
'subject' => $teacher->getSubject()
];
}
return null;
}
}

// 定义 Student 类
class Student {
private $schoolManager;

public function __construct(SchoolManager $schoolManager) {
$this->schoolManager = $schoolManager;
}

public function getTeacherDetails($teacherIndex) {
return $this->schoolManager->getTeacherInfo($teacherIndex);
}
}

// 使用示例
// 创建 SchoolManager 对象
$schoolManager = new SchoolManager();

// 添加教师到 SchoolManager
$teacher1 = new Teacher("张老师", "数学");
$teacher2 = new Teacher("李老师", "英语");
$schoolManager->addTeacher($teacher1);
$schoolManager->addTeacher($teacher2);

// 创建 Student 对象并传入 SchoolManager
$student = new Student($schoolManager);

// 学生通过 SchoolManager 获取教师信息
$teacherDetails = $student->getTeacherDetails(0);
if ($teacherDetails) {
echo "教师姓名: ". $teacherDetails['name']. "\n";
echo "教授科目: ". $teacherDetails['subject']. "\n";
}
?>

合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)

定义

尽量使用合成 / 聚合,而不是使用继承来达到复用的目的。合成(Composition)和聚合(Aggregation)是关联关系的两种特殊情况,它们都表示整体与部分的关系,但程度有所不同。

解释

  • 聚合:是一种弱的 “拥有” 关系,体现的是 A 对象可以包含 B 对象,但 B 对象不是 A 对象的一部分,部分可以脱离整体而存在。例如,学校和老师之间就是聚合关系,老师可以独立于学校而存在,即使学校解散了,老师依然可以存在。
  • 合成:是一种强的 “拥有” 关系,体现了严格的部分和整体的关系,部分和整体的生命周期是一致的。例如,人和心脏的关系就是合成关系,心脏是人的一部分,心脏不能脱离人而单独存在。

应用场景举例

聚合场景

在公司管理系统中,公司(Company)和员工(Employee)之间是聚合关系。公司拥有员工,但员工可以独立于公司存在,即使公司倒闭,员工依然可以在其他地方工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
// 定义员工类
class Employee {
private $name;

public function __construct($name) {
$this->name = $name;
}

public function getName() {
return $this->name;
}
}

// 定义公司类
class Company {
private $employees = [];

public function addEmployee(Employee $employee) {
$this->employees[] = $employee;
}

public function displayEmployees() {
foreach ($this->employees as $employee) {
echo $employee->getName() . "\n";
}
}
}

// 使用示例
$employee1 = new Employee("张三");
$employee2 = new Employee("李四");

$company = new Company();
$company->addEmployee($employee1);
$company->addEmployee($employee2);

$company->displayEmployees();
?>

合成场景

在电脑系统中,电脑(Computer)和 CPU(Central Processing Unit)之间是合成关系。CPU 是电脑的核心组成部分,没有 CPU 电脑就无法正常工作,并且 CPU 的生命周期和电脑的生命周期是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
// 定义 CPU 类
class CPU {
private $model;

public function __construct($model) {
$this->model = $model;
}

public function run() {
echo $this->model . " CPU 正在运行\n";
}
}

// 定义电脑类
class Computer {
private $cpu;

public function __construct($cpuModel) {
$this->cpu = new CPU($cpuModel);
}

public function start() {
$this->cpu->run();
}
}

// 使用示例
$computer = new Computer("Intel Core i7");
$computer->start();
?>