S.O.L.I.D Objektiorientoituneen suunnittelun 5 ensimmäistä periaatetta JavaScriptillä

Löysin erittäin hyvän artikkelin, joka selittää S.O.L.I.D. periaatteet, jos olet perehtynyt PHP: hen, voit lukea alkuperäisen artikkelin täältä: S.O.L.I.D: Objektilähtöisen suunnittelun 5 ensimmäistä periaatetta. Mutta koska olen JavaScriptin kehittäjä, olen mukauttanut artikkelin koodimallit JavaScriptiin.

JavaScript on löysästi kirjoitettu kieli, jotkut pitävät sitä toimivana kielenä, toiset sitä pitävät oliokeskeisenä, toiset ajattelevat sen molempia ja toiset ajattelevat, että JavaScriptin luokkien pitäminen on vain väärin. - Dor Tzur

Tämä on vain yksinkertainen "tervetuloa S.O.L.I.D." -artikkeli, se vain valaisee mitä S.O.L.I.D. on.

S.O.L.I.D. TARKOITTAA:

  • S - Yksittäisen vastuun periaate
  • O - avoin suljettu -periaate
  • L - Liskov-korvausperiaate
  • I - Rajapintojen erotteluperiaate
  • D - Riippuvuusprosessin periaate

# Yhden vastuun periaate

Luokalla tulisi olla yksi ja ainoa syy muutokseen, mikä tarkoittaa, että luokalla tulisi olla vain yksi työ.

Oletetaan esimerkiksi, että meillä on joitain muotoja ja halusimme summata kaikki muotojen alueet. No, tämä on melko yksinkertaista, eikö niin?

const ympyrä = (säde) => {
  const proto = {
    tyyppi: 'ympyrä',
    //koodi
  }
  palauta Object.assign (Object.create (proto), {säde})
}
const neliö = (pituus) => {
  const proto = {
    tyyppi: 'neliö',
    //koodi
  }
  palauta Object.assign (Object.create (proto), {pituus})
}

Ensin luomme muotojen tehdastoiminnot ja asetamme tarvittavat parametrit.

Mikä on tehdastoiminto?

JavaScript-sovelluksessa mikä tahansa toiminto voi palauttaa uuden objektin. Kun se ei ole rakennusfunktio tai luokka, sitä kutsutaan tehdasfunktioksi. miksi käyttää tehdastoimintoja, tämä artikkeli antaa hyvän selityksen ja myös tämä video selittää sen hyvin selvästi

Seuraavaksi siirrymme eteenpäin luomalla areaCalculator-tehdastoiminnon ja kirjoittamalla sitten logiikkamme summaamaan kaikkien annettujen muotojen pinta-ala.

