Heim >Web-Frontend >js-Tutorial >Designmuster entmystifizieren

Designmuster entmystifizieren

Susan Sarandon
Susan SarandonOriginal
2024-11-03 10:54:02366Durchsuche

Die KI-Ära ist also da, der große Sprung nach vorne, der im Moment Node-Code mit const fetch = require('node-fetch') ausspuckt? (gilt ab heute sowohl für ChatGPT als auch für Gemini) und speist eine weitere Drehung der zyklischen Maschine, die das Internet und seine Inhalte darstellt.

In dieser Verschmelzung von Inhalten tauchen wieder Designmuster auf

Demystifying Design Patterns

Von Beiträgen, die erklären, wie man Entwurfsmuster in Node(???) anwendet, bis hin zu Beiträgen, die mit allen Details veraltete Dinge erklären, wie zum Beispiel, wie man das Factory-Muster in Java anwendet (Java 8 wurde im März 2014 veröffentlicht Lambdas hinzugefügt ).

Definition

Sind Sie schon einmal auf den Refactoring-Guru gestoßen?
Es handelt sich um eine Website, die Sie wahrscheinlich auf Ihrer Lernreise in die Informatik, insbesondere in die Programmierung, besucht haben. Der Abschnitt mit den Designmustern ist ziemlich gut erklärt und einer der im Laufe der Jahre in verschiedenen Foren am häufigsten geteilten Abschnitte.

Wenn wir uns mit der Definition dessen befassen, was Designmuster sind, finden wir:

Entwurfsmuster sind typische Lösungen für häufige Probleme
im Softwaredesign. Jedes Muster ist wie eine Blaupause
die Sie anpassen können, um ein bestimmtes Problem zu lösen
Designproblem in Ihrem Code.

Warum dann dieser Beitrag? Ich meine, es gibt jede Menge Informationen auf der oben verlinkten Website; das könnte alles sein.

Die Sache ist, dass es mir immer schwerfiel, diese Definition zu akzeptieren... „um ein bestimmtes Designproblem in meinem Code zu lösen“... in meinem Code? Hat mein Code ein Problem, das ich lösen muss?

Definition, neu interpretiert

Was wirklich passiert, ist, dass ich ein bestimmtes „Etwas“ programmieren muss, für das es der im Projekt verwendeten Programmiersprache an Abstraktionen mangelt.

Schlicht und einfach. Für den Fall, dass Ihnen das noch nicht gefällt, sehen wir uns einige Beispiele mit Code an.

Dies ist eine wirklich einfache Implementierung des Factory Pattern in Java (hauptsächlich eine objektorientierte Programmiersprache).

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;   
  }
}

Dann hat Java 8 (März 2014, nur für den Fall, dass Sie es vergessen haben) Lambdas (ein Konzept aus der funktionalen Programmierung) hinzugefügt, damit wir stattdessen Folgendes tun können:

Map<String, Supplier<Shape>> shapeFactory = new HashMap<>();
shapeFactory.put("CIRCLE", Circle::new);
shapeFactory.put("SQUARE", Square::new);

Shape circle = shapeFactory.get("CIRCLE").get();

Das Factory-Design-Muster wird nie wieder benötigt (zumindest in Java).

Ja, ich weiß, dass das Fabrikmuster das Beispiel ist, das die meisten Leute ständig verwenden, aber was passiert mit den anderen? Und was passiert in anderen Programmiersprachen?

Dies ist das Besuchermuster in 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); 

Der folgende Code macht genau das Gleiche, verwendet jedoch Reflexion (die Fähigkeit einer Sprache, ihre eigenen Objekte zur Laufzeit zu untersuchen und zu manipulieren) anstelle des Besuchermusters:

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);

Jetzt das Beobachtermuster, auch in 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!");

Dasselbe, aber unter Verwendung des integrierten (in der Node-API) EventEmitter:

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;   
  }
}

An diesem Punkt haben Sie vielleicht erkannt, dass das „Problem“ die OOP-Implementierung ist, und Sie hätten völlig Recht, aber nicht vollständig.

Jedes Programmierparadigma, insbesondere wenn es in seiner reinsten Form betrachtet wird, hat seine Macken, Schwierigkeiten oder „Dinge, die nicht auf einem geradlinigen Weg erreicht werden können“, wenn Sie so wollen.

Kommen wir zum Bereich der funktionalen Programmierung. Sie haben wahrscheinlich schon von Monaden gehört.

Ganz gleich, ob Sie in die Denkfalle der mathematischen Definition geraten sind oder nicht, wir – Softwareentwickler – könnten Monaden auch als Entwurfsmuster verstehen. Denn in einer Welt reiner Funktionen, in der nichts Unerwartetes passiert, ist es schwierig, sich einen Nebeneffekt vorzustellen, aber die meisten Softwareprodukte brauchen Nebenwirkungen, also wie können wir...?

Dies ist ein Beispiel der IO-Monade in Haskell:

Map<String, Supplier<Shape>> shapeFactory = new HashMap<>();
shapeFactory.put("CIRCLE", Circle::new);
shapeFactory.put("SQUARE", Square::new);

Shape circle = shapeFactory.get("CIRCLE").get();

Der Nebeneffekt (Lesen einer Datei) ist in der IO-Monade enthalten.

Fügen wir ein monadisches Beispiel mit Typoskript hinzu;

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); 

Ein Klassiker, ich habe die Monade vielleicht 50 Mal im Internet gesehen, aber was ist das eigentlich?

Das Problemdas es zu lösen versucht:

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);

