S.O.L.I.D De første 5 principper for objektorienteret design med JavaScript

Jeg har fundet en meget god artikel, der forklarer S.O.L.I.D. principper, hvis du er bekendt med PHP, kan du læse den originale artikel her: S.O.L.I.D: De første 5 principper for objektorienteret design. Men da jeg er en JavaScript-udvikler, har jeg tilpasset kodeeksemplerne fra artiklen til JavaScript.

JavaScript er et løst typisk sprog, nogle betragter det som et funktionelt sprog, andre betragter det som et objektorienteret sprog, nogle synes, det er begge dele, og nogle synes, at det er helt klart forkert at have klasser i JavaScript. - Dor Tzur

Dette er bare en simpel "velkomst til S.O.L.I.D." -artikel, den kaster ganske enkelt lys over, hvad S.O.L.I.D. er.

SOLID. STÅR FOR:

  • S - Enkeltansvarsprincip
  • O - Åben lukket princip
  • L - Liskov-substitutionsprincippet
  • I - Grænsefladesegregeringsprincip
  • D - Afhængighedsinversionsprincippet

# Enkeltansvarsprincip

En klasse skal have én og kun en grund til at ændre sig, hvilket betyder, at en klasse kun skal have et job.

For eksempel siger vi, at vi har nogle figurer, og vi ønskede at opsummere alle områderne i figurerne. Nå, dette er ret simpelt, ikke?

const cirkel = (radius) => {
  const proto = {
    type: 'Cirkel',
    //kode
  }
  return Object.assign (Object.create (proto), {radius})
}
const square = (længde) => {
  const proto = {
    type: 'firkant'
    //kode
  }
  return Object.assign (Object.create (proto), {length})
}

Først opretter vi vores figurer fabriksfunktioner og opsætter de krævede parametre.

Hvad er en fabriksfunktion?

I JavaScript kan enhver funktion returnere et nyt objekt. Når det ikke er en konstruktørfunktion eller -klasse, kaldes det en fabriksfunktion. hvorfor man bruger fabriksfunktioner giver denne artikel en god forklaring, og denne video forklarer det også meget tydeligt

Derefter går vi videre ved at oprette fabrikken Funktion areaCalculator og derefter skrive vores logik for at opsummere området for alle leverede figurer.

const areaCalculator = (s) => {
  const proto = {
    sum () {
      // logik til summen
    },
    output () {
     vende tilbage '
       

         Summen af ​​de områder med angivne former:          $ {This.sum ()}             }   }   return Object.assign (Object.create (proto), {former: s}) }

For at bruge areaCalculator-fabriksfunktionen skal vi blot kalde funktionen og videregive en række figurer og vise output i bunden af ​​siden.

const former = [
  cirkel (2),
  kvadrat (5),
  kvadrat (6)
]
const-områder = områdeberegner (figurer)
console.log (areas.output ())

Problemet med output-metoden er, at areaCalculator håndterer logikken for at udsende dataene. Derfor, hvad hvis brugeren ville udsende dataene som json eller noget andet?

Hele logikken ville blive håndteret af funktionen areaCalculator-fabrikken, det er, hvad 'Enkeltansvarprincip' rynker mod; funktionen areaCalculator skal kun opsummere arealerne med angivne former, det skal ikke være ligegyldigt om brugeren vil have JSON eller HTML.

Så for at løse dette kan du oprette en SumCalculatorOutputter fabriksfunktion og bruge denne til at håndtere uanset logik, du har brug for, hvordan sumområdet for alle medfølgende figurer vises.

SumCalculatorOutputter-fabriksfunktionen kunne synes godt om dette:

const former = [
  cirkel (2),
  kvadrat (5),
  kvadrat (6)
]
const-områder = områdeberegner (figurer)
const output = sumCalculatorOputter (områder)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

Uanset hvilken logik du har brug for for at udsende dataene til brugerne håndteres nu af sumCalculatorOutputter fabriksfunktion.

# Åben-lukket princip

