Aceasta este o parte a unei serii mici despre mirosurile de cod și eventualele refactorizări. Publicul țintă pe care l-am avut în minte sunt începători care au auzit despre acest subiect și care probabil au vrut să aștepte un pic înainte de a intra în aceste ape avansate. Următorul articol se referă la "Envy caracteristică", "Surgery Shotgun" și "Schimbare divergentă".
Ceea ce îți dai seama rapid cu mirosurile de cod este că unii dintre ei sunt veri foarte apropiați. Chiar și refactorizările lor sunt uneori legate - de exemplu, Clasa Inline și Clasa de extragere nu sunt atât de diferite.
Cu ajutorul unei clase, de exemplu, extragi întreaga clasă în timp ce scapi de cea originală. Așa că extrageți clasa cu puțină răsucire. Ideea pe care încerc să o fac este să nu vă simțiți copleșiți de numărul de mirosuri și refactorizări și cu siguranță să nu vă descurajați de numele lor inteligenți. Lucrurile ca Chirurgia Pușcă, Feature Envy și Divergent Change ar putea suna fantezie și intimidare pentru oamenii care tocmai au început. Poate că mă înșel, bineînțeles.
Dacă vă scufundați puțin în acest subiect și jucați cu câteva refactorizări pentru a vă mirosi codul, veți vedea rapid că acestea ajung adesea în același parc de joc. O mulțime de refactorizări sunt strategii diferite pentru a ajunge la un punct în care aveți clase care sunt concise, bine organizate și concentrate pe un mic număr de responsabilități. Cred că este corect să spun că, dacă reușești să faci asta, vei fi înaintea pachetului de cele mai multe ori - nu că a fi înaintea altora este atât de importantă, dar un astfel de design de clasă lipsește adesea în codul de la oameni înainte de a sunt considerați "experți".
Deci, de ce nu intrați devreme în joc și construiți o fundație concretă pentru proiectarea codului. Nu credeți care ar putea fi propria voastră narațiune că acesta este un subiect avansat pe care ar trebui să-l scoateți pentru o vreme până când sunteți gata. Chiar dacă sunteți un începător, dacă faceți pași mici, vă puteți împacheta capul în jurul mirosurilor și refactorizărilor dvs. mult mai devreme decât ați putea crede.
Înainte de a ne scufunda în mecanică, vreau să repet un punct important din primul articol. Nu orice miros este în mod inerent rău, și nu orice refactorizare merită mereu. Trebuie să decideți la fața locului - când aveți toate informațiile relevante la dispoziția dumneavoastră - în cazul în care codul dvs. este mai stabil după o refactorizare și dacă merită timpul dvs. pentru a repara mirosul.
Să revedem un exemplu din articolul precedent. Am extras o lungă listă de parametri pentru #assign_new_mission
intr-o parametru obiect prin Misiune
clasă. Până atât de răcoros.
M cu invidie caracteristică
"ruby class M def assign_new_mission (mission) print" Misiunea # mission.mission_name a fost atribuită # # mission.agent_name cu scopul de a # mission.objective "dacă mission.licence_to_kill imprimă" Licența de a ucide a fost acordată. "alt tip de imprimare" Licența de a ucide nu a fost acordată "
clasa Misiune attr_reader: mission_name,: agent_name,: objective,: license_to_kill
def initialize (nume_sunt: nume_sunt, agent_name: agent_name, obiectiv: obiectiv, license_to_kill: license_to_kill) @mission_name = nume_locatie @agent_name = nume_vanzator @obiectiv = obiectiv @licence_to_kill = license_to_kill end end
m = M.new
misiune = Mission.new (numele misiunii: "Octopussy", agent_name: "James Bond", obiectiv: "găsiți dispozitivul nuclear", license_to_kill: true)
m.assign_new_mission (misiune)
"
Am menționat succint cum putem simplifica M
clasa chiar mai mult prin mutarea metodei #assign_new_mission
la noua clasă pentru obiectul parametru. Ceea ce nu am abordat a fost faptul că M
a avut o formă ușor de curabilă caracter invidiat de asemenea. M
a fost prea nerăbdător despre atributele lui Misiune
. Pune altfel, a cerut prea multe "întrebări" despre obiectul misiunii. Nu este doar un caz rău de micromanagement, ci și un miros de cod foarte comun.
Lasă-mă să-ți arăt ce vreau să spun. În M # assign_new_mission
, M
este "invidios" despre datele din noul obiect de parametru și dorește să îl acceseze peste tot.
În plus, aveți și un obiect de parametri Misiune
care este doar responsabil pentru date chiar acum - care este un alt miros, a Clasa de date.
Toată această situație îți spune în principiu asta #assign_new_mission
vrea să fie în altă parte și M
nu are nevoie să cunoască detaliile despre modul în care misiunile sunt atribuite. La urma urmei, de ce nu ar fi responsabilitatea misiunii de a atribui noi misiuni? Amintiți-vă întotdeauna să puneți lucrurile împreună care, de asemenea, se schimbă împreună.
M fără invidie caracteristică
"rubin clasa M def assign_new_mission (misiune) mission.assign end end
clasa Misiune attr_reader: mission_name,: agent_name,: objective,: license_to_kill
def initialize (nume_locatie: nume_sunt, agent_name: agent_name, obiectiv: obiectiv, license_to_kill: license_to_kill) @mission_name = nume_sunt @agent_name = nume_activitate @obiectiv = obiectiv @licence_to_kill =
def assign print "Misiunea # mission_name a fost asignată la # agent_name cu scopul de a # objective." dacă license_to_kill print "Licența de a ucide a fost acordată" altceva print "Licența de a ucide nu a fost acordat "
m = M.new mission = Mission.new (numele misiunii: "Octopussy", agent_name: "James Bond", obiectiv: "găsiți dispozitivul nuclear", license_to_kill: true) m.assign_new_mission (mission)
După cum puteți vedea, am simplificat lucrurile destul de puțin. Metoda a slăbit semnificativ și deleagă comportamentul obiectului responsabil. M
nu mai solicită detalii despre misiune și, cu siguranță, rămâne departe de a se implica în modul în care se imprimă misiunile. Acum se poate concentra pe adevărata ei slujbă și nu trebuie să fie deranjată dacă se schimbă detalii despre misiunile misiunii. Mai mult timp pentru jocurile mintale și pentru a vâna agenții necinstiți. Win-win!
Elementele de invidie ale rasei cuprind entanglement - prin care nu mă refer la tipul bun, cel care permite informațiilor să călătorească mai repede decât lumina înfricoșătoare - vorbesc despre cel care, cu timpul, ar putea lăsa impulsul tău de dezvoltare să se prăbușească la o oprire mai apropiată. Nu e bine! De ce? Efectele de efracție în codul dvs. vor crea rezistență! O schimbare într-un singur loc flutură prin tot felul de lucruri și ajungeți ca un zmeur într-un uragan. (Ok, un pic prea dramatic, dar îmi dau un B + pentru referința Bond acolo.)
Ca un antidot general pentru invidia caracteristică, doriți să vă orientați spre proiectarea unor clase care se preocupă în cea mai mare parte de propriile lucruri și au - dacă este posibil - responsabilități unice. Pe scurt, clasele ar trebui să fie ceva asemănător cu otakus prietenos. Din punct de vedere social, care ar putea să nu fie cel mai sănătos dintre comportamente, dar pentru proiectarea de clase, adesea este o îndrumare rezonabilă pentru a vă menține impulsul acolo unde ar trebui să fie -!
Numele este un pic prost, nu-i așa? Dar, în același timp, este o descriere destul de precisă. Sună ca o afacere serioasă, și este! Din fericire nu este atât de greu de înțeles, dar totuși este unul dintre mirosurile de coduri mai proaste. De ce? Pentru că generează dublarea ca și alta și este ușor să pierzi din vedere toate schimbările pe care trebuie să le faci pentru a rezolva lucrurile. Ce se întâmplă în timpul operației de pușcă este efectuarea unei schimbări într-o singură clasă / fișier și trebuie de asemenea să atingeți multe alte clase / fișiere care trebuie să fie actualizate. Sper că nu sună ca un moment bun pentru care sunteți în.
De exemplu, ați putea crede că puteți scăpa cu o mică schimbare într-un singur loc și apoi să vă dați seama că trebuie să treci printr-o grămadă de fișiere pentru a face fie aceeași schimbare, fie pentru a rezolva altceva care este rupt din cauza acesteia. Nu bine, deloc! Suna mai mult ca un motiv bun pentru care oamenii incep sa urasca codul cu care se confrunta.
Dacă aveți un spectru cu codul DRY pe o parte, atunci codul care necesită adesea operația de pușcă este destul de mult pe capătul opus. Nu fiți leneși și lăsați-vă să intrați în acel teritoriu. Sunt sigur că preferați să deschideți un fișier și să aplicați modificările acolo și să faceți acest lucru. Asta e genul de leneș pentru care ar trebui să te străduiți!
Pentru a evita acest miros, iată o scurtă listă de simptome pe care le puteți căuta:
Ce inseamna atunci cand vorbim despre codul cuplat? Să spunem că avem obiecte A
și B
. Dacă acestea nu sunt cuplate, puteți schimba unul dintre ele fără a afecta celălalt. În caz contrar, de cele mai multe ori vei avea de-a face cu celălalt obiect.
Aceasta este o problemă, iar operația cu pușca este un simptom al cuplării strânse. Deci, asigurați-vă întotdeauna cât de ușor vă puteți schimba codul. Dacă este relativ ușor, înseamnă că nivelul de cuplare este acceptabil scăzut. Acestea fiind spuse, îmi dau seama că așteptările dvs. ar fi nerealiste dacă vă așteptați să puteți evita cuplarea tot timpul cu orice preț. Asta nu se va întâmpla! Veți găsi motive întemeiate pentru a decide împotriva acelor condiționări care necesită înlocuire polimorfismul. Într-un astfel de caz, un pic de cuplare, chirurgie cu pușcă și păstrarea API-ului de obiecte în sincronizare este bine în valoare de a scăpa de o tonă de declarații de caz printr-o Obiect nul (mai mult despre asta într-o piesă ulterioară).
Cel mai frecvent puteți aplica unul dintre următoarele refactorizări pentru a vindeca rănile:
Să ne uităm la un cod. Acest exemplu este o felie a modului în care aplicația Spectre gestionează plățile între contractori și clienți răi. Am simplificat plățile printr-o taxă standard pentru contractori și clienți. Deci, nu contează dacă Spectre are sarcina de a răpi o pisică sau de a scoate o țară întreagă: taxa rămâne aceeași. Același lucru este valabil și pentru ceea ce plătesc contractorii. În cazurile rare, o operațiune merge spre sud și o altă Nr. 2 trebuie să sari literalmente rechin, Spectre oferă o restituire completă pentru a menține clienții răi fericit. Spectrul utilizează o bijuterie de plată proprietate, care este în esență un substituent pentru orice tip de procesor de plată.
În primul exemplu de mai jos, ar fi o durere dacă Spectre a decis să utilizeze o altă bibliotecă pentru a gestiona plățile. Ar fi implicate mai multe părți în mișcare, dar pentru a demonstra o intervenție chirurgicală la pușcă, această sumă de complexitate va trebui să cred:
Exemplu cu miros de operație a pușcă:
"clasa rubin EvilClient # ...
STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000
def accept_new_client PaymentGem.create_client (email) sfârșit
def charge_for_initializing_operation evil_client_id = PaymentGem.find_client (e-mail) .payments_id PaymentGem.charge (rău_client_id, STANDARD_CHARGE) sfârșit
def charge_for_successful_operation evil_client_id = PaymentGem.find_client (email) .payments_id PaymentGem.charge (rău_client_id, BONUS_CHARGE) final sfârșit
operație de clasă # ...
REFUND_AMOUNT = 10000000
return refund_id = PaymentGem.find_transaction (payments_id) PaymentGem.refund (transaction_id, REFUND_AMOUNT) final sfârșit
clasa Contractor # ...
STANDARD_PAYOUT = 200000 BONUS_PAYOUT = 1000000
def process_payout spectre_agent_id = PaymentGem.find_contractor (e-mail) .payments_id dacă operația.enemy_agent == 'James Bond' && operation.enemy_agent_status == 'ucis în acțiune' PaymentGem.transfer_funds (spectre_agent_id, BONUS_PAYOUT) altceva PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) sfârșitul capătului final "
Când te uiți la acest cod ar trebui să te întrebi: "Ar trebui să EvilClients
să fii cu adevărat preocupat de modul în care procesatorul de plăți acceptă noi clienți răi și cum sunt percepuți pentru operațiuni? "Desigur, nu! Este o idee bună să se împrăștie diferitele sume pentru a plăti peste tot? Ar trebui să apară detaliile implementării procesorului de plăți în oricare dintre aceste clase? Cu siguranță nu!
Uită-te la asta de aici. Dacă doriți să schimbați ceva despre felul în care gestionați plățile, de ce ar trebui să deschideți EvilClient
clasă? În alte cazuri, ar putea fi un utilizator sau un client. Dacă vă gândiți la asta, nu are sens să-i familiarizați cu acest proces.
În acest exemplu, ar trebui să fie ușor de văzut că modificările aduse modului în care acceptați și transferați plățile creează efecte de rupere în întregul cod. De asemenea, dacă doriți să modificați suma pe care o încasezi sau o transferați contractantului dvs., veți avea nevoie de modificări suplimentare peste tot. Exemple principale de chirurgie cu arma. Și în acest caz avem doar trei clase. Imaginați-vă durerea dacă este implicată o complexitate puțin mai realistă. Da, de asta sunt făcute coșmarurile. Să aruncăm o privire la un exemplu care este puțin mai sănătos:
Exemplu fără miros de chirurgie cu arme și clasă extrasă:
"Clasa Ruby PaymentHandler STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000 REFUND_AMOUNT = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000 BONUS_CONTRACTOR_PAYOUT = 1000000
def initialize (payment_handler = PaymentGem) @payment_handler = sfârșitul payment_handler
def accept_new_client (rău_client) @ pay_handler.create_client (evil_client.email) sfârșit
def charge_for_initializing_operation (rău_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (rău_client_id, STANDARD_CHARGE) sfârșit
def charge_for_successful_operation (rău_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (rău_client_id, BONUS_CHARGE) sfârșit
return refund (operațiune) transaction_id = @ payment_handler.find_transaction (operation.payments_id) @ payment_handler.refund (transaction_id, REFUND_AMOUNT) sfârșit
def contractor_payout (contractor) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id if operation.enemy_agent == 'James Bond' && operation.enemy_agent_status == 'Killed in action' @ payment_handler.transfer_funds (spectre_agent_id, BONUS_CONTRACTOR_PAYOUT) payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) end end end
clasa EvilClient # ...
def accept_new_client PaymentHandler.new.accept_new_client (auto) sfârșitul
def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end
def charge_for_successful_operation (operațiune) PaymentHandler.new.charge_for_successful_operation (self) end end
operație de clasă # ...
compensare de plată PaymentHandler.new.refund (self) end end
clasa Contractor # ...
def process_payout PaymentHandler.new.contractor_payout (auto) end end "
Ceea ce am făcut aici este împachetarea PaymentGem
API în clasa noastră. Acum avem un loc central în care aplicăm modificările noastre dacă decidem că, de exemplu, a SpectrePaymentGem
ar funcționa mai bine pentru noi. Nu mai atingeți fișierele care nu au legătură cu fișierele cu mai multe plăți, dacă trebuie să ne adaptăm la modificări. În clasele care se ocupă de plăți, am instanțiat pur și simplu PaymentHandler
și să delege funcționalitatea necesară. Ușor, stabil și fără nici un motiv să se schimbe.
Și nu numai că am conținut totul într-un singur fișier. În cadrul PaymentsHandler
clasa, există un singur loc pe care trebuie să îl schimbăm și să facem referire la un posibil procesor de plată nou inițializa
. Asta e în cartea mea. Sigur, dacă noul serviciu de plată are un API complet diferit, trebuie să ajustați corpurile a două metode în PaymentHandler
. Este un preț mic pe care să-l plătiți în comparație cu operația de pușcă plină - este mai mult ca o intervenție chirurgicală pentru un mic fragment în degetul tău. Afacere buna!
Dacă nu sunteți atent atunci când scrieți testele pentru un procesor de plată ca acesta - sau orice serviciu extern pe care trebuie să vă bazați - puteți să vă faceți griji pentru dureri de cap când modificați API-ul. Și ei "suferă de schimbare", bineînțeles. Și întrebarea nu este că ei vor schimba API-ul lor, numai când.
Prin încapsularea noastră, suntem într-o poziție mult mai bună pentru a ne împiedica metodele noastre de procesare a plăților. De ce? Deoarece metodele pe care le stubăm sunt ale noastre și se schimbă numai atunci când dorim ca ele. Aceasta este o mare victorie. Dacă sunteți nou la testare și acest lucru nu este clar pentru dvs., nu vă faceți griji despre asta. Nu vă grăbiți; acest subiect poate fi dificil la început. Pentru că este un astfel de avantaj, am vrut să-l menționez doar de dragul exhaustivității.
După cum puteți vedea, am simplificat procesarea plăților destul de puțin în acest exemplu prostește. Aș fi putut curăța și rezultatul final mai mult, dar scopul a fost să demonstrez clar mirosul și cum puteți să scăpați de el prin abstractizare.
Dacă nu sunteți complet mulțumit de această clasă și vedeți oportunități de refăcut, vă salut - și sunt fericit să vă credităm. Vă recomandăm să vă bateți! Un început bun ar putea fi modul în care vă aflați payments_id
s. De asemenea, clasa a devenit puțin aglomerată ...
Schimbarea divergente este, într-un fel, opusul operației de pușcă - unde vreți să schimbați un lucru și trebuie să faceți o schimbare prin intermediul unei mulțimi de fișiere diferite. Aici o singură clasă este adesea schimbată din diferite motive și în moduri diferite. Recomandarea mea este de a identifica părți care se schimbă și le extrag într-o clasă separată care se poate concentra pe acea responsabilitate unică. Aceste clase, la rândul lor, nu ar trebui să aibă mai mult decât un motiv pentru a schimba - dacă nu, un alt miros de schimbare de schimbare este cel mai probabil așteaptă să te muște.
Clasele care suferă de schimbări divergente sunt cele care se schimbă foarte mult. Cu instrumente cum ar fi Churn, puteți măsura cât de des anumite părți ale codului dvs. au trebuit să se schimbe în trecut. Cu cât mai multe puncte găsiți pe o clasă, cu atât este mai mare probabilitatea ca schimbările divergente să fie la lucru. De asemenea, nu aș fi surprins dacă exact aceste clase sunt cele care provoacă cele mai multe bug-uri în general.
Nu mă înțelegeți greșit: schimbarea adesea nu este direct mirosul. Este totuși un simptom util. Un alt simptom foarte comun și mai explicit este că acest obiect trebuie să jongleze mai mult decât o singură responsabilitate. principiu de responsabilitate unică SRP este o îndrumare excelentă pentru a preveni mirosul acestui cod și pentru a scrie cod mai stabil în general. Poate fi dificil de urmat, dar totuși merită încă să mănânci.
Să ne uităm la acest exemplu urât de mai jos. Am modificat puțin exemplul chirurgiei cu arma. Blofeld, șeful Spectrului, ar putea fi cunoscut de micromanage, dar mă îndoiesc că ar fi interesat de jumătate din lucrurile pe care le are această clasă.
"Spectrul clasic rubinic
STANDARD_CHARGE = 10000000 STANDARD_PAYOUT = 200000
def cost_for_initializing_operation (client) evil_client_id = PaymentGem.find_client (client.email) .payments_id PaymentGem.charge (rău_client_id, STANDARD_CHARGE) sfârșit
def contractor_payout (contractor) spectre_agent_id = PaymentGem.find_contractor (contractor.email) .payments_id PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) sfârșit
def assign_new_operation (operațiune) operation.contractor = 'Unele tip dude' operation.objective = 'Furați o navă de chestii valoroase' operation.deadline = 'Miezul nopții, sfârșitul lunii noiembrie'
def print_operation_assignment (operațiune) print "# operation.contractor este alocat pentru # operation.objective. Termenul de misiune se încheie la # operation.deadline "
def dispose_of_agent (spectre_agent) pune "Ați dezamăgit această organizație. Știi cum Spectre se ocupă de eșec. Repede # spectre_agent.code_name! "Sfârșitul final"
Spectru
clasa are prea multe lucruri diferite despre care este preocupat:
Șapte responsabilități diferite pe o singură clasă. Nu e bine! Trebuie să schimbi modul în care sunt eliminați agenții? Un vector pentru schimbarea Spectru
clasă. Doriți să gestionați plățile în mod diferit? Un alt vector. Tu aduci driftul.
Deși acest exemplu este departe de a fi realist, povestește încă cât de ușor este să comporți în mod inutil un comportament care trebuie schimbat frecvent într-un singur loc. Putem face mai bine!
"clasa rubinie Spectre # ...
def dispose_of_agent (spectre_agent) pune "Ați dezamăgit această organizație. Știi cum Spectre se ocupă de eșec. "Good bye # spectre_agent.code_name!" Sfârșitul final
clasa PaymentHandler STANDARD_CHARGE = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000
# ...
def initialize (payment_handler = PaymentGem) @payment_handler = sfârșitul payment_handler
def charge_for_initializing_operation (rău_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (rău_client_id, STANDARD_CHARGE) sfârșit
def contractor_payout (contractor) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id @ payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) end end end
clasa EvilClient # ...
def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end end
clasa Contractor # ...
def process_payout PaymentHandler.new.contractor_payout (auto) sfârșitul final
clasa Operare attr_accessor: contractor,: objective,: deadline
def initialize (attrs = ) @contractor = attrs [: contractor] @objective = attrs [: object] @deadline = attrs [: deadline]
def print_operation_assignment print "# contractor este atribuit # # obiectivului. Termenul de finalizare a misiunii se încheie la # deadline. "End end"
Aici am extras o grămadă de clase și le-am dat propriile lor responsabilități unice - și, prin urmare, propria lor cauză conținea motive să se schimbe.
Doriți să gestionați plățile în mod diferit? Acum nu va trebui să atingeți Spectru
clasă. Trebuie să plătiți sau să plătiți în mod diferit? Din nou, nu este nevoie să deschideți fișierul pentru Spectru
. Operațiunile de imprimare sunt acum operațiunile de operare - de unde aparțin. Asta e. Nu prea complicat, cred, dar cu siguranta unul dintre cele mai comune mirosuri ar trebui sa inveti sa te descurci devreme.
Sper că ați ajuns la punctul în care vă simțiți gata să încercați aceste refactorizări în propriul cod și să aveți un cod de identificare mai ușor ce miroase în jurul vostru. Feriți-vă că tocmai am început, dar că ați abordat deja câteva dintre cele mari. Pun pariu că nu era așa de complicat cum credeai odată!
Sigur, exemplele din lumea reală vor fi mult mai provocatoare, dar dacă ați înțeles mecanismele și modelele pentru a vedea mirosurile, veți fi cu siguranță capabili să vă adaptați rapid complexității realiste.