解释JavaScript设计模式--用例子来解释

263 阅读15分钟

大家好!在这篇文章中,我将解释什么是设计模式以及为什么它们是有用的。

我们还将介绍一些最流行的设计模式,并给出每个模式的例子。让我们开始吧!

目录

什么是设计模式?

设计模式是由四位C++工程师在1994年出版的《设计模式》一书中推广的。设计模式:可重用的面向对象软件的要素》一书,由四位C++工程师于1994年出版。

该书探讨了面向对象编程的能力和陷阱,并描述了23种有用的模式,你可以通过这些模式来解决常见的编程问题。

这些模式不是算法或具体实现。它们更像是想法、观点和抽象,在某些情况下可以用来解决某类问题。

模式的具体实现可能因许多不同的因素而不同。但重要的是它们背后的概念,以及它们如何帮助我们实现一个更好的问题解决方案。

说到这里,请记住这些模式是在考虑到OOP C++编程的情况下想出来的。当涉及到更多的现代语言如JavaScript或其他编程范式时,这些模式可能不会同样有用,甚至可能给我们的代码添加不必要的模板。

尽管如此,我认为了解它们作为一般的编程知识是很好的。

题外话。如果你不熟悉编程范式OOP,我最近写了两篇关于这些主题的文章。

总之...现在我们已经把介绍讲完了,设计模式被分为三大类:创造型、结构型和行为型。让我们简单地探讨一下每一类。🧐

创造性的设计模式

创造性模式由用于创建对象的不同机制组成。

单子模式

单子是一种设计模式,它确保一个类只有一个不可变的实例。简单地说,单子模式包括一个不能被复制或修改的对象。当我们想为我们的应用程序拥有一些不可变的单点真理时,它通常是有用的。

比方说,我们想在一个单一的对象中拥有我们应用程序的所有配置。而且我们希望不允许对该对象进行任何重复或修改。

实现这种模式的两种方法是使用对象字面和类:

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 removed
Object.freeze(Config)

Config.start() // "App has started"
Config.update() // "App has updated"

Config.name = "Robert" // We try to add a new key
console.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 type
class 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 factory
const 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 }

创建者模式

构建者模式被用来以 "步骤 "创建对象。通常,我们会有一些函数或方法来为我们的对象添加某些属性或方法。

这种模式最酷的地方在于,我们将属性和方法的创建分离成不同的实体。

如果我们有一个类或一个工厂函数,我们实例化的对象将总是拥有该类/工厂中声明的所有属性和方法。但使用构建器模式,我们可以创建一个对象,并只对其应用我们需要的 "步骤",这是一个更灵活的方法。

这与对象组合有关,这个话题我在这里已经谈过了:

// We declare our objects
const 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 them
const 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 parameters
addFlyingAbility(bug1)
bug1.fly() // output: "Now Buggy McFly can fly!"

addSpeechAbility(bug2)
bug2.saySmthg() // output: "Martiniano Buggland walks the walk and talks the talk!"

原型(Prototype)模式

原型模式允许你使用另一个对象作为蓝图来创建一个对象,继承其属性和方法。

如果你已经在JavaScript周围呆了一段时间,你可能对原型继承和JavaScript如何围绕它工作很熟悉。

最终的结果与我们使用类所得到的非常相似,但有更多的灵活性,因为属性和方法可以在对象之间共享,而不依赖于同一个类:

// We declare our prototype object with two methods
const enemy = {
    attack: () => console.log("Pim Pam Pum!"),
    flyAway: () => console.log("Flyyyy like an eagle!")
}

// We declare another object that will inherit from our prototype
const bug1 = {
    name: "Buggy McFly",
    phrase: "Your debugger doesn't work with me!"
}

// With setPrototypeOf we set the prototype of our object
Object.setPrototypeOf(bug1, enemy)

// With getPrototypeOf we read the prototype and confirm the previous has worked
console.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 cities
const 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 add
const 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 have
const toMillionsAdapter = city => { city.habitants = parseFloat((city.habitants/1000000).toFixed(1)) }

toMillionsAdapter(BuenosAires)

// We add the new city to the array
citiesHabitantsInMillions.push(BuenosAires)

// And this function returns the largest habitants number
const MostHabitantsInMillions = () => {
    return Math.max(...citiesHabitantsInMillions.map(city => city.habitants))
}

