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

Build Complex Domains in Rails

Mike AbiEzzi
September 25, 2014

Build Complex Domains in Rails

Rails models are simple, but your domain’s models might not be as simple as Rails would like them to be.

Modeling large, complex domains "the Rails way” can cause some serious pain. Ruby and Rails are supposed to make developers happy. Let's not allow “the Rails way” and complex domains to take the “happy” out of ruby development.

Join me as I’ll walk through a set of easy to implement Domain Driven Design (DDD) pointers. The goal is to make sure your model’s business logic stay under control no matter how complex your domain is or gets. Your application will be able to sustain steady growth and dramatically reduce business rule related defects, all the while, staying easy to grok.

I'll walk you through:

How communicating the domain properly will make an imprint on your codebase and product.
How creating boundaries around clusters of models will make your code easier to understand, manage, and interact with.
How immutable objects eliminate unnecessary complexities.
How data store access expresses the domain's intentions.
How to represent natural business transactions between models.

Mike AbiEzzi

September 25, 2014
Tweet

Other Decks in Programming

Transcript

  1. AIM OF THIS TALK ✘ Custom DDD architectures! ✘ DDD

    design patterns! ✘ Advanced DDD topics (e.g. Repository Pattern) (e.g. Bounded Context)
  2. AIM OF THIS TALK ✘ Custom DDD architectures! ✘ DDD

    design patterns! ✘ Advanced DDD topics (e.g. Repository Pattern) (e.g. Bounded Context) custom is expensive!
  3. AIM OF THIS TALK ✓ DDD principles ✓ DDD+Rails w/o

    a heavy investment we want to hit the ground running!
  4. 1. Defining the domain! 2. Communicating the domain! 3. Relationships

    between models! 4. Aggregates! 5. Data access! 6. Value Objects! 7. Domain Services
  5. Developer 1 n App Store Customer App n n Purchase

    Version 1 n 1..6 Screenshot 1 brainstorm
  6. Developer 1 n App Store Customer App Install n n

    n n Purchase Version 1 n 1..6 Screenshot 1 brainstorm
  7. Developer 1 n App Store Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 brainstorm
  8. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 refine
  9. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Developer/ Company refine
  10. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Developer/ Company Seller refine
  11. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller refine
  12. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller refine
  13. COMMUNICATION Domain! Expert Software! Expert brainstorm → draw diagrams →

    ! speak out assumptions → let them correct you → refine
  14. RESPONSIBILITIES “Domain experts should object to terms or structures that

    are awkward or inadequate to convey domain understanding” -- Eric Evans
  15. RESPONSIBILITIES “Domain experts should object to terms or structures that

    are awkward or inadequate to convey domain understanding” “Developers should watch for ambiguity or inconsistency that will trip up design.” -- Eric Evans
  16. UBIQUITOUS LANGUAGE “a common, rigorous language between developers and users

    […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  17. UBIQUITOUS LANGUAGE User “a common, rigorous language between developers and

    users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  18. UBIQUITOUS LANGUAGE Product! Owner User “a common, rigorous language between

    developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  19. UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User “a common, rigorous

    language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  20. Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User “a common,

    rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  21. Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User Designer “a

    common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  22. Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User Designer

    “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  23. Code Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User

    Designer “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  24. Code Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User

    Designer “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler Ensure one consistent language
  25. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  26. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end relationships
  27. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end ✓ relationships
  28. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end ✓ ✘ relationships
  29. App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  30. 1 n App Store Developer 1 n Customer App Install

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  31. 1 n App Store Developer 1 n Customer App Purchase

    Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  32. 1 n App Store Developer 1 n Customer App Purchase

    Comment 1 n Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  33. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  34. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  35. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  36. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  37. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  38. release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Submit a new release of an app
  39. app.submit_release(“1.1.0”, screenshots: ["shot1.png", "shot2.png"]) release = Release.new( version_major: 1, version_minor:

    1, ...) release.screenshots << [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Submit a new release of an app
  40. app.submit_release(“1.1.0”, screenshots: ["shot1.png", "shot2.png"]) release = Release.new( version_major: 1, version_minor:

    1, ...) release.screenshots << [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Describe domain behaviors with methods Submit a new release of an app
  41. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 AGGREGATE Release! version_number
  42. class App < ActiveRecord::Base ... ! def submit_release ... def

    approve_release ... def flag_for_abuse ... ! def mark_as_staff_favorite ... ! end
  43. class App < ActiveRecord::Base ... ! def submit_release ... def

    approve_release ... def flag_for_abuse ... ! def mark_as_staff_favorite ... ! end Tells a story of how the domain works
  44. Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  45. Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  46. Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  47. class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end
  48. class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end One expressive point of entry
  49. class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end One expressive point of entry Aggregate roots are the domain’s only ! point of entry for data access.
  50. ▾ app/! ▾ models/! ▾ apps/! app.rb! release.rb! review.rb! screenshot.rb!

    version_number.rb! ▸ customers/! ▸ sellers/ Aggregate Aggregate Aggregate
  51. ENTITY VALUE OBJECT a thing describes a thing class Customer

    class Name Me
 “Mike AbiEzzi” My little cousin
 “Mike AbiEzzi”
  52. ENTITY VALUE OBJECT a thing describes a thing unique independent!

    of attributes has a lifecycle class Customer class Name
  53. ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes has a lifecycle class Customer class Name
  54. ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes has a lifecycle class Customer class Name “Mike AbiEzzi” “Mike AbiEzzi”
  55. ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes immutable has a lifecycle class Customer class Name
  56. ENTITY VALUE OBJECT a thing describes a thing can change

    state doesn’t reference anything unique independent! of attributes immutable has a lifecycle class Customer class Name
  57. ENTITY VALUE OBJECT a thing describes a thing can change

    state doesn’t reference anything unique independent! of attributes avoids design complexities immutable has a lifecycle class Customer class Name
  58. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Release! version_number What’s what?
  59. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Entity Release! version_number What’s what?
  60. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Entity Release! version_number Entity What’s what?
  61. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what?
  62. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what? ?
  63. App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what? ? Value
  64. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  65. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  66. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  67. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  68. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  69. class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  70. class VersionNumber attr_reader :major, :minor, :build ... def next_major_version new

    VersionNumber( major + 1, minor, build) end ! def next_minor_version ... def next_build_version ... end VALUE OBJECT factory methods
  71. class VersionNumber attr_reader :major, :minor, :build ... def next_major_version new

    VersionNumber( major + 1, minor, build) end ! def next_minor_version ... def next_build_version ... end VALUE OBJECT factory methods
  72. 3 ways to persist value objects A. Inline on the

    Entity’s table B. Serialized on the Entity’s table
  73. 3 ways to persist value objects A. Inline on the

    Entity’s table B. Serialized on the Entity’s table C. In its own table
  74. Release! VersionNumber! Entity Value 1 1 A. Inline on the

    Entity’s table releases version_major version_minor version_build …
  75. A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  76. A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  77. A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  78. A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  79. require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  80. require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  81. require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  82. require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  83. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  84. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  85. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  86. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  87. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  88. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  89. C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  90. Gift an App SERVICE 1. Charge the gifter that’s purchasing

    the app. 2. Assign access rights to the giftee receiving the app.
  91. Gift an App SERVICE 1. Charge the gifter that’s purchasing

    the app. 2. Assign access rights to the giftee receiving the app. Facilitates a transaction between two aggregates
  92. class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  93. class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  94. class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  95. class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  96. class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  97. ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ...
  98. ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates
  99. ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates entry point to domain behaviors & ! data retrieval
  100. ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates cross aggregate transactions entry point to domain behaviors & ! data retrieval