通过示例揭开设计模式的神秘面纱
因此,人工智能时代已经到来,这是一个巨大的飞跃,目前,它用const fetch = require('node-fetch')
😳吐出 Node 代码(截至今天,ChatGPT 和 Gemini 都是如此),并为互联网及其内容的循环机器提供另一次旋转。
在内容融合中,设计模式再次出现
从解释如何在 Node(???) 中应用设计模式的帖子到详细解释过时内容的帖子,例如如何在 Java 中应用工厂模式(2014 年3 月发布的 Java 8添加了 Lambdas)。
定义
有没有偶然发现过“重构大师”?
你可能在学习计算机科学,尤其是编程的过程中访问过这个网站。它对设计模式的讲解非常透彻,多年来一直是各个论坛分享最多的网站之一。
如果我们去定义设计模式,我们会发现:
设计模式是软件设计中常见问题的典型解决方案
。每个模式就像一个蓝图,你可以自定义它来解决 代码中的
特定设计问题。
那为什么发这个帖子呢?我是说,上面链接的网站上有很多信息,这些应该够了。
问题是,我一直很难接受这个定义……“解决我代码中的某个设计问题”……我的代码里有我需要解决的问题吗?
重新定义
实际情况是,我需要编写某个“东西”的代码,但项目中使用的编程语言缺乏对它的抽象。
简单明了。万一你还没理解,我们来看一些代码示例。
这是 Java(主要是面向对象的编程语言)中工厂模式的一个非常简单的实现。
public class ShapeFactory {
public Shape createShape(String type) {
if (type.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (type.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
然后 Java 8 (2014 年 3 月,以防你忘记)添加了 Lambdas(函数式编程的一个概念),所以我们可以这样做:
Map<String, Supplier<Shape>> shapeFactory = new HashMap<>();
shapeFactory.put("CIRCLE", Circle::new);
shapeFactory.put("SQUARE", Square::new);
Shape circle = shapeFactory.get("CIRCLE").get();
不再需要工厂设计模式(至少在 Java 中)。
是的,我知道工厂模式是大多数人经常使用的例子,但其他模式又如何呢?在其他编程语言中又如何呢?
这是 Typescript 中的访问者模式:
interface Shape {
draw(): void;
accept(visitor: ShapeVisitor): void;
}
class Circle implements Shape {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
draw() {
console.log("Drawing a circle");
}
accept(visitor: ShapeVisitor) {
visitor.visitCircle(this);
}
}
class Square implements Shape {
sideLength: number;
constructor(sideLength: number) {
this.sideLength = sideLength;
}
draw() {
console.log("Drawing a square");
}
accept(visitor: ShapeVisitor) {
visitor.visitSquare(this);
}
}
interface ShapeVisitor {
visitCircle(circle: Circle): void;
visitSquare(square: Square): void;
}
class AreaCalculator implements ShapeVisitor {
private area = 0;
visitCircle(circle: Circle) {
this.area = Math.PI * circle.radius * circle.radius;
console.log(`Circle area: ${this.area}`);
}
visitSquare(square: Square) {
this.area = square.sideLength * square.sideLength;
console.log(`Square area: ${this.area}`);
}
getArea(): number {
return this.area;
}
}
// Using the Visitor
const circle = new Circle(5);
const square = new Square(4);
const calculator = new AreaCalculator();
circle.accept(calculator);
square.accept(calculator);
下面的代码做了完全相同的事情,但是使用反射(语言在运行时检查和操作其自身对象的能力)而不是访问者模式:
interface Shape {
draw(): void;
}
class Circle implements Shape {
// ... (same as before)
radius: number;
}
class Square implements Shape {
// ... (same as before)
sideLength: number;
}
function calculateArea(shape: Shape) {
if (shape instanceof Circle) {
const circle = shape as Circle; // Type assertion
const area = Math.PI * circle.radius * circle.radius;
console.log(`Circle area: ${area}`);
} else if (shape instanceof Square) {
const square = shape as Square; // Type assertion
const area = square.sideLength * square.sideLength;
console.log(`Square area: ${area}`);
}
}
const circle = new Circle(5);
const square = new Square(4);
calculateArea(circle);
calculateArea(square);
现在观察者模式也用 TypeScript 来实现:
interface Observer {
update(data: any): void;
}
class NewsPublisher {
private observers: Observer[] = [];
subscribe(observer: Observer) {
this.observers.push(observer);
}
unsubscribe(observer: Observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(news:
string) {
this.observers.forEach(observer => observer.update(news));
}
}
class NewsletterSubscriber implements Observer {
update(news: string) {
console.log(`Received news: ${news}`);
}
}
// Using the Observer
const publisher = new NewsPublisher();
const subscriber1 = new NewsletterSubscriber();
const subscriber2 = new NewsletterSubscriber();
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.notify("New product launched!");
相同但使用内置(在 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(`All subscribers received the news: ${news}`);
});
publisher.publish("New product launched!");
此时,您可能已经意识到“问题”在于 OOP 实现,而且您说得很对,但并不完全正确。
每种编程范式,特别是其最纯粹的形式,都有其怪癖、困难或“无法直线实现的事情”,如果你愿意的话。
让我们进入函数式编程领域。你可能听说过 Monads。
无论你是否陷入数学定义的思维陷阱,我们——软件开发者——都可以将 Monad 理解为设计模式。这是因为在纯函数的世界里,没有任何意外发生,很难想象副作用,但大多数软件产品都需要副作用,那么我们该如何……?
这是 Haskell 中 IO Monad 的一个示例:
main :: IO ()
main = do
fileContent <- readFile "myFile.txt"
putStrLn fileContent
副作用(读取文件)包含在 IO monad 中。
让我们使用 typescript 添加一个 monadic 示例;
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));
}
}
}
// Usage
const user = Maybe.just({ name: "Alice", age: 30 });
const userName = user.map(u => u.name); // Maybe<string> with value "Alice"
const noUser = Maybe.nothing();
const noUserName = noUser.map(u => u.name);
一个经典的例子,我在互联网上见过大概 50 次这个可能的 monad,但它到底是什么?
它试图解决的问题:
let user;
我们忘了定义对象的属性了!😩
在实际用例中,这主要是来自副作用的输入,例如从数据库或文件中读取
所以现在如果我们这样做:
const userName = user.value.name; // Uncaught TypeError: Cannot read properties of undefined (reading 'value')
程序爆炸。
不使用 Maybe monad 的解决方案:
const userName = user?.value?.name; // undefined
程序不会爆炸。
由于可选链运算符,在 JavaScript 或 typescript 中可能不需要monad,但如果您使用的语言没有实现它……那么,您可以应用可能 monad 或者我应该说设计模式?
是的,我知道,有些人刚学会 Maybe 就迫不及待地把它一次性用在了 6 个业余项目上,现在我却在派对上笑着跟你说“你不需要它”。不过你仍然可以使用它,事实上,如果你觉得它很酷,我诚邀你使用它(毕竟这是你的代码 + 有了它,你想干什么都可以!🤭)
但回到基础。其他范式怎么样?如果你能跳出 OOP/FP 的框架思考,我很喜欢!
所有范式肯定都有其自己的重复解决方案和技术,即使它们并不总是正式称为“设计模式”。
这里有几个例子(感谢 Gemini 避免了我的思考,感谢我漂亮的格式和附加值😁):
逻辑编程:
- 约束逻辑编程:这种范式涉及定义变量之间的约束和关系,然后让系统找到满足这些约束的解。回溯和约束传播等技术对于这种范式中高效解决问题至关重要。(在处理人工智能时非常有用)。
- 演绎数据库:这类数据库使用逻辑规则和推理从现有数据中获取新信息。前向/后向链接等技术是这类数据库运作的基础,可以被视为此类范式中的模式。
并发编程:
- 消息传递:在并发系统中,多个进程同时执行,消息传递是一种常用的通信和协调技术。生产者-消费者模式和读取器-写入器模式等模式提供了成熟的解决方案,用于管理资源的并发访问并确保数据一致性。
- 同步原语:这些是一些低级结构,例如互斥锁、信号量和条件变量,用于控制并发程序中对共享资源的访问。虽然它们并非传统意义上的“模式”,但它们代表了针对常见并发挑战的明确解决方案。
面向数据编程:
- 数据转换管道:此范式强调通过一系列操作来转换数据。诸如map、filter和reduce 之类的技术(在函数式编程中也很常见,并且自 JavaScript 引入以来就被广泛使用)是构建这些管道的基本构件,可以被视为此范式中的模式。
- 实体-组件-系统 (ECS):这种架构模式在游戏开发和其他数据密集型应用中很流行。它涉及将实体分解为组件(数据)和系统(逻辑),从而提高数据局部性和高效处理能力。
有很多“技术”和“模式”,如果您好奇的话,这个列表只是为了给您提供线索。
希望您觉得这篇文章有用,很快就会读给您听!
🔖 总结一下,适合急着赶时间的人!
虽然“设计模式”一词与面向对象编程 (OOP) 密切相关,但其他范式也拥有一套各自反复使用的解决方案和技术。这些技术解决了这些范式所面临的特定挑战和限制,为常见问题提供了成熟的解决方案。因此,即使它们并不总是被正式标记为“设计模式”,它们在引导开发人员找到有效且可维护的解决方案方面也发挥着类似的作用。
我们可以将设计模式理解为众所周知的解决方法,以修补我们正在使用的编程语言缺乏抽象的功能。
这篇文章几乎全部由我撰写,具体示例由 Gemini 1.5 Pro 提供
鏂囩珷鏉ユ簮锛�https://dev.to/joelbonetr/demystifying-design-patterns-45m3