S.O.L.I.D Primele 5 principii ale proiectării orientate pe obiect cu JavaScript

Am găsit un articol foarte bun care explică S.O.L.I.D. principii, dacă sunteți familiarizat cu PHP, puteți citi articolul original aici: S.O.L.I.D: Primii 5 principii ale proiectării orientate pe obiecte. Dar, deoarece sunt dezvoltator JavaScript, am adaptat exemplele de cod din articol în JavaScript.

JavaScript este un limbaj tipizat slab, unii îl consideră un limbaj funcțional, alții îl consideră un limbaj orientat spre obiecte, unii cred că sunt ambele, iar unii consideră că faptul că are clase în JavaScript este pur și simplu greșit. - Dor Tzur

Acesta este doar un simplu articol „binevenit pentru S.O.L.I.D.”, pur și simplu aruncă lumină asupra a ceea ce S.O.L.I.D. este.

SOLID. STANDS PENTRU:

  • S - Principiul responsabilității unice
  • O - Principiul închis deschis
  • L - Principiul substituției Liskov
  • I - Principiul de segregare a interfeței
  • D - Principiul inversiunii dependenței

# Principiul responsabilității unice

O clasă ar trebui să aibă unul și un singur motiv de schimbare, ceea ce înseamnă că o clasă ar trebui să aibă un singur loc de muncă.

De exemplu, să spunem că avem unele forme și am vrut să însumăm toate zonele formelor. Ei bine, este destul de simplu, nu?

cerc cerc = (radius) => {
  const proto = {
    tip: „Cerc”,
    //cod
  }
  returnează Object.assign (Object.create (proto), {radius})
}
const square = (lungime) => {
  const proto = {
    tip: „pătrat”,
    //cod
  }
  returnează Object.assign (Object.create (proto), {lungime})
}

În primul rând, ne creăm funcțiile fabricii de forme și setăm parametrii necesari.

Ce este o funcție din fabrică?

În JavaScript, orice funcție poate returna un obiect nou. Când nu este o funcție sau o clasă de constructor, se numește funcție fabrică. de ce să folosiți funcțiile din fabrică, acest articol oferă o explicație bună și acest videoclip îl explică foarte clar

În continuare, vom continua creând funcția de fabrică areaCalculator și apoi scriem logica noastră pentru a rezuma aria tuturor formelor furnizate.