console.log(MostHabitantsInMillions()) // 8.9

装饰器模式

装饰器模式让你把新的行为附加到对象上,把它们放在包含行为的包装对象里面。如果你对React和高阶组件(HOC)有点熟悉,这种方法可能会让你印象深刻。

从技术上讲,React中的组件是函数,不是对象。但如果我们考虑一下React Context或Memo的方式,我们可以看到,我们把一个组件作为一个孩子传递给这个HOC,由于这个孩子组件能够访问某些功能。

在这个例子中,我们可以看到ContextProvider组件是作为prop接收子组件的:


import { useState } from 'react'
import Context from './Context'

const ContextProvider: React.FC = ({children}) => {

    const [darkModeOn, setDarkModeOn] = useState(true)
    const [englishLanguage, setEnglishLanguage] = useState(true)

    return (
        <Context.Provider value={{
            darkModeOn,
            setDarkModeOn,
            englishLanguage,
            setEnglishLanguage
        }} >
            {children}
        </Context.Provider>
    )
}

export default ContextProvider

然后,我们将整个应用程序包裹在它周围:

export default function App() {
  return (
    <ContextProvider>
      <Router>

        <ErrorBoundary>
          <Suspense fallback={<></>}>
            <Header />
          </Suspense>

          <Routes>
              <Route path='/' element={<Suspense fallback={<></>}><AboutPage /></Suspense>}/>

              <Route path='/projects' element={<Suspense fallback={<></>}><ProjectsPage /></Suspense>}/>

              <Route path='/projects/helpr' element={<Suspense fallback={<></>}><HelprProject /></Suspense>}/>

              <Route path='/projects/myWebsite' element={<Suspense fallback={<></>}><MyWebsiteProject /></Suspense>}/>

              <Route path='/projects/mixr' element={<Suspense fallback={<></>}><MixrProject /></Suspense>}/>

              <Route path='/projects/shortr' element={<Suspense fallback={<></>}><ShortrProject /></Suspense>}/>

              <Route path='/curriculum' element={<Suspense fallback={<></>}><CurriculumPage /></Suspense>}/>

              <Route path='/blog' element={<Suspense fallback={<></>}><BlogPage /></Suspense>}/>

              <Route path='/contact' element={<Suspense fallback={<></>}><ContactPage /></Suspense>}/>
          </Routes>
        </ErrorBoundary>

      </Router>
    </ContextProvider>
  )
}

稍后,使用useContext 钩子,我可以从我的应用程序中的任何组件访问定义在Context中的状态:


const AboutPage: React.FC = () => {

    const { darkModeOn, englishLanguage } = useContext(Context)
    
    return (...)
}

export default AboutPage

同样,这可能不是本书作者在写这个模式时想到的确切实现,但我相信想法是一样的。把一个对象放在另一个对象中,这样它就可以访问某些功能。)

Facade模式

Facade模式为一个库、一个框架或任何其他复杂的类集提供了一个简化的接口。

好吧......我们可能会有很多这样的例子,对吗?我的意思是,React本身或任何 gazillion库在那里用于几乎所有与软件开发有关的东西。特别是当我们考虑到声明式编程时,它是关于提供抽象,将复杂性从开发者的眼中隐藏起来。

一个简单的例子是JavaScript的map,sort,reducefilter 函数,这些函数的工作方式就像引擎盖下面的好的for 循环。

另一个例子是现在用于UI开发的任何库,如MUI。正如我们在下面的例子中所看到的,这些库为我们提供的组件带来了内置的特性和功能,帮助我们更快更容易地构建代码。

但所有这些在编译后都变成了简单的HTML元素,这是浏览器唯一能理解的东西。这些组件只是抽象的东西,在这里是为了让我们的生活更轻松。

thewolfofwallstreet-fairydust

