Acest articol nou-prietenos acoperă o altă rundă de mirosuri și refactorizări pe care ar trebui să vă familiarizați cu începutul carierei. Acoperim declarațiile de caz, polimorfismul, obiectele nula și clasele de date.
Acest lucru ar putea fi, de asemenea, numit "lista de miros miros" sau ceva de genul. Declarațiile de caz sunt un miros deoarece provoacă duplicarea - ele sunt adesea inelegante. Ele pot conduce, de asemenea, la clase inutile mari, deoarece toate aceste metode care răspund diferitelor scenarii de caz (și potențial în creștere) sfârșesc adesea în aceeași clasă - care are apoi tot felul de responsabilități mixte. Nu este un caz rar că aveți o mulțime de metode private care ar fi mai bine în clasele proprii.
O problemă mare cu declarațiile de caz apare dacă doriți să le extindeți. Apoi trebuie să schimbați acea metodă particulară - posibil din nou și din nou. Și nu numai acolo, pentru că de multe ori au gemeni repetate peste tot ceea ce are nevoie acum și de o actualizare. O modalitate foarte bună de a reproduce erorile. Așa cum ați putea să vă amintiți, vrem să fim deschise pentru extindere, dar închise pentru modificări. Aici, modificarea este inevitabilă și doar o chestiune de timp.
Îți faci mai greu să extragi și să reutilizezi codul, plus că este o bomba bâlbâită. Adesea, codul de vizualizare depinde de astfel de declarații de caz - care apoi duplică mirosul și deschideți poarta larg deschisă pentru o rundă de chirurgie cu pușcă în viitor. Ouch! De asemenea, interogarea unui obiect înainte de a găsi metoda potrivită pentru a executa este o încălcare urâtă a principiului "Spune-Nu-întreabă".
Există o tehnică bună pentru a face față nevoilor de declarații de caz. Un cuvânt fantezos! polimorfismul. Acest lucru vă permite să creați aceeași interfață pentru diferite obiecte și să folosiți orice obiect este necesar pentru diferite scenarii. Puteți schimba doar obiectul adecvat și se adaptează la nevoile dvs., deoarece are aceleași metode pe el. Comportamentul lor sub aceste metode este diferit, dar atâta timp cât obiectele răspund la aceeași interfață, Ruby nu-i pasă. De exemplu, VegetarianDish.new.order
și VeganDish.new.order
se comporta diferit, dar ambii răspund #Ordin
la fel. Voi pur și simplu doriți să comandați și să nu răspundeți la multe întrebări, cum ar fi dacă mâncați ouă sau nu.
Polimorfismul este implementat prin extragerea unei clase pentru ramura instrucțiunii de caz și mutarea acestei logici într-o nouă metodă din clasa respectivă. Continuați să faceți acest lucru pentru fiecare picior în copac condițional și să le oferiți același nume de metodă. În felul acesta încapsulează acest comportament unui obiect care este cel mai potrivit pentru a face acest tip de decizie și nu are nici un motiv să se schimbe în continuare. Vezi, în felul ăsta poți să eviți toate aceste întrebări năprazitoare pe un obiect - spui doar ce ar trebui să facă. Când apare necesitatea unor cazuri mai condiționale, creați doar o altă clasă care are grijă de acea singură responsabilitate sub același nume de metodă.
Declaratie de caz logica
În cazul în care: terorismul @standard_fee + @ terorism_fee atunci când: extortion @standard_fee + @extortion_fee sfârșesc sfârșitul contra_intel_op = Operation.new (mission_type: : counter_intelligence) counter_intel_op.price terror_op = Operație.new (mission_type:: terorism) teroare_op.price revenge_op = Operation.new (mission_type:: revenge) revenge_op.price extortion_op = Operație.new (mission_type:: extortion) extortion_op.price
În exemplul nostru, avem un operație
clasa pe care trebuie să o întrebe în jurul său mission_type
înainte de a vă putea spune prețul. Este ușor să vezi asta Preț
metoda așteaptă să se schimbe atunci când se adaugă un nou tip de operație. Când doriți să afișați acest lucru și în opinia dvs., va trebui să aplicați și acea modificare acolo. (FYI, pentru vizualizări puteți folosi paralele polimorfe în Rails pentru a evita ca aceste declarații de caz să fie blastificate peste tot.)
Clasele polimorfe
class CounterIntelligenceOperation def price @standard_fee + @counter_intelligence_fee clasa de sfârșit de sfârșit TerorismOperare def price @standard_fee + @ terrorism_fee sfârșitul clasei finale RevengeOperation def price @standard_fee + @revenge_fee sfârșitul clasei de clasă ExtortionOperation def price @standard_fee + @extortion_fee end end counter_intel_op = CounterIntelligenceOperation.new counter_intel_op .price terror_op = CounterIntelligenceOperation.new teroare_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price
Deci, în loc să coborâm la gaura de iepure, am creat o grămadă de clase de operații care au cunoștințele lor despre cât de mult se adaugă taxele la prețul final. Le putem spune doar să ne dea prețul lor. Nu trebuie întotdeauna să scapi de original (operație
) clasa - numai atunci când constatați că ați extras toate cunoștințele pe care le-a avut.
Cred că logica din spatele cazurilor este inevitabilă. Scenariile în care trebuie să treceți printr-un fel de listă de verificare, înainte de a găsi obiectul sau comportamentul care se ocupă de acest lucru, sunt prea frecvente. Întrebarea este pur și simplu cum sunt tratate cel mai bine. Sunt în favoarea faptului că nu le repetăm și că folosind programele orientate spre obiecte programarea mă oferă pentru proiectarea unor clase discrete care pot fi schimbate cu ușurință prin interfața lor.
Verificarea nulului peste tot este un tip special de miros al declarației de caz. Solicitarea unui obiect despre zero este adesea un fel de afirmație de caz ascuns. Manipularea în mod condiționat cu condiția să aibă forma object.nil?
, object.present?
, object.try
, și apoi un fel de acțiune în acest caz zero
se arată la petrecere.
O altă întrebare mai plictisitoare este de a cere un obiect despre adevărul său - ergo dacă există sau dacă este nul - și apoi să luați niște acțiuni. Se pare inofensiv, dar este doar o deghizare. Nu vă lăsați păcăliți: operatorii ternari sau ||
operatorii se încadrează, de asemenea, în această categorie. Puneți altfel, condiționările nu numai că apar clar identificabile ca dacă nu
, if-else
sau caz
declarații. Ei au modalități mai subtile de a vă prăbuși petrecerea. Nu întrebați obiecte pentru nilitatea lor, ci spuneți-le dacă obiectul căii tale fericite lipsește că un obiect nul este acum responsabil pentru a răspunde la mesajele tale.
Obiectele nulă sunt clase obișnuite. Nu este nimic deosebit în privința lor - doar un "nume fantezist". Exportați o logică condițională legată de zero
și apoi o tratați polimorfic. Conțineți acel comportament, controlați fluxul aplicației dvs. prin intermediul acestor clase și aveți, de asemenea, obiecte care sunt deschise pentru alte extensii care le corespund. Gândește-te la cum Proces
(NullSubscription
) ar putea crește în timp. Nu numai că este mai uscată și yadda-yadda-yadda, ci și mai descriptivă și mai stabilă.
Un mare beneficiu al utilizării obiectelor nula este că lucrurile nu pot exploda la fel de ușor. Aceste obiecte răspund acelorași mesaje ca obiectele emulate - nu trebuie întotdeauna să copiați întregul API peste obiecte nulite, desigur, ceea ce dă aplicației dvs. puține motive să se înnebunească. Totuși, rețineți că obiectele nulă încapsulează logica condițională fără al elimina complet. Pur și simplu găsești o casă mai bună pentru asta.
Din moment ce ai o mulțime de acțiuni legate de nulă în aplicația ta este destul de contagioasă și nesănătoasă pentru aplicația ta, îmi place să mă gândesc la obiecte nulă ca la "Null Containment Pattern" (Te rog, nu mă da în judecată!). De ce contagioase? Pentru că dacă treci zero
în jur, alt loc în ierarhia ta, o altă metodă este, mai devreme sau mai târziu, obligată să te întrebi dacă zero
este în oraș - ceea ce duce apoi la o nouă rundă de luare a contramăsurilor pentru a rezolva un astfel de caz.
Pune altfel, nilul nu este misto să stea cu el, pentru că cererea prezenței sale devine contagioasă. Cereți obiecte pentru zero
este, cel mai probabil, întotdeauna un simptom al designului slab - nu este ofensă și nu se simte rău! - Am fost toți acolo. Nu vreau să merg "nerăbdător" cu privire la cât de neprietenos poate fi, dar trebuie să menționăm câteva lucruri:
În ansamblu, scenariul în care un obiect lipsește este foarte frecvent. Un exemplu adesea citat este o aplicație care are o înregistrare Utilizator
și a NilUser
. Dar, deoarece utilizatorii care nu există există un concept prostește dacă acea persoană navighează în mod clar în aplicația dvs., probabil că este mai Oaspete
care încă nu sa înscris. Abonamentul lipsă ar putea fi a Proces
, o taxă zero ar putea fi a freebie
, si asa mai departe.
Denumirea obiectelor dvs. nulă este uneori evidentă și ușoară, uneori super-greu. Încercați totuși să nu mai numiți toate obiectele nula cu un "Null" sau "Nu" de conducere. Poți face mai bine! Oferind un pic de context merge mult, cred. Alegeți un nume mai specific și mai semnificativ, ceva ce reflectă situația reală de utilizare. În acest fel, veți comunica mai clar celorlalți membri ai echipei și viitorului dvs. de sine, desigur.
Există câteva modalități de a implementa această tehnică. Vă voi arăta unul care cred că este simplu și ușor de început. Sper că este o bază bună pentru înțelegerea mai multor abordări implicate. Când întâlniți în mod repetat un fel de condițional cu care se ocupă zero
, știți că este timpul să creați pur și simplu o nouă clasă și să mutați acest comportament. După aceea, lăsați clasa originală să știe că acest nou jucător se ocupă acum de nimic.
În exemplul de mai jos, puteți vedea că Spectru
clasa întreabă prea mult despre zero
și dezordinează codul inutil. Vrea să ne asigurăm că avem unul evil_operation
înainte de a decide să taxeze. Puteți vedea încălcarea textului "Spune-Nu-întreabă"?
Un alt aspect problematic este motivul pentru care Spectre ar trebui să aibă grijă de punerea în aplicare a unui preț zero. încerca
metoda cere, de asemenea, șiretlic dacă evil_operation
are o Preț
să se ocupe de nimic prin intermediul sau
(||
) afirmație. evil_operation.present?
face aceeași greșeală. Putem simplifica acest lucru:
clasa Spectre includ ActiveModel :: Modelul attr_accessor: credit_card,: evil_operation def taxa excepția cazului în care evil_operation.nil? evil_operation.charge (credit_card) sfârșitul sfârșitului def are_discount? evil_operation.present? && evil_operation.has_discount? sfârșitul prețului def. evil_operation.try (: price) || 0 sfârșitul clasei de clasă EvilOperation include ActiveModel :: Model attr_accessor: discount,: price def has_discount? taxa de credit final de credit (credit_card) credit_card.charge (preț) sfârșitul final
clasa NoOperație taxă def (carte de credit) "Nici o operațiune rău înregistrat" sfârșitul def has_discount? false sfarsit def price 0 sfarsit clasa end Spectre includ ActiveModel :: Modelul attr_accessor: credit_card,: evil_operation def sarcina evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? sfârșitul prețului de defiltare rău_operation.price privat def_operation @evil_operation || NoOperation.new clasa de sfârșit de clasă EvilOperation include ActiveModel :: Model attr_accessor: discount,: price def has_discount? taxa de credit final de credit (credit_card) credit_card.charge (preț) sfârșitul final
Cred că acest exemplu este suficient de simplu pentru a vedea imediat ce obiecte elegante pot fi elegante. În cazul nostru, avem a NoOperation
clasa null care știe cum să se ocupe de o operațiune rău neexplicată prin:
0
sume.Am creat această nouă clasă care se ocupă de nimic, un obiect care gestionează absența lucrurilor și o schimbă în clasa veche dacă zero
apare. API-ul este cheia aici, deoarece dacă se potrivește cu cea a clasei originale, puteți schimba în mod perfect obiectul nul cu valorile de întoarcere de care avem nevoie. Asta am făcut exact în metoda privată Spectre # evil_operation
. Dacă avem evil_operation
obiect, avem nevoie să folosim acest lucru; dacă nu, ne folosim zero
chameleon care știe cum să facă față acestor mesaje, deci evil_operation
nu va mai reveni niciodată. Duck tastând la cele mai bune.
Logica noastră condițională problematică este acum înfășurată într-un singur loc, iar obiectul nostru nul este responsabil de comportament Spectru
cauta. USCAT! Am reconstruit cam aceeași interfață din obiectul original, pe care nilul nu a reușit să o gestioneze. Amintiți-vă, nimeni nu reușește să primească mesaje - nimeni acasă, întotdeauna! De acum încolo, doar spune obiectelor ce să facă fără să le ceară mai întâi "permisiunea" lor. Ce este de asemenea cool este că nu este nevoie să atingeți EvilOperation
clasa la toate.
Nu în ultimul rând, am scăpat de verificarea dacă există o operațiune rea Spectre # has_discount?
. Nu este nevoie să vă asigurați că există o operațiune pentru a obține reducerea. Ca rezultat al obiectului null, Spectru
clasa este mult mai subțire și nu împarte responsabilitățile altor clase la fel de mult.
O bună orientare pentru a lua de la acest lucru este de a nu verifica obiectele dacă sunt înclinați să facă ceva. Comandă-i ca un sergent de foraj. Așa cum se întâmplă adesea, probabil că nu este rece în viața reală, dar este un sfat bun pentru Programarea Object-Oriented. Acum dă-mi 20!
În general, toate avantajele de la utilizarea polimorfismului în loc de declarații de caz se aplică și obiectelor Null. Dupa toate acestea. este doar un caz special de declarații de caz. Același lucru este valabil și pentru dezavantaje:
Să închidem acest articol cu ceva ușor. Acesta este un miros deoarece este o clasă care nu are niciun comportament, cu excepția obținerii și setării datelor - nu este nimic mai mult decât un container de date fără metode suplimentare care fac ceva cu aceste date. Acest lucru le face pasiv, deoarece aceste date sunt păstrate acolo pentru a fi utilizate de alte clase. Și știm deja că nu este un scenariu ideal pentru că strigă caracter invidiat. În general, vrem să avem clase care au date de stare de care au grijă - precum și comportament prin metode care să poată acționa asupra acestor date fără a împiedica prea multe ori să le pătrundă în alte clase.
Puteți începe cursuri de refactorizare ca acestea, eventual extragând comportamentul altor clase care acționează asupra datelor în clasa de date. Făcând acest lucru, este posibil să atrageți încet un comportament util pentru aceste date și să-i dați o casă potrivită. Este absolut bine dacă aceste clase cresc comportamentul în timp. Uneori puteți muta o metodă întreagă cu ușurință și, uneori, trebuie să extrageți mai întâi părți ale unei metode mai mari și apoi să le extrageți în clasa de date. Când sunteți îndoieli, dacă puteți reduce accesul altor clase pe clasa de date, trebuie să mergeți cu siguranță și să mutați acest comportament. Scopul dvs. ar trebui să fie retragerea la un moment dat a getters și setters care au dat altor obiecte accesul la aceste date. Ca rezultat, veți avea o legătură mai mică între clase - ceea ce este întotdeauna o victorie!
Vezi, nu era atât de complicat! Există o mulțime de tehnici de lingo fantezie și complicate de sondare în jurul mirosurilor codului, dar, sperăm, ați realizat că mirosurile și refactorizările lor au, de asemenea, câteva trăsături care sunt limitate în număr. Miroasele de cod nu vor dispărea niciodată sau nu vor deveni irelevante - cel puțin până când trupurile noastre nu vor fi îmbunătățite de AI care ne lasă să scriem acel tip de cod de calitate de care suntem departe ani de lumină în prezent.
În cursul ultimelor trei articole, v-am prezentat o bună parte din cele mai importante și cele mai comune scenarii de miros de cod pe care le veți întâlni în timpul carierei dvs. de programare orientate spre obiecte. Cred că aceasta a fost o introducere bună pentru începători la acest subiect și sper că exemplele de cod au fost suficient de amănunțite pentru a urma cu ușurință firul.
Odată ce ați dat seama de aceste principii pentru proiectarea codului de calitate, veți lua noi mirosuri și refactorizările lor în cel mai scurt timp. Dacă ați făcut-o atât de departe și vă simțiți că nu ați pierdut imaginea de ansamblu, cred că sunteți gata să vă apropiați de nivelul șefului OOP.