软件设计原则与模式:构建灵活可扩展的软件架构
开闭原则(Open - Closed Principle,OCP)
软件系统会随着时间不断变化,系统需求的改变和演进是主要的催化剂。一个好的软件系统必须能够适应变化,在其整个生命周期中优雅地演进。而一个僵化、脆弱且抗拒变化的软件系统则是糟糕设计的体现,相反,具有弹性、灵活性和可扩展性的软件系统才具备良好面向对象架构的标志性特征。开闭原则(OCP)为实现可扩展和适应性强的软件架构提供了必要的框架。
开闭原则由 Bertrand Meyer 提出,它主张软件模块的设计和实现应该对扩展开放,对修改关闭。也就是说,应避免对软件模块进行修改,而是通过编写新代码来添加新的系统功能。虽然编写易于扩展和维护的代码一开始会花费更多时间,但后期会带来巨大的回报,这就是所谓的“设计红利”。
要编写符合开闭原则的代码,关键在于依赖抽象而非具体实现。因为抽象往往更加稳定(正确设计的抽象非常稳定)。在 Java 中,可以通过使用抽象基类或接口以及动态多态行为来实现这一点。代码应该仅依赖于接口方法和通过抽象方法承诺的行为。一个仅依赖于抽象的代码模块将表现出对修改关闭但对扩展开放的特性。
下面是一个遵循开闭原则的代码示例,实现了一个简单的海军舰队模型,不同类型的船只可以配备不同类型的动力装置和武器:
// Example 24.13: Vessel.java
public abstract class Vessel {
private Plant its_plant = null;
private Weapon its_weapon = null;
private String its_name = null;
public Vessel(Plant plant, Weapon weapon, String name){
its_weapon = weapon;
its_plant = plant;
its_name = name;
System.out.println("The vessel " + its_name + " created!");
}
/* ********************************************************
Public Abstract Methods - must be implemented in
derived classes.
*********************************************************/
public abstract void lightoffPlant();
public abstract void shutdownPlant();
public abstract void trainWeapon();
public abstract void fireWeapon();
/* ********************************************************
toString() Method - may be overridden in subclasses.
*********************************************************/
public String toString(){
return "Vessel name: " + its_name + " " + its_plant.toString() +
" " + its_weapon.toString();
}
protected Weapon getWeapon(){ return its_weapon; }
protected Plant getPlant(){ return its_plant; }
}// end Vessel class definition
// Example 24.14: Plant.java
public abstract class Plant {
private String its_model = null;
public Plant(String model){
its_model = model;
}
public abstract void lightoffPlant();
public abstract void shutdownPlant();
public String toString(){ return "Plant model: " + its_model; }
}
// Example 24.15: Weapon.java
public abstract class Weapon {
private String its_model = null;
public Weapon(String model){
its_model = model;
System.out.println("Weapon object created!");
}
public abstract void trainWeapon();
public abstract void fireWeapon();
public String toString(){ return "Weapon model: " + its_model; }
}
// Example 24.16: CIWS.java
public class CIWS extends Weapon {
public CIWS(String model){
super(model);
System.out.println("CIWS object created!");
}
public void trainWeapon(){
System.out.println("CIWS is locked on target!");
}
public void fireWeapon(){
System.out.println("The CIWS roars to life and fires a zillion bullets at the target!");
}
}
// Example 24.17: Torpedo.java
public class Torpedo extends Weapon {
public Torpedo(String model){
super(model);
System.out.println("Torpedo object created!");
}
public void trainWeapon(){
System.out.println("Torpedo is locked on target!");
}
public void fireWeapon(){
System.out.println("Fish in the water, heading towards target!");
}
}
// Example 24.18: Five_Inch_Gun.java
public class Five_Inch_Gun extends Weapon {
public Five_Inch_Gun(String model){
super(model);
System.out.println("Five Inch Gun object created!");
}
public void trainWeapon(){
System.out.println("Five Inch Gun is locked on target!");
}
public void fireWeapon(){
System.out.println("Blam! Blam! Blam!");
}
}
// Example 24.19: SteamPlant.java
public class SteamPlant extends Plant {
public SteamPlant(String model){
super(model);
System.out.println("SteamPlant object created!");
}
public void lightoffPlant(){
System.out.println("Steam pressure is rising!");
}
public void shutdownPlant(){
System.out.println("Steam plant is secure!");
}
}
// Example 24.20: NukePlant.java
public class NukePlant extends Plant {
public NukePlant(String model){
super(model);
System.out.println("NukePlant object created!");
}
public void lightoffPlant(){
System.out.println("Nuke plant is critical!");
}
public void shutdownPlant(){
System.out.println("Nuke plant is secure!");
}
}
// Example 24.21: GasTurbinePlant.java
public class GasTurbinePlant extends Plant {
public GasTurbinePlant(String model){
super(model);
System.out.println("GasTurbinePlant object created!");
}
public void lightoffPlant(){
System.out.println("Gas Turbine is running and ready to go!");
}
public void shutdownPlant(){
System.out.println("Gas Turbine is secure!");
}
}
// Example 24.22: FleetTestApp.java
public class FleetTestApp {
public static void main(String[] args){
Vessel v1 = new Submarine(new NukePlant("Preasureized Water Mk 85"), new Torpedo("MK 50"),
"USS Falls Church");
v1.lightoffPlant();
v1.trainWeapon();
v1.fireWeapon();
v1.shutdownPlant();
}
}// end FleetTestApp class definition
在这个示例中,
Vessel
、
Plant
和
Weapon
都是抽象类,具体的船只、动力装置和武器类继承自这些抽象类并实现其抽象方法。这样,如果需要添加新的船只、动力装置或武器类型,只需要创建新的子类,而不需要修改现有的抽象类和已有的子类,体现了对扩展开放,对修改关闭的原则。
依赖倒置原则(Dependency Inversion Principle,DIP)
当开闭原则(OCP)和里氏替换原则与契约式设计(LSP/DbC)以一种规范的方式一起使用时,会产生一种理想的程序模块依赖倒置,这与通过功能分解获得的通常的自上而下的模块依赖不同。这种依赖倒置被概括为一个独立的原则,即依赖倒置原则(DIP)。Robert C. Martin 将 DIP 的定义分为两部分:
- A. 高层模块不应依赖于低层模块,两者都应依赖于抽象。
- B. 抽象不应依赖于细节,细节应依赖于抽象。
糟糕软件架构的特征
当一个软件模块依赖于低层软件模块的细节时,它将难以更改和复用。例如,在传统的自上而下的功能依赖关系中,高层模块依赖于低层模块。假设模块 A 的行为依赖于模块 B、C 和 D,模块 B 的行为依赖于模块 E,模块 C 依赖于模块 F 和 G 的行为,模块 D 依赖于模块 H。那么,对模块 E 的更改会影响模块 B,进而影响模块 A。任何模块间的依赖关系,如全局变量,都会进一步使问题复杂化。具有这种架构的复杂软件系统将具有糟糕设计的不良特征,即脆弱、僵化和不可移动。
-
脆弱的软件架构
:当对一个或多个软件模块进行更改时,会以意想不到的方式崩溃。脆弱的软件会导致僵化的软件。
-
僵化的软件架构
:更改起来非常困难和痛苦,以至于程序员不愿意进行更改。
-
不可移动的软件架构
:其特点是无法成功提取软件模块以在另一个系统中复用。软件模块可能表现出理想的行为,但如果它过于依赖其他模块,或者通过模块间的依赖关系与应用架构紧密相连,那么即使不是不可能,也很难在另一个类似的上下文中复用。如果从头重写一个模块比采用和复用该模块更容易,那么该模块就是不可移动的。
良好软件架构的特征
遵循 OCP 和 LSP/DbC 的面向对象软件架构将严重依赖于抽象。这些抽象将出现在软件模块层次结构的顶部或附近。以海军舰队类层次结构为例,
Vessel
、
Weapon
和
Plant
抽象基类为所有低层实现类继承的行为提供了基础。这种继承关系意味着低层派生类依赖于高层基类抽象所指定的行为。
成功实现 DIP 的关键在于选择正确的软件抽象。基于正确类型抽象的软件架构将表现出易于扩展的理想特征。它将具有灵活性,因为它具有可扩展性;它将是非僵化的,因为通过新的派生类添加新功能不会影响现有抽象的行为。最后,依赖于抽象的软件模块通常可以在更广泛的上下文中复用,从而实现更高程度的可移动性。
相关术语和定义
| 术语 | 定义 |
|---|---|
| 抽象(Abstraction) | 区分重要和不重要的部分(即接口与实现的分离) |
| 抽象数据类型(Abstract Data Type) | 将类型的接口与类型的实现分离的类型规范。抽象数据类型表示一组可以通过一组接口方法进行操作的对象 |
| 超类型(Supertype) | 作为相关子类型规范的抽象数据类型 |
| 子类型(Subtype) | 从另一个抽象数据类型派生全部或部分规范的抽象数据类型。子类型可以继承超类型的规范,并在需要时添加额外的内容 |
| 类型规范(Type Specification) | 抽象数据类型行为属性的声明。规范描述了数据抽象的重要特征 |
| 封装(Encapsulation) | 将私有实现细节隐藏在公共可访问接口后面的行为 |
| 前置条件(Precondition) | 在调用抽象数据类型接口方法期间必须为真的条件、约束或一组约束,以确保其正常运行 |
| 后置条件(Postcondition) | 抽象数据类型方法完成执行时必须满足的条件、约束或一组约束 |
| 继承层次结构(Inheritance Hierarchy) | 一组实现每个抽象数据类型之间超类型和子类型关系的抽象数据类型规范 |
| 类(Class) | 抽象数据类型的声明,指定一组对象共有的一组属性和接口方法 |
| 抽象类(Abstract Class) | 抽象数据类型的声明,指定一组对象共有的一组属性和接口方法。一个或多个接口方法被声明为抽象的,因此推迟到子类实现 |
| 子类(Subclass) | 从另一个可能是抽象的类获取全部或部分规范的抽象数据类型的声明 |
| 类不变式(Class Invariant) | 关于对象状态的断言,对于对象可能假定的所有可能状态都必须为真 |
通过理解和应用开闭原则、依赖倒置原则以及相关的术语和概念,可以构建出更易于理解、推理和扩展的软件架构。在后续的学习和实践中,我们还可以进一步结合设计模式来优化软件设计。
下面是一个简单的 mermaid 流程图,展示了糟糕软件架构中模块依赖的影响:
graph LR
A --> B
A --> C
A --> D
B --> E
C --> F
C --> G
D --> H
E -.-> B -.-> A
这个流程图展示了模块之间的依赖关系,以及一个模块的更改如何影响其他模块。
在实际的软件开发中,我们应该尽量避免这种复杂的依赖关系,遵循依赖倒置原则,让模块依赖于抽象,从而提高软件的可维护性和可复用性。
同时,为了更好地掌握这些原则和概念,我们可以通过一些技能构建练习和项目来加深理解。例如:
-
研究相关书籍
:阅读 Bertrand Meyer 的《Object - Oriented Software Construction, Second Edition》和 Robert C. Martin 的《Designing Object - Oriented C++ Applications Using The Booch Method》。
-
进行网络搜索
:搜索里氏替换原则、开闭原则、依赖倒置原则和 Meyer 契约式编程等关键词。
-
设计修改
:创建一个新的检查异常
InvalidPreconditionException
,并使用它代替 Java 断言机制来表示无效的前置条件。
通过这些练习和项目,我们可以将理论知识应用到实际开发中,不断提升自己的软件开发能力。
软件设计原则与模式:构建灵活可扩展的软件架构
软件设计原则总结
设计良好的软件架构通常具备三个关键特性:易于理解、易于推理以及易于扩展。前面提到的里氏替换原则(Liskov Substitution Principle,LSP)和 Bertrand Meyer 的契约式设计(Design by Contract,DbC)编程是紧密相关的原则,它们有助于程序员更好地推理子类型的行为。
在方法重写时,派生类方法的前置条件应与基类方法相同或更弱,绝不能加强基类方法的前置条件,否则会使程序员难以推理子类型对象的行为,甚至导致代码出错。方法参数类型可视为前置条件的特殊情况,重写方法的参数类型应与被重写方法的参数类型相同或更弱,基类被认为是比其子类更弱的类型。方法返回类型可视为后置条件的特殊情况,重写方法的返回类型应比客户端代码期望的类型更强,子类被认为是比其基类更强的类型。
开闭原则(OCP)旨在优化面向对象软件架构设计,使其能够适应变化。软件模块应设计为对修改关闭,对扩展开放,这通过依赖软件抽象来实现。在 Java 中,意味着使用抽象基类或接口进行设计,并考虑动态多态行为。OCP 严重依赖于里氏替换原则和契约式设计(LSP/DbC)。
当 OCP 和 LSP/DbC 一起应用时,会产生依赖倒置原则(DIP)。DIP 的关键在于高层软件模块不应依赖于低层细节,所有层次的软件模块都应依赖于抽象。当软件架构实现了 DIP 的目标时,它将更易于扩展和维护,软件模块也更易于在其他上下文中复用。
设计模式概述
设计模式是软件开发中经过验证的解决方案,它们代表了一种知识复用的形式。了解设计模式可以彻底改变我们构建软件架构的方式。虽然设计模式众多,但下面将重点介绍单例模式(Singleton)、工厂模式(Factory)、模型 - 视图 - 控制器模式(Model - View - Controller,MVC)和命令模式(Command),并展示如何将它们组合起来创建健壮、灵活的应用架构。
工厂模式(Factory Pattern)
工厂模式的目的是提供一种创建对象的方式,将对象的创建和使用分离。通过工厂类,可以根据需要创建不同类型的对象,而调用者无需了解对象的具体创建过程。
以下是一个简单的工厂类示例,用于根据类名加载其他类:
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ClassFactory {
public static Object createClass(String className) {
try {
Class<?> clazz = Class.forName(className);
Constructor<?> constructor = clazz.getConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException | NoSuchMethodException |
InstantiationException | IllegalAccessException |
InvocationTargetException e) {
e.printStackTrace();
return null;
}
}
}
在这个示例中,
ClassFactory
类的
createClass
方法接受一个类名作为参数,使用
Class.forName
方法动态加载类,并通过反射创建该类的实例。
单例模式(Singleton Pattern)
单例模式的目的是确保一个类只有一个实例,并提供一个全局访问点。以下是一个简单的单例模式实现:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在这个示例中,
Singleton
类的构造函数是私有的,这确保了外部无法直接创建该类的实例。通过
getInstance
方法可以获取该类的唯一实例。
命令模式(Command Pattern)
命令模式的目的是将请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。以下是一个简单的命令模式示例:
// 命令接口
interface Command {
void execute();
}
// 具体命令类
class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
// 接收者类
class Receiver {
public void action() {
System.out.println("Receiver is performing an action.");
}
}
// 调用者类
class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand() {
if (command != null) {
command.execute();
}
}
}
在这个示例中,
Command
接口定义了命令的执行方法,
ConcreteCommand
类实现了该接口,并在
execute
方法中调用
Receiver
类的
action
方法。
Invoker
类负责设置和执行命令。
模型 - 视图 - 控制器模式(Model - View - Controller,MVC)
MVC 模式的目的是将业务逻辑、数据和界面显示分离,从而提高代码的可维护性和可扩展性。以下是一个简单的 MVC 模式示例:
// 模型类
class Model {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
// 视图类
class View {
public void displayData(String data) {
System.out.println("Displaying data: " + data);
}
}
// 控制器类
class Controller {
private Model model;
private View view;
public Controller(Model model, View view) {
this.model = model;
this.view = view;
}
public void updateData(String data) {
model.setData(data);
view.displayData(model.getData());
}
}
在这个示例中,
Model
类负责存储数据,
View
类负责显示数据,
Controller
类负责协调模型和视图,当数据更新时,控制器会更新模型并通知视图显示新数据。
设计模式的组合应用
可以将 MVC、工厂、单例和命令模式组合起来,创建灵活的应用架构。例如,使用工厂模式创建模型、视图和控制器对象,使用单例模式确保某些关键对象只有一个实例,使用命令模式处理用户的操作请求。
以下是一个简单的组合应用示例:
// 使用工厂模式创建对象
class AppFactory {
public static Model createModel() {
return new Model();
}
public static View createView() {
return new View();
}
public static Controller createController(Model model, View view) {
return new Controller(model, view);
}
}
// 使用单例模式的配置类
class AppConfig {
private static AppConfig instance;
private String configData;
private AppConfig() {
configData = "Default Config";
}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
public String getConfigData() {
return configData;
}
}
// 使用命令模式处理用户操作
class UpdateDataCommand implements Command {
private Controller controller;
private String newData;
public UpdateDataCommand(Controller controller, String newData) {
this.controller = controller;
this.newData = newData;
}
@Override
public void execute() {
controller.updateData(newData);
}
}
public class Main {
public static void main(String[] args) {
// 创建对象
Model model = AppFactory.createModel();
View view = AppFactory.createView();
Controller controller = AppFactory.createController(model, view);
// 获取配置信息
AppConfig config = AppConfig.getInstance();
System.out.println("App Config: " + config.getConfigData());
// 执行命令
Command command = new UpdateDataCommand(controller, "New Data");
command.execute();
}
}
在这个示例中,
AppFactory
类使用工厂模式创建模型、视图和控制器对象,
AppConfig
类使用单例模式确保配置信息只有一个实例,
UpdateDataCommand
类使用命令模式处理用户的数据更新请求。
总结
通过掌握开闭原则、依赖倒置原则以及各种设计模式,我们可以构建出更加灵活、可扩展和易于维护的软件架构。以下是一些关键要点总结:
-
设计原则
:开闭原则使软件模块对扩展开放,对修改关闭;依赖倒置原则让高层模块和低层模块都依赖于抽象,提高软件的可维护性和可复用性。
-
设计模式
:工厂模式分离对象的创建和使用,单例模式确保类只有一个实例,命令模式封装请求,MVC 模式分离业务逻辑和界面显示。
-
组合应用
:将不同的设计模式组合起来,可以创建出更健壮、灵活的应用架构。
在实际开发中,我们应该根据具体的需求和场景选择合适的设计原则和模式,不断实践和总结经验,以提高自己的软件开发能力。
下面是一个 mermaid 流程图,展示了 MVC 模式的工作流程:
graph LR
A[用户操作] --> B[控制器]
B --> C[模型]
C --> B
B --> D[视图]
D --> A
这个流程图清晰地展示了用户操作如何通过控制器影响模型,模型的变化又如何通过控制器反馈到视图,最后视图将结果展示给用户。
通过不断学习和实践这些设计原则和模式,我们可以更好地应对软件开发中的各种挑战,构建出高质量的软件系统。同时,建议大家通过更多的技能构建练习和项目实践,进一步巩固所学知识,提升自己的编程水平。例如,可以尝试将这些原则和模式应用到实际的项目中,如之前提到的 RobotRat 项目,评估应用这些原则后项目的改进情况,思考如何进一步优化设计以充分发挥每个原则和模式的优势。
超级会员免费看

被折叠的 条评论
为什么被折叠?



