设计模式
构造器模式和原型模式
ES6之前
构造器模式
function User(name,age) {
this.name = name;
this.age = age;
this.say = function() {
console.log(`我叫${this.name}, 今年${this.age}岁`);
}
}
//实例化之后使用
const zs = new User("张三", 18);
const ls = new User("李四", 18);
zs.say === ls.say; //false原型模式
function User(name,age) {
this.name = name;
this.age = age;
tser.proptotype.say = function() {
console.log(`我叫${this.name}, 今年${this.age}岁`);
}
}
const zs = new User("张三", 18);
const ls = new User("李四", 18);
zs.say === ls.say; //true区别
- 原型模式将方法定义在原型上可以节省内存,因为所有实例都指向同一个方法实例。而将方法定义在构造函数内部则是为了能够让每个实例拥有自己的版本。
ES6
使用class,构造器+原型,自动将属性挂载到实例上,将方法挂载在原型上
class User {
name: string
age: number
//放之前的构造器代码
constructor(name: string, age: number) {
this.name = name
this.age = age
}
//放之前的原型代码
say() {
console.log(`我叫${this.name}, 今年${this.age}岁`)
}
}
简单工厂模式
工厂模式:设计一个工厂函数,接收不同的参数,工厂根据参数不同进行实例化所需要的对象,无需知道创建的具体细节。但是当判断较多修改代码较为麻烦。
class User {
constructor(name, skill) {
this.name = name;
this.skill = skill;
}
static UserFactory(name) {
switch(name) {
case "zs":
return new User("zs", ['js','java','python']);
break;
case 'ls':
return new User("ls", ['c++','c#']);
break;
case "ww":
return new User('ww', ['eat']);
break;
}
}
}
//使用
const user = User.UserFactory("zs");
抽象工厂模式
抽象工厂:不直接生成实例,只是告诉你该实例化哪个类
class User {
constructor (name, skill) {
this.name = name;
this.skill = skill;
}
sayHello() {
console.log("welcome")
}
skilltree() {
}
}
class HighUser extends User {
constructor(name) {
super(name , ['js','java','python'])
}
//一系列方法
}
class MidUser extends User {
constructor(name) {
super(name ,['c++','c#'])
}
....
}
class LowUser extends User {
constructor(name) {
super(name ,['eat'])
}
....
}
function AbstractUserFactory(name) {
switch(name) {
case "zs":
return HighUser
break;
case 'ls':
return MidUser;
break;
case "ww":
return LowUser;
break;
}
}抽象工厂模式返回你需要的类,需要自己进行实例化,相比于简单工厂模式,工厂代码中的耦合性有所降低。
建造者模式
- 建造者模式:创建复杂对象的设计模式,将复杂对象的构建与它的表示分离,使同样的构建过程可以创建不同的表示
- 主要包括四部分:产品,抽象类,具体建造者,指挥者四部分
普通建造者模式示例
主要四部分
// 产品 |
使用
public class Client { |
构建过程和表示分离
- 构建过程:指创建和组装一个复杂对象的步骤。在上面的示例中,构建过程就是
buildCpu
、buildGpu
、buildRam
和buildStorage
这四个方法。这些方法定义了如何创建和组装Computer
对象。 - 表示:这是指复杂对象的最终形态。在上面的示例中,表示就是
Computer
类。Computer
类定义了一个复杂对象的属性和行为。 - 释意:我们可以改变对象的构建过程,而不影响对象的表示。例如,我们可以创建一个新的建造者类,比如
LaptopBuilder
,它有不同的buildCpu
、buildGpu
、buildRam
和buildStorage
方法。然后,我们可以使用Director
类来使用这个新的建造者类,构建一个新的Computer
对象。尽管构建过程改变了,但是Computer
对象的表示(即它的属性和行为)并没有改变。
简单构建者模式(支持链式调用)
- 大部分简单情况下,对于Director指挥者的部分我们并不需要,所以就有了简单构造者模式
实现
public class Computer { |
使用
public class Client { |
链式调用优点
- 可以灵活设置属性:使用构造方法,实例化时如果设置部分属性的组合,那就需要多个构造方法,不灵活
- 可以设置属性后,在build()方法中统一处理逻辑:对于一些逻辑处理,一些属性的判断可能依靠其他属性,必须按照先后顺序规则依次set。
注意点
- 目标类的构造方式必须传入builder对象
- builder类位于目标类内,且用static描述
- builder类中的set方法返回值必须为builder本身,这是可以链式调用的关键
- builder中的build方法用来实现目标对象的创建
观察者模式
定义
- 包括观察者(Observer),被观察者(Subject)以及行为,通过定义一种订阅机制,当对象状态发生改变时,所有依赖其的对象都会得到通知
示例
Observer
接口定义了一个通用的update
方法,所有具体的观察者类都需要实现这个方法来响应被观察者状态的变化。ConcreteObserverA
和ConcreteObserverB
是具体的观察者类,它们实现了Observer
接口并提供了update
方法的具体实现。Subject
类是抽象被观察者,它维护了一个观察者列表,并提供了add
、remove
和notifyObservers
方法。其中,notifyObservers
方法负责在被观察者状态变化时遍历观察者列表并调用每个观察者的update
方法。ConcreteSubject
是具体被观察者类,它继承自Subject
并添加了一个state
属性及对应的getState
和setState
方法。当setState
方法被调用且状态实际发生变化时,会触发notifyObservers
方法,通知所有注册的观察者。- 运行主程序
Main
,您将看到当ConcreteSubject
对象的状态发生变化时,两个具体的观察者ConcreteObserverA
和ConcreteObserverB
都会收到通知并打印出接收到的消息。
//观察者 |
设计模式六大原则
开闭原则(Open-Closed Principle,OCP)
定义
对扩展开放,对修改关闭。
实现开闭原则的主要策略是使用抽象构建框架,使用实现扩展细节。以下是一些常用的策略:
- 使用接口或抽象类:接口和抽象类都是定义规范和抽象层,子类或实现类提供具体的实现。当需求变化时,只需要增加新的实现类或子类,而无需修改原有代码。
- 使用设计模式:很多设计模式都是为了满足开闭原则而生的。例如,策略模式允许我们在运行时选择算法,模板方法模式允许我们改变算法的某些步骤,装饰器模式允许我们动态地添加或修改行为等。
- 使用依赖注入:依赖注入是一种编程技术,它允许我们在运行时改变程序的依赖关系。这样,我们可以在不修改代码的情况下改变程序的行为。
依赖注入示例:NotificationService
依赖于
MessageService,但是它并不知道具体使用的是哪个实现类。
MessageService的具体实现是通过构造函数注入的,这就是依赖注入。当我们需要改变消息发送的方式时,我们只需要更改
MessageService的实现类,而不需要修改
NotificationService的代码。例如,如果我们想要使用 SMS 来发送消息,我们只需要在 Spring 的配置文件中更改
MessageService的实现类为
SMSService`,而不需要修改 NotificationService 的代码。这就是开闭原则的体现。public interface MessageService {
void sendMessage(String message);
}
public class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Email Message Sent: " + message);
}
}
public class SMSService implements MessageService {
public void sendMessage(String message) {
System.out.println("SMS Message Sent: " + message);
}
}import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
public class NotificationService {
private MessageService messageService;
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
里氏替换原则
定义
- 子类可以扩展父类的功能,但不能改变父类原有的功能。当将基类替换成它的子类对象,程序不会产生异常。
关注点
行为一致性
- 子类对象在替换其基类对象时,应确保在程序中表现出与基类对象相同的行为。这意味着在任何使用基类的地方,都可以无缝地使用其子类对象,而不会影响程序的正确性。这要求子类对基类接口的实现必须遵循基类定义的约定,包括方法的输入/输出、异常处理、状态变化等。
前置条件
子类在重写基类方法时,不得放宽基类方法的前置条件(即方法的输入要求)。例如,如果基类方法要求输入参数是非空的,那么子类方法也必须保持这一要求,不得接受空参数。
反例:基类
PaymentProcessor
,其中有一个方法processPayment(amount: number)
,要求输入的amount
必须大于0。子类DiscountedPaymentProcessor
覆盖了该方法,但在实现中允许处理金额为0的支付请求。class PaymentProcessor {
processPayment(amount: number) {
if (amount <= 0) throw new Error('Amount must be greater than zero.');
// ...正常的支付处理逻辑
}
}
//折扣处理
class DiscountedPaymentProcessor extends PaymentProcessor {
processPayment(amount: number) {
if (amount === 0) console.log('Processing free payment.');
else super.processPayment(amount);
}
}解决方法: 子类应保持与基类相同的前置条件,不允许处理金额为0的支付请求。如果需要特殊处理这种情况,应在其他方法或逻辑中处理,而不是直接放宽基类方法的前置条件。
class DiscountedPaymentProcessor extends PaymentProcessor {
processFreePayment() {
console.log('Processing free payment.');
}
processPayment(amount: number) {
if (amount <= 0) this.processFreePayment();
else super.processPayment(amount);
}
}
后置条件
子类方法应至少提供与基类方法相同的后置条件(即方法的输出保证)。理想情况下,子类方法还应努力提供更强的后置条件或保证,以增强方法的行为。比如,如果基类方法承诺在成功执行后将对象置于某种有效状态,子类方法也应确保同样的有效状态。
反例: 基类
List
有一个方法add(item: T): boolean
,承诺在成功添加元素时返回true
,否则返回false
。子类FixedSizeList
覆盖了该方法,当列表已满时直接忽略添加请求,返回undefined
。class List<T> {
add(item: T): boolean {
// ...正常的添加逻辑,返回是否成功添加
}
}
class FixedSizeList<T> extends List<T> {
add(item: T) {
if (this.isFull()) return; // 返回 undefined,违反后置条件
super.add(item);
}
}解决方法: 子类应遵循基类方法的后置条件,即使在列表满时也应该返回一个布尔值,表明添加操作的结果。可以返回
false
表示添加失败。class FixedSizeList<T> extends List<T> {
add(item: T) {
if (this.isFull()) return false; // 返回 false,表示添加失败
super.add(item);
}
}
异常行为
子类方法抛出的异常类型应与基类方法保持一致,或者为基类方法抛出异常类型的子类型。这样,客户端代码无需因使用子类对象而改变对异常的处理方式。如果基类方法声明抛出某个异常,子类方法可以抛出该异常的同类型或子类型,但不应抛出基类方法未声明的异常。
反例: 基类
FileManager
有一个方法deleteFile(path: string)
,在文件不存在时抛出FileNotFoundException
。子类CloudFileManager
覆盖了该方法,当云文件不存在时抛出CloudStorageException
。class FileManager {
deleteFile(path: string) {
if (!fileExists(path)) throw new FileNotFoundException();
// ...正常的文件删除逻辑
}
}
class CloudFileManager extends FileManager {
deleteFile(path: string) {
if (!cloudFileExists(path)) throw new CloudStorageException(); // 抛出不同的异常类型
super.deleteFile(path);
}
}解决方法: 子类应保持与基类方法相同的异常层次结构。在本例中,可以继续抛出
FileNotFoundException
或其子类,以保持对客户端代码的兼容性。class CloudStorageException extends FileNotFoundException {
// ...
}
class CloudFileManager extends FileManager {
deleteFile(path: string) {
if (!cloudFileExists(path)) throw new CloudStorageException(); // 抛出基类异常的子类
super.deleteFile(path);
}
}
依赖倒置原则(Dependency Inversion Principle,DIP)
定义
- 高层模块不依赖于低层模块
- 都应依赖于抽象而非具体实现
- 细节依赖抽象而非抽象依赖细节
- 面向接口编程
实现
其实以上四点的核心就是抽象不应该依赖于细节,细节应该依赖于抽象,
在传统的程序设计中,高层模块(复杂的、业务逻辑多的模块)依赖于低层模块(简单的、基础的模块),而低层模块是基于具体的实现细节来设计的。这样一来,高层模块就会依赖于这些实现细节,这就导致了代码的耦合度高,不利于维护和扩展。
不管是高层模块还是低层模块,都应该基于抽象(如接口或抽象类)来设计。这样,高层模块就不会依赖于低层模块的实现细节,而是依赖于抽象;低层模块的实现细节也是基于抽象,也就是说,细节依赖于抽象。
这样做的好处是,当我们需要改变实现细节(如使用不同的数据源)时,只需要提供一个新的抽象实现,而不需要修改依赖于这些细节的高层模块的代码。这就降低了代码的耦合度,提高了代码的可维护性和可扩展性。
示例:在这个例子中,NotificationService(高层模块)不直接依赖于 EmailSender 或 SMSSender(低层模块),而是依赖于 MessageSender 抽象。这就是面向接口编程。
// 抽象
interface MessageSender {
void send(String message);
}
// 细节
class EmailSender implements MessageSender {
public void send(String message) {
// Send email
}
}
class SMSSender implements MessageSender {
public void send(String message) {
// Send SMS
}
}
// 高层模块
class NotificationService {
private MessageSender messageSender;
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notify(String message) {
messageSender.send(message);
}
}
// 使用
public class Client {
public static void main(String[] args) {
NotificationService service = new NotificationService(new EmailSender());
service.notify("Hello, world!");
}
}反例:
NotificationService
直接依赖于EmailSender
或SMSSender
,如果我们想要修改消息发送的实现(从邮件切换到短信发送),就需要修改NotificationService
代码,这就违反了开闭原则(实体对扩展开放,对修改关闭)// 细节
class EmailSender {
public void send(String message) {
// Send email
}
}
class SMSSender {
public void send(String message) {
// Send SMS
}
}
// 高层模块
class NotificationService {
private EmailSender emailSender;
public NotificationService() {
this.emailSender = new EmailSender(); // 直接依赖于具体的实现
}
public void notify(String message) {
emailSender.send(message);
}
}
// 使用
public class Client {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.notify("Hello, world!");
}
}