Die ersten 5 Prinzipien des objektorientierten Designs mit JavaScript

Ich habe einen sehr guten Artikel gefunden, in dem die S.O.L.I.D. Prinzipien, wenn Sie mit PHP vertraut sind, können Sie den Originalartikel hier lesen: S.O.L.I.D: Die ersten 5 Prinzipien des objektorientierten Designs. Da ich jedoch ein JavaScript-Entwickler bin, habe ich die Codebeispiele aus dem Artikel in JavaScript angepasst.

JavaScript ist eine lose geschriebene Sprache, manche halten es für eine funktionale Sprache, andere für eine objektorientierte Sprache, manche für beides und manche für falsch, Klassen in JavaScript zu haben. - Dor Tzur

Dies ist nur ein einfacher Artikel „Willkommen bei S.O.L.I.D.“, der lediglich Aufschluss darüber gibt, was S.O.L.I.D. ist.

SOLIDE. STEHT FÜR:

  • S - Grundsatz der einheitlichen Verantwortung
  • O - Offenes geschlossenes Prinzip
  • L - Liskov-Substitutionsprinzip
  • I - Prinzip der Schnittstellentrennung
  • D - Abhängigkeitsumkehrprinzip

# Prinzip der Einzelverantwortung

Eine Klasse sollte nur einen Grund haben, sich zu ändern, was bedeutet, dass eine Klasse nur einen Job haben sollte.

Angenommen, wir haben einige Formen und wollten alle Bereiche der Formen zusammenfassen. Nun, das ist ziemlich einfach, oder?

const circle = (radius) => {
  const proto = {
    Typ: "Kreis",
    //Code
  }
  return Object.assign (Object.create (proto), {radius})
}
const square = (length) => {
  const proto = {
    Typ: "Square",
    //Code
  }
  return Object.assign (Object.create (proto), {length})
}

Zuerst erstellen wir unsere Shapes Factory-Funktionen und richten die erforderlichen Parameter ein.

Was ist eine Fabrikfunktion?

In JavaScript kann jede Funktion ein neues Objekt zurückgeben. Wenn es sich nicht um eine Konstruktorfunktion oder -klasse handelt, wird es als Factory-Funktion bezeichnet. In diesem Artikel finden Sie eine gute Erklärung, und in diesem Video wird dies auch sehr deutlich

Als Nächstes erstellen wir die areaCalculator-Factory-Funktion und schreiben dann unsere Logik, um die Fläche aller bereitgestellten Formen zu summieren.

