为了账号安全,请及时绑定邮箱和手机立即绑定

设计模式详解:实例中的编程抽象艺术

所以AI时代已经到来,目前这种飞跃表现为在代码中使用const fetch = require('node-fetch')这样的Node.js语法(截至今天,无论是ChatGPT还是Gemini也都如此),并再次推动了这一不断循环的机器——互联网内容的又一轮回。

在这些内容混合在一起的地方,我们又看到了设计模式的浮现。

点击图片查看

从介绍如何在Node(???)中应用设计模式的文章,到详细讲解如何在Java中应用工厂模式等过时内容的文章(Java 8于2014年3月发布,新增了Lambda函数)。

定义是什么呢?

你是否曾经偶然遇到过重构专家
在你学习计算机科学,尤其是编程学习过程中,你可能曾经访问过这个网站。该网站的设计模式部分解释得相当清晰,多年来在各种论坛上被频繁分享。

当我们看看[设计模式的定义]时,会发现:

设计模式是解决软件设计中常见问题的常见方案。每个模式就像一个蓝图,你可以根据特定的设计问题对其进行调整来解决你代码中的特定问题。

那为什么是这篇帖子呢?我的意思是,链接的网站上有许多信息;这些信息都包含在链接的网站中。

问题是这样的,我总是很难接受这个定义……“解决我代码中的特定设计问题”……在我的代码里?我的代码里需要解决某个问题吗?

定义,重新想象

实际上,我需要编写某种“东西”,这种东西对于项目中使用的编程语言来说,缺乏相应的抽象功能。

简单来说。如果这还不够清楚,我们来看一些代码示例。

这是在Java(一种面向对象的编程语言)中实现的特别简单的工厂模式。

    public class ShapeFactory {
      public Shape createShape(String type) {
        if (type.equalsIgnoreCase("圆形")) {
          return new Circle();
        } else if (type.equalsIgnoreCase("正方形")) {
          return new Square();
        } 
        return null;   
      }
    }

点全屏 取消全屏

Java 8 然后添加了 Lambda(一种函数式编程的概念),这样我们就可以这样做:(2014年3月,以防你忘记了)

    Map<String, Supplier<Shape>> shapeFactory = new HashMap<>();
    // 创建一个形状工厂
    shapeFactory.put("圆形", Circle::new);
    shapeFactory.put("正方形", Square::new);

    Shape 圆形 = shapeFactory.get("圆形").get();

进入全屏 退出全屏

再也不用工厂模式了(至少在Java里不用了)。

没错,我知道工厂模式是大多数人都经常使用的例子,但其他模式又怎么样?其他编程语言里又会怎样?

以下是在 Typescript 中的访问者模式:

    interface Shape {
      draw(): void;
      accept(访问者: ShapeVisitor): void; 
    }

    class Circle implements Shape {
      半径: number;

      constructor(半径: number) {
        this.半径 = 半径;   

      }

      draw() {
        console.log("绘制一个圆");
      }

      accept(访问者: ShapeVisitor) {
        访问者.visitCircle(this);
      }
    }

    class Square implements Shape {
      边长: number;

      constructor(边长: number) {
        this.边长 = 边长;
      }

      draw() {
        console.log("绘制一个正方形");
      }

      accept(访问者: ShapeVisitor) {
        访问者.visitSquare(this);
      }
    }

    interface 形状访问者 {
      visitCircle(circle: Circle): void;
      visitSquare(square: Square): void;
    }

    class AreaCalculator implements 形状访问者 {
      private 面积 = 0;

      visitCircle(circle: Circle) { 
        this.面积 = Math.PI * circle.半径 * circle.半径;
        console.log(`圆面积: ${this.面积}`);
      }

      visitSquare(square: Square) {
        this.面积 = square.边长 * square.边长;
        console.log(`正方形面积: ${this.面积}`);
      }

      getArea(): number {
        return this.面积;
      }
    }

    // 使用访问者
    const 圆 = new Circle(5);
    const 正方形 = new Square(4);
    const 计算器 = new AreaCalculator();

    圆.accept(计算器); 
    正方形.accept(计算器); 

全屏模式 退出全屏

以下代码做了同样的事情,但使用了反射(即程序在运行时自我检查和操作其对象的能力)而不是访问者模式来实现。

    interface Shape {
      draw(): void;
    }

    class Circle implements Shape { 
      // ... 如前
      radius: number;
    }

    class Square implements Shape {
      // ... 如前
      sideLength: number;
    }

    function calculateArea(shape: Shape) {
      if (shape instanceof Circle) {
        const circle = shape as Circle; // 类型断言:将 shape 断言为 Circle 类型
        const area = Math.PI * circle.radius * circle.radius;
        console.log(`圆形面积:${area}`);
      } else if (shape instanceof Square) {
        const square = shape as Square; // 类型断言:将 shape 断言为 Square 类型
        const area = square.sideLength * square.sideLength;
        console.log(`正方形面积:${area}`);
      }
    }

    const circle = new Circle(5);
    const square = new Square(4);

    calculateArea(circle);
    calculateArea(square);

进入全屏,退出全屏

现在来看看观察者模式,用 TypeScript 来实现:

    interface Observer {
      update(信息: any): void;
    }

    class NewsPublisher {
      private obs: Observer[] = [];

      订阅(观察者: Observer) {
        this.obs.push(观察者);
      }

      退订(观察者: Observer) {
        this.obs = this.obs.filter(o => o !== 观察者);
      }

      发送通知(新闻: string) {
        this.obs.forEach(观察者 => 观察者.update(新闻));
      }
    }

    class Newsletter订阅者 implements Observer {
      update(新闻: string) {
        console.log(`收到新闻: ${新闻}`);
      }
    }

    // 使用
    const publisher = new NewsPublisher();
    const subscriber1 = new Newsletter订阅者();
    const subscriber2 = new Newsletter订阅者();

    publisher.订阅(subscriber1);
    publisher.订阅(subscriber2);

    publisher.发送通知("新产品的发布");

全屏显示,退出全屏

使用内置在 Node API 中的 EventEmitter:

    import { EventEmitter } from 'events';

    class NewsPublisher extends EventEmitter {
      publish(news: string) {
        this.emit('news', news);
      }
    }

    const publisher = new NewsPublisher();

    publisher.on('news', (news) => {
      console.log(`大家收到了新闻: ${news}`);
    });

    publisher.publish("新产品发布!");

进入全屏,退出全屏

在那个时候,你可能已经意识到问题可能出在面向对象的实现上,你说得没错,但还不完全。

每种编程范型,尤其是以最纯粹的形式来看,都有其独特的怪癖和困难,或“无法直接实现的事情”,诸如此类。

让我们进入函数式编程的领域。你应该听说过 Monad(也许听起来很陌生)。

无论你是否被数学定义的思维陷阱所迷惑,我们这些软件开发者也可以将单子理解为设计模式。因为在只有纯函数的世界里,很难想象一个副作用,但大多数软件产品都需要副作用,那我们该怎么办呢?

下面是一个关于 Haskell 中 IO Monad 的例子:

main :: IO ()
主函数 main = do
  fileContent <- 从文件 "myFile.txt" 读取内容
  输出文件内容 fileContent

进入全屏 退出全屏

读取文件这一副作用包含在 IO 单子(IO monad)中。

让我们用 TypeScript 添加一个单子编程模式的例子。

    class Maybe<T> {
      private value: T | null;

      constructor(value: T | null) {
        this.value = value;
      }

      static just<T>(value: T): Maybe<T> {
        return new Maybe(value);
      }

      static nothing<T>(): Maybe<T> {
        return new Maybe<T>(null);
      }

      map<U>(fn: (value: T) => U): Maybe<U> {
        if (this.value === null) {
          return Maybe.nothing<U>();
        } else {
          return Maybe.just(fn(this.value));
        }
      }
    }

    // 示例用法
    const user = Maybe.just({ name: "Alice", age: 30 });
    const userName = user.map(u => u.name); // 值为 'Alice' 的 Maybe<string>

    const noUser = Maybe.nothing(); // 创建一个无值的 Maybe
    const noUserName = noUser.map(u => u.name); // 尝试获取不存在的名字

