Refactoring codice di elaborazione dei pagamenti in bizantino con un budget limitato [chiuso]

8

Ho lavorato su una grande applicazione Ruby on Rails per diversi anni. È stato ereditato in cattivo stato, ma la maggior parte degli errori di produzione sono stati risolti con il tempo. Ci sono alcune sezioni che non sono state toccate come il codice di elaborazione dei pagamenti. Il codice funziona per la maggior parte, tranne che ogni volta che un addebito viene negato dal processore di pagamento, l'utente riceve un errore 500 invece di un messaggio utile. Mi piacerebbe refactoring il codice per renderlo più facile da mantenere. Fornirò una breve descrizione di come funziona.

Ho rimosso tutto il codice di gestione degli errori dai seguenti snippet.

Il labirinto inizia in un controller:

  def submit_credit_card
    ...
    @credit_card = CreditCard.new(params[:credit_card].merge(:user => @user))
    @credit_card.save
    ...
    @submission.do_initial_charge(@user)
    ...
  end

Quindi nel modello Submission :

  def do_initial_charge(user)
    ...
    self.initial_charge = self.charges.create(:charge_type => ChargeType.find(1), :user => user)
    self.initial_charge.process!
    self.initial_charge.settled?
  end

Nel modello Charge :

  aasm column: 'state' do
    ...
    event :process do
      transitions :from => [:created, :failed], :to => :settled, :guard => :transaction_successful?
    end
    ...
  end

  def initialize(*params)
    super(*params)
    ...
    self.amount = self.charge_type.amount
  end

  def transaction_successful?
    user.reload
    credit_card = CreditCard.where(user_id: user_id).last
    cct = self.cc_transactions.build(:user => user, :credit_card => credit_card, :cc_last_four => credit_card.num_last_four, :amount => amount, :charge_id => id)
    cct.process!
    if self.last_cc_transaction.success
      self.update_attribute(:processed, Time.now)
      return true
    else
      self.fail!
      return false
    end
  end

Ci sono un sacco di bit discutibili sopra come ricaricare user e trovare l'ultimo CreditCard piuttosto che passare in quello appena salvato. Anche questo codice dipende da un ChargeType caricato dal database con un ID hard-coded.

In CcTransaction continuiamo lungo il percorso:

  def do_process
    response = credit_card.process_transaction(self)
    self.authorization = response.authorization
    self.avs_result    = response.avs_result[:message]
    self.cvv_result    = response.cvv_result[:message]
    self.message       = response.message
    self.params        = response.params.inspect
    self.fraud_review  = response.fraud_review?
    self.success       = response.success?
    self.test          = response.test
    self.response      = response.inspect
    self.save!
    self.success
  end

Tutto ciò sembra fare è salvare un record nella tabella di database cc_transactions . L'elaborazione del pagamento effettiva viene eseguita nel modello CreditCard . Non ti annoierò con i dettagli di quella classe. Il lavoro effettivo viene eseguito da ActiveMerchant::Billing::AuthorizeNetCimGateway .

Quindi abbiamo almeno 5 modelli coinvolti ( Submission , Charge , ChargeType , CcTransaction e CreditCard ). Se dovessi farlo da zero, userei solo un singolo modello Payment . Ci sono solo 2 tipi di addebito, quindi vorrei codificare questi valori come variabili di classe. Non memorizziamo i dettagli della carta di credito, quindi il modello non è necessario. Le informazioni sulla transazione possono essere memorizzate nella tabella payments . I pagamenti non riusciti non devono essere salvati.

Potrei entrare e fare questo refactoring abbastanza facilmente tranne che per il requisito che nulla dovrebbe mai andare storto sul server di produzione. Ciascuna delle classi ridondanti ha molti metodi che possono essere chiamati da qualsiasi punto della base di codice. Esiste una suite di test di integrazione ma la copertura non è al 100%.

Come dovrei procedere per refactoring questo mentre assicura che non si rompa nulla? Se passassi attraverso le 5 classi di pagamento e grep ed ogni metodo per scoprire dove sono chiamate, c'è un'alta probabilità di perdere qualcosa. Il client è già utilizzato per l'esecuzione del codice corrente e l'introduzione di nuovi bug non è accettabile. Oltre ad aumentare la copertura del test al 100%, esiste un modo per ridefinirlo con la certezza che nulla si romperà?

    
posta Reed G. Law 30.11.2015 - 06:41
fonte

