Quick Left: Confidently Build Complex Domains in Rails

Preview:

DESCRIPTION

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.

Citation preview

CONFIDENTLY BUILD COMPLEX DOMAINS!

IN RAILS

Mike AbiEzzi

WHAT’S A DOMAIN?

WHY?software gets complex fast

… and nobody wants a train wreck of models

▾ app/! ▸ assets/! ▸ controllers/! ▸ helpers/! ▸ mailers/! ▸ models/! ▸ views/

Developer 1 n

App Store

Customer

App

Installnn

n

n

Purchase

Comment

1

nn

1Version

1

n

1..6

Screenshot

1

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

1n

App Store

Developer 1 n

Customer

App

Installnn

n

n

Purchase

Comment

1

nn

1

Review!comment!

rating

Version

1

n

1..6

Screenshot

1 Release!version_number

Developer/ CompanySeller

COMMUNICATION

Domain!Expert

Software!Expert

brainstorm → draw diagrams → !speak out assumptions → let them correct you → refine

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

UBIQUITOUS LANGUAGE“a common, rigorous language between

developers and users […]

Domain!Expert DeveloperProduct!

Owner

UserDesigner Code

the need for it to be rigorous, since software doesn't cope well with ambiguity”

— Martin Fowler

Ensure one consistent language

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

App1

Comment

1n

Review!comment!

rating

n

1..6

Screenshot

1

AGGREGATE

Release!version_number

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

ENTITY VALUEa 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

App1

Comment

1n

Review!comment!

rating

n

1..6

Screenshot

1

Value

Value

Entity

Release!version_number

Entity

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

class Screenshot include Comparable attr_reader :file, :position ... ! def <=>(other) position <=> other.position end end

comparableVALUE OBJECT

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

one-to-many or one-to-one (1-n or 1-1)

C. In its own tableB. Serialized on the Entity’s table

3 ways to persist value objects

A. Inline on the Entity’s tableone-to-one (1-1)

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

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)

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

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

1n

Purchase

1n

Customer

▾ app/! ▸ models/! ▾ services/! create_app.rb! gift_app.rb! refund_purchase.rb! ...

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

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 *

▾ app/! ▾ models/! ▾ apps/! app.rb! release.rb! review.rb! screenshot.rb! version_number.rb! ▸ customers/! ▸ sellers/! ▸ services/

IN SUMMARY

Continuously refine the domain with a

domain expert

1

Insist on having a ubiquitous language for

seamless communication

2

Constrain relationships to only what the domain needs

3

Create aggregates that express domain concepts and manage complexity

4

Create value objects to eliminate unnecessary

complexity

5

Create domain services to express transactions !between aggregates

6

Express the domain’s intent through!

well defined data access

7

▾ app/! ▸ assets/! ▸ controllers/! ▸ helpers/! ▸ mailers/! ▸ models/! ▸ views/

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

Some Pro Tips1. 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)

Thanks for Listening!

@mjezzi

www.virtual-genius.com/

www.mikeabiezzi.com

Recommended