学习目标
- 3.1 设计模式概述
- 3.2 软件可复用问题和面向对象设计原则
- 一、软件可复用问题
- 二、面向对象设计原则
- 1. 单一责任原则(Single Responsibility Principle, SRP)
- 2. 开放-封闭原则(Open-Closed Principle, OCP)
- 3. 里氏替换原则(Liskov Substitution Principle, LSP)
- 4. 依赖倒置原则(Dependency Inversion Principle, DIP)
- 5. 接口隔离原则(Interface Segregation Principle, ISP)
- 6. 迪米特法则(Least Knowledge Principle,LKP)
- 7. 合成/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)
- 3.3 设计模式的应用
- 3.3.1 工厂方法模式
- 1. 简单工厂模式(Simple Factory Pattern)
- 2. 工厂方法模式(Factory Method Pattern)
- 3.抽象工厂模式(Abstract Factory Pattern)
- 3.3.2 代理模式
- 代理模式包含如下角色
前面课程中已经学习了面向对象的三大特征,在后续的学习过程中对面向对象的认识会不断深入,不断提高运用面向对象思想解决问题的能力。(如果没有了解可以去我主页看看Java开发之框架基础技术第1-2章(2023版本IEDA)来学习)本章学习面向对象的一些高级应用一一设计模式。设计模式被广泛运用在java框架技术中,学习设计模式对于理解框架的工作原理会有所帮助。
学习方法
设计模式虽有很多种,但总是可以从解锅台、提高复用性这些方向来理解。首先要明确每种设计模式的使用场景,明确其要解决的问题,进而理解其解决该问题的思路。
3.1 设计模式概述
设计模式(Design Pattern)是人们在长期的软件开发中对一些经验的总结,是对某些特定问题经过实践检验的特定解决方法。就像兵法中的三十六计,总结了36种对于战争中某些场合的可行性计谋战术一一"围魏救赵"“声东击西”"走为上"等,可以说三十六计中的每一计都是一种模式。
- 创建型模式(Creational Patterns)
创建型模式主要用于对象的创建,它们通过隐藏对象的创建逻辑来提供更大的灵活性。
示例:单例模式(Singleton Pattern)
单例模式确保一个类仅有一个实例,并提供一个全局访问点。
java">public class Singleton { // 私有静态变量,保存类的唯一实例 private static Singleton instance; // 私有构造函数,防止外部通过new创建实例 private Singleton() {} // 提供一个全局的静态方法,返回唯一实例 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } // 其他方法...
}
- 结构型模式(Structural Patterns)
结构型模式关注于类、接口和对象之间的组合关系,以创建更大的结构。
示例:适配器模式(Adapter Pattern)
适配器模式将一个类的接口转换成客户端期望的另一个接口形式,使类之间的接口不兼容问题可以通过一个中间类来解决。
java">// 目标接口
public interface Target { void request();
} // 需要适配的类
public class Adaptee { public void specificRequest() { // 具体请求 }
} // 适配器类
public class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void request() { adaptee.specificRequest(); }
}
- 行为型模式(Behavioral Patterns)
行为型模式主要关注对象之间的通信和交互方式。
示例:观察者模式(Observer Pattern)
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
java">// 抽象主题类
public abstract class Subject { // 维护一个观察者列表 private List<Observer> observers = new ArrayList<>(); // 注册观察者 public void registerObserver(Observer o) { observers.add(o); } // 移除观察者 public void removeObserver(Observer o) { observers.remove(o); } // 通知所有观察者 protected void notifyObservers() { for (Observer observer : observers) { observer.update(this); } } // 抽象的通知方法 public abstract void stateChanged();
} // 抽象观察者类
public interface Observer { void update(Subject subject);
} // 具体实现
// ...(具体主题类和观察者类的实现)
注意
上述代码示例仅用于说明设计模式的基本思想和结构,并未包含完整的错误处理和优化逻辑。在实际应用中,您可能需要根据具体需求进行调整和完善。
设计模式的学习和应用需要结合具体的项目实践,通过不断尝试和反思来加深对设计模式的理解和应用能力。
3.2 软件可复用问题和面向对象设计原则
软件可复用问题和面向对象设计原则是两个紧密相连的概念。在软件开发中,可复用性是一个重要的目标,它旨在通过重用已有的软件组件来降低开发成本、提高开发效率和软件质量。面向对象设计原则则为实现软件的可复用性提供了指导和支持。
一、软件可复用问题
软件复用(Software Reuse)是使用已有的软件组件去实现或更新软件系统的过程。复用可以降低开发成本、缩短开发周期、提高软件质量,并促进软件标准化。然而,要实现软件的有效复用,需要解决以下几个关键问题:
- 组件的明确定义和标准化:可复用的组件需要具有明确的定义和标准化的接口,以便在不同的系统中被重用。
- 组件的独立性:组件之间应该尽可能少地相互依赖,以提高其独立性和可移植性。
- 组件的文档化和可理解性:良好的文档和易于理解的设计是复用组件的前提。
- 组件的测试和验证:复用前需要对组件进行充分的测试和验证,以确保其稳定性和可靠性。
二、面向对象设计原则
面向对象设计原则为软件的可复用性提供了指导和支持。以下是一些关键的面向对象设计原则:
1. 单一责任原则(Single Responsibility Principle, SRP)
一个类应该仅有一个引起它变化的原因。这有助于保持类的简洁和可维护性,从而提高其可复用性。
java">// 示例:一个类只负责一个功能
public class UserService { // 负责用户注册的逻辑 public void registerUser(User user) { // 注册逻辑... } // 如果有其他与用户相关的功能,应该放在其他类中
}
2. 开放-封闭原则(Open-Closed Principle, OCP)
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在添加新功能时,应该通过扩展现有系统来实现,而不是修改现有代码。这有助于保持系统的稳定性和可复用性。
java">// 示例:通过策略模式实现OCP
interface PaymentStrategy { void pay(double amount);
} class CreditCardPayment implements PaymentStrategy { public void pay(double amount) { // 信用卡支付逻辑... }
} class CashPayment implements PaymentStrategy { public void pay(double amount) { // 现金支付逻辑... }
} class PaymentProcessor { private PaymentStrategy strategy; public PaymentProcessor(PaymentStrategy strategy) { this.strategy = strategy; } public void processPayment(double amount) { strategy.pay(amount); }
} // 客户端代码
PaymentProcessor processor = new PaymentProcessor(new CreditCardPayment());
processor.processPayment(100.0);
3. 里氏替换原则(Liskov Substitution Principle, LSP)
子类型必须能够替换掉它们的基类型。这要求子类在继承基类时,必须保持与基类相同的行为特性,以确保在父类出现的地方可以使用子类来替换。
java">// 父类
class Rectangle { protected double width; protected double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } // 计算面积 public double area() { return width * height; }
} // 子类,尝试遵循里氏替换原则
class Square extends Rectangle { // 由于Square的特殊性,这里不能直接使用父类的构造器 // 因为Square的宽和高必须相等 public Square(double side) { super(side, side); // 调用父类构造器,强制宽和高相等 } // 尝试修改面积方法(但这样做可能违反里氏替换原则) // @Override // public double area() { // return width * width; // 直接使用width的平方,但这会破坏里氏替换原则 // } // 为了保持里氏替换原则,我们不覆盖area方法 // 而是添加一个新的方法来计算正方形的特定属性(比如周长) public double perimeter() { return 4 * width; // 因为是正方形,所以周长是4倍的边长 }
} // 客户端代码
public class LiskovSubstitutionDemo { public static void main(String[] args) { Rectangle rect = new Rectangle(4, 5); System.out.println("Rectangle area: " + rect.area()); // 输出矩形的面积 // 尝试用Square替换Rectangle Rectangle square = new Square(5); // 这里Square被当作Rectangle使用,符合里氏替换原则 System.out.println("Square area (as Rectangle): " + square.area()); // 输出正方形的面积,通过Rectangle接口 // 如果需要调用Square特有的方法,需要进行类型转换 if (square instanceof Square) { Square s = (Square) square; System.out.println("Square perimeter: " + s.perimeter()); // 输出正方形的周长 } }
}
4. 依赖倒置原则(Dependency Inversion Principle, DIP)
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。这有助于减少模块之间的耦合度,提高系统的灵活性和可复用性。
java">// 定义一个日志记录的接口
interface Logger { void log(String message);
} // 实现日志记录接口的具体类
class FileLogger implements Logger { @Override public void log(String message) { System.out.println("Logging to file: " + message); }
} // 另一个实现日志记录接口的具体类
class ConsoleLogger implements Logger { @Override public void log(String message) { System.out.println("Logging to console: " + message); }
} // 定义一个依赖日志记录的类,这里依赖的是日志的抽象(接口)
class Application { private Logger logger; // 通过构造器注入依赖的日志实现 public Application(Logger logger) { this.logger = logger; } public void execute() { // 使用日志记录功能,但不关心具体的实现 logger.log("Application is starting..."); // 其他业务逻辑... logger.log("Application is ending..."); }
} // 客户端代码
public class DependencyInversionDemo { public static void main(String[] args) { // 可以在运行时根据需要选择日志记录的实现 Application appWithFileLogger = new Application(new FileLogger()); Application appWithConsoleLogger = new Application(new ConsoleLogger()); appWithFileLogger.execute(); appWithConsoleLogger.execute(); }
}
5. 接口隔离原则(Interface Segregation Principle, ISP)
不应该强迫客户依赖于它们不使用的方法。这要求将大的接口拆分成更小的、更具体的接口,以便客户只需要知道它们感兴趣的方法。
java">// 定义一个细粒度的接口,只包含打印功能
interface Printable { void print();
} // 另一个细粒度的接口,只包含扫描功能
interface Scannable { void scan();
} // 假设我们有一个多功能设备,它同时支持打印和扫描
class MultiFunctionDevice implements Printable, Scannable { @Override public void print() { System.out.println("Printing document..."); } @Override public void scan() { System.out.println("Scanning document..."); }
} // 一个只需要打印功能的类
class Printer { private Printable printable; public Printer(Printable printable) { this.printable = printable; } public void performPrintTask() { printable.print(); }
} // 一个只需要扫描功能的类
class Scanner { private Scannable scannable; public Scanner(Scannable scannable) { this.scannable = scannable; } public void performScanTask() { scannable.scan(); }
} // 客户端代码
public class InterfaceSegregationDemo { public static void main(String[] args) { // 创建一个多功能设备实例 MultiFunctionDevice mfd = new MultiFunctionDevice(); // 创建一个Printer实例,只依赖打印功能 Printer printer = new Printer(mfd); printer.performPrintTask(); // 创建一个Scanner实例,只依赖扫描功能 Scanner scanner = new Scanner(mfd); scanner.performScanTask(); }
}
6. 迪米特法则(Least Knowledge Principle,LKP)
迪米特法则又称为最少知道原则,是指一个软件实体应当尽可能少地与其他实体发生相互作用。
java">// 示例:使用中介者模式减少类之间的直接通信
interface Mediator { void send(String message, Colleague colleague);
} interface Colleague { void receive(String message);
} class ConcreteMediator implements Mediator { private List<Colleague> colleagues = new ArrayList<>(); public void register(Colleague colleague) { colleagues.add(colleague); } @Override public void send(String message, Colleague colleague) { for (Colleague c : colleagues) { if (!c.equals(colleague)) { c.receive(message); } } }
} class ConcreteColleague implements Colleague { private Mediator mediator; public ConcreteColleague(Mediator mediator) { this.mediator = mediator; mediator.register(this); } @Override public void receive(String message) { // 处理接收到的消息 System.out.println("Received: " + message); } public void send(String message) { mediator.send(message, this); }
} // 客户端代码
Mediator mediator = new ConcreteMediator();
Colleague c1 = new ConcreteColleague(mediator);
Colleague c2 = new ConcreteColleague(mediator);
c1.send("Hello, World!"); // c2 会接收到这个消息,但c1不知道c2的存在
在这个例子中,ConcreteColleague 类通过 Mediator 类来与其他 Colleague 通信,而不是直接
7. 合成/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)
尽量使用合成/聚合的方式来实现复用,而不是使用继承。这有助于保持类的独立性和灵活性,从而提高其可复用性。
java">// 定义一个车辆接口
interface Vehicle { void move();
} // 定义一个具体的车辆类,使用合成/聚合来复用
class Car implements Vehicle { private Engine engine; // 聚合关系 public Car(Engine engine) { this.engine = engine; } @Override public void move() { System.out.println("Car is moving..."); engine.start(); }
} // 定义一个引擎接口
interface Engine { void start();
} // 定义一个具体的引擎类
class GasolineEngine implements Engine { @Override public void start() { System.out.println("Gasoline engine started."); }
} // 客户端代码
public class CompositionDemo { public static void main(String[] args) { // 创建一个引擎实例 Engine engine = new GasolineEngine(); // 创建一个车辆实例,并注入引擎 Car car = new Car(engine); // 调用车辆移动方法,间接调用引擎的启动方法 car.move(); // 如果需要,可以轻松地替换引擎实现 // Engine electricEngine = new ElectricEngine(); // car = new Car(electricEngine); // car.move(); }
} // 假设我们有一个电动引擎类(未实现,仅作为示例)
// class ElectricEngine implements Engine {
// @Override
// public void start() {
// System.out.println("Electric engine started.");
// }
// }
3.3 设计模式的应用
3.3.1 工厂方法模式
工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它在创建对象时不会暴露创建逻辑给客户端,并且是通过使用一个共同的接口来指向新创建的对象。这个模式涉及到一个单一的工厂类,但它允许客户端代码决定要实例化哪一个类。工厂方法让类的实例化推迟到子类中进行。
在Java中,工厂方法模式通常通过定义一个创建对象的接口,让子类决定实例化哪一个类。工厂方法模式让类的实例化依赖于它们所含的类信息中,并且将实例化的工作延迟到子类中进行。
下面是一个简单的Java代码示例,展示了工厂方法模式:
java">// 定义一个产品接口
interface Product { void use();
} // 实现产品接口的具体产品类
class ConcreteProductA implements Product { @Override public void use() { System.out.println("Using ConcreteProductA"); }
} class ConcreteProductB implements Product { @Override public void use() { System.out.println("Using ConcreteProductB"); }
} // 定义一个创建产品的抽象工厂类
abstract class Creator { // 工厂方法,由子类实现 abstract Product factoryMethod(); // 使用工厂方法创建产品 public void someOperation() { Product product = factoryMethod(); product.use(); }
} // 具体工厂类,实现了抽象工厂类中的工厂方法
class ConcreteCreatorA extends Creator { @Override Product factoryMethod() { return new ConcreteProductA(); }
} class ConcreteCreatorB extends Creator { @Override Product factoryMethod() { return new ConcreteProductB(); }
} // 客户端代码
public class FactoryMethodPatternDemo { public static void main(String[] args) { Creator creatorA = new ConcreteCreatorA(); creatorA.someOperation(); // 输出: Using ConcreteProductA Creator creatorB = new ConcreteCreatorB(); creatorB.someOperation(); // 输出: Using ConcreteProductB }
}
在这个例子中,Product 是一个产品接口,ConcreteProductA 和 ConcreteProductB 是实现了这个接口的具体产品类。Creator 是一个抽象工厂类,它定义了一个工厂方法 factoryMethod(),该方法在子类中具体实现以返回不同类型的产品。ConcreteCreatorA 和 ConcreteCreatorB 是具体工厂类,它们分别实现了 factoryMethod() 方法以返回不同的产品实例。
客户端代码通过调用具体工厂类的 someOperation() 方法来创建和使用产品,而不需要知道具体的产品类是什么。这样,客户端代码与具体的产品类解耦,提高了系统的灵活性和可扩展性。
简答工厂模式包含如下角色
- 工厂(Factory): 简单工厂模式的核心,复制实现创建有实例的逻辑。工厂类提供静态方法,根据传入参数创建所需的产品实例。
- 抽象产品(Produt): 工厂创建的所有实例的父类型,是负责描述所有产品的公共接口。可以是接口或抽象类。
- 具体产品(Conrete product): 抽象产品的实现类,是工厂的创建目标,工厂创建的实例就是某个具体产品类的实例。
客户程序NewsServiceImpl只需要知道工厂和抽象的父类产品(NewsDao接口),不需要关心具体的产品如何创建(不需要知道接口的具体实现),内部如何变化。具体产品(接口实现类)被父类型(接口)包装,与客户程序解耦合,不影响客户程序(Service接口实现类)的复用。
1. 简单工厂模式(Simple Factory Pattern)
角色:
工厂类(Factory Class): 负责创建具体产品类的实例。
抽象产品类(Abstract Product Class): 定义产品的公共接口。
具体产品类(Concrete Product Classes): 实现了抽象产品类所定义的接口。
java">// 抽象产品类
interface Car { void drive();
} // 具体产品类1
class Audi implements Car { @Override public void drive() { System.out.println("Driving Audi"); }
} // 具体产品类2
class BMW implements Car { @Override public void drive() { System.out.println("Driving BMW"); }
} // 工厂类
class CarFactory { public static Car getCar(String type) { if (type.equalsIgnoreCase("audi")) { return new Audi(); } else if (type.equalsIgnoreCase("bmw")) { return new BMW(); } return null; }
} // 客户端代码
public class FactoryPatternDemo { public static void main(String[] args) { Car audi = CarFactory.getCar("audi"); audi.drive(); Car bmw = CarFactory.getCar("bmw"); bmw.drive(); }
}
简单工厂模式不适合创建逻辑比较复杂的情况,复杂的产品逻辑会导致工厂方法难以维护,并且增加新的产品就需要修改工厂方法判断逻辑,这与开闭原则相违背。而工厂方法模式是对简单工厂模式的进一步抽象化,工厂方法模式的主要角色如下(对工厂进一步抽象)。
- 抽象产品(Product): 定义了产品的规范,描述了产品的主要特性和功能(Dao接口)。
- 抽象工厂(Abstract Factory): 提供了创建产品的接口,声明创建方法,该方法返回值为抽象产品类型,调用者通过抽象工厂接口访问具体工厂的方法来创建产品(提供创建Dao接口实现类实例的接口)。
- 具体工厂(Concrete Factory): 实现抽象工厂中的抽象创建方法,完成某个具体产品的创建,具体工厂和具体产品之间存在对应关系(负责创建Dao接口实现类的实例)。
2. 工厂方法模式(Factory Method Pattern)
角色:
抽象工厂类(Abstract Factory Class): 声明一个用于创建对象的操作接口。
具体工厂类(Concrete Factory Classes): 实现抽象工厂类声明的接口,创建具体产品的实例。
抽象产品类(Abstract Product Class): 定义产品的接口。
具体产品类(Concrete Product Classes): 实现了抽象产品类所定义的接口。
java">// 抽象产品类
interface Car { void drive();
} // 具体产品类1
class Audi implements Car { @Override public void drive() { System.out.println("Driving Audi"); }
} // 具体产品类2
class BMW implements Car { @Override public void drive() { System.out.println("Driving BMW"); }
} // 抽象工厂类
interface CarFactory { Car createCar();
} // 具体工厂类1
class AudiFactory implements CarFactory { @Override public Car createCar() { return new Audi(); }
} // 具体工厂类2
class BMWFactory implements CarFactory { @Override public Car createCar() { return new BMW(); }
} // 客户端代码
public class FactoryMethodPatternDemo { public static void main(String[] args) { CarFactory audiFactory = new AudiFactory(); Car audi = audiFactory.createCar(); audi.drive(); CarFactory bmwFactory = new BMWFactory(); Car bmw = bmwFactory.createCar(); bmw.drive(); }
}
3.抽象工厂模式(Abstract Factory Pattern)
角色:
抽象工厂类(Abstract Factory Class): 提供一个创建一系列相关或相互依赖对象的接口。
具体工厂类(Concrete Factory Classes): 实现抽象工厂类声明的接口,创建具体产品的实例。
抽象产品类(Abstract Product Classes): 定义了一组产品的接口。
具体产品类(Concrete Product Classes): 实现了抽象产品类所定义的接口。
代码示例:
由于抽象工厂模式涉及多个产品族和多个等级结构的产品,代码示例会相对复杂,这里仅给出框架性的描述。
java">// 抽象产品A
interface ProductA { void operationA();
} // 具体产品A1
class ConcreteProductA1 implements ProductA { @Override public void operationA() { // 实现 }
} // 抽象产品B
interface ProductB { void operationB();
} // 具体产品B1
3.3.2 代理模式
在生活中,我们经常听说房产中介、婚介、经纪人等社会角色,这些都是代理模式的实现体现。这种模式其实也是单一职责原则的体现,就好像一个要买房,中间会涉及很多的环节,部分流程复杂而且专业。
代理模式包含如下角色
- 抽象主题(Subject): 通过接口或抽象类声明业务方法(NewsDao接口)。
- 真实主题(Real Subject): 实现了抽象主题中的具体业务,是实施代理的目标对象,即代理对象所代表的真实对象,是最终要引用的对象(NewsDao接口的实现类)。
- 代理(Proxy): 提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能。
(在接口中定义一个买房的方法,真实对张三去实现买房操作。)
代理模式(Proxy Pattern)是一种结构型设计模式,它为其他对象提供一种代理以控制对这个对象的访问。代理模式通常用于在客户端和目标对象之间创建一个中介,以增加功能、控制访问、减少系统间耦合等。
代理模式主要有三种类型:静态代理、动态代理(包括JDK动态代理和CGLIB动态代理,主要基于Java语言)和远程代理。下面分别给出静态代理和JDK动态代理的简单代码示例。
静态代理
静态代理通常是在编译时就确定了代理类,代理类和被代理类都实现了相同的接口。
java">// 接口
interface Image { void display();
} // 被代理类
class RealImage implements Image { private String fileName; public RealImage(String fileName) { this.fileName = fileName; loadFromDisk(fileName); } private void loadFromDisk(String fileName) { System.out.println("Loading " + fileName); } @Override public void display() { System.out.println("Displaying " + fileName); }
} // 代理类
class ProxyImage implements Image { private RealImage realImage; private String fileName; public ProxyImage(String fileName) { this.fileName = fileName; } @Override public void display() { if (realImage == null) { realImage = new RealImage(fileName); } realImage.display(); }
} // 客户端
public class ProxyPatternDemo { public static void main(String[] args) { Image image = new ProxyImage("test.jpg"); // 图像将从磁盘加载 image.display(); }
}
JDK动态代理
JDK动态代理是在运行时动态地创建代理类,需要被代理的对象必须实现一个或多个接口。
java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy; // 接口
interface Image { void display();
} // 被代理类
class RealImage implements Image { private String fileName; public RealImage(String fileName) { this.fileName = fileName; } @Override public void display() { System.out.println("Displaying " + fileName); }
} // 代理类的InvocationHandler实现
class ImageInvocationHandler implements InvocationHandler { private Object target; public ImageInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在方法调用之前可以添加额外的处理 System.out.println("Before method " + method.getName()); // 调用原始对象的方法 Object result = method.invoke(target, args); // 在方法调用之后可以添加额外的处理 System.out.println("After method " + method.getName()); return result; }
} // 客户端
public class DynamicProxyPatternDemo { public static void main(String[] args) { Image realImage = new RealImage("test.jpg"); // 创建代理对象 Image proxyImage = (Image) Proxy.newProxyInstance( Image.class.getClassLoader(), new Class[]{Image.class}, new ImageInvocationHandler(realImage) ); // 调用代理对象的方法 proxyImage.display(); }
}
在JDK动态代理中,Proxy.newProxyInstance() 方法用于动态地创建代理对象,它需要三个参数:类加载器、接口数组(被代理类实现的接口)和InvocationHandler实例。通过实现InvocationHandler接口的invoke方法,我们可以在方法调用前后添加自定义的逻辑。