Wir haben vergessen, die Eigenschaften unseres Objekts zu definieren! ?

In einem realen Anwendungsfall wäre dies meist die Eingabe eines Nebeneffekts, wie das Lesen aus einer Datenbank oder einer Datei

Wenn wir das jetzt tun:

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!");

Das Programm explodiert.

Die Lösung ohne die Maybe-Monade:

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!");

Das Programm explodiert nicht.

Die vielleicht-Monade ist in JavaScript oder Typoskript aufgrund des optionalen Verkettungsoperators nicht erforderlich. Wenn Sie jedoch eine Sprache verwenden, die sie nicht implementiert, können Sie die vielleicht-Monade oder anwenden Soll ich Designmuster sagen?

Ja, ich weiß, es gibt Leute, die gerade das Vielleicht-Ding gelernt haben und es eifrig auf 6 Nebenprojekte gleichzeitig angewendet haben, und jetzt bin ich der Lacher auf der Party, weil ich dir sage: „Du brauchst es nicht“. Du kannst es aber trotzdem verwenden, ich lade dich sogar dazu ein, wenn du es cool findest (am Ende des Tages ist es dein Code mit diesem hübschen Gesicht, mit dem du machen kannst, was du willst! ?)


Aber zurück zum Wesentlichen. Was ist mit anderen Paradigmen? Wenn Sie über den OOP/FP-Rahmen hinaus denken, gefällt es mir!

Alle Paradigmen haben definitiv ihre eigenen wiederkehrenden Lösungen und Techniken, auch wenn sie nicht immer formal als „Entwurfsmuster“ bezeichnet werden.

Hier sind ein paar Beispiele (danke Gemini, dass du mir das Nachdenken erspart hast, danke mir für die hübsche Formatierung und den Mehrwert?):

Logikprogrammierung:
  • Constraint Logic Programming: Bei diesem Paradigma geht es darum, Einschränkungen und Beziehungen zwischen Variablen zu definieren und das System dann Lösungen finden zu lassen, die diese Einschränkungen erfüllen. Techniken wie Backtracking und Constraint Propagation sind in diesem Paradigma für eine effiziente Problemlösung von entscheidender Bedeutung. (Sehr nützlich im Umgang mit KI).
  • Deduktive Datenbanken: Diese Datenbanken nutzen logische Regeln und Schlussfolgerungen, um neue Informationen aus vorhandenen Daten abzuleiten. Techniken wie Vorwärts-/Rückwärtsverkettung sind für die Funktionsweise dieser Datenbanken von grundlegender Bedeutung und könnten als Muster innerhalb dieses Paradigmas betrachtet werden.
Gleichzeitige Programmierung:
  • Message Passing: In parallelen Systemen, in denen mehrere Prozesse gleichzeitig ausgeführt werden, ist Message Passing eine gängige Technik zur Kommunikation und Koordination. Muster wie Produzent-Konsumer und Leser-Schreiber bieten etablierte Lösungen für die Verwaltung des gleichzeitigen Zugriffs auf Ressourcen und die Gewährleistung der Datenkonsistenz.
  • Synchronisationsprimitive: Dies sind Konstrukte auf niedriger Ebene wie Mutexe, Semaphoren und Bedingungsvariablen, die zur Steuerung des Zugriffs auf gemeinsam genutzte Ressourcen verwendet werden in gleichzeitigen Programmen. Obwohl sie keine „Muster“ im herkömmlichen Sinne sind, stellen sie wohldefinierte Lösungen für häufige Herausforderungen der Parallelität dar.

Datenorientierte Programmierung:

  • Datentransformationspipelines: Dieses Paradigma legt den Schwerpunkt auf die Transformation von Daten durch eine Reihe von Vorgängen. Techniken wie map, filter und reduce (auch in der funktionalen Programmierung üblich und seit ihrer Einführung VIEL in Javascript verwendet) sind grundlegende Bausteine ​​für die Konstruktion diese Pipelines und könnten als Muster innerhalb dieses Paradigmas betrachtet werden.
  • Entity-Component-System (ECS): Dieses Architekturmuster ist in der Spieleentwicklung und anderen datenintensiven Anwendungen beliebt. Dabei geht es darum, Entitäten in Komponenten (Daten) und Systeme (Logik) zu zerlegen und so die Datenlokalität und effiziente Verarbeitung zu fördern.

Es gibt eine Menge „Techniken“ und „Muster“, diese Liste soll Ihnen nur Anregungen geben, wenn Sie neugierig sind.

Ich hoffe, dass Sie das nützlich finden, ich werde es bald lesen!

Demystifying Design Patterns


? Zusammenfassung, für die Eiligen!

Während der Begriff „Entwurfsmuster“ am engsten mit OOP verbunden ist, haben andere Paradigmen ihre eigenen wiederkehrenden Lösungen und Techniken. Diese Techniken gehen auf die spezifischen Herausforderungen und Einschränkungen dieser Paradigmen ein und bieten etablierte Ansätze für häufige Probleme. Auch wenn sie nicht immer offiziell als „Entwurfsmuster“ bezeichnet werden, dienen sie einem ähnlichen Zweck, indem sie Entwickler zu effektiven und wartbaren Lösungen führen.

Wir können Entwurfsmuster als bekannte Problemumgehungen zum Patchen von Funktionen verstehen, für die der von uns verwendeten Programmiersprache Abstraktionen fehlen.

Dieser Beitrag wurde fast ausschließlich von mir geschrieben, angegebene Beispiele stammen von Gemini 1.5 Pro

Das obige ist der detaillierte Inhalt vonDesignmuster entmystifizieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn