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. The goal is to allow your Rails application to express the domain so that the domain's business logic is clear-cut, easy to grok, and designed in a way that reduces unnecessary complexities and coupling.
7. 1. Process of defining the domain!
2. Communicating the domain!
3. Relationships between models!
4. Aggregates!
5. Value Objects!
6. Domain Services!
7. Data access
10. COMMUNICATION
“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
11. UBIQUITOUS LANGUAGE
“a common, rigorous language between
developers and users […]
Domain!
Expert Developer
Product!
Owner
User
Designer Code
the need for it to be rigorous, since software
doesn't cope well with ambiguity”
— Martin Fowler
Ensure one consistent language
12. app.submit_release(“1.1.0”, ... ,
screenshots: ["shot1.png", "shot2.png"])
release = Release.new(
major: 1, minor: 1, build: 0, ...)
release.screenshots << [
Screenshot.new(file: "shot1.png"),
Screenshot.new(file: "shot2.png")]
release.status = :submitted
!
app.releases << release
A BETTER WAY?Describe a domain behavior with a methodEasy to understandSimplifies InteractionManages complexities
Submit a new release of your app
14. class App < ActiveRecord::Base
...
!
def create_release ...
def submit_release ...
def approve_release ...
def flag_for_abuse ...
!
def mark_as_staff_favorite ...
!
end
Tells a story of how the domain works
15. ENTITY VALUE
a thing describes a thing
can change state doesn’t reference
anything
unique independent!
of attributes
avoids design
complexities
immutablehas a lifecycle
class Customer class Name
17. class Screenshot
attr_reader :file, :position
def initialize(file, position)
@file, @position = file, position
end
def ==(other)
file == other.file &&
position == other.position
end
end
VALUE OBJECT immutable / equality
19. 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 factories
20. one-to-many or one-to-one (1-n or 1-1)
C. In its own table
B. Serialized on the Entity’s table
3 ways to persist value objects
A. Inline on the Entity’s table
one-to-one (1-1)
21. class Release < ActiveRecord::Base
def version_number= vn
version_number_major = vn.major
version_number_minor = vn.minor
version_number_build = vn.build
end
!
def version_number
new VersionNumber(
version_number_major,
version_number_minor,
version_number_build)
end
end
(1-1)A. Inline on the Entity’s table
22. class Release < ActiveRecord::Base
composed_of :version_number,
mapping [
%w(version_number_major major),
%w(version_number_minor minor),
%w(version_number_build build) ]
...
end
Release attributes
VersionNumber attributes
Inline on the Entity’s table (1-1)
23. 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
24. class GiftApp
def self.execute(app, gifter, giftee)
Purchase.create(
app: app, customer: gifter, ...)
AccessRight.create(
app: app, customer: giftee,
purchase: purchase, ...)
end
end
AccessRight
1
n
Purchase
1
n
Customer
26. Data Access
App.where(
"create_at > ? and purchase_count > ?",
1.week.ago, 10000).all
class App < ActiveRecord::Base
scope :new_and_noteworthy, -> {
where("create_at > ? and
purchases > ?",
1.week.ago, 10000) }
end
Use scopes
27. class App < ActiveRecord::Base
scope :new_and_noteworthy, ...
scope :staff_picks, ...
scope :most_popular, ...
...
def create_release(…)
def submit_release(…)
...
end
One expressive point of entry
* you only need to retrieve aggregate roots *
39. Some Pro Tips
1. Don’t let your database diverge to far from
your domain model!
2. Don’t try to build and maintain a huge
diagram of your domain!
3. Separate you domain logic from view logic!
4. You can use the gem fig_leaf to privatize
ActiveRecord methods (e.g. create, where)