Den här nybörjarvänliga artikeln täcker en annan lukningsrunda och refactorings du borde bekanta dig med tidigt i din karriär. Vi täcker fallutlåtanden, polymorfism, nullobjekt och dataklasser.
Den här kan också kallas "checklista lukt" eller något. Fallutlåtanden är en lukt eftersom de orsakar dubbelarbete - de är ofta oskadliga också. De kan också leda till onödigt stora klasser, eftersom alla dessa metoder som svarar på de olika (och potentiellt växande) fallscenarierna ofta hamnar i samma klass - som då har alla slags blandade ansvarsområden. Det är inte ett sällsynt fall att du har många privata metoder som skulle vara bättre i egna klasser.
Ett stort problem med fallutlåtanden uppstår om du vill expandera dem. Då måste du ändra den speciella metoden - eventuellt om och om igen. Och inte bara där, för ofta har de tvillingar upprepas överallt där man nu behöver en uppdatering också. Ett bra sätt att odla buggar säkert. Som ni kanske kommer ihåg vill vi vara öppna för förlängning men stängda för modifiering. Här är modifiering oundviklig och bara en fråga om tid.
Du gör det svårare för dig själv att extrahera och återanvända kod, plus det är en tippande rotbomb. Ofta syns koden beror på sådana fallutlåtanden-som sedan duplicerar lukten och öppnar porten bred öppen för en runda av hagelgevärsoperation i framtiden. aj! Också fråge ett objekt innan du hittar rätt metod att utföra är en otäck kränkning av principen "Tell-Don't-Ask".
Det finns en bra teknik för att hantera behovet av fallutlåtanden. Fancy ord inkommande! polymorfism. Detta gör att du kan skapa samma gränssnitt för olika objekt och använda vilket objekt som behövs för olika scenarier. Du kan bara byta in det lämpliga objektet och det anpassar sig till dina behov eftersom det har samma metoder på det. Deras beteende under dessa metoder är annorlunda, men så länge objekten svarar på samma gränssnitt bryr sig inte Ruby om. Till exempel, VegetarianDish.new.order
och VeganDish.new.order
beter sig olika, men båda svarar på #ordning
på samma sätt. Du vill bara beställa och svara inte massor av frågor som om du äter ägg eller inte.
Polymorfismen implementeras genom att extrahera en klass för fallet för uppsägningen och flytta den logiken till en ny metod i den klassen. Du fortsätter att göra det för varje ben i det villkorade trädet och ge dem samma metodnamn. På det sättet inkapslar du det beteendet till ett föremål som passar bäst för att göra den typen av beslut och har ingen anledning att förändras ytterligare. Se, så kan du undvika alla dessa snygga frågor på ett objekt - du berätta bara vad det ska göra. När behovet uppstår för mer villkorliga fall skapar du bara en annan klass som tar hand om det enda ansvaret med samma metodnamn.
Falluttalande logik
klassen Operation def price case @mission_tpe när: counter_intelligence @standard_fee + @counter_intelligence_fee när: terrorism @standard_fee + @terrorism_fee när: hämnd @standard_fee + @revenge_fee när: extortion @standard_fee + @extortion_fee slutet slutet end_intel_op = Operation.new (mission_type: : counter_intelligence) counter_intel_op.price terror_op = Operation.new (mission_type:: terrorism) terror_op.price revenge_op = Operation.new (mission_type:: revenge) revenge_op.price extortion_op = Operation.new (mission_type:: extortion) extortion_op.price
I vårt exempel har vi en Drift
klass som behöver fråga om sin mission_type
innan det kan berätta priset. Det är lätt att se det här pris
Metoden väntar bara på att förändras när en ny typ av operation läggs till. När du vill visa det enligt din åsikt måste du också använda den ändringen där. (FYI, för synpunkter kan du använda polymorfa partiklar i Rails för att undvika att dessa fall uttalanden blästras över hela din syn.)
Polymorfa klasser
klass CounterIntelligenceOperation def price @standard_fee + @counter_intelligence_fee slutändsklass TerrorismOperation def price @standard_fee + @terrorism_fee slutändsklass RevengeOperation def price @standard_fee + @revenge_fee slutändsklass ExtortionOperation def price @standard_fee + @extortion_fee slutänden counter_intel_op = CounterIntelligenceOperation.new counter_intel_op .price terror_op = CounterIntelligenceOperation.new terror_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price
Så i stället för att gå ner i kaninhålet skapade vi en massa driftklasser som har egen kunskap om hur mycket deras avgifter bidrar till slutkursen. Vi kan bara berätta för dem att ge oss sitt pris. Du behöver inte alltid bli av med originalet (Drift
) klass-bara när du upptäcker att du har extraherat all den kunskap som den hade.
Jag tror att logiken bakom fallutlåtanden är oundviklig. Scenarier där du måste gå igenom någon form av checklista innan du hittar det föremål eller beteende som får jobbet är helt för vanligt. Frågan är helt enkelt hur hanteras de bäst. Jag är för att inte upprepa dem och använda verktygen objektorienterad programmering ger mig för att utforma diskreta klasser som enkelt kan bytas ut via gränssnittet.
Kollar på noll överallt är en speciell typ av fall uttalande lukt. Att fråga ett objekt om noll är ofta ett slags doldt fall uttalande. Att hantera nil villkorligt kan ha formen av object.nil?
, object.present?
, object.try
, och sedan någon form av åtgärd i fallet noll
visas på din fest.
En annan mer lurig fråga är att fråga ett objekt om sin truthiness-ergo om det existerar eller om det är noll-och sedan vidta några åtgärder. Ser ofarligt ut, men det är bara en förklädnad. Låt dig inte lura: ternära operatörer eller ||
Operatörer faller naturligtvis också i denna kategori. Sätt på annat sätt, conditionals visas inte bara tydligt identifierbara som såvida inte
, om annat
eller fall
uttalanden. De har subtilare sätt att krascha din fest. Fråga inte objekt till deras nil-ness, men berätta för dem om objektet för din lyckliga väg är frånvarande att ett nollobjekt nu är ansvarigt för att svara på dina meddelanden.
Null föremål är vanliga klasser. Det finns inget speciellt om dem - bara ett "fint namn". Du extraherar viss villkorlig logik relaterad till noll
och sedan hanterar du polymorfiskt. Du innehåller det beteendet, kontrollerar flödet av din app via dessa klasser och har också objekt som är öppna för andra tillägg som passar dem. Tänk på hur a Rättegång
(NullSubscription
) klassen kunde växa över tiden. Inte bara är det mer DRY och yadda-yadda-yadda, det är också mycket mer beskrivande och stabilt.
En stor fördel med att använda null objekt är att saker inte kan spränga lika lätt. Dessa objekt svarar på samma meddelanden som objekten emulerade - du behöver inte alltid kopiera hela API-filen till null objekt förstås - vilket ger din app lite anledning att bli galen. Observera dock att nollobjekt inkapslar villkorlig logik utan att helt ta bort den. Du hittar bara ett bättre hem för det.
Eftersom du har mycket nylrelaterad åtgärd i din app är det ganska smittsam och ohälsosam för din app tycker jag om att tänka på nollobjekt som "Nil Containment Pattern" (vänligen inte stämma på mig!). Varför smittsam? För om du passerar noll
runt, någon annan plats i din hierarki, är en annan metod förr eller senare också tvungen att fråga om noll
ligger i stan, vilket leder till en annan omgång av att vidta motåtgärder för att hantera ett sådant fall.
Sätt annorlunda, det är inte coolt att hänga med eftersom det ber om att dess närvaro blir smittsam. Fråga föremål för noll
är sannolikt alltid ett symptom på dålig design-inget brott och känner sig inte dåligt! -Vi har alla varit där. Jag vill inte gå "willy-nilly" om hur ovänliga nul kan vara, men några saker måste nämnas:
Sammantaget kommer scenariot att ett objekt saknas något kommer upp mycket ofta. Ett ofta citerat exempel är en app som har en registrerad Användare
och a NilUser
. Men eftersom användare som inte existerar är ett dumt koncept om personen personligen tittar på din app, är det förmodligen svalare att ha en Gäst
som inte har anmält sig än. En saknad prenumeration kan vara en Rättegång
, en nollavgift kan vara a Gratisgrunka
, och så vidare.
Att namngivna dina nollobjekt är ibland uppenbart och lätt, ibland super hårt. Men försök att hålla sig borta från att namnge alla nollobjekt med en ledande "Null" eller "No". Du kan bättre! Att ge lite kontext går långt, tror jag. Välj ett namn som är mer specifikt och meningsfullt, något som speglar det faktiska användningsfallet. På det sättet kommunicerar du tydligare med andra teammedlemmar och till ditt framtida självklart självklart.
Det finns ett par sätt att implementera denna teknik. Jag ska visa dig en som jag tycker är enkel och nybörjarevänlig. Jag hoppas det är en bra grund för att förstå mer involverade tillvägagångssätt. När du upprepade gånger stöter på något slags villkorligt som handlar om noll
, du vet att det är dags att helt enkelt skapa en ny klass och flytta det beteendet över. Därefter lät du den ursprungliga klassen veta att den här nya spelaren nu handlar med ingenting.
I exemplet nedan kan du se att Spöke
klassen frågar lite för mycket om noll
och clutters upp koden i onödan. Det vill se till att vi har en evil_operation
innan den bestämmer sig för att debitera. Kan du se överträdelsen av "Tell-Don't-Ask"?
En annan problematisk del är varför Specter skulle behöva bry sig om genomförandet av ett nollpris. De Prova
Metoden frågar också snyggt om evil_operation
har en pris
att hantera ingenting genom eller
(||
) påstående. evil_operation.present?
gör samma misstag. Vi kan förenkla detta:
klass Specter inkluderar ActiveModel :: Modell attr_accessor: credit_card,: evil_operation def charge såvida inte evil_operation.nil? evil_operation.charge (credit_card) slutänden def has_discount? evil_operation.present? && evil_operation.has_discount? slutet av priset evil_operation.try (: price) || 0 slutändsklass EvilOperation inkluderar ActiveModel :: Modell attr_accessor: rabatt,: pris def has_discount? rabatt slutavgift (kreditkort) credit_card.charge (pris) slutändan
class NoOperation def charge (kreditkort) "Ingen ond operation registrerad" slut def has_discount? felaktigt slutpris 0 slutändsklass Spektra inkluderar ActiveModel :: Modell attr_accessor: credit_card,: evil_operation def laddning evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? avsluta def price evil_operation.price slut privat def evil_operation @evil_operation || NoOperation.new slutändsklass EvilOperation inkluderar ActiveModel :: Modell attr_accessor: rabatt,: pris def has_discount? rabatt slutavgift (kreditkort) credit_card.charge (pris) slutändan
Jag antar att det här exemplet är tillräckligt enkelt för att se omedelbart hur eleganta nollobjekt kan vara. I vårt fall har vi en NoOperation
nollklass som vet hur man ska hantera en obefintlig ond operation via:
0
mängder.Vi skapade den här nya klassen som handlar om ingenting, ett objekt som hanterar frånvaron av grejer och byter in i den gamla klassen om noll
dyker upp. API: n är nyckeln här, för om den matchar den i den ursprungliga klassen kan du byta in nollobjektet med de returvärden vi behöver. Det är precis vad vi gjorde i den privata metoden Spöke # evil_operation
. Om vi har evil_operation
objekt, vi behöver vi använda det Om inte, använder vi vår noll
kameleont som vet hur man hanterar dessa meddelanden, så evil_operation
kommer aldrig tillbaka noll längre. Anka typ som bäst.
Vår problematiska villkorliga logik är nu inslaget på ett ställe, och vårt nollobjekt ansvarar för beteendet Spöke
letar efter. TORR! Vi har byggt upp samma gränssnitt från det ursprungliga objektet som noll inte kunde hantera. Kom ihåg, inget gör ett dåligt jobb vid mottagandet av meddelanden-ingen hemma, alltid! Från och med nu berättar det bara om vad man ska göra utan att fråga dem för deras "tillstånd". Vad som också är coolt är att det inte var nödvändigt att röra på EvilOperation
klass alls.
Sist men inte minst blev jag av med kontrollen om en ond operation är närvarande i Spöke # has_discount?
. Inget behov av att se till att en operation finns för att få rabatten. Som ett resultat av null objektet, den Spöke
klassen är mycket slankare och delar inte andra klassers ansvar lika mycket.
En bra riktlinje att ta bort från detta är att inte kontrollera objekt om de är benägna att göra något. Beordra dem som en borrsergent. Som så ofta är det förmodligen inte coolt i verkligheten, men det är bra råd för objektorienterad programmering. Ge mig nu 20!
I allmänhet gäller alla fördelar med att använda polymorfism istället för fallutlåtanden för Null Objects också. Trots allt. Det är bara ett speciellt fall av fallutlåtanden. Detsamma gäller för nackdelar:
Låt oss stänga denna artikel med något ljus. Det här är en lukt eftersom det är en klass som inte har något beteende förutom att få och ställa in dess data. Det är i grunden inget annat än en datakontainer utan extra metoder som gör något med den data. Det gör dem passiva i naturen eftersom dessa data hålls där för att användas av andra klasser. Och vi vet redan att detta inte är ett ideal scenario eftersom det skriker presentera avund. I allmänhet vill vi ha klasser som har statligt meningsdata som de tar hand om - såväl som beteende via metoder som kan agera på den data utan mycket hinder eller snooping i andra klasser.
Du kan börja refactoring klasser som dessa genom att eventuellt extrahera beteendet från andra klasser som handlar om data i din dataklass. Genom att göra det kan du sakta dra till sig användbart beteende för den data och ge det ett riktigt hem. Det är helt bra om dessa klasser växer beteende över tiden. Ibland kan du enkelt flytta en hel metod, och ibland måste du extrahera delar av en större metod först och sedan extrahera den i dataklassen. När du är i tvivel, om du kan minska tillgången som andra klasser har på din dataklass, borde du definitivt gå och se över det. Ditt mål bör vara att gå i pension vid något tillfälle getters och setters som gav andra objekt tillgång till den data. Som ett resultat kommer du att ha lägre koppling mellan klasser-vilket alltid är en vinst!
Se, det var inte så komplicerat! Det finns en hel del fina lingo och komplicerade ljudtekniker kring kodluktar, men förhoppningsvis insåg du att luktar och deras refactorings också delar ett antal egenskaper som är begränsade i antal. Kodluktar kommer aldrig att gå iväg eller bli irrelevanta - åtminstone inte förrän våra kroppar förstärks av AIs som låt oss skriva den typ av kvalitetskod vi är ljusår borta från för tillfället.
Under de senaste tre artiklarna har jag presenterat dig en bra bit av de viktigaste och vanligaste kodluftsscenarierna du kommer att stöta på under din objektorienterade programmeringskarriär. Jag tycker att det här var en bra introduktion för nybörjare till det här ämnet, och jag hoppas att kodexemplen var tillräckligt stora för att enkelt följa tråden.
När du väl har räknat ut dessa principer för att utforma kvalitetskod kommer du att hämta nya lukter och deras refactorings på nolltid. Om du har gjort det så långt och känner att du inte tappade den stora bilden, tror jag att du är redo att komma till chefsnivå OOP status.