Huolimatta siitä, mitä pidämme hyvänä koodina, se vaatii aina yksinkertaista laatua - koodin on oltava ylläpidettävä. Oikea sisennys, puhtaat muuttujien nimet, 100-prosenttinen testipeitto ja paljon muuta voivat toimia vain pisteeseen saakka. Kaikki koodit, joita ei voida ylläpitää ja joita ei voida mukauttaa suhteellisen helposti muuttuviin vaatimuksiin, ovat vain vanhentuneita koodeja. Meidän ei tarvitse kirjoittaa suurta koodia, kun yritämme rakentaa prototyyppiä, konseptitodistusta tai vähiten elinkelpoista tuotetta, mutta kaikissa muissa tapauksissa meidän on aina kirjoitettava ylläpidettävä koodi. Tätä on pidettävä ohjelmistotuotannon ja suunnittelun perustavanlaatuisena ominaisuutena.
Tässä artikkelissa aion keskustella siitä, kuinka yhden vastuun periaate ja jotkut sen ympärillä pyörivät tekniikat voivat antaa koodillesi tämän laadun. Hyvän koodin kirjoittaminen on taidetta, mutta muutama periaate voi aina auttaa kehitystyötäsi suunnittelemaan vahvan, ylläpidettävän ohjelmiston tuottamiseen.
Lähes jokainen kirja uudesta MVC-kehyksestä (MVP, MVVM tai muu M **) on täynnä huonoja koodiesimerkkejä. Nämä esimerkit yrittävät osoittaa, mitä puitteet tarjoavat. Mutta he päätyvät myös antamaan huonoja neuvoja aloittelijoille. Esimerkkejä, kuten 'sanotaan, että meillä on malli ORM X, mallimoottori Y näkemyksemme ja niin meillä on ohjaimet käsittelemään kaikkea ”he eivät saavuta muuta kuin jättimäisiä ohjaimia. Näiden kirjojen puolustamiseksi esimerkkien on kuitenkin tarkoitus osoittaa, kuinka helppoa voit käyttää niiden kehyksiä. Niitä ei ole tarkoitettu opettamaan ohjelmistosuunnittelua. Mutta lukijat, jotka seuraavat näitä esimerkkejä, ymmärtävät vasta vuosien kuluttua, kuinka haitallista on, että projektissasi on monoliittisia koodinpaloja.
Mallit ovat sovelluksesi ydin. Jos sinulla on erilliset mallit muusta sovelluslogiikasta, ylläpito on paljon helpompaa, riippumatta siitä kuinka monimutkainen sovellus voi olla. Jopa monimutkaisissa sovelluksissa mallin hyvä toteutus voi johtaa erittäin ilmeikkään koodiin. Ja tämän saavuttamiseksi sinun on aloitettava varmistamalla, että mallisi tekevät vain sen, mitä on tarkoitus tehdä, ja älä välitä siitä, mitä niiden ympärille rakennettu sovellus tekee. Se ei myöskään käsittele sitä, mikä taustalla oleva tallennuskerros on - riippuuko sovelluksesi SQL-tietokannasta vai tallentaakö se kaiken tekstitiedostoihin?
Jatkamalla tätä artikkelia huomaat, että loistava koodi on kyse huolen erottamisesta.
Olet todennäköisesti kuullut periaatteista KIINTEÄ : yksittäinen vastuu, avoin ja suljettu, liskov-korvaaminen, rajapintojen erottelu ja riippuvuuden vaihtaminen Ensimmäinen kirjain S edustaa yhtenäisen vastuun periaatetta ( SIRPPI ) ja sen merkitystä ei voida yliarvioida. Sanoisin jopa, että se on välttämätön ja tärkeä edellytys hyvälle koodille. Itse asiassa mistä tahansa huonosti kirjoitetusta koodista löytyy aina luokka, jolla on useampi kuin yksi vastuu - form1.cs tai index.php, joka sisältää muutaman tuhannen koodirivin, ei ole outoa, ja me kaikki todennäköisesti olemme nähneet. tai tehty.
Katsotaanpa esimerkkiä C #: ssä (ASP.NET MVC ja Entity Framework). Vaikka et olisikaan C # -kehittäjä , pienellä OOP-kokemuksella voit edetä helposti.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction('Index'); } ... (many more methods like Create here) }
Tämä on luokka OrderController tavallinen ja sen menetelmä on esitetty Luoda . Tällaisissa ohjaimissa näen usein tapauksia, joissa luokka itse Tilaus sitä käytetään pyyntöparametrina. Mutta mieluummin käytän erityispyyntöluokkia. Tietenkin jälleen kerran, SIRPPI !
Huomaa yllä olevassa koodinpätkässä, kuinka ohjain tietää liikaa tilauksen tekemisestä, mukaan lukien muun muassa objektin tallentaminen Tilaus , lähettää sähköposteja jne. Tämä on yksinkertaisesti liian paljon työtä yhdelle luokalle. Jokaisesta pienestä muutoksesta kehittäjän on vaihdettava koko ohjaimen koodi. Ja siltä varalta, että toisen ohjaimen on myös luotava komentoja, kehittäjät turvautuvat useimmiten koodin kopioimiseen ja liittämiseen. Ohjainten tulisi hallita vain kokonaisprosessia, eikä niiden tulisi sisällyttää prosessilogiikkaa jokaiseen osaan.
Mutta tänään on päivä, jolloin lopetamme näiden jättimäisten kuljettajien kirjoittamisen!
Otetaan ensin kaikki liiketoimintalogiikat ohjaimesta ja siirretään se luokkaan Tilaa palvelu :
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction('Index'); }
Kun tämä on tehty, ohjain tekee nyt vain sen, mitä sen pitäisi tehdä: ohjaa prosessia. Hän tietää vain näkemykset, luokat Tilaa palvelu Y OrderRequest - työsi suorittamiseen tarvittavat vähimmäistiedot, jotka ovat pyyntöjen hallinta ja vastausten lähettäminen.
Siten harvoin ohjainkoodia muutetaan. Muut komponentit, kuten näkymät, pyyntöobjektit ja palvelut, voivat muuttua, kun ne on sidottu liiketoiminnan vaatimuksiin, mutta eivät ajureita.
Tämä se on SIRPPI , ja koodin kirjoittamiseen on monia tekniikoita, jotka täyttävät tämän periaatteen. Esimerkki tästä on riippuvuusinjektio (mihin on myös hyötyä kirjoita testattava koodi ).
On vaikea kuvitella suurta projektia, joka perustuu yhtenäisen vastuun periaatteeseen ilman riippuvuuden injektiota. Katsotaanpa taas luokkamme Tilaa palvelu :
public class OrderService { public void Create(...) { // Creating the order(and let’s forget about reserving here, it’s not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Tämä koodi toimii, mutta ei kovin ihanteellinen. Ymmärtää miten luodaan luokan menetelmä toimii Tilaa palvelu on pakko ymmärtää SMTP . Ja jälleen, kopioi ja liitä on ainoa tapa jäljitellä tätä käyttöä SMTP kun tarpeen. Mutta pienellä refaktorilla, joka voi muuttua:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Paremmin! Mutta luokka Tilaa palvelu tiedät edelleen paljon sähköpostien lähettämisestä. Tarvitset juuri luokan SmtpMailer lähettää sähköpostia. Entä jos haluamme muuttaa sitä myöhemmin? Entä jos haluamme tulostaa erityiseen lokitiedostoon lähetetyn sähköpostin sisällön sen sijaan, että lähetämme sen kehitysympäristöömme? Entä jos haluamme testata luokkaa Tilaa palvelu ? Jatka uudelleenkorjaamista luomalla käyttöliittymä IMailer :
public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer toteuttaa tämän käyttöliittymän. Lisäksi sovelluksemme käyttää IoC-säilöä ja voimme määrittää sen niin IMailer luokka toteuttaa SmtpMailer . Tilaa palvelu voidaan muuttaa seuraavasti:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(, , ); } }
Nyt siirrymme eteenpäin! Käytin tätä tilaisuutta tehdäksesi uuden muutoksen. Tilaa palvelu luottaa nyt käyttöliittymään IOrderRepository vuorovaikutuksessa komponentin kanssa, joka tallentaa kaikki tilauksemme. Et enää välitä siitä, miten kyseinen käyttöliittymä toteutetaan tai mikä tallennustekniikka ruokkii sitä. Nyt luokka Tilaa palvelu sinulla on vain koodi, joka käsittelee tilausten liiketoimintalogiikkaa.
Tällä tavalla, jos testaaja löytää jotain vikaa sähköpostiviestien lähettämisessä, kehittäjä tietää tarkalleen, mistä etsiä: luokka SmtpMailer . Jos alennuksissa oli jotain vikaa, kehittäjä tietää jälleen mistä etsiä: luokan koodi Tilaa palvelu (tai jos olet hyväksynyt SIRPPI sydämestä niin se voi olla DiscountService ).
En kuitenkaan vieläkään pidä OrderService.Create-menetelmää:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(, , ); }
Sähköpostin lähettäminen ei itse asiassa ole osa tilauksen luomisen päävirtaa. Vaikka sovellus ei lähettäisi sähköpostia, tilaus luodaan edelleen oikein. Kuvittele myös tilanne, jossa sinun on lisättävä uusi asetus käyttäjän asetusalueelle, jonka avulla he voivat kieltäytyä sähköpostin vastaanottamisesta tilauksen tekemisen jälkeen. Sisällyttää tämä luokkaamme Tilaa palvelu , meidän on otettava käyttöön riippuvuus: IUserParametersService . Lisää sijainti ja sinulla on jo uusi riippuvuus, IT-kääntäjä (tuottaa oikeat sähköpostit käyttäjän valitsemalla kielellä). Useat näistä toimista ovat tarpeettomia, varsinkin ajatus lisätä niin paljon riippuvuuksia ja päätyä rakentajaan, joka ei sovi näyttöön. Löysin a hieno esimerkki tästä Magenton koodikannassa (a CMS PHP-muodossa kirjoitettu verkkokauppaohjelmisto) luokassa, jolla on 32 riippuvuutta!
Joskus on vaikea kuvitella, miten tämä logiikka erotetaan, ja Magento-luokka on todennäköisesti yhden tällaisen tapauksen uhri. Siksi pidän tapahtumavetoisesta tavasta:
namespace .Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Joka kerta, kun tilaus luodaan, sen sijaan, että lähetettäisiin sähköpostia suoraan luokalta Tilaa palvelu , erityistapahtumaluokka luodaan Tilaus luotu ja tapahtuma syntyy. Jossain sovellustapahtumien käsittelijöissä se määritetään. Yksi heistä lähettää sähköpostin asiakkaalle.
namespace .EventHandlers { public class OrderCreatedEmailSender : IEventHandler { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
Luokka Tilaus luotu on merkitty Sarjattavissa muuten. Pystymme käsittelemään tämän tapahtuman laatikosta tai tallentamaan sen sarjana sarjaan (Redis, ActiveMQ tai mikä tahansa muu) ja käsittelemään sen erillisenä prosessina / ketjuna verkkopyyntöjä käsitteleväksi. Sisään Tämä artikkeli , kirjoittaja selittää yksityiskohtaisesti, mikä on tapahtumapohjainen arkkitehtuuri (Kiinnitä huomiota sisällä olevaan liiketoimintalogiikkaan OrderController ).
Jotkut saattavat väittää, että nyt on vaikea ymmärtää, mitä tapahtuu tilausta luodessa. Mutta se ei ole lainkaan totta. Jos sinusta tuntuu tällä tavoin, hyödynnä vain omasi TÄSSÄ . Kun löydät luokan kaikki käyttötarkoitukset Tilaus luotu klo TÄSSÄ , voimme nähdä kaikki tapahtumaan liittyvät toiminnot.
Mutta milloin minun pitäisi käyttää riippuvuusinjektiota ja milloin tapahtumapohjaista lähestymistapaa? Tähän kysymykseen vastaaminen ei ole aina helppoa, mutta yksinkertainen sääntö, joka voi auttaa sinua, on käyttää riippuvuusinjektiota kaikkiin sovelluksen päätoimintoihin ja tapahtumavetoista lähestymistapaa kaikkiin toissijaisiin toimiin. Käytä esimerkiksi riippuvuusinjektiota esimerkiksi luodaksesi tilauksen luokan sisällä Tilaa palvelu kanssa IOrderRepository , samoin kuin sähköpostiviestien lähettämisen delegoiminen, mikä ei ole olennainen osa tilauksen luomisen päävirtaa, jollekin tapahtumankäsittelijälle.
Aloitamme erittäin tärkeällä ohjaimella, vain yhdellä luokalla, ja lopetamme yksityiskohtaisella luokkakokoelmalla. Näiden muutosten edut ilmenevät jonkin verran esimerkeistä. Näiden esimerkkien parantamiseksi on kuitenkin edelleen monia tapoja. Esimerkiksi menetelmä OrderService.Create voidaan siirtää omaan luokkaan: OrderCreator . Koska tilausten luominen on itsenäinen liiketoimintalogiikan yksikkö, joka noudattaa yhden vastuun periaatetta, on luonnollista, että sillä on oma luokka ja omat riippuvuussarjansa. Samalla tavalla tilauksen poistaminen ja peruuttaminen voidaan toteuttaa omissa luokissaan.
Kun kirjoitin erittäin paritettua koodia, jotain samanlaista kuin tämän artikkelin ensimmäinen esimerkki, kaikki muutokset, vaikka pienetkin, vaatimuksissa voivat johtaa moniin muutoksiin koodin muissa osissa. SIRPPI auttaa kehittäjiä kirjoittamaan parittoman koodin, jossa jokaisella luokalla on oma työnsä. Jos jokin tämän työn määritys muuttuu, kehittäjä tekee muutoksia vain kyseiseen luokkaan. Muutos ei todennäköisesti hajota koko sovellusta, koska muiden luokkien tulisi tehdä työnsä kuten aikaisemmin, ellei niitä alun perin rikki.
Koodin kehittäminen näitä tekniikoita käyttäen ja yhden vastuun periaatteen noudattaminen voi tuntua pelottavalta tehtävältä, mutta ponnistelut tuottavat tulosta, kun projekti kasvaa ja kehitys jatkuu.