1 risposta

20

Considera se questo codice ha davvero bisogno di refactoring . Il codice potrebbe essere sgradevole ed esteticamente dispiaciuto per te come sviluppatore di software, ma se funziona, probabilmente non dovresti riprogettarlo. E in base alla tua domanda, sembra che il codice funzioni in gran parte.

Questo articolo classico di Joel on Software evidenzia tutti i rischi legati alla necessità di riscrivere inutilmente il codice. È un errore costoso, ma molto allettante. L'intera cosa vale la pena di leggere prima, ma questo passaggio sulla complessità apparentemente inutile sembra particolarmente appropriato:

Back to that two page function. Yes, I know, it's just a simple function to display a window, but it has grown little hairs and stuff on it and nobody knows why. Well, I'll tell you why: those are bug fixes. One of them fixes that bug that Nancy had when she tried to install the thing on a computer that didn't have Internet Explorer. Another one fixes that bug that occurs in low memory conditions. Another one fixes that bug that occurred when the file is on a floppy disk and the user yanks out the disk in the middle. That LoadLibrary call is ugly but it makes the code work on old versions of Windows 95.

Each of these bugs took weeks of real-world usage before they were found. The programmer might have spent a couple of days reproducing the bug in the lab and fixing it. If it's like a lot of bugs, the fix might be one line of code, or it might even be a couple of characters, but a lot of work and time went into those two characters.

Sì, il codice è inutilmente complesso. Ma ancora, alcuni dei passaggi che sembrano non necessari potrebbero essere lì per una ragione. E se vai a riscriverlo, dovrai imparare di nuovo tutte quelle lezioni.

Ricaricare l'utente, ad esempio, sembra inutile: che è esattamente il motivo per cui dovresti preoccuparti di cambiarlo. Nessuno sviluppatore (anche uno cattivo) avrebbe introdotto un passo del genere nella sua progettazione iniziale. È stato quasi sicuramente messo lì per risolvere qualche problema. Forse è un problema che il refactoring del design "corretto" eliminerà ... ma forse no.

Inoltre, un altro piccolo punto, non sono convinto dalla tua opinione che non è necessario salvare i pagamenti falliti. Potrei pensare a due motivi validi per farlo: registrare prove di possibili frodi (ad esempio qualcuno che prova vari numeri di carta) e supporto clienti (un cliente afferma di continuare a cercare di pagare, e non funziona ... vorrai prove di qualsiasi tentativo di pagamento per aiutare a risolvere questo problema). Questo mi fa pensare che forse non hai pienamente pensato ai requisiti del sistema, e non sono così semplici come credi che siano.

Risolvi i singoli problemi senza cambiare radicalmente il design. Hai menzionato un problema: c'è un errore 500 quando il pagamento non riesce. Questo problema dovrebbe essere facile da risolvere, probabilmente solo aggiungendo una o due righe nella posizione corretta. Non è una logica sufficiente per strappare ogni cosa a parte per rendere il design "corretto".

Potrebbero esserci altri problemi, ma se il codice funziona al 99% delle volte, è improbabile che questi siano problemi fondamentali che richiedono una riscrittura.

Se il design attuale è incorporato in tutto il codice, potrebbe anche essere necessario spendere una buona quantità di sforzi per risolvere un problema senza modificare il design.

In alcuni casi potrebbe essere necessario effettuare una riprogettazione più ampia. Ma ciò richiederebbe una motivazione più strong che hai dato finora. Ad esempio, se si prevede di sviluppare ulteriormente il modello di pagamento e introdurre nuove funzionalità significative, la pulizia del codice avrebbe dei vantaggi. O forse il design contiene un difetto di sicurezza fondamentale che deve essere risolto.

Ci sono motivi per rifattorizzare una grande porzione di codice. Ma se lo fai, aumenta la copertura del test al 100%. Questo è probabilmente qualcosa che ti farà risparmiare tempo complessivo .

    
risposta data 30.11.2015 - 07:56
fonte

Leggi altre domande sui tag