面向对象设计原则是一组指导软件设计的原则,其中GRASP(General Responsibility Assignment Software Patterns)是其中的一部分。这些原则帮助设计者确定类应该负责执行哪些职责,以及如何分配这些职责。在下面的文档中,我们将详细介绍九大GRASP原则,并提供Java语言的代码示例,以便更好地理解这些原则的应用。
原则: Information Expert原则建议一个类应该负责处理自身拥有的信息,或者说,一个拥有必要信息的类应该负责执行相关操作。
详细说明: Information Expert关注的是哪个类在执行一个操作时最具有相关的信息。如果某个类拥有完成某个职责所需要的所有信息,那么它就是信息专家。这有助于确保每个类都在处理与自身职责相关的任务。
理解:当我们不确定某个职责该分配给类A还是类B的时候,我们可以遵循这个原则。这个设计原则和单一设计原则不同,单一职责原则考虑的是单个类中的职责是否都属于一类职责。而信息专家模式考虑则是该把该同一类职责放进类A还是类B中。假设我们有一个长方形Rectangle类(类中有width和height属性)和一个Measure类,我们应该把getArea()方法放进Rectangle中去,还是将width和height参数传给Measure类,在Measure中实现getArea()呢?依照该准则,既然Rectangle方法已经有了实现getArea()所必须的属性的话,那么就把该把getArea()方法放进Retangle类中。同理如果有一个计算属性呢?假设是长宽高比例widthHeightRatio的话,也遵循该原则。
举例: 考虑一个图书管理系统,有一个Book
类表示图书信息:
class Book {
private String title;
private String author;
// 构造函数、访问器和其他方法
public void displayBookInfo() {
System.out.println("Title: " + title + ", Author: " + author);
}
}
在这个例子中,Book
类是信息专家,因为它拥有并处理有关图书的信息。
原则: Creator原则建议一个类应该创建与之关联或组合的类的实例。
如果符合下面的一个或者多个条件,则可将创建类A实例的职责分配给类B
详细说明: Creator原则指导在哪个类应该负责创建与其关联的对象实例。这有助于确保对象的创建与其使用者解耦,提高系统的灵活性和可维护性。
**理解:**在面向对象的设计当中,无法避免去创建对象。假设对象B创建对象A,那么对象B就产生了与对象A的耦合。而这种耦合是无法消除的,即使你将创建对象A的职责分配给对象C,这种耦合还是存在的,只是从对象B转移到对象C上,系统内还是依然存在这个耦合,无法避免。那么当我们无法消除耦合的时候,我们应该考虑的是如何降低这个耦合的耦合度。这个原则给出了指导方针。以上的几个条件潜在的表明了,其实B已经对A有了耦合,既然B已经存在了对A的耦合,那么我们不妨再将创建A的职责分配给他。这样分配的话,系统内仅存在一个A与B的耦合。如果将创建A的职责分配给C的话,那么系统内就会存在B与A(B包含A、B频繁使用A等条件)和C与A这两个耦合。
举例: 考虑一个在线购物系统,有一个ShoppingCart
类和一个Order
类:
class ShoppingCart {
public Order createOrder() {
return new Order();
}
}
在这个例子中,ShoppingCart
类是创建者,负责创建与其关联的Order
类的实例。
原则: 把接收或者处理系统事件消息的职责分配给一个类。这个类可以代表:整个系统、设备或者子系统;系统事件发生时对应的用例场景,在相同的用例场景中使用相同的控制器来处理所有的系统事件。。
详细说明: Controller原则有助于维护系统的一致性和可扩展性。它指导在哪个类应该负责协调和控制系统的活动。
**理解:**一个控制器是负责接收或者处理事件的组件对象。MVC模式中的C就是控制器模式。而一个控制器应该处理一类事件。例如我们项目中经常会有的UserController就承担添加用户,删除用户的事件。一个子系统需要定义多个控制器,分别对应不同的事件处理。一般来说,控制器应当把要完成的功能委托给Service或者其他业务处理对象,它只负责协调和控制业务流程,尽量不要包含太多业务逻辑。
举例: 假设有一个在线购物系统,包含ShoppingCart
和PaymentProcessor
两个类。在这里,Controller原则建议将购物车和支付的控制逻辑分配给一个独立的控制器类,比如ShoppingCartController
:
原则: Low Coupling原则建议尽量减少类之间的依赖关系。
以下是一些耦合关系的体现:
详细说明: 低耦合有助于系统的可维护性和可扩展性,因为当一个类的改变不会影响到其他类时,系统更容易进行修改和更新。
**理解:**在以上的这些耦合条件中,出现得越多代表耦合程度越高。这些条件简单笼统的来说就是A对B的“感知”。这种感知体现在对象属性、方法参数、方法返回值以及接口上面。高耦合的类过多地依赖其他类,这种设计将会导致:一个类的修改导致其他类产生较大影响,系统难以维护和理解。在重用一个高耦合的类时不得不重用它所依赖的其他类,系统重用性差。如何降低耦合的程度有以下一些方法:尽量减少对其他类的引用,提高方法和属性的访问权限,尽量使用组合/聚合原则来替代继承。其实面向对象编程中的多态就是一种降低类型耦合的方法,如果没有多态的话,我们的方法需要知道所有子类类型,而多态的话只需要知道父类即可。降低了类型耦合。
举例: 考虑一个图书馆管理系统,有一个Library
类和一个Book
类。使用聚合来减少类之间的依赖关系:
class Library {
private List<Book> books;
public void addBook(Book book) {
books.add(book);
}
}
在这个例子中,Library
类通过聚合的方式引入了Book
类,实现了低耦合。
原则: High Cohesion原则建议一个类应该有高度相关的职责,即一个类应该专注于一个功能领域。
详细说明: 高内聚有助于确保一个类的方法和属性彼此关联,从而提高类的可读性和可维护性。
**理解:**很直观的例子就是,如果类的功能都是高内聚并职责单一的,类的复杂性就降低了,复杂性降低导致维护的成本也就降低了。在传统的Dao设计模式当中,我们应该尽量拆分细粒度职责单一的Dao供Service进行调用。在Service当中,哪一类的数据操作调用哪一个Dao就显而易见,并且单个Dao不会太过膨胀导致维护性变差。高内聚也代表了高隔离,高隔离就意味着,在修改某一个方法的时候,不至于影响到太多其他类。
举例: 考虑一个汽车管理系统,有一个Car
类:
class Car {
private Engine engine;
private Transmission transmission;
// 相关的汽车功能方法
}
在这个例子中,Car
类具有高内聚性,因为它包含了与汽车相关的引擎和传动系统。
原则: Polymorphism原则建议使用多态性来实现通用性和灵活性。
详细说明: 多态性允许以通用的方式处理不同类型的对象,从而提高系统的灵活性和可扩展性。
**理解:**在面向对象的设计当中经常要根据对象的类型来进行对应的操作。假设我们有一个画图Draw类,有多个图形类Rectangle、Circle、Square。如果要按照不同图形类进行绘制的话,就需要在Draw类的方法中使用if-else的程序结构,依次判断类型进行绘制。如果新增一个图形类的话,就又需要对这段代码进行更改。这就违反了开闭原则。而采用多态的形式,将绘制的具体步骤交给图形类的子类实现。就不用使用if-else的程序结构,在新增图形类的时候也不需要修改Draw类。通过引入多态,子类对象可以覆盖父类对象的行为,更好地适应变化。策略模式、工厂方法模式就是关于多态比较好的例子。
举例: 考虑一个图形绘制系统,有一个Shape
接口和具体的实现类:
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
// 绘制圆形的逻辑
}
}
class Square implements Shape {
@Override
public void draw() {
// 绘制正方形的逻辑
}
}
在这个例子中,Shape
接口和具体的实现类展示了多态的概念。
原则: 原则建议可以创建一个不代表真实世界概念的类,以实现低耦合、高内聚的目标。
详细说明: 纯虚构的类可能不对应实际系统中的实体,但它们有助于组织和分离系统的不同部分,从而提高系统的可维护性。
**理解:**在OO设计时,系统内的大多数类都是来源于现实世界中的真实类(领域模型)。然而,在给这些类分配职责时,有可能会遇到一些很难满足低耦合高内聚的设计原则。纯虚构模式对这一问题给出的方案是:给人为制造的类分配一组高内聚的职责,该类并不代表问题领域的概念,而代表虚构出来的事物。比较明显的一个例子就是适配器模式,通过虚构出适配器这么一个概念来解耦两个对象之间的耦合。
适配器类举例
在适配器模式中,适配器类是一个纯虚构类,它没有对应于真实世界中的实体,而是引入为了解决两个不同接口之间的耦合问题。
适配器模式的场景:
假设有一个现有的系统,其中包含一个接口 OldInterface
,而你引入了一个新的类 NewClass
,它实现了一个新的接口 NewInterface
。现在,你想要在系统中使用 NewClass
,但是由于 OldInterface
和 NewInterface
不兼容,需要一个适配器来使它们协同工作。
适配器模式的类结构:
OldInterface
:现有系统的接口。NewInterface
:新引入的接口。NewClass
:实现了 NewInterface
的新类。Adapter
(适配器):这是纯虚构的类,它的唯一目的是将 NewClass
适配到 OldInterface
中。Java 代码示例:
// 现有系统的接口
interface OldInterface {
void oldMethod();
}
// 新引入的接口
interface NewInterface {
void newMethod();
}
// 新的类,实现了新接口
class NewClass implements NewInterface {
public void newMethod() {
System.out.println("NewClass implements NewInterface");
}
}
// 适配器类,纯虚构的类,目的是适配 NewClass 到 OldInterface
class Adapter implements OldInterface {
private NewClass adaptee;
public Adapter(NewClass adaptee) {
this.adaptee = adaptee;
}
public void oldMethod() {
adaptee.newMethod();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
NewClass newClass = new NewClass();
Adapter adapter = new Adapter(newClass);
// 使用适配器调用现有系统的接口
adapter.oldMethod();
}
}
在这个例子中,Adapter
类是一个纯虚构的类,它没有现实世界的对应。它的目的是通过调用 NewClass
的方法来适配 OldInterface
,从而使得现有系统能够与新的类协同工作,解决了接口不兼容的问题。这就是纯虚构的一个实际应用场景。
许多项目都需要对数据库进行操作,将系统中的一些对象进行持久化。信息专家模式给出的建议是将持久化的职责分配给具体的每一个模型类。但是这种建议已经被证明是不符合高内聚低耦合原则的。于是,现在的做法往往会在项目中加入类似于DAO或者Repository这样的类。这些类在领域模型中是并不存在的。
原则: Indirection原则建议通过引入一个中介者或者通过委托来降低类之间的耦合度。
详细说明: 通过间接方式减少类之间的直接依赖关系,有助于提高系统的灵活性和可维护性。
理解:“中介”简单来说就是通过一个中间人来处理一件事。本来直接联系的两个对象可以通过另一个中间对象进行交互,这样做便实现了隔离和解耦,一个对象的变动不会影响另一个对象,仅会影响到中间对象。在设计模式当中的适配器模式,桥接模式都采用了一个中间对象来进行解耦。
举例: 考虑一个购物系统,有一个PaymentGateway
类:
class PaymentGateway {
public void processPayment(Order order) {
// 处理支付逻辑
}
}
class ShoppingCart {
private PaymentGateway paymentGateway;
public void checkout() {
paymentGateway.processPayment(this.order);
}
}
在这个例子中,ShoppingCart
类通过间接的方式使用了PaymentGateway
类来处理支付逻辑,降低了直接依赖的耦合度。
原则: Protected Variations原则建议保护系统的稳定性,通过封装不稳定的因素。
详细说明: 当系统中的某个元素(类、模块等)可能会发生变化时,为了保护其他元素不受这个变化的影响,我们应该定义一个稳定的接口。通过这个接口,其他元素与变化点进行交互,而不是直接与变化点的具体实现交互。这样,如果未来变化发生,我们只需要修改接口的实现而不影响其他部分的代码。
解决方案:
示例:
考虑一个文件读取的例子,我们可以定义一个 FileReader
接口,并有两个不同的实现类 PlainTextFileReader
和 BinaryFileReader
。其他模块只需要与 FileReader
接口交互,而不需要直接与具体的文件读取实现交互。如果未来需要支持新的文件类型,我们只需扩展 FileReader
接口而不需要修改其他部分的代码。
public interface FileReader {
String read(String filePath);
}
public class PlainTextFileReader implements FileReader {
public String read(String filePath) {
// 读取纯文本文件的实现
}
}
public class BinaryFileReader implements FileReader {
public String read(String filePath) {
// 读取二进制文件的实现
}
}
通过这样的设计,我们保护了其他模块免受文件读取实现的变化的影响,实现了受保护变化的原则。
问题:如何解决不相容的接口问题,或者如何为具有不同接口的类似构件提供稳定的接口?
解决方案:通过中介适配器对象,将构件的原有接口转换为其他接口
如何体现: