Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fat models must die

Fat models must die

Il concetto “Fat Models, Skinny controller” è da sempre uno dei cavalli di battaglia di Rails ed è uno dei principi fondamentali intorno a cui ruota il suo stack. Purtroppo, seguire ciecamente questo pattern spesso porta ad una crescita smisurata delle responsabilità dei modelli, che col passare del tempo e dei commit si trasformano in matasse di codice ingarbugliato e ingestibile.

In questo talk verranno esplorate differenti metodologie che si possono seguire nella pratica per mantenere il controllo del proprio progetto. Si descriveranno i pattern più diffusi proposti dalla community Rails per risolvere il problema della crescita del codice nel medio-lungo periodo: incominciando con concerns e presenters per passare a service objects e DCI, verranno spiegati i pregi dell’utilizzare pratiche più OOP per gestire con soddisfazione la complessità delle nostre applicazioni.

Lanyrd: http://lanyrd.com/2013/rubyday13/sckdyg/

Stefano Verna

June 14, 2013
Tweet

More Decks by Stefano Verna

Other Decks in Programming

Transcript

  1. Model View Controller Come tutti sappiamo, un progetto Rails pulito

    ci mette a disposizione l'MVC, e all'inizio di un progetto web, l'MVC è fa esattamente al caso nostro. I modelli trattano col database e i controller caricano i dati da mostrare nelle viste.
  2. Skinny controller, fat model Di tanto in tanto applichiamo il

    concetto di "Fat Models, Skinny Controller", per estrarre pezzi di logica dai controller e spostarli nel modello, limitando la duplicazione del codice e il suo riuso. Fin qui, tutto perfetto.
  3. $ wc -l app/models/* | sort -rn | sed -n

    '2,4p' 625 app/models/activity.rb 407 app/models/task.rb 364 app/models/user.rb Col passare del tempo, la storia però lentamente cambia. Le features aumentano, i metodi "extra" presenti nei modelli crescono inesorabilmente, fino ad arrivare a trovare modelli da più di 600 righe di codice.
  4. Owners Of our application business logic I modelli quindi a

    poco a poco diventano a tutti gli effetti i "possessori" della logica della nostra applicazione.
  5. Knows too much or does too much God Object Formalmente

    oggetti di questo tipo si chiamano God Objects. Fanno molte cose diverse, hanno molte facce e sono onnipresenti.
  6. Is it wrong? Prima di uscire fuori dai binari Rails,

    siamo sicuri sia sbagliato? È una domanda più che lecita.
  7. Software Design Questa domanda ovviamente è legata al tema del

    design del software. Non siamo i primi ad occuparci di questo problema; una trentina d'anni di letteratura possono aiutarci a trovare una risposta.
  8. Robert C. Martin Uncle Bob © Mike Clark Uncle Bob

    dal 1970 tratta il problema della programmazione ad oggetti
  9. Two values of software Recentemente, nella sua serie "Clean Coders",

    ha parlato di quelli che secondo lui sono i due valori fondamentali del software.
  10. Software meets the current needs of the current user 2nd

    value Secondo Uncle Bob, il secondo valore più importante del software è quello di soddisfare i bisogni dell’utente, ovvero che implementi le funzionalità per il quale il software stesso è stato pensato.
  11. Software is easy to change 1st value Per Uncle Bob

    il valore primario del software è la sua capacità facilitare il proprio cambiamento!
  12. Agile In effetti, il movimento agile ci insegna che i

    requisiti cambiano in continuazione. Un software profittevole è un software mantenibile nel tempo, che sia capace di adattarsi ai continui cambiamenti di specifiche. Ed è proprio il suo design ad influire maggiormente sulla sua capacità di adattarsi.
  13. SOLID Principles Uncle Bob ha pensato anche a definire serie

    di principi concreti a cui rifarsi durante la scrittura del codice per ottenere codice malleabile. Questi principi prendono il nome di principi SOLID.
  14. Single Responsibility Principle Ai fini della nostra domanda iniziale possiamo

    fermarci al primo, il più importante: il principio della Singola Responsabilità.
  15. SRP Every class should have a single responsibility, and that

    responsibility should be entirely encapsulated by the class. Ogni classe dovrebbe avere una, e una sola responsabilità, e quella responsabilità dev’essere di pertinenza esclusiva di quella classe.
  16. A class should have one, and only one, reason to

    change SRP Il concetto di "responsabilità" è abbastanza soggettivo e quindi pensiamo a una regola pratica: una classe dovrebbe avere un solo motivo per cui essere modificata.
  17. let’s talk about Rails (again) Ottimo. Quindi ricominciamo a parlare

    di Rails, dopo la spolverata di letteratura che ci siamo fatti.
  18. ActiveRecord::Base Persist and represent DB rows Le classi che derivano

    da ActiveRecord::Base rappresentano i dati persistiti sul database. Quindi la gestione della persistenza dovrebbe essere la loro unica responsabilità, l'unico motivo per cui dovremmo andare a modificarle.
  19. Control HTTP requests/ responses at high level ActionController::Base Invece l’unica

    responsabilità dei controller Rails dovrebbe essere la gestione ad alto livello di richieste e risposte HTTP. Dovrebbero instradare domande e risposte, niente di più.
  20. Where do we put business logic? Ok, bene, ma quindi

    se ne' il modello ne' il controller dovrebbero contenere altro, dove possiamo mettere la nostra business logic? Come possiamo suddividere le nostre responsabilità?
  21. ActiveSupport::Concern Dai tempi del suggerimento “skinny controller, fat models”, @dhh

    ha proposto un’unica ulteriore soluzione, i Concern, implementata nella libreria ActiveSupport. Di cosa si tratta e come si dovrebbe usare?
  22. require 'active_support/concern' module NameGreeter extend ActiveSupport::Concern included do attr_accessor :name

    end def greet puts "Hi! I'm #{name}!" end module ClassMethods def build(name) self.new.tap do |instance| instance.name = name end end end end class User include NameGreeter end user = User.build("Mark") user.greet >> “Hi! I’m Mark!” Un Concern è una sorta di mixin evoluto, in grado di estendere una classe sia con metodi di istanza, che metodi di classe, che valutare parti di codice come fossimo dentro alla definizione della classe.
  23. class Document < ActiveRecord::Base include Taggable include Visible include Dropboxed

    include Omasake end In questo documento, vediamo come vengano importati alcuni concern come taggable, visible e dropboxed.
  24. app/models/concerns app/controllers/concerns Rails 4 L'idea per Rails 4 è quella

    di invitare gli sviluppatori ad usare i concerns sia a livello di modello che di controller, piazzandoli in app/models/concerns e app/controllers/concerns, che faranno parte dei load paths di default.
  25. Rails Drama Ma come è normale nel mondo Rails, il

    suggerimento non è stato ben accolto da tutti: non tutti pensano che i Concern siano la migliore soluzione possibile al problema della crescita incontrollata delle nostre applicazioni.
  26. Models are still God Objects! Runtime La prima critica mossa

    è che i concern a livello logico sono separati tra di loro, ma in realtà, a runtime, il modello a tutti gli effetti continua ad essere un god object con più responsabilità, in grado di controllare buona parte dell'applicativo.
  27. class User include NameGreeter end User.ancestors.include? NameGreeter >> true La

    seconda critica mossa è più teorica, ma di uguale importanza, e riguarda la natura dei mixin. Se guardiamo la lista delle superclassi di un oggetto che include un modulo, ritroviamo il modulo stesso.
  28. Mixins are a form of Inheritance Quindi ci rendiamo conto

    che l’inclusione di un modulo, tecnicamente, non è altro che una forma di ereditarietà multipla...
  29. Gang of Four Favor object composition to class inheritance Ritornando

    alla letteratura tecnica, nel 1995 la Gang of Four ci dice di preferire la composizione degli oggetti rispetto alla ereditarietà.
  30. is-a Inheritance has-a Composition A cat is a pet A

    cat has a tail L’ereditarietà rappresenta una relazione "io-sono" con la superclasse, mentre la composizione di oggetti rappresenta una relazione meno rigida, di tipo "io-posseggo". Ma perché la GoF dice di preferire la composizione?
  31. Easier to change Spesso ci si ritrova ad avere classi

    che inizialmente sembrano appartenere ad una stessa famiglia, ma che col tempo assumono comportamenti sempre più discordanti. Con l'ereditarietà in questi casi si è costretti a mantenere l'interfaccia della superclasse, la composizione no.
  32. Quindi proviamo ad immaginarci una applicazione Rails, che all'aumentare della

    complessità intrinseca, invece che crescere in verticale nelle solite 2-3 classi
  33. Cresca “orizzontalmente”, distribuendo la logica in tante piccole classi, ognuna

    con una singola responsabilità, una interfaccia ristretta ben definita verso il mondo esterno, ponendo l’accento su una fitta comunicazione tra questi oggetti.
  34. OOP in RUBY Questa è la vera programmazione a oggetti,

    in grado di rendere la nostra applicazione mantenibile nel futuro. Se iniziamo a lavorare effettivamente ad oggetti - e Ruby è uno dei liguaggi migliori per farlo - possiamo iniziare a parlare di...
  35. Design pattern. Un pattern è una soluzione generale a un

    problema riconosciuto, un modello da applicare per risolvere alcune problematiche ricorrenti durante la progettazione e lo sviluppo del software. Il concetto di design pattern è stati introdotto proprio dalla Gang of Four nel 1995, e parecchi pattern da loro descritti sono tranquillamente riutilizzabili nei nostri applicativi Ruby on Rails, con ottimi risultati in termini di flessibilità e testing.
  36. We need to calculate stats about football matches Scenario Immaginiamo

    un sito che gestisca scommesse sportive: abbiamo bisogno di calcolare statistiche relative a risultati calcistici.
  37. class Match < ActiveRecord::Base def first_half_win? fh_made > fh_taken end

    def second_half_win? sh_made > sh_taken end def first_half_loss?; ... ; end def second_half_loss?; ... ; end def first_half_draw?; ... ; end def second_half_draw?; ... ; end end Il nostro modello Match, si occupa di salvare il risultato di una singola partita di calcio. La prima soluzione che ci viene in mente è di aggiungere una serie di metodi direttamente nel nostro modello. Però, in questo modo leghiamo la logica del pareggio al modello. Come si può fare altrimenti?
  38. Value Object Il primo pattern di cui parliamo è chiamato

    Value Object. I Value Objects sono semplici entità che rappresentano un singolo dato. Esempi possono essere numeri, date o numeri telefonici, o nel nostro caso, risultati sportivi.
  39. Equality is dependent on their value rather than an identity

    Property Due value object sono uguali se contengono lo stesso dato; inoltre spesso sono immutabili, quindi una volta creati non possono più essere modificati.
  40. class Score < Struct.new(:goals_made, :goals_taken) def win? goals_made > goals_taken

    end def draw? goals_made == goals_taken end def loss? goals_made < goals_taken end end
  41. class Match < ActiveRecord::Base def first_half_score Score.new(fh_made, fh_taken) end def

    second_half_score Score.new(sh_made, sh_taken) end end In questo caso la ripetizione di codice per gestire primo e secondo tempo era evidente, ma è possibile creare Value Objects in moltissimi casi. Testare value object ovviamente è molto più rapido rispetto a testare modelli.
  42. Struct #hash #eql? La classe Struct nella standard library di

    Ruby implementa già tutti i metodi necessari a forzare l'uguaglianza tra due istanze che hanno i medesimi valori per tutti i campi.
  43. We are closing an e- commerce purchase using Order, Customer

    and Product objects Scenario Nuovo scenario: durante la fase di pagamento in un e-commerce, dobbiamo far interagire una serie di modelli, per esempio Ordine, Cliente e Prodotto
  44. Action is complex Questi sono esempi di casi che rivelano

    una forte esigenza di trovare un luogo nel quale inserire una serie di istruzioni eterogenee. Quando una azione inizia a diventare complessa...
  45. Action interacts with external services O abbiamo bisogno di interagire

    con servizi di terze parti fuori dal nostro controllo...
  46. class ItemsController < ApplicationController def add_to_cart item = Item.find(params[:id]) AddToCart.new(current_user,

    item).execute! end end Vediamo un esempio. Nel controller si delega l’intera logica di aggiunta al carrello ad un nuovo oggetto. Il testing è semplificato moltissimo, in quanto a questo punto basta verificare che il controller chiami il metodo `execute!` dell’oggetto AddToCart.
  47. class AddToCart < Struct.new(:user, :item) def execute! cart.add_item!(item) end private

    def cart user.active_cart end end Il command object prende in carico gli oggetti che rappresentano il contesto nel quale agire (utente e prodotto), e si occupa di specificare le operazioni per realizzare l’interazione.
  48. Encapsulate all the logic for a specific scenario I vantaggi?

    Danno la possibilità di incapsulare tutta la logica legata ad uno specifico scenario in una sola classe, liberando sia i controller che i modelli da responsabilità che non gli competono, facendo quindi diminuire le dipendenze dirette...
  49. Reuse logic in multiple contexts Permettono inoltre di riutilizzare la

    stessa logica in contesti diversi (pensiamo sempre all’aggiunta di una interfaccia CLI alla nostra applicazione).
  50. Filter events by full-text search, distance and category Scenario Altro

    scenario: immaginiamoci un geo-blog, con una mappa nel quale sono localizzati una serie di eventi. Vogliamo dare la possibilità all'utente di ricercare in full-text, per distanza rispetto ad una coordinata e filtrare per categorie.
  51. class EventsController < ApplicationController def index @events = Event.published if

    params[:lat] && params[:lng] && params[:distance] @events = @events.near(lat, lng, distance) end if params[:query].present? @events = @events.matching(params[:query]) end if params[:category_id].present? @events = @events.in_category(params[:category_id]) end end end Il Rails Way® è quello di creare uno scope per ogni tipologia di ricerca, e effettuando un chaining tra gli scope a livello di controller, a seconda dei parametri ricevuti. Questo non è necessariamente sbagliato, ma quando la logica inizia a diventare complessa, e le condizioni ad aumentare, possiamo pensare di introdurre..
  52. Responsible for returning a result set based on business rules

    Purpose Lo scopo principale di un query object è quello di estrarre dal modello la logica di composizione delle query SQL, quando queste diventano troppo complesse, o legate ad un specifico scopo
  53. class EventsQuery < OpenStruct def scope(scope = Events.scoped) scope =

    scope.published if lat && lng && distance scope = scope.near(lat, lng, distance) end if query.present? scope = scope.matching(query) end if category_id.present? scope = scope.of_category(category_id) end scope end end In pratica, encapsuliamo tutta la logica di costruzione della query SQL in una singola classe. In questo esempio, la classe deriva da OpenStruct per poter accettare un hash di attributi e avere quei valori disponibili in lettura.
  54. DRY up your views Nulla vieta di sfruttare un Query

    Object anche a livello di vista, in modo da far riempire in automatico il form di ricerca sulla base della ricerca corrente.
  55. class EventsQuery < OpenStruct include ActiveModel::Conversion extend ActiveModel::Naming def persisted?

    false end # ... end Basta includere una serie di moduli che Rails si aspetta di trovare implementati,
  56. <%= form_for @query, as: :query, url: events_path, method: :get do

    |f| %> <%= f.text_field :query %> <% # ... %> <% end %> e voilà.
  57. Send mail notifications after the creation of new comments Scenario

    Altro scenario: vogliamo propagare notifiche via mail alla creazione di nuovi commenti. Un classicone.
  58. class Comment < ActiveRecord::Base after_create :send_notification! private def send_notification! AppMailer.comment_submission(self).deliver

    end end Se dovessimo farlo a livello di hook ActiveRecord faremmo così. Qual’è il problema?
  59. Model hooks are for data integrity Gli hook ActiveRecord dovrebbero

    essere usati per mantenere una validità dei dati nel nostro sistema, non a sviluppare logiche business laterali.
  60. Do we always want notifications? Oltre tutto, siamo proprio sicuri

    che tutte le volte che venga creato un commento, vogliamo una notifica? Pensiamo ad un import di massa.
  61. Command Object! Aspetta, ma questo è un caso perfetto per

    un command object! Vero, ma possiamo rendere "invisibile" la sua presenza al controller, implementandolo a mo’ di..
  62. Proxy Il decoratore permette di aggiungere funzionalità ad un metodo

    di un secondo oggetto, senza modificare quello originario o modificarne l’interfaccia verso l’esterno. In qualche modo possiamo pensarlo come ad un proxy trasparente lato utilizzatore.
  63. class CommentsController < ApplicationController def create @comment = Comment.new(params[:comment]) if

    @comment.save redirect_to blog_path, notice: "Comment was posted." else render "new" end end end
  64. class CommentsController < ApplicationController def create @comment = Notifier.new(Comment.new(params[:comment])) if

    @comment.save redirect_to blog_path, notice: "Comment was posted." else render "new" end end end
  65. class Notifier < Struct(:comment) def save comment.save && send_notification! end

    private def send_notification! AppMailer.comment_submission(comment).deliver true end end class Comment < ActiveRecord::Base end
  66. Exhibit a.k.a. Presenter La stessa tecnica di composizione e replica

    di signature che troviamo nei Decorator, può essere sfruttata anche in altri contesti, per esempio quello della presentazione. Parliamo di un concetto abbastanza noto, quello dei Presenter o, come sarebbe meglio chiamarli, Exhibit.
  67. The primary goal of exhibits is to insert a model

    object within a rendering context. Purpose Lo scopo principale di un exhibit è un modello ad un contesto in cui viene presentato.
  68. It’s a decorator Unrecognized messages are passed through to the

    underlying object Il comportamento base di un presenter è quello di inoltrare ogni chiamata che riceve all'oggetto originale, fungendo quindi da proxy completo, su ogni metodo.
  69. Augment models behaviour when it is needed Ma sfruttando la

    conoscenza del contesto, un presenter può decidere di ridefinire uno o più metodi dell'oggetto originale per migliorarne la presentazione, così come di aggiungere nuovi metodi non distruttivi.
  70. - user = UserPresenter.new(@user, self) = user.link_to = user.bio =

    user.gravatar Attraverso l’uso presenter, la vista è molto più intuitiva e semplice da leggere.
  71. require 'delegate' class UserPresenter < SimpleDelegator def initialize(user, context) @context

    = context super(user) # Set up delegation end def gravatar_url md5 = Digest::MD5.hexdigest(email.downcase) "http://gravatar.com/avatar/#{md5}.png" end def gravatar @context.image_tag(gravatar_url) end end L'oggetto della standard library SimpleDelegator ha già tutto quello che serve per implementare un pattern di questo tipo. Un oggetto SimpleDelegator permette di inoltrare ogni metodo non esistente all'oggetto originario. Nel nostro caso è sufficiente fare un override del costruttore per permettere di passare anche il contesto.
  72. Seamless integration Il vantaggio di questo pattern è la sua

    facilità di inclusione in progetti già avviati.
  73. OOP alternative to Rails helpers Lo scopo quindi è quello

    di offrire una alternativa più ad oggetti rispetto agli helper procedurali che Rails ci mette a disposizione.
  74. DCI Data, Context & Interaction Il DCI è sarà l’ultimo

    pattern che consideriamo ed è anche stato l’ultimo in ordine temporale ad essere stato preso in considerazione dalla community Rails.
  75. Separate what the system is (data) from what the system

    does (behaviour). Purpose Il DCI nasce come tentativo di separare componenti del nostro codice che cambiano più frequentemente, da quelle che invece vengono modificate meno frequentemente. Tipicamente, la parte meno soggetta a cambiamenti sono i dati che contiene il nostro sistema, sono i comportamenti del nostro sistema e i suoi requisiti a subire il maggior numero di modifiche.
  76. Use case Con il DCI, il comportamento del sistema viene

    rappresentato e suddiviso in casi d’uso, intesi proprio nell’accezione UML.
  77. Context Roles Nel DCI le classi che descrivono singoli casi

    d’uso prendono il nome di contesti, ed il compito di ciascun contesto è, proprio come nell’UML, quello di definire una serie di ruoli, o attori, che sono quelli richiesti durante la sua esecuzione.
  78. Transfer money between two accounts Scenario L’esempio più semplice è

    quello in cui si trasferisce una certa quantità di denaro da un conto ad un altro.
  79. class Account < Struct.new(:owner, :balance) end account = Account.new("Bob", 500.0)

    Nel pratico, il DCI si aspetta di avere dei dati intesi come oggetti “stupidi”, che di per sè non implementano alcun particolare comportamento se non quello della persistenza.
  80. bob_account = Account.new("Bob", 100.0) alice_account = Account.new("Alice", 50.0) context =

    MoneyTransferContext.new( bob_account, alice_account ) context.transfer(10.0) puts bob_account.amount # => 90.0 puts alice_account.amount # => 60.0 Dall’esterno, l’utilizzo di un context DCI è molto simile a quello di Command Object.
  81. class MoneyTransferContext < Struct.new(:source, :destination) module SourceRole def withdraw(amount) self.balance

    -= amount end end module DestinationRole def deposit(amount) self.amount += amount end end end All’interno però si iniziano a vedere le differenze, perchè il primo compito di un contesto DCI è proprio quello di definire i ruoli del caso d’uso. Questi ruoli sono a tutti gli effetti dei mixin, che verranno applicati a runtime sui dati puri che fungeranno da attori all’interno del caso d’uso.
  82. #extend Se in altri linguaggi, l’applicazione a runtime di una

    serie di metodi ad una sola istanza di classe sarebbe difficile o impossibile, in Ruby il metodo extend ci permette di realizzare il comportamento in una riga di codice
  83. bob_account = Account.new("Bob", 100.0) alice_account = Account.new("Alice", 50.0) bob_account.extend SourceRole

    bob_account.withdraw(amount) # => 90.0 alice_account.withdraw(amount) # NoMethodError: undefined method...
  84. class MoneyTransferContext < Struct.new(:source, :destination) # ... def transfer(amount) source.extend

    SourceRole destination.extend DestinationRole source.withdraw(amount) destination.deposit(amount) end end L’ultimo passaggio all’interno del context è quello di definire il metodo che esegue il comando. La prima fase è sempre quella di applicazione dei ruoli ai dati che vengono passati, la seconda è quella di comando effettiva, nella quale i dati interpretano il ruolo e viene espressa la logica di interazione vera e propria.
  85. Domain model Use case Role Data Context Interaction Abbiamo quindi

    completato l’acronimo. Con Data si intendono i modelli/entità del nostro dominio, con Context specifici casi d’uso, e con Interaction l’insieme degli attori interni ad ogni caso d’uso.
  86. Give first-class status to system behavior Vantaggi? Dare massima evidenza

    i casi d’uso presenti sistema, quindi ottenere un mapping diretto tra valori business (espressi come use case) e codice
  87. Improved spatial locality Migliorare la vicinanza spaziale dei componenti: i

    dati sono insieme ai dati, i comportamenti sono isolati e il contesto manipola il tutto ad un livello di astrazione molto alto.
  88. MVC/ActiveRecord cannot solve everything Tuttavia, nè il paradigma MVC nè

    ActiveRecord possono risolvere tutti i nostri problemi.
  89. When in doubt, Composition is your ally Usare tanti oggetti

    piccoli e comporli insieme è una tecnica che funziona.
  90. Don’t be afraid of new objects Creare nuovi oggetti, anche

    slegati dal database, non deve farci paura.
  91. Think of Rails as a dependency Rails deve essere visto

    per quello che è, una dipendenza, esterna, non il cuore della nostra applicazione, che dovrebbe venire definito a parte.
  92. Whatever works best in your domain! Non sentiamoci legati a

    sfruttare necessariamente un pattern, l’importante è seguire quello che l’applicazione chiede per meglio astrarre e descrivere il suo comportamento!
  93. Sandi Metz Practical Object Oriented Design in Ruby © Sebastian

    Delmont Può aiutare, almeno all’inizio, esercitarsi a seguire 4 consigli pratici di Sandi Metz (leggetevi il suo libro POODR se non l’avete già fatto)
  94. Classes can be no longer than one hundred lines of

    code. 1st rule Le classi non devono superare le 100 righe.
  95. Methods can be no longer than five lines of code.

    2nd rule I metodi non devono essere più lunghi di 5 righe, spazi esclusi :)
  96. Pass no more than four parameters into a method. Hash

    options are parameters. 3rd rule Non passare mai più di quattro parametri ad un metodo. Le chiavi di un hash sono parametri.
  97. It’s tough, but rewarding! All’inizio è dura, non sempre ha

    senso seguirli con rigore, ma è un ottimo modo per acquistare il giusto approccio!