const areaCalculator = (s) => {
  const proto = {
    Summe() {
      // Logik zur Summe
    },
    Ausgabe () {
     return `
       

         Summe der Flächen der zur Verfügung gestellten Formen:          $ {this.sum ()}             }   }   return Object.assign (Object.create (proto), {shapes: s}) }

Um die areaCalculator-Factory-Funktion zu verwenden, rufen wir einfach die Funktion auf, übergeben ein Array von Formen und zeigen die Ausgabe unten auf der Seite an.

const shapes = [
  Kreis (2),
  Quadrat (5),
  Quadrat (6)
]
const areas = areaCalculator (Formen)
console.log (areas.output ())

Das Problem bei der Ausgabemethode besteht darin, dass der areaCalculator die Logik zur Ausgabe der Daten verarbeitet. Was ist also, wenn der Benutzer die Daten als JSON oder etwas anderes ausgeben möchte?

Die gesamte Logik würde von der areaCalculator-Factory-Funktion verwaltet. Die Factory-Funktion areaCalculator sollte nur die Bereiche der bereitgestellten Formen summieren. Dabei sollte es unerheblich sein, ob der Benutzer JSON oder HTML wünscht.

Um dies zu beheben, können Sie eine SumCalculatorOutputter-Factory-Funktion erstellen und diese verwenden, um die Logik zu verarbeiten, die Sie für die Anzeige der Summenbereiche aller bereitgestellten Formen benötigen.

Die Factory-Funktion sumCalculatorOutputter würde folgendermaßen funktionieren:

const shapes = [
  Kreis (2),
  Quadrat (5),
  Quadrat (6)
]
const areas = areaCalculator (Formen)
const output = sumCalculatorOputter (Bereiche)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

Nun wird jede Logik, die Sie zur Ausgabe der Daten an die Benutzer benötigen, von der Werksfunktion sumCalculatorOutputter verarbeitet.

# Open-Closed-Prinzip

Objekte oder Entitäten sollten zur Erweiterung geöffnet, aber zur Änderung geschlossen sein.
Offen für Erweiterungen bedeutet, dass wir der Anwendung neue Funktionen oder Komponenten hinzufügen können sollten, ohne den vorhandenen Code zu beschädigen.
Closed for modification bedeutet, dass wir keine grundlegenden Änderungen an der vorhandenen Funktionalität vornehmen sollten, da dies Sie dazu zwingen würde, einen Großteil des vorhandenen Codes zu überarbeiten - Eric Elliott

In einfacheren Worten bedeutet dies, dass eine Klassen- oder Factory-Funktion in unserem Fall leicht erweiterbar sein sollte, ohne die Klasse oder Funktion selbst zu ändern. Sehen wir uns die areaCalculator-Factory-Funktion an, insbesondere die Summenmethode.

Summe () {
 
 const area = []
 
 für (Form von this.shapes) {
  
  if (shape.type === 'Square') {
     area.push (Math.pow (shape.length, 2)
   } else if (shape.type === 'Circle') {
     area.push (Math.PI * Math.pow (shape.length, 2)
   }
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Wenn wir wollten, dass die Summenmethode die Bereiche von mehr Formen summieren kann, müssten wir mehr if / else-Blöcke hinzufügen, was gegen das Open-Closed-Prinzip verstößt.

Eine Möglichkeit, diese Summenmethode zu verbessern, besteht darin, die Logik zum Berechnen der Fläche jeder Form aus der Summenmethode zu entfernen und sie an die Factory-Funktionen der Form anzuhängen.

const square = (length) => {
  const proto = {
    Typ: "Square",
    Bereich () {
      return Math.pow (this.length, 2)
    }
  }
  return Object.assign (Object.create (proto), {length})
}

Dasselbe sollte für die Kreisfactory-Funktion getan werden, eine Flächenmethode sollte hinzugefügt werden. Nun sollte es so einfach sein, die Summe aller bereitgestellten Formen zu berechnen:

Summe() {
 const area = []
 für (Form von this.shapes) {
   area.push (shape.area ())
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Jetzt können wir eine weitere Formklasse erstellen und diese bei der Berechnung der Summe übergeben, ohne unseren Code zu beschädigen. Nun tritt jedoch ein anderes Problem auf: Woher wissen wir, dass das in den areaCalculator übergebene Objekt tatsächlich eine Form ist oder ob die Form eine Methode namens area hat?

Das Codieren einer Schnittstelle ist ein wesentlicher Bestandteil von S.O.L.I.D., ein kurzes Beispiel ist das Erstellen einer Schnittstelle, die von jeder Form implementiert wird.

Da JavaScript keine Schnittstellen hat, werde ich Ihnen zeigen, wie dies in TypeScript erreicht wird, da TypeScript das klassische OOP für JavaScript und den Unterschied zu reinem JavaScript Prototypal OO modelliert.

interface ShapeInterface {
 Bereich (): Nummer
}
Klasse Circle implementiert ShapeInterface {
 Lassen Sie Radius: Zahl = 0
 Konstruktor (r: Nummer) {
  this.radius = r
 }
 
 öffentlicher Bereich (): Nummer {
  return MATH.PI * MATH.pow (this.radius, 2)
 }
}

Das obige Beispiel zeigt, wie dies in TypeScript erreicht wird. Unter der Haube kompiliert TypeScript den Code in reines JavaScript und im kompilierten Code fehlen Schnittstellen, da JavaScript nicht über diese verfügt.

Wie können wir dies in Ermangelung von Schnittstellen erreichen?

Funktion Zusammensetzung zur Rettung!

Zuerst erstellen wir die Factory-Funktion shapeInterface. Da es sich um Interfaces handelt, wird unser shapeInterface mithilfe der Funktionskomposition so abstrahiert wie ein Interface. Eine ausführliche Erläuterung der Komposition finden Sie in diesem großartigen Video.

const shapeInterface = (state) => ({
  Typ: 'shapeInterface',
  area: () => state.area (state)
})

Dann implementieren wir es zu unserer quadratischen Fabrikfunktion.

const square = (length) => {
  const proto = {
    Länge,
    Typ: "Square",
    area: (args) => Math.pow (args.length, 2)
  }
  const basics = shapeInterface (proto)
  const composite = Object.assign ({}, Grundlagen)
  return Object.assign (Object.create (zusammengesetzt), {length})
}

Und das Ergebnis des Aufrufs der Funktion square factory ist das nächste:

const s = square (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
an area ()
// Ausgabe
OBJ
 {Länge: 5}
PROTO
 {Typ: 'shapeInterface', Bereich: [Funktion: Bereich]}
25

In unserer areaCalculator-Summenmethode können wir überprüfen, ob es sich bei den bereitgestellten Formen tatsächlich um ShapeInterface-Typen handelt. Andernfalls wird eine Ausnahme ausgelöst:

Summe() {
  const area = []
  für (Form von this.shapes) {
    if (Object.getPrototypeOf (shape) .type === 'shapeInterface') {
       area.push (shape.area ())
     } else {
       Neuen Fehler auslösen ('Dies ist kein shapeInterface-Objekt')
     }
   }
   return area.reduce ((v, c) => c + = v, 0)
}

Da JavaScript keine Unterstützung für Interfaces wie typisierte Sprachen bietet, zeigt das obige Beispiel, wie wir es simulieren können. Wir simulieren jedoch nicht nur Interfaces, sondern verwenden Closures und Funktionszusammensetzungen, wenn Sie nicht wissen, was a ist Abschluss ist dieser Artikel erklärt es sehr gut und zur Ergänzung finden Sie in diesem Video.

# Liskov-Substitutionsprinzip

Sei q (x) eine beweisbare Eigenschaft für Objekte vom Typ T. Dann sollte q (y) für Objekte vom Typ S beweisbar sein, wobei S ein Subtyp von T ist.

Dies bedeutet lediglich, dass jede Unterklasse / abgeleitete Klasse durch ihre Basis- / Elternklasse ersetzt werden sollte.

Mit anderen Worten, so einfach wie das, sollte eine Unterklasse die Methoden der übergeordneten Klasse so überschreiben, dass die Funktionalität aus Sicht eines Clients nicht beeinträchtigt wird.

Wenn wir weiterhin unsere areaCalculator-Factory-Funktion verwenden, sagen wir, wir haben eine volumeCalculator-Factory-Funktion, die die areaCalculator-Factory-Funktion erweitert, und in unserem Fall, um ein Objekt zu erweitern, ohne Änderungen in ES6 zu unterbrechen, verwenden wir Object.assign () und das Object. getPrototypeOf ():

const volumeCalculator = (s) => {
  const proto = {
    Typ: 'volumeCalculator'
  }
  const areaCalProto = Object.getPrototypeOf (areaCalculator ())
  const inherit = Object.assign ({}, areaCalProto, proto)
  return Object.assign (Object.create (erben), {shapes: s})
}

# Prinzip der Schnittstellentrennung

Ein Client sollte niemals gezwungen werden, eine Schnittstelle zu implementieren, die er nicht verwendet, oder Clients sollten nicht gezwungen werden, von Methoden abhängig zu sein, die sie nicht verwenden.

Wenn wir unser Formenbeispiel fortsetzen, wissen wir, dass wir auch feste Formen haben. Da wir also auch das Volumen der Form berechnen möchten, können wir der Formschnittstelle einen weiteren Kontrakt hinzufügen:

const shapeInterface = (state) => ({
  Typ: 'shapeInterface',
  area: () => state.area (state),
  volume: () => state.volume (state)
})

Jede Form, die wir erstellen, muss die Volumenmethode implementieren. Wir wissen jedoch, dass Quadrate flache Formen sind und keine Volumen haben. Daher würde diese Schnittstelle die Funktion square factory zwingen, eine Methode zu implementieren, die sie nicht verwendet.

Das Prinzip der Schnittstellentrennung lehnt dies ab. Stattdessen können Sie eine andere Schnittstelle namens solidShapeInterface erstellen, die den Volumenkontrakt aufweist, und feste Formen wie Würfel usw. können diese Schnittstelle implementieren.

const shapeInterface = (state) => ({
  Typ: 'shapeInterface',
  area: () => state.area (state)
})
const solidShapeInterface = (state) => ({
  Typ: "solidShapeInterface",
  volume: () => state.volume (state)
})
const cubo = (length) => {
  const proto = {
    Länge,
    Typ: "Cubo",
    area: (args) => Math.pow (args.length, 2),
    volume: (args) => Math.pow (args.length, 3)
  }
  const basics = shapeInterface (proto)
  const complex = solidShapeInterface (proto)
  const composite = Object.assign ({}, Grundlagen, komplex)
  return Object.assign (Object.create (zusammengesetzt), {length})
}

Dies ist ein viel besserer Ansatz, aber es ist eine Gefahr, darauf zu achten, wenn die Summe für die Form berechnet wird, anstatt das shapeInterface oder ein solidShapeInterface zu verwenden.

Sie können eine andere Schnittstelle erstellen, z. B. manageShapeInterface, und diese sowohl für flache als auch für feste Formen implementieren. Auf diese Weise können Sie leicht erkennen, dass sie über eine einzige API zum Verwalten der Formen verfügt. Beispiel:

const manageShapeInterface = (fn) => ({
  Typ: 'manageShapeInterface',
  berechne: () => fn ()
})
const circle = (radius) => {
  const proto = {
    Radius,
    Typ: "Kreis",
    area: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  const basics = shapeInterface (proto)
  const abstraccion = manageShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, Grundlagen, Abstraktion)
  return Object.assign (Object.create (composite), {radius})
}
const cubo = (length) => {
  const proto = {
    Länge,
    Typ: "Cubo",
    area: (args) => Math.pow (args.length, 2),
    volume: (args) => Math.pow (args.length, 3)
  }
  const basics = shapeInterface (proto)
  const complex = solidShapeInterface (proto)
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  const composite = Object.assign ({}, Grundlagen, Abstraktion)
  return Object.assign (Object.create (zusammengesetzt), {length})
}

Wie Sie bis jetzt sehen können, handelt es sich bei den Schnittstellen in JavaScript um Factory-Funktionen für die Funktionskomposition.

Und hier, mit manageShapeInterface, abstrahieren wir wieder die Berechnungsfunktion, was wir hier und in den anderen Schnittstellen (wenn wir es Schnittstellen nennen können) tun, verwenden wir "Funktionen hoher Ordnung", um die Abstraktionen zu erzielen.

Wenn Sie nicht wissen, was eine Funktion höherer Ordnung ist, können Sie sich dieses Video ansehen.

# Prinzip der Abhängigkeitsinversion

Entitäten müssen von Abstraktionen abhängen, nicht von Konkretionen. Es heißt, dass das High-Level-Modul nicht vom Low-Level-Modul abhängen darf, sondern von Abstraktionen.

JavaScript benötigt als dynamische Sprache keine Abstraktionen, um die Entkopplung zu erleichtern. Daher ist die Bestimmung, dass Abstraktionen nicht von Details abhängen dürfen, für JavaScript-Anwendungen nicht besonders relevant. Die Bestimmung, dass High-Level-Module nicht von Low-Level-Modulen abhängen sollten, ist jedoch relevant.

Aus funktionaler Sicht können diese Behälter- und Injektionskonzepte mit einer einfachen Funktion höherer Ordnung oder einem Muster vom Typ Hole-in-the-Middle gelöst werden, die direkt in die Sprache integriert sind.

Wie hängt die Abhängigkeitsinversion mit Funktionen höherer Ordnung zusammen? ist eine Frage, die in stackExchange gestellt wird, wenn Sie eine ausführliche Erklärung wünschen.

Das hört sich vielleicht aufgebläht an, ist aber wirklich leicht zu verstehen. Dieses Prinzip ermöglicht eine Entkopplung.

Und wir haben es schon einmal geschafft, lassen Sie uns unseren Code mit dem manageShapeInterface überprüfen und wie wir die Berechnungsmethode durchführen.

const manageShapeInterface = (fn) => ({
  Typ: 'manageShapeInterface',
  berechne: () => fn ()
})

Was die Factory-Funktion manageShapeInterface als Argument erhält, ist eine Funktion höherer Ordnung, die für jede Form die Funktionalität entkoppelt, um die erforderliche Logik für die endgültige Berechnung zu erzielen. Sehen Sie, wie dies in den Shapes-Objekten erfolgt.

const square = (radius) => {
  // code
 
  const abstraccion = manageShapeInterface (() => basics.area ())
 
 // mehr Code ...
}
const cubo = (length) => {
  // code
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  // mehr Code ...
}

Für das Quadrat müssen wir nur die Fläche der Form berechnen, und für einen Quader müssen wir die Fläche mit dem Volumen summieren, und das ist alles, was erforderlich ist, um die Kopplung zu vermeiden und die Abstraktion zu erhalten.

# Vollständige Codebeispiele

  • Sie können es hier herunterladen: solid.js

# Weiterführende Literatur und Verweise

  • FEST die ersten 5 Prinzipien von OOD
  • 5 Prinzipien, die Sie zu einem SOLID JavaScript Developer machen
  • FESTE JavaScript-Serie
  • SOLID-Prinzipien mit Typescript

# Fazit

„Wenn Sie die SOLID-Prinzipien auf die Spitze treiben, gelangen Sie zu etwas, das die funktionale Programmierung sehr attraktiv macht“ - Mark Seemann

JavaScript ist eine Multiparadigma-Programmiersprache, und wir können die soliden Prinzipien auf sie anwenden, und das Beste daran ist, dass wir es mit dem funktionalen Programmierparadigma kombinieren und das Beste aus beiden Welten herausholen können.

Javascript ist auch eine dynamische Programmiersprache und sehr vielseitig
Was ich vorgestellt habe, ist nur eine Möglichkeit, diese Prinzipien mit JavaScript zu erreichen. Möglicherweise sind dies bessere Optionen, um diese Prinzipien zu erreichen.

Ich hoffe, Ihnen hat dieser Beitrag gefallen. Ich erkunde derzeit noch die JavaScript-Welt. Daher bin ich offen für Rückmeldungen und Beiträge. Wenn er Ihnen gefällt, empfehlen Sie ihn einem Freund, geben ihn weiter oder lesen ihn erneut.

Du kannst mir folgen #twitter @ cramirez_92
https://twitter.com/cramirez_92

Bis zum nächsten Mal