const areaCalculator = (s) => {
  const proto = {
    suma () {
      // logica la sumă
    },
    ieșire () {
     returnare `
       

         Suma zonelor formelor furnizate:          $ {This.sum ()}             }   }   returnează Object.assign (Object.create (proto), {formes: s}) }

Pentru a utiliza funcția de fabrica areaCalculator, apelăm pur și simplu la funcție și trecem într-o serie de forme și afișăm ieșirea din partea de jos a paginii.

forme const = = [
  cerc (2),
  pătrat (5),
  pătrat (6)
]
const areas = areaCalculator (forme)
console.log (areas.output ())

Problema cu metoda de ieșire este aceea că areaCalculator se ocupă de logica de ieșire a datelor. Prin urmare, ce se întâmplă dacă utilizatorul dorește să emită datele ca json sau altceva?

Toată logica ar fi gestionată de funcția de fabrică areaCalculator, aceasta este cea cu care „principiul responsabilității unice” se încruntă; funcția fabrică zoneCalculator ar trebui să însume doar zonele de forme furnizate, nu ar trebui să le pese dacă utilizatorul dorește JSON sau HTML.

Așadar, pentru a remedia această problemă, puteți crea o funcție de fabrică SumCalculatorOutputter și puteți folosi aceasta pentru a gestiona orice logică aveți nevoie pentru modul în care sunt afișate suprafețele sumelor tuturor formelor furnizate.

Funcția de fabrică sumCalculatorOutputter ar funcționa astfel:

forme const = = [
  cerc (2),
  pătrat (5),
  pătrat (6)
]
const areas = areaCalculator (forme)
const output = sumCalculatorOputter (zone)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

Acum, orice logică de care aveți nevoie pentru a emite datele către utilizatori este acum gestionată de funcția din fabrică sumCalculatorOutputter.

# Principiul deschis-închis

Obiectele sau entitățile ar trebui să fie deschise pentru extindere, dar închise pentru modificare.
Deschis pentru extensie înseamnă că ar trebui să putem adăuga noi funcții sau componente la aplicație fără a încălca codul existent.
Închis pentru modificare înseamnă că nu ar trebui să introducem modificări de rupere la funcționalitatea existentă, deoarece asta te-ar obliga să refaci o mulțime de coduri existente - Eric Elliott

În cuvinte mai simple, înseamnă că o funcție de clasă sau de fabrică în cazul nostru, ar trebui să fie ușor extensibilă fără a modifica clasa sau funcția în sine. Să ne uităm la funcția fabrică zoneCalculator, în special la metoda sumei.

suma () {
 
 const area = []
 
 for (forma acestui.shapes) {
  
  if (forma.type === 'Pătrat') {
     area.push (Math.pow (formă.lungime, 2)
   } else if (forma.type === 'Cercul') {
     area.push (Math.PI * Math.pow (shape.length, 2)
   }
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Dacă am dori ca metoda sumei să poată să însumeze suprafețele cu mai multe forme, ar trebui să adăugăm mai multe dacă / altfel blochează și asta merge împotriva principiului deschis-închis.

O modalitate prin care putem îmbunătăți această metodă a sumei este eliminarea logicii pentru calcularea ariei fiecărei forme din metoda sumei și atașarea acesteia la funcțiile fabricii ale formei.

const square = (lungime) => {
  const proto = {
    tip: „pătrat”,
    zona () {
      returnați Math.pow (această lungime, 2)
    }
  }
  returnează Object.assign (Object.create (proto), {lungime})
}

Același lucru ar trebui făcut și pentru funcția de fabrică a cercului, ar trebui adăugată o metodă de zonă. Acum, pentru a calcula suma oricărei forme furnizate ar trebui să fie la fel de simplu:

suma () {
 const area = []
 for (forma acestui.shapes) {
   area.push (shape.area ())
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Acum putem crea o altă clasă de formă și o putem trece atunci când calculăm suma fără a ne încălca codul. Cu toate acestea, acum apare o altă problemă, de unde știm că obiectul trecut în zonaCalculator este de fapt o formă sau dacă forma are o metodă denumită zonă?

Codificarea către o interfață este o parte integrantă a S.O.L.I.D., un exemplu rapid este crearea unei interfețe, pe care fiecare formă o pune în aplicare.

Întrucât JavaScript nu are interfețe, vă voi arăta cum se va realiza acest lucru în TypeScript, deoarece TypeScript modelează OOP-ul clasic pentru JavaScript și diferența cu OO prototipală JavaScript pur.

interfață ShapeInterface {
 zona (): număr
}
class Circle implementează ShapeInterface {
 let raza: număr = 0
 constructor (r: număr) {
  acest.radius = r
 }
 
 zonă publică (): număr {
  returnează MATH.PI * MATH.pow (this.radius, 2)
 }
}

În exemplul de mai sus demonstrează modul în care se va realiza acest lucru în TypeScript, dar sub capota TypeScript compilează codul la JavaScript pur, iar în codul compilat îi lipsește interfețele, deoarece JavaScript nu îl are.

Deci, cum putem realiza acest lucru, în lipsa interfețelor?

Funcția Compoziție la salvare!

Mai întâi creăm funcția de fabricare a formaInterface, așa cum vorbim despre interfețe, forma noastră de interfață va fi la fel de abstractă ca o interfață, folosind compoziția funcției, pentru o explicație profundă a compoziției, vedeți acest video excelent.

const formeInterface = (stare) => ({
  tip: 'formaInterface',
  zona: () => state.area (statul)
})

Apoi îl implementăm în funcția noastră de fabricație pătrată.

const square = (lungime) => {
  const proto = {
    lungime,
    tip: „pătrat”,
    zona: (args) => Math.pow (lungimea args., 2)
  }
  elementele de bază const = formaInterface (proto)
  const composite = Object.assign ({}, elementele de bază)
  returnează Object.assign (Object.create (compozit), {lungime})
}

Iar rezultatul apelării funcției fabricii pătrate va fi următorul:

const s = pătrat (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
s.area ()
// ieșire
OBJ
 {lungime: 5}
PROTO
 {tip: 'formaInterface', zonă: [Funcție: zonă]}
25

În zona noastră Metoda sumei de calculator putem verifica dacă formele furnizate sunt de fapt tipuri de formăInterface, altfel aruncăm o excepție:

suma () {
  const area = []
  for (forma acestui.shapes) {
    if (Object.getPrototypeOf (forma) .type === 'formeInterface') {
       area.push (shape.area ())
     } altfel {
       aruncă o nouă eroare („acesta nu este un obiect shapeInterface”)
     }
   }
   return area.reduce ((v, c) => c + = v, 0)
}

iar, din moment ce JavaScript nu are suport pentru interfețe precum limbile tipizate, exemplul de mai sus demonstrează modul în care îl putem simula, dar mai mult decât simularea interfețelor, ceea ce facem este să folosim închideri și compoziție funcțională dacă nu știți ce sunt închidere este acest articol îl explică foarte bine și pentru completare vezi acest videoclip.

# Principiul de substituție Liskov

Fie q (x) să fie o proprietate probabilă pentru obiectele de x de tip T. Atunci q (y) ar trebui să fie probabile pentru obiectele y de tip S unde S este un subtip de T.

Toate acestea afirmă este faptul că fiecare subclasă / clasă derivată ar trebui să fie substituibilă clasei lor de bază / părinte.

Cu alte cuvinte, la fel de simplu, o subclasă ar trebui să înlocuiască metodele clasei părinte într-un mod care să nu distrugă funcționalitatea din punctul de vedere al unui client.

Folosind totuși funcția noastră de fabrică areaCalculator, să spunem că avem o funcție de fabrică volumCalculator care extinde funcția de fabrică areaCalculator, iar în cazul nostru pentru extinderea unui obiect fără a încălca modificări în ES6, o facem folosind Object.assign () și Object. getPrototypeOf ():

const volumeCalculator = (s) => {
  const proto = {
    tip: 'volumCalculator'
  }
  const areaCalProto = Object.getPrototypeOf (areaCalculator ())
  const ered = Object.assign ({}, areaCalProto, proto)
  returnează Object.assign (Object.create (mostrește), {formes: s})
}

# Principiul de segregare a interfeței

Un client nu trebuie niciodată obligat să implementeze o interfață pe care nu o folosește sau clienții nu ar trebui să fie obligați să depindă de metodele pe care nu le utilizează.

Continuând exemplul nostru de forme, știm că avem și forme solide, așa că, de asemenea, am dori să calculăm volumul formei, putem adăuga un alt contract la formaInterface:

const formeInterface = (stare) => ({
  tip: 'formaInterface',
  zona: () => state.area (starea),
  volum: () => state.volume (stare)
})

Orice formă pe care o creăm trebuie să implementeze metoda volumului, dar știm că pătratele sunt forme plane și că nu au volume, astfel încât această interfață ar forța funcția de fabricație pătrată să implementeze o metodă la care nu are niciun folos.

Principiul de segregare a interfeței nu spune acest lucru, în schimb, puteți crea o altă interfață numită solidShapeInterface care are contractul de volum și forme solide precum cuburi etc. pot implementa această interfață.

const formeInterface = (stare) => ({
  tip: 'formaInterface',
  zona: () => state.area (statul)
})
const solidShapeInterface = (stare) => ({
  tip: 'solidShapeInterface',
  volum: () => state.volume (stare)
})
const cubo = (lungime) => {
  const proto = {
    lungime,
    tip: „Cubo”,
    aria: (args) => Math.pow (lungimea args., 2),
    volum: (args) => Math.pow (lungime args., 3)
  }
  elementele de bază const = formaInterface (proto)
  const complex = solidShapeInterface (proto)
  const composite = Object.assign ({}, elementele de bază, complexul)
  returnează Object.assign (Object.create (compozit), {lungime})
}

Aceasta este o abordare mult mai bună, dar o problemă de care trebuie să aveți grijă este momentul în care trebuie să calculați suma pentru formă, în loc să folosiți formaInterface sau un solidShapeInterface.

Puteți crea o altă interfață, poate manageShapeInterface și să o implementați atât pe formele plate cât și pe cele solide, astfel puteți vedea cu ușurință că are o singură API pentru gestionarea formelor, de exemplu:

const manageShapeInterface = (fn) => ({
  tip: 'manageShapeInterface',
  calculați: () => fn ()
})
cerc cerc = (radius) => {
  const proto = {
    rază,
    tip: „Cerc”,
    suprafață: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  elementele de bază const = formaInterface (proto)
  const abstraccion = manageShapeInterface (() => elementele de bază.area ())
  const composite = Object.assign ({}, elementele de bază, abstractizarea)
  returnează Object.assign (Object.create (compozit), {radius})
}
const cubo = (lungime) => {
  const proto = {
    lungime,
    tip: „Cubo”,
    aria: (args) => Math.pow (lungimea args., 2),
    volum: (args) => Math.pow (lungime args., 3)
  }
  elementele de bază const = formaInterface (proto)
  const complex = solidShapeInterface (proto)
  const abstraccion = manageShapeInterface (
    () => bazics.area () + complex.volume ()
  )
  const composite = Object.assign ({}, elementele de bază, abstractizarea)
  returnează Object.assign (Object.create (compozit), {lungime})
}

După cum puteți vedea până acum, ceea ce am făcut pentru interfețe în JavaScript sunt funcțiile din fabrică pentru compoziția funcțiilor.

Și aici, cu manageShapeInterface, ceea ce facem este să facem din nou abstractizarea funcției de calcul, ceea ce facem aici și în celelalte interfețe (dacă le putem numi interfețe), folosim „funcții de ordin înalt” pentru a realiza abstractiunile.

Dacă nu știți care este o funcție de comandă mai mare, puteți accesa acest videoclip.

# Principiul inversării dependenței

Entitățile trebuie să depindă de abstractizări și nu de concreții. Acesta afirmă că modulul la nivel înalt nu trebuie să depindă de modulul de nivel scăzut, ci ar trebui să depindă de abstractizări.

Ca limbaj dinamic, JavaScript nu necesită utilizarea de abstractizări pentru a facilita decuplarea. Prin urmare, precizarea că abstracțiile nu ar trebui să depindă de detalii nu este deosebit de relevantă pentru aplicațiile JavaScript. Cu toate acestea, este importantă precizarea că modulele la nivel înalt nu ar trebui să depindă de modulele de nivel scăzut.

Din punct de vedere funcțional, aceste containere și concepte de injecție pot fi rezolvate cu o funcție simplă de comandă mai mare sau cu un model de tip „orificiu în mijloc” care sunt încorporate chiar în limbaj.

Cum este legată inversarea dependenței cu funcțiile de ordin superior? este o întrebare pusă în stackExchange dacă doriți o explicație profundă.

Acest lucru poate suna balonat, dar este foarte ușor de înțeles. Acest principiu permite decuplarea.

Și am făcut-o înainte, permiteți revizuirea codului nostru cu manageShapeInterface și modul în care realizăm metoda de calcul.

const manageShapeInterface = (fn) => ({
  tip: 'manageShapeInterface',
  calculați: () => fn ()
})

Ceea ce funcția de fabrică manageShapeInterface primește, întrucât argumentul este o funcție de comandă mai mare, care decuplează pentru fiecare formă funcționalitatea pentru a realiza logica necesară pentru a ajunge la calculul final, să vedem cum se face acest lucru în obiectele forme.

const square = (radius) => {
  // cod
 
  const abstraccion = manageShapeInterface (() => elementele de bază.area ())
 
 // mai mult cod ...
}
const cubo = (lungime) => {
  // cod
  const abstraccion = manageShapeInterface (
    () => bazics.area () + complex.volume ()
  )
  // mai mult cod ...
}

Pentru pătrat, ceea ce trebuie să calculăm este doar obținerea formei, iar pentru un cub, ceea ce avem nevoie este însumarea zonei cu volumul și asta este tot ce trebuie pentru a evita cuplarea și pentru a obține abstracția.

# Exemple de coduri complete

  • Puteți obține aici: solid.js

# Citire în continuare și referințe

  • SOLID primele 5 principii ale OOD
  • 5 Principii care vă vor face un dezvoltator JavaScript SOLID
  • Seria JavaScript SOLID
  • Principii SOLID folosind Typescript

# Concluzie

„Dacă duceți principiile SOLID la extremele lor, ajungeți la ceva care face ca programarea funcțională să pară destul de atractivă” - Mark Seemann

JavaScript este un limbaj de programare cu mai multe paradigme și putem aplica principiile solide acestuia, iar marea dintre acestea este că îl putem combina cu paradigma funcțională de programare și să obținem tot ce este mai bun din ambele lumi.

Javascript este, de asemenea, un limbaj de programare dinamic și foarte versatil
ceea ce am prezentat este doar o modalitate de realizare a acestor principii cu JavaScript, pot fi opțiuni mai bune în atingerea acestor principii.

Sper că v-a plăcut această postare, în prezent explorez încă lumea JavaScript, așa că sunt deschis să accept feedback sau contribuții, iar dacă v-a plăcut, recomand-o unui prieten, să o împărtășiți sau să o citiți din nou.

Poți să mă urmezi pe #twitter @ cramirez_92
https://twitter.com/cramirez_92

Până data viitoare