Uansett hva vi anser som god kode, krever det alltid enkel kvalitet - koden må være vedlikeholdbar. Riktig innrykk, rene variabelnavn, 100% testdekning og mer kan bare fungere opp til et punkt. Enhver kode som ikke kan vedlikeholdes og som ikke kan tilpasses relativt enkelt til endrede krav, er kode som bare håper å være foreldet. Vi trenger kanskje ikke å skrive god kode når vi prøver å bygge en prototype, konseptbevis eller minimalt levedyktig produkt, men i alle andre tilfeller bør vi alltid skrive kode som kan vedlikeholdes. Dette er noe som bør betraktes som en grunnleggende kvalitet på programvareutvikling og design.
I denne artikkelen skal jeg diskutere hvordan prinsippet om enkeltansvar og noen teknikker som dreier seg om det kan gi koden din denne kvaliteten. Å skrive god kode er en kunst, men noen få prinsipper kan alltid bidra til å gi utviklingsarbeidet den retningen det trenger for å produsere sterk, vedlikeholdbar programvare.
Nesten hver bok om noe nytt MVC-rammeverk (MVP, MVVM eller annen M **) er full av eksempler på dårlig kode. Disse eksemplene prøver å vise hva rammeverket har å tilby. Men de ender også med å gi dårlige råd til nybegynnere. Eksempler som “la oss si at vi har denne ORM X for modellene våre, malmotoren Y for våre synspunkter slik at vi vil ha kontrollere til å håndtere alt ”de oppnår ikke annet enn gigantiske kontrollere. Imidlertid, til forsvar for disse bøkene, er eksemplene ment å demonstrere hvor enkelt du kan bruke rammeverket. De er ikke ment å undervise i programvaredesign. Men lesere som følger disse eksemplene, innser bare etter år hvor kontraproduktivt det er å ha monolittiske biter av kode i prosjektet ditt.
Modeller er hjertet i søknaden din. Hvis du har separate modeller fra resten av applikasjonslogikken, blir vedlikehold mye enklere, uansett hvor komplisert applikasjonen kan bli. Selv for kompliserte applikasjoner kan god implementering av modellen resultere i ekstremt uttrykksfull kode. Og for å oppnå dette må du begynne med å sørge for at modellene dine bare gjør det de er ment å gjøre, og ikke bekymre deg for hva applikasjonen bygget rundt dem gjør. Dessuten håndterer det ikke hva det underliggende lagringslaget er - avhenger applikasjonen av en SQL-database eller lagrer den alt i tekstfiler?
Når vi fortsetter denne artikkelen, vil du innse at god kode handler om separasjon av bekymring.
Du har sikkert hørt om prinsippene FAST : enkeltansvar, åpen stengt, liskov-erstatning, grensesnitt-segregering og avhengighetsinversjon. Den første bokstaven, S, representerer prinsippet om enkelt ansvar ( SEGEL ) og dens betydning kan ikke overvurderes. Jeg vil til og med si at det er en nødvendig og viktig forutsetning for god kode. Faktisk, i en hvilken som helst kode som er dårlig skrevet, kan du alltid finne en klasse som har mer enn ett ansvar - form1.cs eller index.php, som inneholder noen få tusen linjer med kode, er ikke rart, og alle av oss har sannsynligvis sett. eller ferdig.
La oss se på et eksempel i C # (ASP.NET MVC and Entity framework). Selv om du ikke er en C # utvikler , med litt OOP-opplevelse kan du enkelt komme deg videre.
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) }
Dette er en klasse OrderController vanlig og metoden vises Skape . I kontrollere som dette ser jeg ofte tilfeller der selve klassen Rekkefølge den brukes som en forespørselsparameter. Men jeg foretrekker å bruke spesielle forespørselstimer. Selvfølgelig nok en gang, SEGEL !
Legg merke til i kodebiten som vises ovenfor hvordan kontrolleren vet for mye om 'å legge inn en ordre', inkludert, men ikke begrenset til, lagring av objektet Rekkefølge , send e-post osv. Dette er rett og slett for mye arbeid for en enkelt klasse. For hver liten endring må utvikleren endre koden for hele kontrolleren. Og bare hvis en annen kontroller også må opprette kommandoer, bruker utviklere oftest til å kopiere og lime inn koden. Kontrollere skal bare kontrollere den samlede prosessen og ikke være vert for hver bit av prosesslogikken.
Men i dag er dagen vi slutter å skrive disse gigantiske driverne!
La oss først trekke ut all forretningslogikken fra kontrolleren og flytte den til en klasse OrderService :
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'); }
Når dette er gjort, gjør kontrolleren nå bare det den skal: kontrollere prosessen. Han kjenner bare utsikten, klassene OrderService Y Bestill forespørsel - det minste settet med informasjon som er nødvendig for at du skal gjøre jobben din, som er styring av forespørsler og sending av svar.
Dermed vil sjeldne kontrollerkoder endres. Andre komponenter, som visninger, forespørsel om objekter og tjenester, kan endres ettersom de er knyttet til forretningskrav, men ikke drivere.
Dette er det SEGEL , og det er mange teknikker for å skrive kode som oppfyller dette prinsippet. Et eksempel på dette er avhengighetsinjeksjon (noe som også er nyttig for skriv testbar kode ).
Det er vanskelig å forestille seg et stort prosjekt basert på prinsippet om enkelt ansvar uten avhengighetsinjeksjon. La oss ta en titt på klassen vår igjen OrderService :
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(); } }
Denne koden fungerer, men er ikke veldig ideell. For å forstå hvordan create class-metoden fungerer OrderService , blir tvunget til å forstå kompleksiteten til SMTP . Og igjen er kopiering og liming den eneste måten å gjenskape denne bruken av SMTP hvor nødvendig. Men med litt refactoring kan det endres:
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 } }
Bedre! Men klassen OrderService du vet fortsatt mye om å sende e-post. Du trenger nøyaktig klassen SmtpMailer å sende en e-post. Hva om vi vil endre det senere? Hva om vi vil skrive ut innholdet i e-posten som sendes til en spesiell loggfil i stedet for å sende den i vårt utviklingsmiljø? Hva om vi vil teste klassen vår OrderService ? La oss fortsette med refactoring ved å lage et grensesnitt IMailer :
public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer vil implementere dette grensesnittet. Applikasjonen vår vil også bruke en IoC-container, og vi kan konfigurere den slik at IMailer implementeres av klassen SmtpMailer . OrderService kan endres som følger:
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(, , ); } }
Nå går vi fremover! Jeg benyttet anledningen til å gjøre en ny endring. OrderService er nå basert på grensesnittet IOrderRepository for å samhandle med komponenten som lagrer alle våre bestillinger. Du bryr deg ikke lenger om hvordan grensesnittet er implementert eller hvilken lagringsteknologi som mater det. Nå klasse OrderService du har bare kode som omhandler forretningslogikken til ordrene.
På denne måten, hvis en tester finner noe som ikke fungerer som den skal når du sender e-post, vet utvikleren nøyaktig hvor han skal lete: klasse SmtpMailer . Hvis noe var galt med rabattene, vet utvikleren igjen hvor han skal se: klassekoden OrderService (eller i tilfelle du har godtatt SEGEL fra hjertet så kan det være DiscountService ).
Imidlertid liker jeg fremdeles ikke OrderService.Create-metoden:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(, , ); }
Å sende en e-post er faktisk ikke en del av strømmen for oppretting av hovedordre. Selv om applikasjonen ikke sender e-post, fortsetter bestillingen å bli opprettet riktig. Tenk deg også en situasjon der du må legge til et nytt alternativ i brukerinnstillingsområdet som lar dem velge å motta en e-post etter å ha bestilt en bestilling. Å innlemme dette i klassen vår OrderService , må vi innføre en avhengighet: IUserParametersService . Legg til stedet, så har du allerede en ny avhengighet, IT-oversetter (for å produsere riktige e-postmeldinger på språket du bruker). Flere av disse handlingene er unødvendige, spesielt ideen om å legge til så mange avhengigheter og ende opp med en konstruktør som ikke passer til skjermen. Jeg fant en godt eksempel av dette i Magento-kodebasen (a CMS e-handelsprogramvare skrevet i PHP) i en klasse som har 32 avhengigheter!
Noen ganger er det vanskelig å forestille seg hvordan man skal skille denne logikken, og Magento-klassen er sannsynligvis et offer for en av disse tilfellene. Derfor liker jeg den “begivenhetsdrevne” måten:
namespace .Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Hver gang en ordre opprettes, i stedet for å sende en e-post direkte fra klassen OrderService , klassen for spesielle begivenheter opprettes Bestill opprettet og en hendelse genereres. Et eller annet sted i applikasjonshendelsesbehandlerne blir den konfigurert. En av dem vil sende en e-post til kunden.
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(...); } } }
Klasse Bestill opprettet er merket som Serialiserbar forresten. Vi kan håndtere denne hendelsen ut av boksen eller lagre den seriell på rad (Redis, ActiveMQ eller noe annet) og behandle den i en egen prosess / tråd til den som håndterer nettforespørsler. I denne artikkelen , forklarer forfatteren i detalj hva som er hendelsesdrevet arkitektur (Ikke vær oppmerksom på forretningslogikken innen OrderController ).
Noen vil kanskje hevde at det nå er vanskelig å forstå hva som skjer når du oppretter ordren. Men det er ikke sant i det hele tatt. Hvis du føler deg slik, er det bare å dra nytte av funksjonaliteten til din HER . Når du finner alle bruksområdene i klassen Bestill opprettet på HER , kan vi se alle handlingene knyttet til hendelsen.
Men når skal jeg bruke avhengighetsinjeksjon, og når skal jeg bruke en hendelsesbasert tilnærming? Det er ikke alltid lett å svare på dette spørsmålet, men en enkel regel som kan hjelpe deg er å bruke Dependency Injection for alle dine hovedaktiviteter i applikasjonen og den hendelsesdrevne tilnærmingen for alle sekundære handlinger. Bruk for eksempel Dependency Injection med ting som, opprett en ordre i klassen OrderService med IOrderRepository , i tillegg til å delegere sending av e-post, som ikke er en viktig del av hovedordrenes opprettelsesflyt, til noen hendelsesbehandler.
Vi starter med en veldig viktig kontroller, bare en klasse, og slutter med en omfattende samling av klasser. Fordelene med disse endringene fremgår noe av eksemplene. Imidlertid er det fortsatt mange måter å forbedre disse eksemplene på. For eksempel metoden OrderService.Create kan flyttes til en egen klasse: OrderCreator . Siden ordreopprettelse er en uavhengig enhet for forretningslogikk, som følger prinsippet om enkelt ansvar, er det naturlig at den har sin egen klasse med sitt eget sett av avhengigheter. På samme måte kan sletting og kansellering av en ordre implementeres i sine egne klasser.
Da jeg skrev svært sammenkoblet kode, noe som ligner på det første eksemplet i denne artikkelen, kan enhver endring, uansett hvor liten, i kravene føre til mange endringer i andre deler av koden. SEGEL hjelper utviklere med å skrive kode som er 'uparret', der hver klasse har sitt eget arbeid. Hvis noen spesifikasjoner av denne jobben endres, gjør utvikleren bare endringer i den aktuelle klassen. Endringen er mindre sannsynlig å bryte hele søknaden, ettersom andre klasser burde gjøre jobben sin som før, med mindre de først var ødelagte.
Å utvikle kode ved hjelp av disse teknikkene og følge prinsippet om enkelt ansvar kan virke som en skremmende oppgave, men innsatsen vil lønne seg når prosjektet vokser og utviklingen fortsetter.