S.O.L.I.D De eerste 5 principes van Object Oriented Design met JavaScript

Ik heb een heel goed artikel gevonden waarin de S.O.L.I.D. wordt uitgelegd. principes, als u bekend bent met PHP, kunt u het originele artikel hier lezen: S.O.L.I.D: The First 5 Principles of Object Oriented Design. Maar omdat ik een JavaScript-ontwikkelaar ben, heb ik de codevoorbeelden uit het artikel aangepast in JavaScript.

JavaScript is een los getypte taal, sommigen beschouwen het als een functionele taal, anderen beschouwen het als een objectgeoriënteerde taal, sommigen denken dat het allebei is, en sommigen denken dat het hebben van klassen in JavaScript gewoon fout is. - Dor Tzur

Dit is slechts een eenvoudig "welkom bij S.O.L.I.D." -artikel, het werpt eenvoudig licht op wat S.O.L.I.D. is.

SOLIDE. BETEKENT:

  • S - Eén verantwoordelijkheidsbeginsel
  • O - Open gesloten principe
  • L - Liskov-substitutiebeginsel
  • I - Principesegregatieprincipe
  • D - Afhankelijkheid Inversie principe

# Eén verantwoordelijkheidsprincipe

Een klas moet maar één reden hebben om te veranderen, wat betekent dat een klas maar één baan moet hebben.

Stel bijvoorbeeld dat we enkele vormen hebben en dat we alle gebieden van de vormen wilden optellen. Nou, dit is vrij eenvoudig toch?

const circle = (radius) => {
  const proto = {
    type: 'Cirkel',
    //code
  }
  return Object.assign (Object.create (proto), {radius})
}
const square = (lengte) => {
  const proto = {
    type: 'Vierkant',
    //code
  }
  return Object.assign (Object.create (proto), {length})
}

Eerst maken we onze fabrieksfuncties voor vormen en stellen we de vereiste parameters in.

Wat is een fabrieksfunctie?

In JavaScript kan elke functie een nieuw object retourneren. Als het geen constructorfunctie of -klasse is, wordt het een fabrieksfunctie genoemd. waarom fabrieksfuncties te gebruiken, biedt dit artikel een goede uitleg en deze video legt het ook heel duidelijk uit

Vervolgens gaan we verder door de fabrieksfunctie areaCalculator te maken en vervolgens onze logica op te schrijven om het gebied van alle verstrekte vormen samen te vatten.

