Dit newbie-vriendelijke artikel bespreekt een nieuwe ronde van geuren en verfijningen die je al vroeg in je carrière zou moeten leren kennen. We behandelen case-statements, polymorfisme, nul-objecten en dataklassen.
Deze kan ook "checklistgeur" of zoiets heten. Gevalverklaringen zijn een geur omdat ze verdubbeling veroorzaken - ze zijn vaak ook onelegant. Ze kunnen ook leiden tot onnodig grote klassen omdat al deze methoden die reageren op de verschillende (en mogelijk groeiende) casuscenario's vaak eindigen in dezelfde klas, die dan allerlei gemengde verantwoordelijkheden heeft. Het is geen zeldzaam geval dat je veel privémethoden hebt die beter af zouden zijn in hun eigen klassen.
Een groot probleem met case-uitspraken doet zich voor als u ze wilt uitbreiden. Dan moet je die specifieke methode veranderen-mogelijk opnieuw en opnieuw. En niet alleen daar, want vaak hebben ze een tweeling die overal wordt herhaald en die nu ook een update nodig heeft. Een geweldige manier om bugs zeker te fokken. Zoals u zich wellicht herinnert, willen we openstaan voor verlenging, maar worden we gesloten voor aanpassing. Hier is modificatie onvermijdelijk en slechts een kwestie van tijd.
Je maakt het moeilijker voor jezelf om code uit te pakken en opnieuw te gebruiken, plus het is een tikkende rommelbom. Vaak is de zichtcode afhankelijk van dergelijke case-statements - die vervolgens de geur dupliceren en de poort wijd open openen voor een ronde van shotgun-operatie in de toekomst. Ouch! Ook het ondervragen van een object voordat je de juiste methode vindt om uit te voeren is een vervelende overtreding van het "Tell-Don't-Ask" -principe.
Er is een goede techniek om de behoefte aan case-statements af te handelen. Fancy word binnengekomen! polymorfisme. Hiermee kunt u dezelfde interface voor verschillende objecten maken en elk object gebruiken dat nodig is voor verschillende scenario's. U kunt gewoon het juiste object omwisselen en het past zich aan aan uw behoeften omdat het dezelfde methoden heeft. Hun gedrag onder deze methoden is anders, maar zolang de objecten op dezelfde interface reageren, maakt Ruby het niet uit. Bijvoorbeeld, VegetarianDish.new.order
en VeganDish.new.order
zich anders gedragen, maar beide reageren op #bestellen
dezelfde manier. Je wilt gewoon veel vragen bestellen en niet beantwoorden, bijvoorbeeld als je eieren eet of niet.
Polymorfisme wordt geïmplementeerd door een klasse uit te pakken voor de vertakking van de case-statement en die logica naar een nieuwe methode in die klasse te verplaatsen. U blijft dat doen voor elk been in de voorwaardelijke structuur en geeft ze allemaal dezelfde naam voor de methode. Op die manier kapselt u dat gedrag in een object dat het best geschikt is om dat soort beslissingen te nemen en heeft geen reden om verder te veranderen. Kijk, op die manier kun je al deze zeurende vragen over een voorwerp vermijden - je vertelt het gewoon wat het zou moeten doen. Wanneer de noodzaak zich voordoet voor meer voorwaardelijke gevallen, maakt u gewoon een andere klasse die voor die ene verantwoordelijkheid zorgt onder dezelfde naam van de methode.
Case Statement Logic
class Operation def prijsvraag @mission_tpe wanneer: counter_intelligence @standard_fee + @counter_intelligence_fee wanneer: terrorism @standard_fee + @terrorism_fee wanneer: revenge @standard_fee + @revenge_fee wanneer: afpersing @standard_fee + @extortion_fee end-end-end counter_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
In ons voorbeeld hebben we een Operatie
klas die rond moet vragen mission_type
voordat het je zijn prijs kan vertellen. Het is gemakkelijk om dat te zien prijs
methode wacht gewoon om te wijzigen wanneer een nieuw soort bewerking wordt toegevoegd. Als u dat ook in uw weergave wilt weergeven, moet u die wijziging daar ook toepassen. (Ter informatie: voor views kun je polymorfe partials gebruiken in Rails om te voorkomen dat deze case-statements over je hele gezicht worden vernietigd.)
Polymorfe klassen
class CounterIntelligenceOperation def prijs @standard_fee + @counter_intelligence_fee end end class TerrorismeOperation def prijs @standard_fee + @terrorism_fee end end class RevengeOperation def prijs @standard_fee + @revenge_fee end end class AfpersingOperation def prijs @standard_fee + @extortion_fee end end 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
Dus in plaats van dat konijnenhol te verlaten, creëerden we een aantal operationele klassen die hun eigen kennis hebben over hoeveel hun honorarium overeenkomt met de uiteindelijke prijs. We kunnen ze gewoon vertellen ons hun prijs te geven. Je hoeft niet altijd van het origineel af te komen (Operatie
) alleen in de klas als je ontdekt dat je alle kennis die het had, hebt geëxtraheerd.
Ik denk dat de logica achter case statements onvermijdelijk is. Scenario's waarbij u een soort controlelijst moet doorlopen voordat u het object of het gedrag vindt dat de klus moet klaren, is gewoon te gewoon. De vraag is gewoon hoe ze het best worden behandeld. Ik ben er voorstander van ze niet te herhalen en gebruik te maken van de tools die objectgeoriënteerd programmeren biedt voor het ontwerpen van discrete klassen die eenvoudig kunnen worden uitgewisseld via hun interface.
Nergens op niks controleren is een speciaal soort geur uit de case-statement. Een object over nul vragen is vaak een soort verborgen case-statement. Nihil voorwaardelijk behandelen kan de vorm aannemen van object.nil?
, object.present?
, object.try
, en dan een soort van actie in de zaak nul
verschijnt op je feestje.
Een andere, meer stiekeme vraag is om een object te vragen naar zijn waarheidsgehalte - ergo als het bestaat of als het nul is - en dan enige actie te ondernemen. Ziet er ongevaarlijk uit, maar het is slechts een vermomming. Laat je niet misleiden: ternaire operatoren of ||
operators vallen natuurlijk ook in deze categorie. Anders gezegd, conditionals worden niet alleen duidelijk herkenbaar als tenzij
, if-else
of geval
statements. Ze hebben subtielere manieren om je feestje te laten crashen. Vraag geen voorwerpen om hun nihilheid, maar zeg hen als het object voor uw gelukkige pad afwezig is dat een nul object nu de leiding heeft om te reageren op uw berichten.
Null-objecten zijn gewone klassen. Er is niets speciaals aan hen - slechts een "mooie naam". Je extraheer er een voorwaardelijke logica uit nul
en dan behandel je het polymorf. Je bevat dat gedrag, beheert de stroom van je app via deze klassen en hebt ook objecten die open staan voor andere extensies die bij hen passen. Denk na over hoe a proces
(NullSubscription
) klasse kan na verloop van tijd groeien. Niet alleen is het DROOG en yadda-yadda-yadda, het is ook veel meer beschrijvend en stabiel.
Een groot voordeel van het gebruik van lege objecten is dat dingen niet zo gemakkelijk kunnen opblazen. Deze objecten reageren op dezelfde berichten als de geëmuleerde objecten. U hoeft de hele API natuurlijk niet altijd naar nul-objecten te kopiëren, waardoor uw app weinig reden heeft om gek te worden. Merk echter op dat nulobjecten conditionele logica inkapselen zonder deze volledig te verwijderen. U vindt er gewoon een beter huis voor.
Omdat het hebben van veel nihil-gerelateerde actie in je app behoorlijk besmettelijk en ongezond is voor je app, vind ik het leuk om nul-objecten te zien als het "nul-insluitingspatroon" (alsjeblieft, daag me niet uit!). Waarom besmettelijk? Omdat als je slaagt nul
rond, ergens anders in je hiërarchie, wordt een andere methode vroeg of laat ook gedwongen om te vragen of nul
is in de stad - wat vervolgens leidt tot een nieuwe ronde van tegenmaatregelen nemen om een dergelijk geval aan te pakken.
Anders gezegd, niks is niet cool om mee om te gaan, want vragen om zijn aanwezigheid wordt besmettelijk. Objecten vragen nul
is hoogstwaarschijnlijk altijd een symptoom van een slecht ontwerp - geen aanstoot en voelt niet slecht! - we zijn er allemaal geweest. Ik wil niet "willy-nilly" gaan over hoe onvriendelijk nul kan zijn, maar een paar dingen moeten worden vermeld:
Over het algemeen komt het scenario dat een object iets mist vaak naar voren. Een vaak genoemd voorbeeld is een app die een geregistreerde heeft Gebruiker
en een NilUser
. Maar omdat gebruikers die niet bestaan een dom concept zijn als die persoon duidelijk door uw app browst, is het waarschijnlijk koeler om een Gast
die zich nog niet heeft aangemeld. Een ontbrekend abonnement kan een zijn proces
, een nullast kan een zijn freebie
, enzovoorts.
Het benoemen van uw nulobjecten is soms duidelijk en gemakkelijk, soms superhard. Probeer echter om alle nulobjecten een naam te geven met een leidende "Null" of "Nee". Je kan beter! Het geven van een beetje context gaat een lange weg, denk ik. Kies een naam die specifieker en betekenisvoller is, iets dat de werkelijke use case weerspiegelt. Op die manier communiceer je natuurlijk duidelijker met andere teamleden en met je toekomstige zelf.
Er zijn een aantal manieren om deze techniek te implementeren. Ik zal je er een laten zien waarvan ik denk dat het rechtdoorzee en beginnersvriendelijk is. Ik hoop dat het een goede basis is om meer betrokken benaderingen te begrijpen. Wanneer je herhaaldelijk een soort van voorwaardelijke ontmoeting tegenkomt nul
, je weet dat het tijd is om gewoon een nieuwe klas te maken en dat gedrag te laten verdwijnen. Daarna laat je de oorspronkelijke klas weten dat deze nieuwe speler nu met niets omgaat.
In het onderstaande voorbeeld kunt u zien dat de Spook
klas vraagt een beetje te veel over nul
en verstopt de code onnodig. Het wil ervoor zorgen dat we een evil_operation
voordat het besluit om op te laden. Zie je de overtreding van "Tell-Don't-Ask"?
Een ander problematisch onderdeel is waarom Spectre zich zou moeten bekommeren om de implementatie van een nulprijs. De proberen
methode vraagt ook stiekem of het evil_operation
heeft een prijs
om niets te verwerken via de of
(||
) uitspraak. evil_operation.present?
maakt dezelfde fout. We kunnen dit vereenvoudigen:
class Spectre include ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge tenzij evil_operation.nil? evil_operation.charge (credit_card) end end def has_discount? evil_operation.present? && evil_operation.has_discount? einde def prijs evil_operation.try (: prijs) || 0 eindklasse EvilOperation omvat ActiveModel :: Model attr_accessor: discount,: price def has_discount? discount end def charge (credit_card) credit_card.charge (price) end end
klasse NoOperation def charge (creditcard) "Geen slechte operatie geregistreerd" end def has_discount? false end def prijs 0 end end class Spectre include ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? einde def prijs evil_operation.price end private def evil_operation @evil_operation || NoOperation.new end end class EvilOperation omvat ActiveModel :: Model attr_accessor: discount,: price def has_discount? discount end def charge (credit_card) credit_card.charge (price) end end
Ik denk dat dit voorbeeld eenvoudig genoeg is om meteen te zien hoe elegant nul-objecten kunnen zijn. In ons geval hebben we een Geen operatie
null klasse die weet hoe om te gaan met een niet-bestaande slechte operatie via:
0
hoeveelheden.We hebben deze nieuwe klasse gemaakt die zich bezighoudt met het niets, een object dat omgaat met de afwezigheid van spullen en het in de oude klas inwisselt als nul
verschijnt. De API is hier de sleutel omdat als deze overeenkomt met die van de oorspronkelijke klasse, u het nulobject naadloos kunt omwisselen met de retourwaarden die we nodig hebben. Dat is precies wat we deden in de privémethode Specter # evil_operation
. Als we de hebben evil_operation
object, we moeten dat gebruiken; zo niet, dan gebruiken we onze nul
kameleon die weet hoe om te gaan met deze berichten, dus evil_operation
zal nooit meer nul terugkomen. Eenden typen op zijn best.
Onze problematische voorwaardelijke logica is nu op één plek verpakt en ons nul-object heeft de leiding over het gedrag Spook
zoekt naar. DROOG! We hebben een beetje dezelfde interface heropgebouwd van het originele object dat nihil niet aankon. Vergeet niet dat niets slecht werkt bij het ontvangen van berichten - niemand thuis, altijd! Vanaf nu vertellen objecten alleen wat ze moeten doen zonder ze eerst te vragen naar hun "toestemming". Wat ook cool is, is dat het niet nodig was om de EvilOperation
klas helemaal.
En als laatste, maar daarom niet minder belangrijk, heb ik de cheque afgelegd als er een slechte operatie aanwezig is Specter # has_discount?
. Het is niet nodig om ervoor te zorgen dat er een bewerking bestaat om de korting te krijgen. Als gevolg van het nulobject, de Spook
klas is veel slanker en deelt de verantwoordelijkheden van andere klassen evenmin.
Een goede richtlijn om hier afstand van te doen is om geen voorwerpen te controleren als ze geneigd zijn iets te doen. Geef ze de opdracht als een boorsergeant. Zoals zo vaak het geval is, is het waarschijnlijk niet cool in het echte leven, maar het is een goed advies voor objectgeoriënteerde programmering. Geef me nu 20!
Over het algemeen zijn alle voordelen van het gebruik van Polymorfisme in plaats van Case-uitspraken ook van toepassing op Null Objects. Ten slotte. het is gewoon een speciaal geval van case statements. Hetzelfde geldt voor nadelen:
Laten we dit artikel afsluiten met iets lichts. Dit is een geur omdat het een klasse is die geen gedrag vertoont, behalve het ophalen en instellen van zijn gegevens - het is in feite niets meer dan een gegevenscontainer zonder extra methoden die iets met die gegevens doen. Dat maakt ze passief van aard, omdat deze gegevens daar worden bewaard om door andere klassen te worden gebruikt. En we weten al dat dit geen ideaal scenario is omdat het schreeuwt eigenschap afgunst. Over het algemeen willen we klassen hebben die gegevens bevatten die voor de toestand zijn bedoeld - evenals gedrag via methoden die op die gegevens kunnen reageren zonder veel hinder of andere klassen op te zoeken.
Je kunt klassen als deze gaan refactoren door het gedrag mogelijk uit andere klassen te halen die op de gegevens werken in je gegevensklasse. Door dat te doen, kunt u langzaam nuttig gedrag aantrekken voor die gegevens en het een goed thuis geven. Het is helemaal goed als deze klassen in de loop van de tijd gedrag vertonen. Soms kun je een hele methode gemakkelijk verplaatsen, en soms moet je eerst delen van een grotere methode extraheren en vervolgens extraheren in de dataklasse. In geval van twijfel, als je de toegang kunt verminderen die andere klassen op je dataklasse hebben, moet je er zeker voor gaan en dat gedrag omgooien. Je doel zou moeten zijn om op een gegeven moment de getters en setters die andere objecten toegang gaven tot die data terug te trekken. Als gevolg hiervan heb je een lagere koppeling tussen klassen, wat altijd een overwinning is!
Kijk, het was niet zo ingewikkeld! Er is veel chique jargon en gecompliceerde klinktechnieken rondom code ruikt, maar hopelijk realiseerde je je dat geuren en hun refactorings ook een aantal eigenschappen delen die beperkt zijn in aantal. Code geuren zullen nooit verdwijnen of irrelevant worden - althans niet totdat onze lichamen worden versterkt door AI's die ons de kwaliteitscode laten weten waar we op dit moment lichtjaren vandaan zijn.
In de loop van de laatste drie artikelen heb ik een groot deel van de belangrijkste en meest voorkomende codescenescenario's gepresenteerd die u tegen zult komen tijdens uw objectgeoriënteerde programmeercarrière. Ik denk dat dit een goede introductie was voor nieuwkomers in dit onderwerp en ik hoop dat de codevoorbeelden uitgebreid genoeg waren om de draad gemakkelijk te kunnen volgen.
Als je eenmaal deze principes hebt bedacht voor het ontwerpen van kwaliteitscode, zul je in een mum van tijd nieuwe geuren en hun refactorings oppikken. Als je het tot nu toe hebt gemaakt en het gevoel hebt dat je het grote plaatje niet hebt verloren, dan denk ik dat je klaar bent om de OOP-status van de chef-level te bereiken.