说起设计模式,大家对此可能都有一些自己的见解,有的人并不了解设计模式,但并不意味平常开发中没有用到设计模式,今天就设计模式整体做一个浅谈。
什么是设计模式
那么什么是设计模式呢?
设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便地复用成功的设计和体系结构。
说人话就是:设计模式就是在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。
最早的设计模式是由四人组 简称GoF的四位软件工程学者在《设计模式:可复用面向对象软件的基础》一书,在书中共提供了 23 种模式来解决面向对象程序设计中的各种问题,但是要注意的是这些“好的设计”并不是 GoF 发明的,而是早已经存在于软件开发中。一个稍有经验的程序员也许在不知不觉中数次使用过这些设计模式。
GoF最大的功绩是把这些好的设计从浩瀚的面向对象世界中挑选出来,并且给予它们一个好听又好记的名字。在软件设计中,一个好的设计方案有了名字之后,才能被更好地传播,人们才有更多的机会去分享和学习它们。
设计模式的分类
在这 23 种模式中,可以根据意图或者目的来进行分类:
- 创建型模式:提供一种创建对象的机制,增加已有代码的灵活性和复用性
- 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效
- 行为型模式:负责对象间的高效沟通和职责分配。
最早的存在一种说法,设计模式仅仅是就基于类的面向对象的语言而言的。 《设计模式》最初讲的确实是静态类型语言中的设计模式,原书大部分代码由 C++ 写成,但设计模式实际上是解决某些问题的一种思想,与具体使用的语言无关。
模式社区和语言一直都在发展,如今,除了主流的面向对象语言,函数式语言的发展也非常迅猛。在函数式或者其他编程范型的语言中,设计模式依然存在,只是一个多或者少的区别。
要注意:设计模式与设计原则并非强绑定,设计模式与面向对象开发也并非强绑定 设计原则贯穿着整个软件开发,而设计模式是一种解决问题的思想
设计模式的五大原则
整个设计模式,大概都是围绕着所谓的 代码的复用性,可拓展性,高内聚低耦合这几个特性为核心的
具体围绕着solid五大原则为主
单一职责原则
单一职责原则简称SRP 即 一个类应该只有一个引起它变化的原因
说人话就是:个类应该只负责一项任务,如果不遵循单一职责原则,在一个类里面书写了过多的功能,那么就会导致修改一个功能的时候,影响其他功能
SRP 是所有原则中最简单,但同时也是最难正确应用的原则之一。对于初学者来讲,最大的难度就是何时应该分离,是否应该分离。
另外,只有在职责确定要发生变化的时候,此时分离才有意义。如果两个职责耦合在一起,但是这两个职责没有发生变化的征兆,我们也不需要进行分离。
一方面,我们接受设计原则的指导,但是在学习的时候一定要灵活,根据具体的场景来具体的分析。在实际开发中,其实还是会存在一些违反 SRP 原则的情况
开闭原则
开闭原则 简称OCP 即:软件实体(类、模块、函数)应该对扩展开放,对修改封闭。
这个比较好理解,
这个原则鼓励我们通过扩展来添加新的功能,而不是修改已有的代码。这样就可以减少因为修改旧代码引入新错误的风险 例如 存在一个支付场景,要求支持多种支付方式,
好的做法呢应该如下
interface PaymentMethod {
pay(amount: number): void
}
// 信用卡支付方式
class CreditCardPayment implements PaymentMethod {
pay(amount: number) {
}
}
// PayPal支付方式
class PayPalPayment implements PaymentMethod {
pay(amount: number) {
}
}
// 后期要扩展新的支付方式的时候,扩展新的类即可
// 原来的 PaymentProcessor 这个类不受影响
class PaymentProcessor {
processPayment(amount: number, paymentWay: PaymentMethod){
paymentWay.pay(amount);
}
}
而不是这种写法,这种写法每次增加新的支付方式都会在源代码的基础上进行更改,很有可能出现新代码影响到旧代码的情况,违反了开闭原则
class PaymentProcessor {
processPayment(amount: number, type: string) {
if (type === "creditCard") {
// 处理信用卡支付
} else if (type === "paypal") {
// 处理 paypal 支付
}
// 随着业务的发展,后面要支持更多的支付方式
// 那么就需要修改这个方法,这里就不遵循开闭原则
}
}
里氏替换原则
里氏替换原则简称LSP 即它指出如果类 S 是类 T 的子类,那么类型 T 的对象可以被类型 S 的对象替换(即类型 S 的对象可以作为类型 T 的对象使用),并且保证原来程序的逻辑行为不变。
说人话就是:强调了继承的正确使用方法。在使用继承的时候,要保证子类完全实现父类的行为,并且没有改变父亲接口的预期行为,这样才能够透明去替换父类
接口隔离原则
接口隔离原则简称 ISP 即 规定不应该强迫用户去依赖他们不使用的接口。 说人话就是:在进行接口设计的时候,这个接口不应当设计的臃肿,而应当进行分割,分割成更小的更具体的接口,这样做的好处在于,用户只需要依赖他们需要的接口。
试想一下,你需要封装一个组件库,假设存在按钮与选择框两种类型的组件,他们都有一个方法rander渲染,但是按钮还需要一个点击事件,而选择框则不需要这个点击事件。如果涉及成一个接口的话,那么势必导致选择框也要实现自己本不该有的点击事件,如此造成了选择框这个类实现的时候要去实现一个毫无意义的方法。 大致如下:
interface IUIComponent {
render(): void;
onClick(): void;
....
}
// 在上面的接口设计中,我们在接口里面定义的很多的方法
// 但是有一个问题,并非所有的组件都会用到所有的方法
class Button implements IUIComponent {
render() {
console.log("Rendering button");
}
onClick() {
console.log("Button clicked");
}
}
class Dropdown implements IUIComponent {
render() {
console.log("Rendering dropdown");
}
onClick() {
throw new Error("Not applicable");
} // 对于下拉选择框来说,这个方法不适用
}
那么一个好的方法应该是如何呢?将两个方法分别定义为两个接口,具体实现的时候在分别继承不同的接口,而不需要向上边一样都继承自一个接口,这就是所谓的 依赖它所需要依赖的
interface IRenderable {
render(): void;
}
interface IClickable {
onClick(): void;
}
class Button implements IRenderable, IClickable{
// ...
}
class Dropdown implements IRenderable {
// ...
}
依赖倒置原则
依赖倒置原则 简称DIP 即
- 高层模块不应该依赖于低层模块的实现细节,两者都应该依赖于中间的抽象层
- 抽象不应该依赖于细节,而细节应该依赖于抽象
这条原则鼓励我们在进行接口设计的时候,依赖于接口或者抽象类,而非具体的实现。也就是你经常听到的面向接口编程。
说人话就是:实现一个抽象的中间层,将两个原本耦合的模块进行解耦,转而依赖中间层,而非直接相互依赖
最小知识原则
最小知识原则 又称迪米特法则 即:在设计软件实体(函数、类、模块、组件...)的时候,要尽量减少两者之间的相互了解,只与最直接的朋友进行通信,避免和更远的实体(间接的朋友)进行通信。
简单来讲就是: 如果一个对象需要调用另一个对象的方法,而这个方法需要通过第三个对象来调用,那么这就是违反了最小知识原则。应该通过直接调用或者将调用逻辑封装在第三个对象的方法中来避免这种情况。
说人话就是:A要调用B模块中的一个方法,而B模块中的方法又需要通过C模块调用,那么就违反了此原则,如有需要应当将直接调用C模块的方法,而非借助B模块之手
例如 最主要的是这句话this.engine.battery.getCharge(); 应当直接实现Battery从而实现方法,而非借助Engine这个类,这就是所谓的:避免一个单元深入了解另一个单元的内部实现细节。从另一种程度实现解耦
class Battery {
// 返回一个充电状态
getCharge() {
return true;
}
}
class Engine {
constructor() {
// 内部有一个成员属性,依赖于 Battery 这个类
this.battery = new Battery();
}
start() {
if (this.battery.getCharge()) {
console.log("Engine starts.");
}
}
}
class Car {
constructor(){
// Car 这个类又依赖于 Engine 这个类
this.engine = new Engine();
}
startCar(){
this.engine.battery.getCharge();
}
}
正确做法应该如下
class Battery {
// 返回一个充电状态
getCharge() {
return true;
}
}
class Engine {
constructor() {
// 内部有一个成员属性,依赖于 Battery 这个类
this.battery = new Battery();
}
start() {
if (this.battery.getCharge()) {
console.log("Engine starts.");
}
}
// 新增一个方法,返回 Battery 相关的状态
isBatteryCharged(){
return tis.battery.getCharged();
}
}
class Car {
constructor(){
// Car 这个类又依赖于 Engine 这个类
this.engine = new Engine();
}
startCar(){
// this.engine.battery.getCharge(); // 这里就不在间接的访问 Battery 类
this.engine.isBatteryCharged();
}
}
几种原则的相似与比较
依赖倒置原则与开闭原则
- OCP 主要关注如何设计系统以便于添加新功能时不需要修改既有代码
- DIP 更关注于如何组织依赖关系以减少模块间的耦合
接口隔离原则与单一职责原则
- SRP:该原则的关注点是类和模块的设计,类或者某一个模块功能不能太多,里面只能有一个职责。
- ISP:该原则的关注点是针对接口上面的设计,将一个大接口拆分成更小的接口,从而客户端在实现接口的时候能够更加灵活。
总结一下,虽然两者都是强调“分离”和“专一”的概念,但是 SRP 更加强调的是“做一件事情”,而 ISP 更加强调 的是“知道一件事情”,ISP 就是让每个类只知道它需要知道的最小接口集合。