const areaCalculator = (s) => {
  const proto = {
    summa () {
      // logiikka summaan
    },
    lähtö () {
     palata `
       

         Annettujen muotojen pinta-ala:          $ {This.sum ()}             }   }   palauta Object.assign (Object.create (proto), {muodot: s}) }

Jos haluat käyttää areaCalculator-tehdastoimintoa, soitamme funktiolle ja lähetämme joukon muotoja ja näytämme tulosteen sivun alareunassa.

const muodot = [
  ympyrä (2),
  neliö (5),
  neliö (6)
]
const-alueet = areaCalculator (muodot)
console.log (areas.output ())

Tulostusmenetelmän ongelmana on, että areaCalculator käsittelee logiikan datan tulostamiseksi. Entä jos käyttäjä haluaa tulostaa tiedot json-tiedostona tai jollain muulla?

AreaCalculator -tehdastoiminto hoitaa kaiken logiikan. Tätä kohtaa 'yhden vastuun periaate'; areaCalculator-tehdastoiminnon tulisi summata vain annettujen muotojen alueet, sen ei pitäisi olla kiinnostunut siitä, haluaako käyttäjä JSON- tai HTML-muodon.

Joten tämän korjaamiseksi voit luoda SumCalculatorOutputter-tehdastoiminnon ja käyttää tätä käsittelemään mitä tahansa logiikkaa, jota tarvitset, kuinka kaikkien annettujen muotojen summa-alueet näytetään.

SumCalculatorOutputter-tehdastoiminto toimisi niin:

const muodot = [
  ympyrä (2),
  neliö (5),
  neliö (6)
]
const-alueet = areaCalculator (muodot)
const output = sumCalculatorOputter (alueet)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

Nyt mitä tahansa logiikkaa, jota tarvitset datan lähettämiseen käyttäjille, hoitaa sumCalculatorOutputter-tehdastoiminto.

# Avoin ja suljettu periaate

Objektien tai kokonaisuuksien tulisi olla avoinna laajennukselle, mutta suljettuina modifiointia varten.
Avoinna laajennukselle tarkoittaa, että meidän pitäisi voida lisätä uusia ominaisuuksia tai komponentteja sovellukseen rikkomatta olemassa olevaa koodia.
Muutokselle suljettu tarkoittaa, että meidän ei pidä tehdä rikkomusmuutoksia olemassa olevaan toiminnallisuuteen, koska se pakottaisi sinut reagoimaan paljon nykyistä koodia - Eric Elliott

Yksinkertaisemmin sanoen tarkoittaa, että luokan tai tehdastoiminnon tulisi tapauksessamme olla helposti laajennettavissa muuttamatta luokkaa tai toimintoa itse. Katsotaanpa aluelaskurin tehdastoimintoa, erityisesti sen summamenetelmää.

summa () {
 
 vakioalue = []
 
 varten (tämän muodon muoto) {
  
  if (shape.type === 'Square') {
     alue.push (Math.pow (muodon pituus, 2)
   } else if (shape.type === 'Circle') {
     alue.push (Math.PI * Math.pow (muodon pituus, 2)
   }
 }
 palautusalue.vähennys ((v, c) => c + = v, 0)
}

Jos haluaisimme summamenetelmän pystyvän summaamaan useamman muodon alueet, meidän on lisättävä enemmän, jos / else estää, ja se on avoimen ja suljetun periaatteen vastaista.

Tapa, jolla voimme parantaa tätä summamenetelmää, on poistaa logiikka laskea kunkin muodon pinta-ala pois summamenetelmästä ja liittää se muodon tehdastoimintoihin.

const neliö = (pituus) => {
  const proto = {
    tyyppi: 'neliö',
    alue () {
      palauta Math.pow (tämä.pituus, 2)
    }
  }
  palauta Object.assign (Object.create (proto), {pituus})
}

Sama asia tulisi tehdä ympyrätehdastoiminnolle, aluemenetelmä tulisi lisätä. Nyt minkä tahansa muodon summan laskemiseksi tulisi olla yhtä yksinkertainen:

summa () {
 vakioalue = []
 varten (tämän muodon muoto) {
   area.push (shape.area ())
 }
 palautusalue.vähennys ((v, c) => c + = v, 0)
}

Nyt voimme luoda uuden muodoluokan ja välittää sen laskettaessa summaa rikkomatta koodiamme. Nyt nousee kuitenkin esiin toinen ongelma, kuinka tiedämme, että areaCalculatoriin siirretty esine on itse asiassa muoto tai jos muodolla on menetelmä nimeltään alue?

Liitäntäkoodaus on kiinteä osa S.O.L.I.D., nopea esimerkki on, että luomme rajapinnan, jonka kaikki muodot toteuttavat.

Koska JavaScriptillä ei ole käyttöliittymiä, aion näyttää sinulle, kuinka tämä saavutetaan TypeScriptissä, koska TypeScript mallii klassisen OOP: n JavaScriptille ja ero puhtaan JavaScript Prototypal OO: n kanssa.

käyttöliittymä ShapeInterface {
 alue (): numero
}
luokan Circle toteuttaa ShapeInterface {
 anna säde: luku = 0
 rakentaja (r: numero) {
  tämä.radius = r
 }
 
 julkinen alue (): numero {
  paluu MATH.PI * MATH.pow (this.radius, 2)
 }
}

Yllä olevassa esimerkissä osoitetaan, kuinka tämä saavutetaan TypeScriptissä, mutta konepellin alla TypeScript kokoaa koodin puhtaaseen JavaScriptiin ja käännetyssä koodissa sillä puuttuu käyttöliittymiä, koska JavaScriptillä ei ole sitä.

Joten miten voimme saavuttaa tämän rajapintojen puuttuessa?

Toiminnan koostumus pelastamiseksi!

Ensin luomme shapeInterface-tehdastoiminnon, kun puhumme rajapinnoista, shapeInterface on yhtä tiivistetty kuin käyttöliittymä, joka käyttää funktion koostumusta, koostumuksen syväksi selittämiseksi katso tämä upea video.

const shapeInterface = (tila) => ({
  tyyppi: 'shapeInterface',
  alue: () => osavaltio.alue (osavaltio)
})

Sitten toteutamme sen neliön tehdastoimintoomme.

const neliö = (pituus) => {
  const proto = {
    pituus,
    tyyppi: 'neliö',
    pinta-ala: (args) => Math.pow (args.length, 2)
  }
  const perusteet = shapeInterface (proto)
  const composite = Object.assign ({}, perusteet)
  palauta Object.assign (Object.create (komposiitti), {pituus})
}

Ja tulos neliön tehdastoiminnosta on seuraava:

const s = neliö (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
s.area ()
// lähtö
OBJ
 {pituus: 5}
PROTO
 {tyyppi: 'shapeInterface', alue: [toiminto: alue]}
25

AreaCalculator summa -menetelmässämme voimme tarkistaa, ovatko prodedoidut muodot tosiasiallisesti shapeInterface-tyyppejä, muuten heitämme poikkeuksen:

summa () {
  vakioalue = []
  varten (tämän muodon muoto) {
    if (Object.getPrototypeOf (shape) .type === 'shapeInterface') {
       area.push (shape.area ())
     } muuta {
       heitä uusi virhe ('tämä ei ole shapeInterface-objekti')
     }
   }
   palautusalue.vähennys ((v, c) => c + = v, 0)
}

ja taas, koska JavaScriptillä ei ole tukea sellaisille rajapinnoille kuin tyypilliset kielet, yllä oleva esimerkki osoittaa, kuinka voimme simuloida sitä, mutta enemmän kuin simuloimalla rajapintoja, olemme tekemisissä sulkimien ja toimintojen koostumuksen kanssa, jos et tiedä mitä ovat sulkeminen on tämä artikkeli selittää sen erittäin hyvin ja täydentämiseksi katso tämä video.

# Liskov-korvaamisperiaate

Olkoon q (x) ominaisuus, joka voidaan todistaa tyypin T x objekteista. Sitten q (y): n tulee olla todistettavissa tyypin S kohteille y, joissa S on T: n alatyyppi.

Kaikki tämä väittää, että jokaisen alaluokan / johdetun luokan tulisi olla korvattavissa perus- / vanhemmaluokallaan.

Toisin sanoen, niin yksinkertaista, että alaluokan tulisi ohittaa pääluokkamenetelmät tavalla, joka ei hajotta toimintoa asiakkaan näkökulmasta.

Hyödyntämällä edelleen areaCalculator-tehdastoimintoamme, sanotaan, että meillä on volumeCalculator-tehdastoiminto, joka laajentaa areaCalculator-tehdastoimintoa, ja tässä tapauksessa objektin laajentamiseksi rikkomatta ES6: n muutoksia teemme sen käyttämällä Object.assign () ja Object. getPrototypeOf ():

const volumeCalculator = (s) => {
  const proto = {
    tyyppi: 'volumeCalculator'
  }
  const areaCalProto = Object.getPrototypeOf (areaCalculator ())
  const peri = Object.assign ({}, areaCalProto, proto)
  return Object.assign (Object.create (periä), {muodot: s})
}

# Rajapinnan erotteluperiaate

Asiakasta ei tulisi koskaan pakottaa ottamaan käyttöön käyttöliittymää, jota se ei käytä, tai asiakkaita ei saa pakottaa olemaan riippuvaisia ​​menetelmistä, joita he eivät käytä.

Jatkamalla muotoilumallia, tiedämme, että meillä on myös vakiomuotoisia muotoja, joten koska haluaisimme myös laskea muodon volyymin, voimme lisätä uuden sopimuksen muotoonInterface:

const shapeInterface = (tila) => ({
  tyyppi: 'shapeInterface',
  alue: () => osavaltion alue (osavaltio),
  tilavuus: () => tila.volyymi (tila)
})

Kaikkien luomiemme muotojen on toteutettava tilavuusmenetelmä, mutta tiedämme, että neliöt ovat litteitä ja että niissä ei ole tilavuuksia, joten tämä käyttöliittymä pakottaisi neliön tehdastoiminnon toteuttamaan menetelmän, jota sillä ei ole mitään käyttöä.

Rajapinnan erotteluprosentti ei sano "tälle", sen sijaan voit luoda toisen solidShapeInterface-nimisen käyttöliittymän, jolla on volyymisopimus ja kiinteät muodot, kuten kuutiot jne., Voivat toteuttaa tämän käyttöliittymän.

const shapeInterface = (tila) => ({
  tyyppi: 'shapeInterface',
  alue: () => osavaltio.alue (osavaltio)
})
const solidShapeInterface = (tila) => ({
  tyyppi: 'solidShapeInterface',
  tilavuus: () => tila.volyymi (tila)
})
const cubo = (pituus) => {
  const proto = {
    pituus,
    tyyppi: 'Cubo',
    pinta-ala: (args) => Math.pow (args.length, 2),
    tilavuus: (args) => Math.pow (args.length, 3)
  }
  const perusteet = shapeInterface (proto)
  const kompleksi = solidShapeInterface (proto)
  const composite = Object.assign ({}, perusteet, kompleksi)
  palauta Object.assign (Object.create (komposiitti), {pituus})
}

Tämä on paljon parempi lähestymistapa, mutta varovaisuus, jota on noudatettava, on milloin laskettava muodon summa, sen sijaan, että käytetään muotoaInterface tai solidShapeInterface.

Voit luoda uuden käyttöliittymän, ehkä manageShapeInterface, ja toteuttaa sen sekä tasaisissa että kiinteissä muodoissa. Näin voit helposti nähdä, että sillä on yksi sovellusliittymä muotojen hallintaan, esimerkiksi:

const manageShapeInterface = (fn) => ({
  tyyppi: 'manageShapeInterface',
  laske: () => fn ()
})
const ympyrä = (säde) => {
  const proto = {
    säde,
    tyyppi: 'ympyrä',
    alue: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  const perusteet = shapeInterface (proto)
  const abstraccion = manageShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, perusteet, abstraktio)
  palauta Object.assign (Object.create (komposiitti), {säde})
}
const cubo = (pituus) => {
  const proto = {
    pituus,
    tyyppi: 'Cubo',
    pinta-ala: (args) => Math.pow (args.length, 2),
    tilavuus: (args) => Math.pow (args.length, 3)
  }
  const perusteet = shapeInterface (proto)
  const kompleksi = solidShapeInterface (proto)
  const abstraccion = manageShapeInterface (
    () => perusteet.ala () + kompleksi.volyymi ()
  )
  const composite = Object.assign ({}, perusteet, abstraktio)
  palauta Object.assign (Object.create (komposiitti), {pituus})
}

Kuten tähän asti voitte nähdä, se, mitä olemme tekeneet JavaScriptin rajapinnoille, ovat toimintojen koostumuksen tehdastoiminnot.

Ja täällä, manageShapeInterface, mitä teemme, on abstrakti jälleen laskentafunktiota, mitä teemme täällä ja muissa rajapinnoissa (jos voimme kutsua sitä rajapinnoiksi), käytämme “korkean tilauksen toimintoja” abstraktioiden aikaansaamiseksi.

Jos et tiedä mikä on korkeamman tilauksen toiminto, voit mennä tarkistamaan tämän videon.

# Riippuvuusprosessin periaate

Kokonaisuuksien on oltava riippuvaisia ​​abstraktioista, ei konkreettisista. Siinä todetaan, että korkean tason moduuli ei saa riippua matalan tason moduulista, vaan niiden on oltava riippuvaisia ​​abstraktioista.

Dynaamisena kielenä JavaScript ei vaadi abstraktioiden käyttöä irrottamisen helpottamiseksi. Siksi määräys, jonka mukaan abstraktien ei pitäisi riippua yksityiskohdista, ei ole erityisen merkityksellinen JavaScript-sovelluksissa. Lause, jonka mukaan korkean tason moduulien ei pitäisi riippua matalan tason moduuleista, on kuitenkin merkityksellinen.

Funktionaaliselta kannalta nämä säiliöt ja injektiokonseptit voidaan ratkaista yksinkertaisella korkeamman asteen toiminnolla tai reikä-keskellä -tyyppisellä kuviolla, jotka on rakennettu oikealle kielelle.

Kuinka riippuvuusinversio liittyy korkeamman asteen toimintoihin? on kysymys, joka esitetään stackExchangessa, jos haluat syvän selityksen.

Tämä saattaa kuulostaa paisuneelta, mutta se on todella helppo ymmärtää. Tämä periaate mahdollistaa tuotannosta irrotuksen.

Ja olemme tehneet sen aiemmin, antaa mahdollisuuden tarkistaa koodimme manageShapeInterface-sovelluksella ja miten suoritamme laskentamenetelmän.

const manageShapeInterface = (fn) => ({
  tyyppi: 'manageShapeInterface',
  laske: () => fn ()
})

Se, mitä manageShapeInterface-tehdastoiminto vastaanottaa argumenttina, on korkeamman asteen funktio, joka erottaa jokaisesta muodosta toiminnallisuuden tarvittavan logiikan suorittamiseksi lopulliseen laskelmaan pääsemiseksi. Katso, miten tämä tehdään muotoobjekteissa.

const neliö = (säde) => {
  // koodi
 
  const abstraccion = manageShapeInterface (() => basics.area ())
 
 // lisää koodia ...
}
const cubo = (pituus) => {
  // koodi
  const abstraccion = manageShapeInterface (
    () => perusteet.ala () + kompleksi.volyymi ()
  )
  // lisää koodia ...
}

Neliölle, jota meidän on laskettava, on vain muodon pinta-alan saaminen, ja kuutiolle tarvitsemme summaamalla pinta-ala tilavuudella, ja se on kaikki, mitä tarvitaan kytkemisen välttämiseksi ja abstraktin saamiseksi.

# Täydelliset koodiesimerkit

  • Voit hankkia sen täältä: solid.js

# Lisätietoja ja viitteet

  • Kiinteät OOD: n 5 ensimmäistä periaatetta
  • 5 periaatetta, jotka tekevät sinusta SOLID JavaScript -kehittäjän
  • SOLID JavaScript -sarja
  • SOLID-periaatteet käyttäen Typescriptiä

# Johtopäätös

"Jos otat SOLID-periaatteet äärimmäisyyteen, saavut jotain, joka tekee toiminnallisesta ohjelmoinnista näyttävän varsin houkuttelevalta" - Mark Seemann

JavaScript on moniparadigmainen ohjelmointikieli, ja voimme soveltaa siihen kiinteitä periaatteita, ja hienoa on se, että voimme yhdistää sen toiminnalliseen ohjelmoint paradigmaan ja saada parhaat puolet molemmista maailmoista.

Javascript on myös dynaaminen ohjelmointikieli ja erittäin monipuolinen
esittämäni on vain tapa saavuttaa nämä periaatteet JavaScriptillä, ne saattavat olla parempia vaihtoehtoja näiden periaatteiden saavuttamiseksi.

Toivottavasti nautit tästä viestistä, tutkin edelleen JavaScript-maailmaa, joten olen avoin vastaanottamaan palautetta tai kommentteja, ja jos pidit siitä, suosittele sitä ystävälle, jaa se tai lue se uudelleen.

Voit seurata minua #twitter @ cramirez_92
https://twitter.com/cramirez_92

Seuraavaan aikaan