一个门面...

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 (
    <TableContainer component={Paper}>
      <Table sx={{ minWidth: 650 }} aria-label="simple table">
        <TableHead>
          <TableRow>
            <TableCell>Dessert (100g serving)</TableCell>
            <TableCell align="right">Calories</TableCell>
            <TableCell align="right">Fat&nbsp;(g)</TableCell>
            <TableCell align="right">Carbs&nbsp;(g)</TableCell>
            <TableCell align="right">Protein&nbsp;(g)</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map((row) => (
            <TableRow
              key={row.name}
              sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
            >
              <TableCell component="th" scope="row">
                {row.name}
              </TableCell>
              <TableCell align="right">{row.calories}</TableCell>
              <TableCell align="right">{row.fat}</TableCell>
              <TableCell align="right">{row.carbs}</TableCell>
              <TableCell align="right">{row.protein}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

代理模式

代理模式为另一个对象提供了一个替代物或占位符。这个想法是为了控制对原始对象的访问,在请求到达实际的原始对象之前或之后执行某种动作。

同样,如果你熟悉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)
  }
})

这样一来,如果没有提供令牌或提供了错误的令牌,中间件将返回相应的错误响应。如果提供了有效的令牌,中间件将调用next() ,端点函数将被接下来执行。

我们本来可以在端点本身写同样的代码,在那里验证令牌,而不用担心中间件或其他东西。但问题是现在我们有了一个可以在许多不同的端点中重复使用的抽象概念。

同样,这可能不是作者心中的准确想法,但我相信这是一个有效的例子。我们在控制一个对象的访问,这样我们就可以在一个特定的时刻执行动作。

行为设计模式

行为模式控制不同对象之间的通信和责任分配。

责任链模式

责任链将请求沿着一连串的处理程序传递。每个处理程序决定处理该请求或将其传递给链上的下一个处理程序。

对于这种模式,我们可以使用与之前完全相同的例子,因为Express中的中间件在某种程度上是处理程序,它要么处理一个请求,要么把它传递给下一个处理程序。

如果你想要另一个例子,想想任何一个系统,在这个系统中,你有某些信息需要沿着许多步骤进行处理。在每个步骤中,不同的实体负责执行一个动作,只有在满足某个条件的情况下,信息才会被传递给另一个实体。

一个典型的消耗API的前端应用程序可以作为一个例子:

  • 我们有一个函数负责渲染一个UI组件
  • 一旦渲染完毕,另一个函数会向一个API端点发出请求
  • 如果端点的响应符合预期,信息就会被传递给另一个函数,该函数以特定的方式对数据进行排序,并将其存储在一个变量中
  • 一旦该变量存储了所需的信息,另一个函数就负责在用户界面中渲染它

我们可以看到,在这里我们有许多不同的实体,它们合作执行一项特定的任务。它们中的每一个都负责该任务的一个 "步骤",这有助于代码的模块化和关注点的分离。

迭代器模式

迭代器被用来遍历一个集合中的元素。这在现在使用的编程语言中可能听起来微不足道,但并不总是这样。

总之,我们所掌握的任何一个用于遍历数据结构的JavaScript内置函数(for,forEach,for...of,for...in,map,reduce,filter, 等等)都是迭代器模式的例子。

与我们在更复杂的数据结构(如树或图)中进行遍历的任何遍历算法一样。

观察者模式

观察者模式让你定义一个订阅机制,将发生在他们所观察的对象上的任何事件通知多个对象。基本上,这就像在一个给定的对象上有一个事件监听器,当该对象执行我们所监听的动作时,我们就做一些事情。

React的useEffect钩子可能是一个好例子。useEffect所做的是在我们声明的时刻执行一个给定的函数。

这个钩子分为两个主要部分,可执行的函数和一个依赖性数组。如果数组是空的,就像下面的例子一样,每次组件被渲染时,该函数就会被执行:

  useEffect(() => { console.log('The component has rendered') }, [])

如果我们在依赖关系数组中声明任何变量,那么只有当这些变量发生变化时,函数才会执行:

  useEffect(() => { console.log('var1 has changed') }, [var1])

即使是普通的JavaScript事件监听器也可以被认为是观察者。另外,像RxJS这样的反应式编程和库,用来处理系统中的异步信息和事件,也是这种模式的好例子。

综述

如果你想了解更多关于这个主题的信息,我推荐这个很棒的Fireship视频这个很棒的网站,在那里你可以找到非常详细的解释和插图来帮助你理解每个模式。

一如既往,我希望你喜欢这篇文章,并学到一些新东西。如果你愿意,你也可以在LinkedInTwitter上关注我。

祝贺你,下一篇见!✌️