【设计模式】面向对象设计
面向对象设计
相关文章
面向对象设计
一、什么是面向对象设计
面向对象设计(Object-Oriented Design,OOD)是一种软件设计方法,它将现实世界中的事物看作是对象,并将系统设计为一组相互作用的对象。在面向对象设计中,每个对象都包含数据和操作数据的方法,而对象之间通过消息传递来进行通信和交互。
面向对象设计的主要思想是将系统分解为多个独立的对象,每个对象都有自己的属性和方法,并且可以与其他对象进行交互,从而实现系统的功能。这种设计方式具有以下几个核心特点:
- 封装(Encapsulation)
将数据和操作数据的方法封装在对象内部,通过限制对对象的访问来保护数据的完整性和安全性。
- 继承(Inheritance)
允许一个对象继承另一个对象的属性和方法,并且可以通过扩展或修改已有的功能来创建新的对象。
- 多态(Polymorphism)
允许不同的对象对同一个消息做出不同的响应,使得系统更加灵活和易于扩展。
二、面向对象设计的基本原则
- 单一职责原则(Single Responsibility Principle,SRP)
一个类应该只有一个引起它变化的原因,或者说一个类应该只有一个职责。这可以提高代码的可维护性和可读性。
- 开放-封闭原则(Open-Closed Principle,OCP)
软件实体(类、模块、函数等)应该是可扩展的,但不可修改的。这意味着对于扩展是开放的,可以通过添加新的代码来扩展功能,但对于修改是封闭的,不应该修改现有的代码。
- 里氏替换原则(Liskov Substitution Principle,LSP)
子类型必须能够替换其基类型。换句话说,派生类必须能够在不改变程序正确性的前提下替换其基类。
- 依赖倒置原则(Dependency Inversion Principle,DIP)
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。简单来说,依赖倒置原则要求通过抽象来减少模块之间的直接依赖,从而提高系统的灵活性和可扩展性。
- 接口隔离原则(Interface Segregation Principle,ISP)
客户端不应该被迫依赖于它们不使用的接口。应该将大接口拆分成多个小接口,以便客户端只需知道与其相关的接口。
- 合成/聚合复用原则(Composition/Aggregation Reuse Principle,CARP)
优先使用对象组合(合成)或聚合,而不是继承来达到代码复用的目的。这样做可以避免继承导致的紧耦合和类的脆弱性。
- 最少知识原则(Law of Demeter,LoD)
一个对象应该对其他对象有尽可能少的了解,也就是说,一个对象不应该直接调用其他对象的内部方法,而应该通过其自身的方法或者通过依赖注入来实现所需的功能。
三、单一职责原则
强调一个类或模块应该只有一个引起它变化的原因,或者说一个类或模块应该只有一个职责。这意味着一个类或模块只应该负责一种类型的任务或功能,而不应该承担过多的职责。
1 |
|
反面例子
1 |
|
在这个例子中,我们有一个 UserManagement 类,它负责用户管理的各个方面,包括添加用户、发送欢迎邮件和更新用户统计信息。这个类违反了单一职责原则,因为它承担了太多的职责。
正面例子
1 |
|
我们将功能分解为 UserManager、EmailService 和 StatisticsService 三个类,每个类只负责一个明确的任务。UserManager 负责添加用户,EmailService 负责发送邮件,StatisticsService 负责更新用户统计信息。这样做遵循了单一职责原则,使得代码更加清晰、可维护和可扩展。
三、开放-封闭原则
强调软件实体(类、模块、函数等)应该是可扩展的(Open for extension),但不可修改的(Closed for modification)。换句话说,当需要改变系统的行为时,应该通过扩展现有代码来实现,而不是修改现有的代码。
假设有一个订单处理系统,最初只处理在线订单,但后来要求系统也能够处理电话订单。初始设计可能是这样的:
1 |
|
反面例子
现在要求系统能够处理电话订单,我们可能会直接在 OrderProcessor 类中添加新的方法来处理电话订单:
1 |
|
这种修改违反了开放-封闭原则,因为它修改了现有的类,而不是通过扩展现有类的行为来添加新的功能。如果系统需要处理更多类型的订单,就需要不断地修改 OrderProcessor 类,导致类变得臃肿、复杂,同时增加了修改的风险。
正面例子
为了遵循开放-封闭原则,我们可以使用抽象类和多态性来实现。我们可以创建不同类型的订单处理器类,每个类负责处理特定类型的订单,例如 OnlineOrderProcessor 和 PhoneOrderProcessor:
1 |
|
这种设计遵循了开放-封闭原则,因为它通过扩展抽象类 OrderProcessor 来添加新的功能,而不是修改现有的类。这样做使得系统更加灵活,易于扩展,同时减少了修改现有代码的风险。
四、里氏替换原则
该原则是对继承的一种扩展,强调子类必须能够替换其基类(父类)而不影响程序的正确性。换句话说,父类的实例可以被替换为子类的实例,而程序的行为仍然保持一致。
假设我们有一个汽车类 Car,它有一个 startEngine() 方法用于启动引擎:
1 |
|
反面例子
假设我们有一个飞机类 Airplane,继承自 Car 类,但是它的 startEngine() 方法是无法启动的:
1 |
|
正面例子
假设我们有一个特斯拉类 Tesla,也继承自 Car 类,并且重写了 startEngine() 方法:
1 |
|
现在,特斯拉的引擎是静音启动的,所以 Tesla 类重写了 startEngine() 方法来反映这一点。在这种情况下,如果我们用 Tesla 的实例来替换 Car 的实例,程序的行为仍然是合理的,因为 Tesla 类仍然遵循了启动引擎的行为。
五、依赖倒置原则
强调高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现细节,而具体实现细节应该依赖于抽象。
假设我们有一个报告生成器,它用于生成各种格式的报告,比如 PDF 格式和 CSV 格式。我们可以定义一个报告接口 Report,然后让不同格式的报告类来实现这个接口:
1 |
|
反面例子
1 |
|
如果我们在 ReportGenerator 类中直接实例化具体的报告类,那么 ReportGenerator 类就会依赖于具体的实现细节,违反了依赖倒置原则:
正面例子
1 |
|
我们的报告生成器类 ReportGenerator 将依赖于 Report 接口,而不是具体的报告类:
六、接口隔离原则
强调客户端不应该依赖于它不需要的接口,应该将接口细分成更小的、更具体的部分,以便客户端只需要知道与其相关的接口。
假设我们有一个电子设备接口 ElectronicDevice,它定义了一系列操作电子设备的方法,包括开机、关机和调节音量等:
1 |
|
反面例子
然后我们有一个电视类 Television,它实现了 ElectronicDevice 接口:
1 |
|
现在,假设我们引入了一个只有音响功能的音响类 Speaker,但是它不需要开机和关机功能,那么我们的 Speaker 类还是不得不实现这两个方法:
1 |
|
在这个例子中,虽然 Speaker 类只需要调节音量的功能,但是它仍然不得不实现 powerOn() 和 powerOff() 这两个方法,这就违反了接口隔离原则。
正面例子
现在,我们将 ElectronicDevice 接口拆分成更小的接口,分别是 Powerable 和 VolumeAdjustable:
1 |
|
然后我们的 Television 类只需实现 Powerable 和 VolumeAdjustable 接口中的方法:
1 |
|
而 Speaker 类只需实现 VolumeAdjustable 接口中的方法:
1 |
|
七、合成/聚合复用原则
它是对代码重用的一种指导方针,强调在设计类时应优先使用合成(Composition)或聚合(Aggregation)而不是继承(Inheritance)。
假设我们有一个 Person 类,它需要使用头、手和腿来描述一个人的属性。我们可以使用继承来设计这个类:
1 |
|
反面例子
假设我们有一个 Person 类,它需要使用头、手和腿来描述一个人的属性。我们可以使用继承来设计这个类:
1 |
|
在这个例子中,Person 类与 Head、Hand 和 Leg 类之间存在继承关系,这导致了类之间的高耦合,而且 Person 类可能会受到 Head、Hand 和 Leg 类的改变而影响。
正面例子
现在,让我们使用合成/聚合复用原则来重新设计 Person 类:
1 |
|
在这个例子中,Person 类通过合成的方式将 Head、Hand 和 Leg 对象组合到一起,而不是通过继承,这降低了类之间的耦合度,使得系统更加灵活、可扩展和易于维护。
八、最少知识原则
它指导我们设计类时应该尽量减少对象之间的交互,即一个类应该尽可能少地了解其他类的内部细节。这个原则的目标是降低系统的耦合度,使得系统更加灵活、可维护和可扩展。
反面例子
假设我们有一个购物车类 ShoppingCart,它需要获取用户信息,并根据用户信息显示相应的购物车内容。但是 ShoppingCart 类直接访问了用户类的内部信息:
1 |
|
在这个例子中,ShoppingCart 类直接调用了用户类的 getName() 方法来获取用户的姓名,然后根据用户信息显示购物车内容。这违反了最少知识原则,因为 ShoppingCart 类不应该直接访问用户类的内部信息,而应该通过用户类提供的接口来获取用户的姓名。
正面例子
现在,让我们使用最少知识原则来重新设计 ShoppingCart 类:
1 |
|
在这个例子中,ShoppingCart 类的构造函数接收用户的姓名作为参数,并将其保存在类的实例变量中。在显示购物车内容时,ShoppingCart 类直接使用保存的用户名,而不需要直接访问用户类的内部信息。这样做符合最少知识原则,因为 ShoppingCart 类只与需要的对象进行交互,而不涉及不相关的对象。
九、总结
单一职责原则(Single Responsibility Principle,SRP):
- 概要:一个类应该只有一个引起它变化的原因,即一个类应该只有一个职责。
- 目标:降低类的复杂度,提高类的内聚性和可维护性。
- 实现方法:将不同的功能分离到不同的类中,每个类只负责一个职责。
开放-封闭原则(Open/Closed Principle,OCP):
- 概要:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 目标:使得软件设计能够应对未来的变化,同时尽量减少对现有代码的修改。
- 实现方法:通过抽象、接口、继承、多态等方式来实现代码的可扩展性,从而避免直接修改现有代码。
依赖倒置原则(Dependency Inversion Principle,DIP):
- 概要:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
- 目标:降低模块之间的耦合度,提高系统的灵活性和可维护性。
- 实现方法:通过接口或抽象类定义模块之间的依赖关系,使得高层模块和低层模块都依赖于抽象,而不是具体实现。
接口隔离原则(Interface Segregation Principle,ISP):
- 概要:客户端不应该依赖于它不需要的接口,应该将接口细分成更小的、更具体的部分。
- 目标:降低接口的耦合度,提高系统的灵活性和可维护性。
- 实现方法:根据客户端的需要定义接口,将大接口细分成多个小接口,使得每个接口只包含客户端所需的方法。
合成/聚合复用原则(Composition/Aggregation Reuse Principle):
- 概要:优先使用合成(Composition)或聚合(Aggregation)而不是继承来实现代码的复用。
- 目标:降低类之间的耦合度,使得系统更加灵活、可维护和可扩展。
- 实现方法:通过将已有的类组合到新的类中来实现代码的复用,而不是通过继承已有的类。
最少知识原则(Principle of Least Knowledge,LoD):
- 概要:一个对象应当对其他对象有尽可能少的了解,不要直接与陌生对象通信,而应该通过对象的接口进行通信。
- 目标:降低对象之间的依赖关系,使得系统更加灵活、可维护和可扩展。
- 实现方法:封装对象的内部状态和行为,限制对象之间的通信,避免直接访问其他对象的内部状态。