在软件开发中,设计模式是解决常见问题的可重用解决方案。在面向对象编程中,继承和组合是两种常用的代码复用方式。然而,随着软件需求的不断变化,我们需要更灵活的设计方式来应对不断变化的需求。在本文中,我们将讨论从继承到组合的演进之路,并探讨如何通过组合设计模式来解决问题。
假如:要实现同时记日志和事务提交如何做比较好
不推荐写法(继承关系)
继承设计的缺陷在于它限制了代码的扩展性和灵活性。具体来说,它导致了以下问题:
在上面的 UML 图中,我们展示了使用继承关系实现日志记录和事务管理的示例。在这个示例中,我们有一个抽象类 LoggingRunnable
和 TransactionalRunnable
,它们都包含了一个抽象方法 doRun()
来执行具体的任务。然后,我们有一个具体的类 CodingTask
,它继承自 LoggingRunnable
和 TransactionalRunnable
,并实现了 doRun()
方法来执行编码任务。
现在,假设我们需要修改系统,让每个编码任务先记录日志,然后再进行事务管理。使用继承关系,我们需要在现有的类结构中进行大量的修改,这可能会导致代码变得复杂和难以维护。下面是一个示例代码,展示了在现有系统中添加事务管理的改动:
public abstract class LoggingTransactionalRunnable extends LoggingRunnable {
private final Runnable innerRunnable;
public LoggingTransactionalRunnable(Runnable innerRunnable) {
this.innerRunnable = innerRunnable;
}
@Override
public void run() {
super.run();
beginTransaction();
innerRunnable.run();
commitTransaction();
}
private void beginTransaction() {
// 开启事务
}
private void commitTransaction() {
// 提交事务
}
}
在这个示例中,我们创建了一个新的抽象类 LoggingTransactionalRunnable
,它继承自 LoggingRunnable
,并实现了事务管理的功能。然后,我们修改了 CodingTask
类,使其继承自 LoggingTransactionalRunnable
,以实现先记录日志,然后执行编码任务,最后进行事务管理的功能。
尽管这种方法可以实现需求,但它会导致现有系统的大量修改,可能会破坏现有的代码结构,增加代码的复杂性和难以维护性。因此,使用继承关系来实现这种需求存在 类耦合度高, 单一继承限制,难以复用和测试可能不是最佳选择。
相对于继承关系,组合关系具有更低的耦合度和更高的灵活性。通过组合关系,可以将不同的功能模块化,并在运行时动态地组合它们,从而实现更灵活和可扩展的设计。因此,在设计类和对象时,应尽量避免过度使用继承关系,而是倾向于使用组合关系来实现代码的复用和扩展。
推荐写法(组合关系)
箭头表示实现关系和组合关系。其中 CodingTask、LoggingRunnable 和 TransactionalRunnable 类都实现了 Runnable 接口,表示它们都是可以在单独的线程中运行的任务。而 LoggingRunnable 和 TransactionalRunnable 类分别包含了一个 Runnable 类型的私有属性,表示它们与内部的任务对象发生了组合关系。
//LoggingRunnable.java
public class LoggingRunnable implements Runnable {
private final Runnable runnable;
public LoggingRunnable(Runnable innerRunnable) {
this.runnable = innerRunnable;
}
@Override
public void run() {
long startTime = System.currentTimeMillis();
System.out.println("Task started at "
+ startTime);
//Decorator 模式是一种常见的设计模式,用于动态地为对象添加额外的功能,而不需要修改其原始类。
runnable.run();
System.out.println("Task finished. Elapsed time: "
+ (System.currentTimeMillis() - startTime));
}
}
// CodingTask.java
public class CodingTask implements Runnable {
private final int employeeId;
public CodingTask(int employeeId) {
this.employeeId = employeeId;
}
@Override
public void run() {
System.out.println("Employee " + employeeId
+ " started writing code.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Employee " + employeeId
+ " finished writing code.");
}
}
上面的代码展示了使用组合关系实现的两个类:LoggingRunnable
和 CodingTask
。
-
LoggingRunnable 类:该类实现了
Runnable
接口,并包含了一个私有成员变量runnable
,类型为Runnable
接口。在构造函数中,通过参数传入一个Runnable
对象,然后在run()
方法中,首先记录了任务开始的时间,然后调用传入的Runnable
对象的run()
方法执行具体的任务,最后记录了任务结束的时间。 -
CodingTask 类:该类也实现了
Runnable
接口,表示一个编码任务。在run()
方法中,它简单地打印出员工开始编写代码的消息,然后休眠 5 秒钟(模拟编写代码的过程),最后打印出员工编写代码完成的消息。
这两个类之间的关系是组合关系,即 LoggingRunnable
类包含一个 Runnable
接口的实例。通过这种组合关系,LoggingRunnable
类可以在执行任务前后添加额外的功能,而不需要修改 CodingTask
类的代码。这符合了开闭原则,即对扩展开放,对修改关闭。
功能扩展
如果有一天项目经理添加新需求事务和MQ 整合
对于组合设计模式,你可以想象一个新的扩展类,例如 MessagingTransactionalRunnable
,它可以组合 TransactionalRunnable
和 MessagingRunnable
两个功能,从而实现事务管理和向 MQ 发送消息的组合。下面是一个简单的示例:
public class MessagingTransactionalRunnable implements Runnable {
private final Runnable innerRunnable;
private final String message;
public MessagingTransactionalRunnable(Runnable innerRunnable, String message) {
this.innerRunnable = innerRunnable;
this.message = message;
}
@Override
public void run() {
try {
beginTransaction();
sendMessage(message);
innerRunnable.run();
commit();
} catch (Exception e) {
rollback();
throw new RuntimeException(e);
}
}
private void sendMessage(String message) {
System.out.println("Sending message to MQ: " + message);
// 实现向 MQ 发送消息的逻辑
}
private void commit() {
System.out.println("Commit transaction");
// 实现事务提交的逻辑
}
private void rollback() {
System.out.println("Rollback transaction");
// 实现事务回滚的逻辑
}
private void beginTransaction() {
System.out.println("Begin transaction");
// 实现事务开始的逻辑
}
}
在这个示例中,MessagingTransactionalRunnable
类组合了 TransactionalRunnable
和 MessagingRunnable
两个功能模块,通过调用它们的方法来实现事务管理和向 MQ 发送消息的组合。当 MessagingTransactionalRunnable
对象的 run
方法被调用时,它会先开始事务,然后发送消息到 MQ,执行内部的任务,最后提交或回滚事务。这样,你就可以很方便地使用组合设计模式来扩展功能了。
总结
继承的局限性
继承是面向对象编程中的一种重要概念,它允许子类继承父类的属性和方法。通过继承,可以实现代码的重用和扩展。然而,继承也存在一些局限性:
- 耦合度高: 子类与父类之间存在紧密的耦合关系,子类的实现依赖于父类的具体实现细节。
- 继承链过长: 当继承层次较深时,维护和理解代码变得困难,容易造成代码膨胀和复杂性增加。
- 单一继承: 在单继承语言中,子类只能继承一个父类,限制了代码的灵活性和可复用性。
由于这些局限性,我们需要寻找一种更灵活的设计方式来解决问题。
组合的优势
组合是另一种常见的代码复用方式,它允许将对象组合在一起以实现新的功能。相比于继承,组合具有以下优势:
- 低耦合度: 组合将对象之间的耦合度降低到最低限度,每个对象都可以独立存在并且可以被替换或重用。
- 灵活性: 通过组合,可以动态地组合和重组对象以实现不同的功能,而不需要修改原始类的代码。
- 多态性: 组合提倡面向接口编程,利用多态性来实现代码的灵活性和可扩展性。
扩展阅读
面向对象主题 | 链接 |
---|---|
类与对象 | 链接 |
接口与抽象类 | 链接 |
不可变性 | 链接 |
变继承为组合(精髓一)状态模式 | 链接 |
变继承为组合(精髓二)装饰器模式 | 链接 |