Objekter eller enheder skal være åbne for udvidelse, men lukket for ændring.
Åben til udvidelse betyder, at vi skal kunne tilføje nye funktioner eller komponenter til applikationen uden at bryde eksisterende kode.
Lukket for ændring betyder, at vi ikke bør introducere brudende ændringer i eksisterende funktionalitet, fordi det ville tvinge dig til at refaktorere en masse eksisterende kode - Eric Elliott

Med enklere ord betyder det, at en klasse- eller fabriksfunktion i vores tilfælde let skal kunne udvides uden at ændre selve klassen eller funktionen. Lad os se på funktionen areaCalculator-fabrikken, især det er summetoden.

sum () {
 
 const areal = []
 
 for (form af dette.former) {
  
  if (form.type === 'Firkant') {
     area.push (Math.pow (form.længde, 2)
   } andet hvis (form.type === 'Cirkel') {
     area.push (Math.PI * Math.pow (form.længde, 2)
   }
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Hvis vi ønskede, at summetoden skulle være i stand til at opsummere områderne med flere figurer, ville vi være nødt til at tilføje flere, hvis / andet blokke, og det strider mod det åbne-lukkede princip.

En måde vi kan gøre denne summetode bedre på er at fjerne logikken for at beregne arealet af hver form ud af summetoden og knytte den til formens fabriksfunktioner.

const square = (længde) => {
  const proto = {
    type: 'firkant'
    areal () {
      return Math.pow (this.length, 2)
    }
  }
  return Object.assign (Object.create (proto), {length})
}

Det samme skal gøres for cirkelfabriksfunktionen, en områdemetode skal tilføjes. Nu skal beregningen af ​​summen af ​​en hvilken som helst leveret form være så enkel som:

sum () {
 const areal = []
 for (form af dette.former) {
   area.push (shape.area ())
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Nu kan vi oprette en anden formklasse og videregive den, når vi beregner summen uden at bryde vores kode. Dog opstår der endnu et problem, hvordan ved vi, at objektet, der føres ind i området Beregner, faktisk er en form, eller hvis formen har en metode, der hedder område?

Kodning til en grænseflade er en integreret del af S.O.L.I.D., et hurtigt eksempel er, at vi opretter en grænseflade, som enhver form implementerer.

Da JavaScript ikke har grænseflader, viser jeg dig, hvordan dette opnås i TypeScript, da TypeScript modellerer den klassiske OOP til JavaScript, og forskellen med ren JavaScript Prototypal OO.

interface ShapeInterface {
 område (): antal
}
klasse Cirkel implementerer ShapeInterface {
 lad radius: antal = 0
 konstruktør (r: nummer) {
  this.radius = r
 }
 
 offentligt område (): nummer {
  returner MATH.PI * MATH.pow (this.radius, 2)
 }
}

I eksemplet ovenfor demonstrerer, hvordan dette opnås i TypeScript, men under hætten kompilerer TypeScript koden til ren JavaScript og i den kompilerede kode mangler den grænseflader, da JavaScript ikke har den.

Så hvordan kan vi opnå dette i manglen på grænseflader?

Funktion Komposition til redning!

Først opretter vi formInterface fabriksfunktion, som vi taler om grænseflader, vores FormInterface vil være så abstraheret som en grænseflade ved hjælp af funktionskomposition, for en dyb forklaring af komposition se denne fantastiske video.

const shapeInterface = (tilstand) => ({
  type: 'formInterface',
  område: () => state.area (state)
})

Derefter implementerer vi det til vores firkantede fabriksfunktion.

const square = (længde) => {
  const proto = {
    længde,
    type: 'firkant'
    område: (args) => Math.pow (args.length, 2)
  }
  const basics = formInterface (proto)
  const composite = Object.assign ({}, grundlæggende)
  return Object.assign (Object.create (sammensat), {længde})
}

Og resultatet af at ringe til den firkantede fabriksfunktion vil være den næste:

const s = firkant (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
s.area ()
// output
OBJ
 {længde: 5}
PROTO
 {type: 'formInterface', område: [Funktion: område]}
25

I vores områdeberegningsmetalsmetode kan vi kontrollere, om de angivne former faktisk er typer af formInterface, ellers kaster vi en undtagelse:

sum () {
  const areal = []
  for (form af dette.former) {
    if (Object.getPrototypeOf (form) .type === 'formInterface') {
       area.push (shape.area ())
     } andet {
       smid ny fejl ('dette er ikke et formInterface-objekt')
     }
   }
   return area.reduce ((v, c) => c + = v, 0)
}

og igen, da JavaScript ikke har understøttelse af grænseflader som typiske sprog, viser eksemplet ovenfor, hvordan vi kan simulere det, men mere end simulering af grænseflader, hvad vi laver er at bruge lukninger og funktionskomposition, hvis du ikke ved hvad der er et lukning er denne artikel forklarer det meget godt, og for komplementering se denne video.

# Liskov-substitutionsprincip

Lad q (x) være en egenskab, der kan bevises for objekter af x af type T. Så skal q (y) være beviselig for objekter y af type S, hvor S er en undertype af T.

Alt dette siger, er, at hver underklasse / afledt klasse skal kunne erstatte deres base / forældreklasse.

Med andre ord, så simpelt som det, skal en underklasse tilsidesætte overordnede klassemetoder på en måde, der ikke bryder funktionaliteten fra en klients synspunkt.

Vi gør stadig brug af vores areaCalculator fabriksfunktion, siger vi har en volumeCalculator fabriksfunktion, der udvider areaCalculator fabriksfunktionen, og i vores tilfælde for at udvide et objekt uden at bryde ændringer i ES6 gør vi det ved hjælp af Object.assign () og objektet. getPrototypeOf ():

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

# Grænsefladesegregeringsprincip

En klient bør aldrig tvinges til at implementere en grænseflade, den ikke bruger, eller klienter bør ikke tvinges til at være afhængige af metoder, de ikke bruger.

Fortsæt med vores figureksempel ved vi, at vi også har solide former, så da vi også ønsker at beregne formens volumen, kan vi tilføje en anden kontrakt til formen:

const shapeInterface = (tilstand) => ({
  type: 'formInterface',
  område: () => state.area (state),
  lydstyrke: () => tilstand.volumen (tilstand)
})

Enhver form, vi opretter, skal implementere volumenmetoden, men vi ved, at kvadrater er flade figurer, og at de ikke har volumener, så denne grænseflade ville tvinge den firkantede fabriksfunktion til at implementere en metode, som den ikke har nogen brug af.

Grænsefladesegregationsprincippet siger nej til dette, i stedet kan du oprette en anden grænseflade kaldet solidShapeInterface, der har volumenkontrakten og faste former som terninger osv. Kan implementere denne grænseflade.

const shapeInterface = (tilstand) => ({
  type: 'formInterface',
  område: () => state.area (state)
})
const solidShapeInterface = (tilstand) => ({
  type: 'solidShapeInterface',
  lydstyrke: () => tilstand.volumen (tilstand)
})
const cubo = (længde) => {
  const proto = {
    længde,
    type: 'Cubo',
    område: (args) => Math.pow (args.length, 2),
    volumen: (args) => Math.pow (args.length, 3)
  }
  const basics = formInterface (proto)
  const complex = solidShapeInterface (proto)
  const composite = Object.assign ({}, grundlæggende, kompleks)
  return Object.assign (Object.create (sammensat), {længde})
}

Dette er en meget bedre fremgangsmåde, men en faldgrube, man skal være opmærksom på, er, hvornår man skal beregne summen for formen, i stedet for at bruge formenInterface eller en solidShapeInterface.

Du kan oprette en anden grænseflade, måske administrereShapeInterface, og implementere den på både de flade og solide former, det er sådan, du let kan se, at det har et enkelt API til at styre figurerne, for eksempel:

const manageShapeInterface = (fn) => ({
  type: 'manageShapeInterface',
  beregne: () => fn ()
})
const cirkel = (radius) => {
  const proto = {
    radius,
    type: 'Cirkel',
    område: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  const basics = formInterface (proto)
  const abstraccion = manageShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, grundlæggende, abstraktion)
  return Object.assign (Object.create (composite), {radius})
}
const cubo = (længde) => {
  const proto = {
    længde,
    type: 'Cubo',
    område: (args) => Math.pow (args.length, 2),
    lydstyrke: (args) => Math.pow (args.length, 3)
  }
  const basics = formInterface (proto)
  const complex = solidShapeInterface (proto)
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  const composite = Object.assign ({}, grundlæggende, abstraktion)
  return Object.assign (Object.create (sammensat), {længde})
}

Som du kan se indtil nu, er det, vi har gjort for grænseflader i JavaScript, fabriksfunktioner til funktionskomposition.

Og her, med manageShapeInterface, det, vi laver, abstraherer igen den beregne funktion, hvad vi gør her og i de andre grænseflader (hvis vi kan kalde det grænseflader), bruger vi "high order-funktioner" til at opnå abstraktionerne.

Hvis du ikke ved, hvad en funktion med højere orden er, kan du gå og tjekke denne video.

# Afhængighedsinversionsprincippet

Enheder skal afhænge af abstraktioner, ikke af konkretioner. Det hedder, at modulet på højt niveau ikke må afhænge af modulet på lavt niveau, men at det skal afhænge af abstraktioner.

Som et dynamisk sprog kræver JavaScript ikke brug af abstraktioner for at lette afkoblingen. Derfor er bestemmelsen om, at abstraktioner ikke skal afhænge af detaljer, ikke særlig relevant for JavaScript-applikationer. Bestemmelsen om, at moduler på højt niveau ikke skal afhænge af moduler på lavt niveau, er imidlertid relevant.

Fra et funktionelt synspunkt kan disse containere og injektionskoncepter løses med en simpel højere ordensfunktion eller hul i midten type mønster, der er indbygget lige i sproget.

Hvordan er afhængighedsinversion relateret til funktioner med højere orden? er et spørgsmål, der stilles i stackExchange, hvis du ønsker en dyb forklaring.

Dette lyder muligvis oppustet, men det er virkelig let at forstå. Dette princip muliggør afkobling.

Og vi har gjort det før, lader gennemse vores kode med manageShapeInterface, og hvordan vi udfører beregningsmetoden.

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

Hvad managerShapeInterface-fabriksfunktionen modtager som argument er en funktion af højere orden, der afkobler for hver form funktionaliteten for at udføre den nødvendige logik for at komme til den endelige beregning, lad os se, hvordan dette gøres i formobjekterne.

const square = (radius) => {
  // kode
 
  const abstraccion = manageShapeInterface (() => basics.area ())
 
 // mere kode ...
}
const cubo = (længde) => {
  // kode
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  // mere kode ...
}

For kvadratet er det, vi har brug for at beregne, bare at få formens areal, og for en cubo, hvad vi har brug for, summerer vi arealet med lydstyrken, og det er alt, hvad vi behøver for at undgå koblingen og få abstraktionen.

# Komplette kodeeksempler

  • Du kan få det her: solid.js

# Yderligere læsning og referencer

  • SOLID de første 5 principper i OOD
  • 5 principper, der gør dig til en SOLID JavaScript-udvikler
  • SOLID JavaScript-serie
  • SOLID-principper ved hjælp af Typescript

# Konklusion

"Hvis du tager SOLID-principperne til deres ekstreme sider, kommer du til noget, der får Funktionel programmering til at se ganske attraktiv ud" - Mark Seemann

JavaScript er et programmeringssprog med flere paradigmer, og vi kan anvende de solide principper på det, og det store ved det er, at vi kan kombinere det med det funktionelle programmeringsparadigme og få det bedste fra begge verdener.

Javascript er også et dynamisk programmeringssprog og meget alsidigt
hvad jeg har præsenteret er bare en måde at opnå disse principper med JavaScript, de er muligvis flere bedre muligheder for at nå disse principper.

Jeg håber, at du nød dette indlæg, jeg udforsker stadig JavaScript-verdenen, så jeg er åben for at acceptere feedback eller bidrag, og hvis du kunne lide det, kan du anbefale det til en ven, dele det eller læse det igen.

Du kan følge mig #twitter @ cramirez_92
https://twitter.com/cramirez_92

Indtil næste gang