大家好!在本文中,我将解释什么是设计模式以及它们为何有用。
目录
什么是设计模式?
创意设计模式
单例模式
工厂方法模式
抽象工厂模式
建造者模式
原型模式
结构设计模式
适配器模式
装饰模式
立面图案
代理模式
行为设计模式
责任链模式
迭代器模式
观察者模式
设计模式因《设计模式:可重用面向对象软件的元素》一书而流行起来,该书由四位 C++ 工程师于 1994 年出版。
本书探讨了面向对象编程的功能和陷阱,并描述了 23 种有用的模式,您可以实现这些模式来解决常见的编程问题。
这些模式不是算法或特定的实现。它们更像是想法、观点和抽象概念,在某些情况下可用于解决特定类型的问题。
模式的具体实现可能会根据许多不同的因素而有所不同。但重要的是它们背后的概念,以及它们如何帮助我们为我们的问题找到更好的解决方案。
话虽这么说,请记住这些模式是在考虑 OOP C++ 编程的情况下想到的。当涉及更现代的语言(如 JavaScript 或其他编程范例)时,这些模式可能不会同样有用,甚至可能会向我们的代码添加不必要的样板文件。
尽管如此,我认为了解它们作为一般编程知识还是有好处的。
旁注:如果您不熟悉编程范例或OOP,我最近写了两篇关于这些主题的文章。
无论如何...现在我们已经完成了介绍,设计模式分为三个主要类别:创建模式、结构模式和行为模式。让我们简要探讨一下它们。
创建模式由用于创建对象的不同机制组成。
Singleton是一种设计模式,可确保一个类只有一个不可变的实例。简单地说,单例模式由一个无法复制或修改的对象组成。当我们想要为我们的应用程序提供一些不可变的单点事实时,它通常很有用。
举例来说,我们希望将应用程序的所有配置都放在一个对象中。我们希望禁止对该对象进行任何复制或修改。
实现此模式的两种方法是使用对象文字和类:
const Config = {
start: () => console.log('App has started'),
update: () => console.log('App has updated'),}// We freeze the object to prevent new properties being added and existing properties being modified or removedObject.freeze(Config)Config.start() // "App has started"Config.update() // "App has updated"Config.name = "Robert" // We try to add a new keyconsole.log(Config) // And verify it doesn't work: { start: [Function: start], update: [Function: update] }
使用对象字面量
class Config {
constructor() {}
start(){ console.log('App has started') }
update(){ console.log('App has updated') }}
const instance = new Config()Object.freeze(instance)
使用类
工厂方法模式提供了一个用于创建对象的接口,这些对象在创建后可以进行修改。最酷的一点是,创建对象的逻辑集中在一个地方,从而简化并更好地组织我们的代码。
这种模式被大量使用,也可以通过两种不同的方式实现,通过类或工厂函数(返回对象的函数)。
class Alien {
constructor (name, phrase) {
this.name = name this.phrase = phrase this.species = "alien"
}
fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
sayPhrase = () => console.log(this.phrase)}const alien1 = new Alien("Ali", "I'm Ali the alien!")console.log(alien1.name) // output: "Ali"
使用类
function Alien(name, phrase) {
this.name = name this.phrase = phrase this.species = "alien"}Alien.prototype.fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")Alien.prototype.sayPhrase = () => console.log(this.phrase)const alien1 = new Alien("Ali", "I'm Ali the alien!")console.log(alien1.name) // output "Ali"console.log(alien1.phrase) // output "I'm Ali the alien!"alien1.fly() // output "Zzzzzziiiiiinnnnnggggg"
使用工厂函数
抽象工厂模式允许我们在不指定具体类的情况下生成相关对象系列。当我们需要创建仅共享某些属性和方法的对象时,它非常有用。
它的工作方式是通过呈现一个与客户端交互的抽象工厂。该抽象工厂根据相应的逻辑调用相应的具体工厂。该具体工厂就是返回最终对象的工厂。
基本上,它只是在工厂方法模式上添加了一个抽象层,以便我们可以创建许多不同类型的对象,但仍然与单个工厂函数或类交互。
让我们通过一个例子来看看。假设我们正在为一家汽车公司建模一个系统,该公司当然生产汽车,但也生产摩托车和卡车。
// We have a class or "concrete factory" for each vehicle typeclass Car {
constructor () {
this.name = "Car"
this.wheels = 4
}
turnOn = () => console.log("Chacabúm!!")}class Truck {
constructor () {
this.name = "Truck"
this.wheels = 8
}
turnOn = () => console.log("RRRRRRRRUUUUUUUUUMMMMMMMMMM!!")}class Motorcycle {
constructor () {
this.name = "Motorcycle"
this.wheels = 2
}
turnOn = () => console.log("sssssssssssssssssssssssssssssshhhhhhhhhhham!!")}// And and abstract factory that works as a single point of interaction for our clients// Given the type parameter it receives, it will call the corresponding concrete factoryconst vehicleFactory = {
createVehicle: function (type) {
switch (type) {
case "car":
return new Car()
case "truck":
return new Truck()
case "motorcycle":
return new Motorcycle()
default:
return null
}
}}const car = vehicleFactory.createVehicle("car") // Car { turnOn: [Function: turnOn], name: 'Car', wheels: 4 }const truck = vehicleFactory.createVehicle("truck") // Truck { turnOn: [Function: turnOn], name: 'Truck', wheels: 8 }const motorcycle = vehicleFactory.createVehicle("motorcycle") // Motorcycle { turnOn: [Function: turnOn], name: 'Motorcycle', wheels: 2 }
Builder模式用于按“步骤”创建对象。通常我们会有函数或方法向我们的对象添加某些属性或方法。
这种模式最酷的一点是我们将属性和方法的创建分离到不同的实体中。
如果我们有一个类或工厂函数,我们实例化的对象将始终具有该类/工厂中声明的所有属性和方法。但是使用构建器模式,我们可以创建一个对象并仅应用我们需要的“步骤”,这是一种更灵活的方法。
这与对象组合有关,这是我在这里讨论的主题。
// We declare our objectsconst bug1 = {
name: "Buggy McFly",
phrase: "Your debugger doesn't work with me!"}const bug2 = {
name: "Martiniano Buggland",
phrase: "Can't touch this! Na na na na..."}// These functions take an object as parameter and add a method to themconst addFlyingAbility = obj => {
obj.fly = () => console.log(`Now ${obj.name} can fly!`)}const addSpeechAbility = obj => {
obj.saySmthg = () => console.log(`${obj.name} walks the walk and talks the talk!`)}// Finally we call the builder functions passing the objects as parametersaddFlyingAbility(bug1)bug1.fly() // output: "Now Buggy McFly can fly!"addSpeechAbility(bug2)bug2.saySmthg() // output: "Martiniano Buggland walks the walk and talks the talk!"
原型模式允许您使用另一个对象作为蓝图来创建一个对象,并继承其属性和方法。
如果您使用 JavaScript 一段时间,您可能熟悉原型继承以及 JavaScript 的工作原理。
最终结果与我们使用类获得的结果非常相似,但具有更多的灵活性,因为属性和方法可以在对象之间共享,而不依赖于同一个类。
// We declare our prototype object with two methodsconst enemy = {
attack: () => console.log("Pim Pam Pum!"),
flyAway: () => console.log("Flyyyy like an eagle!")}// We declare another object that will inherit from our prototypeconst bug1 = {
name: "Buggy McFly",
phrase: "Your debugger doesn't work with me!"}// With setPrototypeOf we set the prototype of our objectObject.setPrototypeOf(bug1, enemy)// With getPrototypeOf we read the prototype and confirm the previous has workedconsole.log(Object.getPrototypeOf(bug1)) // { attack: [Function: attack], flyAway: [Function: flyAway] }console.log(bug1.phrase) // Your debugger doesn't work with me!console.log(bug1.attack()) // Pim Pam Pum!console.log(bug1.flyAway()) // Flyyyy like an eagle!
结构模式是指如何将对象和类组装成更大的结构。
适配器允许两个具有不兼容 接口的对象相互交互。
例如,假设您的应用程序查询一个返回XML 的API ,并将该信息发送到另一个 API 来处理该信息。但处理 API 需要JSON。由于两个接口不兼容,因此您无法发送收到的信息。你需要先适应它。
我们可以用一个更简单的例子来形象化相同的概念。假设我们有一系列城市和一个返回这些城市拥有的最大居民数量的函数。我们数组中的居民数量以百万为单位,但我们要添加一个新城市,其居民没有百万转换:
// Our array of citiesconst citiesHabitantsInMillions = [
{ city: "London", habitants: 8.9 },
{ city: "Rome", habitants: 2.8 },
{ city: "New york", habitants: 8.8 },
{ city: "Paris", habitants: 2.1 },] // The new city we want to addconst BuenosAires = {
city: "Buenos Aires",
habitants: 3100000}// Our adapter function takes our city and converts the habitants property to the same format all the other cities haveconst toMillionsAdapter = city => { city.habitants = parseFloat((city.habitants/1000000).toFixed(1)) }toMillionsAdapter(BuenosAires)// We add the new city to the arraycitiesHabitantsInMillions.push(BuenosAires)// And this function returns the largest habitants numberconst MostHabitantsInMillions = () => {
return Math.max(...citiesHabitantsInMillions.map(city => city.habitants))}console.log(MostHabitantsInMillions()) // 8.9
装饰器模式允许您通过将新行为放置在包含行为的包装对象内来将新行为附加到对象上。如果您对 React 和高阶组件 (HOC) 有一定的了解,这种方法可能会让您感到熟悉。
从技术上讲,React 中的组件是函数,而不是对象。但是,如果我们考虑一下 React Context 或Memo的方式,我们可以看到我们将一个组件作为子组件传递给这个 HOC,并且由于这个子组件能够访问某些功能。
在此示例中,我们可以看到 ContextProvider 组件正在接收子项作为 props:
import { useState } from 'react'import Context from './Context'const ContextProvider: React.FC = ({children}) => {
const [darkModeOn, setDarkModeOn] = useState(true)
const [englishLanguage, setEnglishLanguage] = useState(true)
return (
{children}
)}export default ContextProvider
然后我们将整个应用程序围绕它:
export default function App() {
return (
>}>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
>}> }/>
)}
之后,使用该useContext
钩子,我可以从应用程序中的任何组件访问上下文中定义的状态。
const AboutPage: React.FC = () => {
const { darkModeOn, englishLanguage } = useContext(Context)
return (...)}export default AboutPage
同样,这可能不是本书作者在撰写此模式时所想到的确切实现,但我相信想法是相同的。将一个对象放置在另一个对象中,以便它可以访问某些功能。;)
外观模式为库、框架或任何其他复杂的类集提供了简化的接口。
嗯……我们可能可以为此举出很多例子,对吗?我的意思是,React 本身或任何无数的库几乎用于与软件开发相关的任何内容。特别是当我们考虑声明式编程时,一切都是为了提供抽象,从而隐藏开发人员眼中的复杂性。
一个简单的例子就是 JavaScript 的map
、sort
、reduce
和filter
函数,它们的工作原理就像for
底层的良好循环一样。
另一个例子可以是当今用于 UI 开发的任何库,例如MUI。正如我们在下面的示例中看到的,这些库为我们提供了带有内置特性和功能的组件,可以帮助我们更快、更轻松地构建代码。
但所有这些在编译后都会变成简单的 HTML 元素,这是浏览器唯一能理解的东西。这些组件只是抽象概念,旨在让我们的生活更轻松。
import * as React from 'react';import Table from '@mui/material/Table';import TableBody from '@mui/material/TableBody';import TableCell from '@mui/material/TableCell';import TableContainer from '@mui/material/TableContainer';import TableHead from '@mui/material/TableHead';import TableRow from '@mui/material/TableRow';import Paper from '@mui/material/Paper';function createData(
name: string,
calories: number,
fat: number,
carbs: number,
protein: number,) {
return { name, calories, fat, carbs, protein };}const rows = [
createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
createData('Eclair', 262, 16.0, 24, 6.0),
createData('Cupcake', 305, 3.7, 67, 4.3),
createData('Gingerbread', 356, 16.0, 49, 3.9),];export default function BasicTable() {
return (
Dessert (100g serving)
Calories
Fat (g)
Carbs (g)
Protein (g)
{rows.map((row) => (
{row.name}
{row.calories}
{row.fat}
{row.carbs}
{row.protein}
))}
);}
代理模式为另一个对象提供替代或占位符。这个想法是控制对原始对象的访问,在请求到达实际原始对象之前或之后执行某种操作。
再说一遍,如果您熟悉ExpressJS,这可能会让您感到熟悉。Express是一个用于开发NodeJS API的框架,它的特点之一就是使用中间件。中间件只不过是我们可以在任何请求到达端点之前、中间或之后执行的代码片段。
让我们看一个例子。这里我有一个验证身份验证令牌的函数。不要太关注它是如何做到这一点的。只需知道它接收令牌作为参数,一旦完成,它就会调用该next()
函数。
const jwt = require('jsonwebtoken')module.exports = function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token === null) return res.status(401).send(JSON.stringify('No access token provided'))
jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).send(JSON.stringify('Wrong token provided'))
req.user = user next()
})}
该函数是一个中间件,我们可以通过以下方式在 API 的任何端点中使用它。我们只需将中间件放置在端点地址之后和端点函数声明之前:
router.get('/:jobRecordId', authenticateToken, async (req, res) => {
try {
const job = await JobRecord.findOne({_id: req.params.jobRecordId})
res.status(200).send(job)
} catch (err) {
res.status(500).json(err)
}})
这样,如果没有提供token或者提供了错误的token,中间件将返回相应的错误响应。如果提供了有效的令牌,中间件将调用该next()
函数,接下来将执行端点函数。
我们可以在端点本身中编写相同的代码并验证其中的令牌,而不必担心中间件或任何东西。但问题是现在我们有了一个可以在许多不同端点中重用的抽象。
再说一次,这可能不是作者想要的确切想法,但我相信这是一个有效的例子。我们控制对象的访问,以便我们可以在特定时刻执行操作。
行为模式控制不同对象之间的通信和职责分配。
责任链沿着处理程序链传递请求。每个处理程序决定要么处理请求,要么将其传递给链中的下一个处理程序。
对于此模式,我们可以使用与之前相同的示例,因为 Express 中的中间件在某种程度上是处理请求或将其传递给下一个处理程序的处理程序。
如果您想要另一个示例,请考虑您有某些信息需要通过多个步骤进行处理的任何系统。在每个步骤中,不同的实体负责执行操作,并且只有满足特定条件时信息才会传递给另一个实体。
一个使用 API 的典型前端应用程序可以作为示例:
我们有一个负责渲染 UI 组件的函数。
渲染后,另一个函数会向 API 端点发出请求。
如果端点响应符合预期,则信息将传递到另一个函数,该函数以给定方式对数据进行排序并将其存储在变量中。
一旦该变量存储了所需的信息,另一个函数就会负责将其呈现在 UI 中。
我们可以看到这里有许多不同的实体如何协作执行某个任务。他们每个人都负责该任务的一个“步骤”,这有助于代码模块化和关注点分离。
迭代器用于遍历集合中的元素。对于当今使用的编程语言来说,这听起来可能微不足道,但情况并非总是如此。
for
不管怎样,我们可以用来迭代数据结构( 、forEach
、for...of
、for...in
、map
、reduce
、filter
等)的任何 JavaScript 内置函数都是迭代器模式的示例。
与我们编写的任何遍历算法一样,可以迭代更复杂的数据结构(例如树或图)。
观察者模式允许您定义订阅机制来通知多个对象有关它们正在观察的对象所发生的任何事件。基本上,这就像在给定对象上有一个事件侦听器,当该对象执行我们正在侦听的操作时,我们会执行某些操作。
React 的 useEffect 钩子可能是一个很好的例子。useEffect 的作用是在我们声明的那一刻执行给定的函数。
该钩子分为两个主要部分,可执行函数和依赖项数组。如果数组为空(如下例所示),则每次渲染组件时都会执行该函数。
useEffect(() => { console.log('The component has rendered') }, [])
如果我们在依赖数组中声明任何变量,则仅当这些变量发生更改时该函数才会执行。
useEffect(() => { console.log('var1 has changed') }, [var1])
即使是普通的旧式 JavaScript 事件监听器也可以被视为观察者。此外,响应式编程和像RxJS这样的库,用于处理系统中的异步信息和事件,也是这种模式的很好的例子。