全屏模式,退出全屏

一个经典的,我在互联网上大约看过可能单子(Maybe Monad)50次左右,它到底是什么呢?

它试图解决的问题。

let user; // 定义一个用户变量

进入全屏,退出全屏

我们竟然忘了给这个东西定义属性了!

在实际用例中,这通常是读取数据库或文件中的数据。

那么我们现在这么干:

    const userName = user.value.name; // 无法读取 'value' 属性,因为 undefined 没有该属性

切换到全屏模式,退出全屏

程序崩溃了。

不使用 Maybe 类型的解决方法:

    const 用户名 = user?.value?.name; // 未定义

全屏,退出全屏

这个程序不会出问题。

可能单子(Maybe Monad)在JavaScript或TypeScript中并不必要,因为有可选链操作符,但是如果你使用的是不支持这种操作符的语言……嗯,你可以应用这种模式,或者说可能单子?

我知道,有些人刚学了 Maybe 这个东西,就迫不及待地在六个不同的项目中使用它,现在我在聚会上成了笑话的主角,告诉他们“其实你不一定需要它”。不过你还是可以用的,事实上,如果你觉得合适,我鼓励你去用它。(毕竟代码是你写的,再加上你那潇洒的个性,你可以随心所欲地使用它!🤭)

但是让我们回到基础。关于其他范式呢?如果你跳出面向对象编程和函数式编程的框架来思考,我真的很欣赏呢!

所有范式都有自己常见的解决方案和技术,即使这些解决方案和技术并不总被称为“设计模式”。

这里有几个例子,谢谢 Gemini 和我:感谢 Gemini 避免我思考得太辛苦,感谢 Gemini 提供了漂亮的格式和增加了价值 😁

逻辑编程语言:
  • 约束逻辑程序设计:这种范式包括定义约束和变量之间的关系,然后让系统找到满足这些约束条件的解。回溯法约束传播等技术对于这种范式下的高效问题求解至关重要。(在处理AI相关问题时非常有用)。
  • 推理数据库:这些数据库使用逻辑规则和推理从现有数据中推导新的信息。正向/反向链接等技术是这些数据库运作的基石,并可以被视为这种范式的模式。
并发编程:
  • 消息传递机制:在多进程并发执行的系统中,消息传递是一种常见的通信和协调技术。例如生产者-消费者读写者这样的模式提供了管理并发资源访问并确保数据一致性的成熟方法。
  • 同步原语(如互斥锁、信号量和条件变量):这些低级构造用于控制并发程序中共享资源的访问权。虽然它们不像是传统意义上的“模式”,但它们代表了应对常见并发挑战的明确解决方案。

数据驱动编程(一种编程范式):

  • 数据转换管道:这种范式强调通过一系列操作来转换数据,这些操作类似于映射、过滤和减少。像映射、过滤和减少(在函数式编程中也很常见,并且自从被引入JavaScript以来,这些技术在JavaScript中被大量使用)这样的技术是构建这些管道的基本构建块,并可被视为这种范式中的模式。
  • 实体-组件-系统(ECS)架构模式,通常称为ECS架构:这种架构模式在游戏开发和其他需要大量数据处理的应用程序中非常流行。它涉及将实体分解为组件(数据)和系统(逻辑),从而促进数据的局部处理和高效处理。

下面列出了一些技巧和模式,如果你好奇的话,可以从这里找到一些线索和启发。

希望这对你有帮助哦,很快再聊吧!


🔖 快速概要,专为赶时间的你!

虽然“设计模式”这一术语最常与面向对象编程(OOP)联系在一起,但其他编程范式也有自己的一套反复出现的解决方案和技术手段。这些技术手段应对特定的挑战和限制,解决了这些范式中常见的问题。因此,即使它们并不总是被正式标记为“设计模式”,它们在引导开发者找到有效且可维护的解决方案方面起到了类似的作用。

我们可以将设计模式理解为用来弥补我们正在使用的编程语言中缺乏的抽象功能的广为人知的解决方案。

这篇帖子几乎都是我写的,提到的例子是由Gemini 1.5 Pro提供的。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消