const areaCalculator = (s) => {
  const proto = {
    sum () {
      // logica om samen te vatten
    },
    output () {
     retourneren `
       

         Som van de gebieden met verstrekte vormen:          $ {This.sum ()}             }   }   return Object.assign (Object.create (proto), {shapes: s}) }

Om de fabrieksfunctie van areaCalculator te gebruiken, roepen we eenvoudig de functie aan en geven we een reeks vormen door en geven de uitvoer onderaan de pagina weer.

const vormen = [
  (2) liggen,
  square (5),
  square (6)
]
const areas = areaCalculator (vormen)
console.log (areas.output ())

Het probleem met de uitvoermethode is dat de areaCalculator de logica verwerkt om de gegevens uit te voeren. Daarom, wat als de gebruiker de gegevens als json of iets anders wilde uitvoeren?

Alle logica zou worden afgehandeld door de fabrieksfunctie van areaCalculator, dit is wat ‘Single Responsibility-principe’ tegenwerkt; de fabrieksfunctie van areaCalculator moet alleen de gebieden met verstrekte vormen optellen, het maakt niet uit of de gebruiker JSON of HTML wil.

Dus, om dit op te lossen, kunt u een SumCalculatorOutputter-fabrieksfunctie maken en deze gebruiken om alle logica af te handelen die u nodig hebt over hoe de somgebieden van alle verstrekte vormen worden weergegeven.

De fabrieksfunctie sumCalculatorOutputter zou dit leuk vinden:

const vormen = [
  (2) liggen,
  square (5),
  square (6)
]
const areas = areaCalculator (vormen)
const output = sumCalculatorOputter (gebieden)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

De logica die u nodig hebt om de gegevens naar de gebruikers uit te voeren, wordt nu afgehandeld door de fabrieksfunctie van sumCalculatorOutputter.

# Open-gesloten principe

Objecten of entiteiten moeten open staan ​​voor uitbreiding, maar gesloten voor wijziging.
Open voor uitbreiding betekent dat we nieuwe functies of componenten aan de toepassing moeten kunnen toevoegen zonder bestaande code te verbreken.
Gesloten voor wijziging betekent dat we geen brekende wijzigingen in bestaande functionaliteit moeten introduceren, omdat dat je zou dwingen om veel bestaande code te wijzigen - Eric Elliott

In eenvoudiger woorden betekent dit dat een klasse- of fabrieksfunctie in ons geval gemakkelijk uitbreidbaar moet zijn zonder de klasse of functie zelf te wijzigen. Laten we eens kijken naar de fabrieksfunctie van areaCalculator, vooral de sommethode.

sum () {
 
 const area = []
 
 voor (vorm van this.shapes) {
  
  if (shape.type === 'Square') {
     area.push (Math.pow (shape.length, 2)
   } anders if (shape.type === 'Circle') {
     area.push (Math.PI * Math.pow (shape.length, 2)
   }
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Als we wilden dat de sommethode de gebieden met meer vormen kon optellen, zouden we meer if / else-blokken moeten toevoegen en dat druist in tegen het open-gesloten principe.

Een manier om deze sommethode beter te maken, is door de logica te verwijderen om het gebied van elke vorm uit de sommethode te berekenen en aan de fabrieksfuncties van de vorm te koppelen.

const square = (lengte) => {
  const proto = {
    type: 'Vierkant',
    Gebied () {
      retour Math.pow (this.length, 2)
    }
  }
  return Object.assign (Object.create (proto), {length})
}

Hetzelfde moet worden gedaan voor de cirkelfabriekfunctie, een oppervlaktemethode moet worden toegevoegd. Nu moet de som van elke vorm zo eenvoudig zijn als:

sum () {
 const area = []
 voor (vorm van this.shapes) {
   area.push (shape.area ())
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Nu kunnen we een andere vormklasse maken en deze doorgeven bij het berekenen van de som zonder onze code te breken. Nu doet zich echter een ander probleem voor, hoe weten we dat het object dat in de areaCalculator is doorgegeven eigenlijk een vorm is of als de vorm een ​​methode met de naam area heeft?

Codering naar een interface is een integraal onderdeel van S.O.L.I.D., een snel voorbeeld is dat we een interface maken die elke vorm implementeert.

Omdat JavaScript geen interfaces heeft, ga ik je laten zien hoe dit wordt bereikt in TypeScript, aangezien TypeScript de klassieke OOP voor JavaScript modelleert, en het verschil met pure JavaScript Prototypal OO.

interface ShapeInterface {
 gebied (): nummer
}
class Circle implementeert ShapeInterface {
 let radius: number = 0
 constructor (r: number) {
  this.radius = r
 }
 
 openbare ruimte (): nummer {
  terug MATH.PI * MATH.pow (this.radius, 2)
 }
}

In het bovenstaande voorbeeld wordt getoond hoe dit wordt bereikt in TypeScript, maar onder de motorkap compileert TypeScript de code naar pure JavaScript en in de gecompileerde code ontbreekt het aan interfaces, omdat JavaScript het niet heeft.

Dus hoe kunnen we dit bereiken, bij gebrek aan interfaces?

Functiesamenstelling te hulp!

Eerst maken we de fabrieksfunctie shapeInterface, aangezien we het hebben over interfaces, onze shapeInterface zal even abstract zijn als een interface, met behulp van functiesamenstelling, voor een uitgebreide uitleg van de compositie zie deze geweldige video.

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

Vervolgens implementeren we het in onze vierkante fabrieksfunctie.

const square = (lengte) => {
  const proto = {
    lengte,
    type: 'Vierkant',
    area: (args) => Math.pow (args.length, 2)
  }
  const basics = shapeInterface (proto)
  const composite = Object.assign ({}, basis)
  retour Object.assign (Object.create (composiet), {lengte})
}

En het resultaat van het aanroepen van de vierkante fabrieksfunctie is het volgende:

const s = square (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
s.area ()
// uitvoer
OBJ
 {lengte: 5}
PROTO
 {type: 'shapeInterface', area: [Functie: area]}
25

In onze areaCalculator-sommethode kunnen we controleren of de getoonde vormen daadwerkelijk soorten vorminterface zijn, anders vormen we een uitzondering:

sum () {
  const area = []
  voor (vorm van this.shapes) {
    if (Object.getPrototypeOf (shape) .type === 'shapeInterface') {
       area.push (shape.area ())
     } anders {
       throw new Error ('dit is geen shapeInterface-object')
     }
   }
   return area.reduce ((v, c) => c + = v, 0)
}

en nogmaals, omdat JavaScript geen ondersteuning biedt voor interfaces zoals getypte talen, laat het bovenstaande voorbeeld zien hoe we het kunnen simuleren, maar meer dan het simuleren van interfaces, gebruiken we sluitingen en functiesamenstelling als je niet weet wat een sluiting is dit artikel verklaart het heel goed en voor aanvulling zie deze video.

# Liskov-substitutieprincipe

Laat q (x) een eigenschap zijn die aantoonbaar is voor objecten van x van type T. Dan moet q (y) aantoonbaar zijn voor objecten y van type S waarbij S een subtype is van T.

Dit alles zegt dat elke subklasse / afgeleide klasse substitueerbaar moet zijn voor hun basis / ouderklasse.

Met andere woorden, zo simpel als dat, moet een subklasse de methoden van de bovenliggende klasse overschrijven op een manier die de functionaliteit niet uit het oogpunt van een klant breekt.

Nog steeds gebruik makend van onze areaCalculator-fabrieksfunctie, zeggen we dat we een volumeCalculator-fabrieksfunctie hebben die de areaCalculator-fabrieksfunctie uitbreidt, en in ons geval voor het uitbreiden van een object zonder de wijzigingen in ES6 te onderbreken, doen we dit met Object.assign () en het Object. getPrototypeOf ():

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

# Interface segregatie principe

Een client mag nooit worden gedwongen een interface te implementeren die hij niet gebruikt of clients mogen niet worden gedwongen om afhankelijk te zijn van methoden die hij niet gebruikt.

Als we verder gaan met ons voorbeeld met vormen, weten we dat we ook solide vormen hebben, dus omdat we ook het volume van de vorm willen berekenen, kunnen we een ander contract toevoegen aan de vormInterface:

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

Elke vorm die we maken, moet de volumemethode bevatten, maar we weten dat vierkanten platte vormen zijn en dat ze geen volumes hebben, dus deze interface zou de vierkante fabrieksfunctie dwingen een methode te implementeren die hij niet gebruikt.

Het interface-scheidingsprincipe zegt hier nee tegen, maar u kunt in plaats daarvan een andere interface maken met de naam solidShapeInterface die het volumecontract heeft en solide vormen zoals kubussen enz. Kunnen deze interface implementeren.

const shapeInterface = (staat) => ({
  type: 'shapeInterface',
  area: () => state.area (staat)
})
const solidShapeInterface = (staat) => ({
  type: 'solidShapeInterface',
  volume: () => state.volume (staat)
})
const cubo = (lengte) => {
  const proto = {
    lengte,
    type: '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 ({}, basis, complex)
  retour Object.assign (Object.create (composiet), {lengte})
}

Dit is een veel betere aanpak, maar een valkuil waar je op moet letten is wanneer je de som voor de vorm moet berekenen, in plaats van de shapeInterface of een solidShapeInterface te gebruiken.

U kunt een andere interface maken, misschien ShapeInterface beheren, en deze op zowel de platte als de vaste vormen implementeren, op deze manier kunt u gemakkelijk zien dat deze een enkele API heeft voor het beheren van de vormen, bijvoorbeeld:

const manageShapeInterface = (fn) => ({
  type: 'manageShapeInterface',
  berekenen: () => fn ()
})
const circle = (radius) => {
  const proto = {
    radius,
    type: 'Cirkel',
    area: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  const basics = shapeInterface (proto)
  const abstraccion = manageShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, basics, abstraccion)
  retourneer Object.assign (Object.create (composiet), {radius})
}
const cubo = (lengte) => {
  const proto = {
    lengte,
    type: '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 ({}, basics, abstraccion)
  retour Object.assign (Object.create (composiet), {lengte})
}

Zoals u tot nu toe kunt zien, zijn wat we voor interfaces in JavaScript hebben gedaan fabrieksfuncties voor functiesamenstelling.

En hier, met manageShapeInterface, is wat we doen de berekeningsfunctie opnieuw abstract, wat we hier en in de andere interfaces doen (als we het interfaces kunnen noemen), gebruiken we "functies van hoge orde" om de abstracties te bereiken.

Als je niet weet wat een hogere orderfunctie is, kun je deze video bekijken.

# Afhankelijkheid inversie principe

Entiteiten moeten afhankelijk zijn van abstracties en niet van concreties. Hierin staat dat de module op hoog niveau niet afhankelijk moet zijn van de module op laag niveau, maar op abstracties.

Als dynamische taal vereist JavaScript geen abstracties om ontkoppeling te vergemakkelijken. Daarom is de bepaling dat abstracties niet afhankelijk moeten zijn van details niet bijzonder relevant voor JavaScript-toepassingen. De bepaling dat modules op hoog niveau niet afhankelijk mogen zijn van modules op laag niveau, is echter relevant.

Vanuit functioneel oogpunt kunnen deze containers en injectieconcepten worden opgelost met een eenvoudige functie van een hogere orde, of een hole-in-the-middle-type patroon dat direct in de taal is ingebouwd.

Hoe is afhankelijkheidsinversie gerelateerd aan functies van een hogere orde? is een vraag die in StackExchange wordt gesteld als u een uitgebreide uitleg wilt.

Dit klinkt misschien opgeblazen, maar het is echt gemakkelijk te begrijpen. Dit principe maakt ontkoppeling mogelijk.

En we hebben het eerder gemaakt, laten we onze code bekijken met de manageShapeInterface en hoe we de berekeningsmethode bereiken.

const manageShapeInterface = (fn) => ({
  type: 'manageShapeInterface',
  berekenen: () => fn ()
})

Wat de manageShapeInterface-fabrieksfunctie ontvangt als het argument is een functie van hogere orde, die voor elke vorm de functionaliteit ontkoppelt om de benodigde logica te bereiken om tot de uiteindelijke berekening te komen, laat zien hoe dit wordt gedaan in de vormenobjecten.

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

Voor het vierkant moeten we alleen het gebied van de vorm krijgen, en voor een cubo moeten we het gebied samenvatten met het volume en dat is alles wat nodig is om de koppeling te vermijden en de abstractie te krijgen.

# Volledige codevoorbeelden

  • Je kunt het hier downloaden: solid.js

# Verder lezen en referenties

  • SOLID de eerste 5 principes van OOD
  • 5 principes die van u een SOLID JavaScript Developer maken
  • SOLID JavaScript-serie
  • SOLIDE principes met behulp van Typescript

# Gevolgtrekking

"Als je de SOLID-principes tot het uiterste gaat, kom je bij iets dat functionele programmering er behoorlijk aantrekkelijk uitziet" - Mark Seemann

JavaScript is een programmeertaal met meerdere paradigma's, en we kunnen er de solide principes op toepassen, en het mooie is dat we het kunnen combineren met het functionele programmeerparadigma en het beste van beide werelden kunnen krijgen.

Javascript is ook een dynamische programmeertaal en zeer veelzijdig
wat ik heb gepresenteerd is gewoon een manier om deze principes te bereiken met JavaScript, het zijn misschien betere opties om deze principes te bereiken.

Ik hoop dat je dit bericht leuk vond, ik ben momenteel nog bezig met het verkennen van de JavaScript-wereld, dus ik sta open voor feedback of bijdragen en als je het leuk vond, raad het dan aan bij een vriend, deel het of lees het opnieuw.

Je kunt me volgen #twitter @ cramirez_92
https://twitter.com/cramirez_92

Tot de volgende keer