ERIC ROBERT: All right, hi. I'm Eric Roberts
and I'm here to talk to you all about
how you can use software design patterns
to put your Rails app on a diet
and make your tests run really, really fast.
Jokes aside, we will be telling you about
some
design patterns. Not so much making your tests
run
fast. But we're here to talk about domain-driven
design
hexagonal architecture in Rails.
I'm Eric Roberts. I'm a software developer
and I
work at a company in Waterloo, Ontario, Canada
called
Boltmade. I met Declan when we worked together
at
PrintChomp. Before that, I was a frontend
developer for
a number of years, and I'd worked around Rails
applications, but mostly at the view layer
until Declan
dragged me kicking and screaming into backend
development and
made me care about stuff like we're talking
about
today.
And this is the biggest crowd I have ever
presented in front of. So if you'll excuse
me,
I need to take a picture and email my
mom.
DECLAN WHELAN: OK. Hi everybody. Real pleasure
to be
here. My name is Declan. I'm the co-founder
of
a company called PrintChomp, and my story
is about
two years ago, I had a opportunity to launch
PrintChomp, and I was looking at technologies,
and I
decided that Ruby on Rails was really the
best
platform for us. The only challenge was that
neither
me nor anybody on my team knew Ruby nor
Rails.
So it was kind of a brave, maybe, decision,
but it's one that I don't regret. And one
of the things that drew me to the community
was the fact that, or, to the platform, was
the community around sharing, around the openness
of, of,
of sharing code and, also, a lot about, about
the test-focus, which has been pretty important
to me.
But the cool part, I think, about it, was
we intentionally took on a lot of technical
debt,
because I knew that, I knew that I would
not know enough about our domain. It was a
new domain for me, printing, and I also did
not know enough about Ruby. I did not know
about, enough about Rails. So, very intentionally
decided to
do our best, the best that we could, knowing
that we would end up, very likely, with a
pretty heaping mound of technical debt.
And that turned out to be true. And has
anyone had that experience? I don't know.
Yeah. So,
but recently we had an opportunity to build
an
API, and that was really exciting for me.
And
I know there are gonna be some other talks
about APIs here later, and I realized that
I
had, I had two kind of competing things that
could come together. The first was, I had,
I
had logic in our application that I needed
to
share in our API. How was I going to
do that?
And all of our code was sprinkled through
various
bits of our controller and model logic. Secondly,
I
wanted a mechanism to, to have a strategy
for
eliminating the technical debt, and what I
turned to
was domain-driven design hexagonal architectures,
and I want to
share what we've learnt along the way of doing
that, and, and where we're going. So I hope
you're able to learn something from what we've
done.
E.R.: That's great Declan. But what are they
gonna
get out of it?
VIDEO
??: Here's a good idea. Have a point! Makes
it so much more interesting for the listener!
D.W.: Well, that was from Sandy Metz. He said,
you know, if you're gonna have a talk, you
should really have a point, and I think Steve
Martin said it pretty funnily. And I guess,
our,
our number one point is that, there is complexity
in the software that we build. There's complexities
in
the problems that we're solving. And we need
to
embrace that complexity and embrace it in
such a
way that we tackle it and deal with it.
And deal with it head-on. And by doing that,
then we end up having more joy and fun
in our work because it's not just about getting
this functional piece to work, it's about
really trying
to understand our domain and model it and
express
that in our code and make our code as
expressive as possible.
And the second thing that I really realized
was
I knew that our code was a mess, and
I knew some point refactorings that we could
do,
but I didn't know how to, how to, you
know, what did the end look like? If it
was refactored significantly, what would it
look like? What
would the shape look like? What would the
namespaces
be? What would the classes be doing, et cetera.
And domain-driven design and hexagonal architecture
helped me envision
and share with my team what it might look
like.
The third thing that, that, that we would
like
you to take away from this talk is that
there's a lot more to just being a Rails
developer, the, the ideas and patterns, if
you will,
that we, that we're talking about are, some
of
them have been written by some of those people
that DHH had in his slide, including James
Coplain.
But a lot of those patterns have been around
for a long time. SO those patterns and those
ideas and those practices will serve you well
beyond
Rails. They would work in a node application.
They
could work in a desktop application. They
can work
in a wide variety of areas. So by getting
some familiarity with these concepts, you're
able to transfer
those skills to things beyond Rails.
And so I want to ask Eric just to
walk us through kind of some of the, some
of the pain that we had with our initial
Rails development and see if it resonates
with you.
E.R.: All right. So everyone knows what this
is.
It's a Rails folder structure. And it's really
great
when you get started with Rails. You have
these
folders. OK. I, I logically, my things fall
into
these areas. Controllers, models, and views
are really what
we're focusing on right now.
But if the responsibility of your code doesn't
start
with M, V or C, what do you do
then? And we, and we find it kind of
goes like this. You have, you have these areas
of responsibility and you have something that
doesn't really
fit, and you don't know where it goes, so
you just put it on somewhere and things get
a little bigger.
And continuing on, you keep doing this, things
get
bigger, and finally, like, the line between
all of
these things is, is blurred. You don't know
what's
what. It's hard to extract reusable parts
from all
of this, because the, the responsibilities
are split across
all these things.
And you kind of end up with methods like
this. This is one method. It starts on the
left and ends on the right. It's about ninety
lines.
It's not from PrintChomp. It's from another,
another project
that I've worked on. And without setting,
it doesn't
really matter what it does. The point is it's
ugly and you know, it is actually about setting
prices on properties and the date ranges are
available.
So I have a question. We need to add
sales tax to the prices.
Anybody care to take on that refactoring?
It's pretty horrible. But nobody, nobody sits
down and
says, I'm gonna write a ninety line method
today
that does property prices with date ranges.
The initial
spec was probably something a lot simpler.
And because
it's in that spot now, the next person who
comes, comes and looks and that method and
goes,
oh, well OK, well if I just type this
little bit more it'll, it'll do that now.
And this was a little bit of a train
wreck. At some point you probably, the people
working
on this, including myself, probably should
have realized, you
know, forty-five lines might have been the
time to
split it up. Maybe twenty. Maybe less.
But this is, nonetheless, what we ended up
with.
Or, you end up with Rube Goldberg machines
for
sharpening pencils.
So there's a lot of well-known patterns that
can
help you out with this. Has anyone read this
blog post? Yeah. Anybody use any of the patterns
in them?
They're pretty great, right. We use a lot
of
them, and I'm, we're gonna tell you about
some
of them here. But the one thing you've, so
you've extracted all of these things into
small responsibilities.
Your models, your controllers and your views.
They're all
small again.
But, OK, so what? I have a bunch of
little objects that all go, they all know
too
much about each other. They don't fit in any
logical structure. They don't, how, how does
this all
fit together? You've made, like, an awesome
first step,
in that you have small little pieces that
you
can use. But where do you go from there?
And we think that looks kind of like this.
We think that your domain concepts, services,
entities, should
be in the middle, and everything else is outside.
Your database is an extra concern, your views
are
an extra concern. The web, and you know, when
you're designing the API, as Declan talked
about a
little bit, it's an extra concern. So you
can
really focus in on the middle of what your
application actually does.
I'm gonna let Declan talk a little bit more
about that
D.W.: Has anyone here heard of domain-driven
design? Oh,
quite a few people. Awesome. How many people
have
actually, you know, intentionally used it
and, and have
worked with it? So, a, a number. SO cool.
And it can be quite, it can be quite,
it can be quite daunting. And this book by
Eric Evans is kind of what kicked it off.
And this book was I think written in 2005,
and it is a really, really great book, but
it is actually quite difficult to read. But
it's,
I think it might be the only technical book
I've read twice.
And that's partly cause it was really good
and
partly cause it was rather difficult to get
through
some of it. Did anyone else read this book
and have a similar experience? like, it's
really great
stuff and you kind of read and it and
you go, wow, what does that mean to me?
Right?
And we, at the end, at the end, we'll
be posting our deck, and we'll have some references
to other material that, that I've actually
found to
be a bit more addressable or a little more
consumable.
But what Eric Evans talked about was really
tackling
complexity, and he talks about the, you know,
the
critical complexity is really understanding
the domain. What are
the business rules that take place inside
our systems?
If we're gonna add sales tax, what are the
business rules for that sales tax?
What are the, what are the rules around who
can buy what? And by putting those insides
and
thinking of them as the domain of our system,
then we're able to have our outside layers
just
be, if you will, relatively thin facades,
which allows
us to reuse that logic across APIs, across
other
applications. We don't have to duplicate all
of those
business rules.
And the way that he proposes to do that
is through ubiquitous language. And this is
a picture
of the tower of Babel and if anybody knows
the story, it's where everyone in the world
was
speaking a different language and, I once
worked on
a project that was a financial transaction
processing system,
and when I inherited it, the guy who was
proceeded me had this, was a model train aficionado,
so he had the idea of model trains. So
every transaction was like a car, and the
payment
engines were, were train tracks. And he had
all
of these metaphors around transaction processing
and trains, and
was written in Java.
So when I was asked to add a new
feature, fix a bug, I had to understand the
business domain, and then I had to kind of
understand, how did that translate into train
speak, and
then I had to go look at the Java
code to figure it all out, right. So the
coding was easy. The hard part was really
trying
to understand what was being asked and how
did
the code express that, right?
And that's what Eric Evans is talking about
with
ubiquitous language. We want to have the language
that
we speak with domain experts should be readable
in
the code. If I'm order, if I'm a customer
and I can purchase a product, there should
be
a class called customer. There should be a
class
called product. And there should be a verb
in
there that's somewhere that says purchase,
et cetera.
So that there's minimal translation between
the domain experts'
language and the language that my code is
written
in. Ruby gives us a great opportunity to do
that, but this is much more difficult in,
in
other more statically typed languages.
So that's kind of the, the key thing to
take away from domain driven design is to
try
to have your, your concepts expressed in code
that
are meaningful in the words people use.
If they use the word customer, you should
have
a customer class. If they use the word user,
you should have a user class, et cetera.
But beyond that, it also has some kind of
key, I, I dare not say it, patterns? Now,
I'm almost nervous now to say it.
But let me be clear. This isn't science. It's
not, right. Because these patterns were not
dreamed up
in academia ivory towers. These patterns that
we talk
about were empirically derived from people
intentionally doing what
we do, which is write code every day, intentionally
thinking about, how does that, how does those,
how
do those things fit together, and what is
the
essence of what I'm doing?
And can I extract that into words that I
can use to communicate with other people?
The beauty
of patterns is I can talk about a value
object, and if you know what a value object
is and I know what a value object is,
we can have a much richer conversation than,
oh,
I have to have an object whose state is,
whose identity is defined by the state of
its
attributes and then, you know, we can have
a
much richer conversation.
E.R.: You might say you ubiquitous language.
D.W.: Yeah. So we're gonna touch a little
bit
on some of these patterns. But, but the idea
is that these are not academic ivory tower
concepts.
These are empirically driven from people who've
worked in
the field. And if we're gonna, if we're gonna
be successful as an organization, while I
fully agree
that we need to go out and write code
and we need to read code, I totally think
that's true, but we also have to have more
effective ways of communicating knowledge,
so that we're not
all learning the same things from each other
over
and over. WE can learn more easily from each
other. And that's what these patterns are.
And the next kind of piece that we're gonna
talk about is hexagonal architectures. And
it's more than
just that. I think we've alluded to a little
bit. It's really the idea of, you're going
to
have this core domain in the middle, and in,
inside your code, surrounding your outside
core are gonna
be some application level code that expresses
the rules
of your application. And I would draw it slightly
differently, perhaps, but, and then we have
adapters on
the outside that adapt that code to web calls
or database calls or SNTP, or in my case,
APIs.
And so that's the way that we want you
to, that's the way that we are starting to
approach the work at PrintChomp is thinking
about it
in those constructs. And now we want to jump
to some specific patterns and, and show you
some
real code that actually, you know, brings
these to
life. So Eric, you want to talk about form
object?
E.R.: Sure. Unfortunately, I can't tell you
what your
domain is or what necessarily goes in the
middle
of that hexagon. But I can give you some
ways to keep other things out of that hexagon
that you don't need to be concerned with.
One thing that I've been doing lately is form
object. One of the really cool things, you
know,
if you run Rails scaffold, some model name,
you
get a form that you submit, creates the record,
edits the record. And that's pretty great.
But how do you do that when you don't
have a direct one-one mapping with an ActiveModel
record?
So instead of instantiating an ActiveModel
record, I've taken
to instantiating, I'm gonna call it a form
object.
There's a lot of names for a lot of
different things. But this is, this is what
I've
been calling it.
So here's the actual thing that I was building.
On the left hand side, you see that you
select a ticket price. On the right hand side,
your name, email address, and your payment
details. And
this is actually two ActiveRecord models in
my database.
The, the passengers over there on the right,
you
can add a passenger and keep adding it.
And if you've ever worked with nested attributes,
probably
know it's not always that fun. So this is
not using nested attributes. It's done like
this. I
have a class TicketForm that includes ActiveModel::Model,
which is
how I get nice things, like that magic initialize
method of validators.
And the passengers method, if I don't have
any,
returns me a passenger new. That's how it
puts
the name and email address for that first
passenger.
And then tickets, I get out of this by
taking my passengers and mapping them into
new objects.
In the controller, it looks a little bit like
this. So you just pass your params off into
that. You get it back out and you have
tickets. So instead of if ticket dot save,
I
do if ticket_form valid and ticketCharger
charges successfully, then
we've had success. And ticketCharge takes
care of charging
my tickets and knowing that, there was only,
cause
there's only one charge for all the tickets,
right.
You're paying all at once. No point to split
that up.
So that's a really useful thing that I've
found
to, to help when my mappings aren't just totally,
I don't want to just take a record of
the database, put it in, or update it.
And now Declan's gonna tell you a little bit
about request objects.
D.W.: Yeah. In this case, has anyone used
a
form object or something like that? So quite
a
few people. Cool.
Has, has anyone done, used something called
a request
object? Sort of? I was hoping that I invented
this. So maybe I haven't. I don't know. OK.
But, request object is now in, you know, think
of an API as, we're trying to have a
similar behavior to the form object, except
we're trying
to take, remember, we're trying to take complexity
out
of our controllers and out of our models and
put them into more, to simplify our systems.
So the idea with the request object is that
we're gonna pull that code out and put it
into the, into the controller. And now the
request
object is going to receive the request. And
the
code looks sort of like this, right.
There's, I, I've done this slightly differently
than Eric's,
Eric's way, in terms of, of, the kind of
the core of the class. The core of this
class is using a gem called Verdis, which
does
something similar to ActiveModel::Model, except
this allows you basically
to have a plain-old Ruby object and, the way,
and, the cool part is, that, or, I think
it's cool. You can, you can declare attributes,
like
this. So I have actually, in my domain layer,
I have something called the customer and the
billing
and the shipping. And if, if somebody supplies
me
those three things, then I can complete an
order
in my system.
We've, there are a couple of pieces missing
here,
but we wanted to keep it, fit in a
slide. But that's basically it. And we have
the
validation. And the cool part about this is
that
when a request comes in, we just, I just
put in a before loop in the controller that
has a before filter that just basically instantiates
the
request object using some, some reflection
to figure out,
you know, what is the, what is the controller
and what is the action being asked, inferring
the
request class, instantiating it, and then
just passing it
what used to be the params hash.
But now it's actually a rich object that I
can have validations on, et cetera. So the
net
effect of this is that, why would I bother
doing this, right? Well, now the controller,
you know,
the complexity around validating the request
is at the
boundary of my system. That doesn't need to
leak
into the rest of my system. It's almost like,
you know, does anybody use, you know at the
beginning of your methods, you want to put
the
guard clauses to catch the exceptions coming
in at
the beginning of your methods, so that the
rest
of your method is simpler?
This is doing the same thing, except it's
doing
it at, at a higher level abstraction, at the
API request level. And I, this has worked
out
really well for us. So I, I quite like
that one.
Yeah.
E.R.: Great. Service objects are another one
that we've
been using. I think DHH actually had one in
his presentation. I'll give you a hint, he
didn't
like it.
He used one, it, it's something instantiated
by the
controller. We've been looking at controllers
a little bit
as just like, OK, I've received this thing,
pass
it off to somebody else, and then do something
with the result. So we want to keep out
that procedural code from our controllers,
and service objects
are one way you can do that using a
order service to create orders, we've encapsulated
all of
the logic about how to create an order in
this one area.
So if you want to, you can use it
from somewhere else, right. You don't have
to hit
a controller action to create an order. It's
reusable
and extendable. And it has Declan's magic
repository object
in there that we'll get to in a bit.
And the controller, again, just, it's very
simple. Do
this thing, on success do this, on failure,
do
this.
So those are a few of the patterns that
we've been using to help us with this. So
now what?
What's, what's the elephant in the room? Anybody?
How do you get them into Rails? Yeah.
Yeah. That's the one we were thinking of.
D.W.: That's, ActiveRecord. Yeah. I mean,
I mean, the
end, and how do you get into Rails. Yes,
there are some interesting challenges around
that. But I'm
gonna flip you back to the architectural slide
and
just point out that you see what's happened
here
is that we're trying to view the application
pieces
of our solution here being really on the perimeter
of our core system. And the core system composed
of services that may be servicing API or application
requests.
We have some services that may be invariant
across
any call, and those would be at the very
center. And we have things called entities
which are
on the inside. And the key part that's, that
we haven't talked about, which actually I'm
planning the
most difficult part in this is this repository
which
is the bottom, which is the r in the
bottom right hand corner.
And it's job is to talk to the ActiveRecord
model which is in the green, and create an
entity object which is the blue object, and,
and
how does that actually work? So I'm gonna
show
you a bit of code, and this is, this
is the first time I've shown my Ruby code
in public. So please be kind.
And there are probably way better ways to
do
this. but this is the repository that I've,
I've,
there are some other methods here, but I just
wanted to fit on what happen, you know, show
you the simple one.
So this is what a save looks like. So
the save takes a domain object and it converts
it through something called a mapper, which
maps the
domain object onto a ActiveRecord object,
which is called
a record here. Then I, then it calls record
dot save, assigns the id and returns the response.
So that's pretty straightforward. All that's
really happened is,
and I really tried to do this and so
far I've been able to, I don't want to
have domain dot save. I want to have repository
dot save domain, so that there's no persistence
that's
leaking into my domain objects. Persistence,
I want to
be a secondary concern to what that object
is
really doing.
And so far, it's worked, although sometimes
it's caused
me some difficulty. The method messing down
there is
kind of cool. At least I think it's cool.
And what it's doing is it's introducing a
scope
object, which I'll show you in a, well here's
the scope object. That's OK.
And it creates a scope object, and this was
the trickiest code that I had to write, but
what it does, what it, the end result of
this code is, that allows you to chain call
any of your ActiveRecord find methods of your
scopes
and chain them together. So you can now use,
so in other words, with this logic, wherever
you
have like ActiveRecord dot, you know, find
where id
greater than 122 is activated and so on, wherever
you might have a chain like that, you can
still use that chain now because of this scope
class with your domain objects.
Except, instead of getting back your ActiveRecord
object, you're
getting back its domain representation. Does
that make sense?
yeah?
OK. And then the mapper is what maps them
across, right. And some cases, the mapping
is like
really, really simple. What's the next slide
here? Oh,
yeah. Yeah. There it is right there.
So the mapper, because of the way vertice
works
and the way ActiveRecord works with attributes,
you can
almost just instantiate one from the other
just passing
the attributes back and forth. So it's actually
quite
easy except when it's not easy, and then what
happens, what I'm doing now is just, wherever
I
have something that doesn't fit this model,
I just
subclass this mapper with a custom mapper
and override
those methods, more or less.
And that's the part that I think there, would
be more expressive ways to do that mapping
and
that's what I'm starting to look at now. But
so far this has actually worked pretty well.
And it's allowed me to completely separate
the way
I think about persistence from the way I think
about my domain object. SO we before we, before
I had this, we had, used to have an
order class, and I kid you not, and I'm
in, I'm a Rails noob so you can shoot
me, but it had forty-eight attributes in the
ActiveRecord::Model,
right.
That is now represented by about eight classes
that
separate out all the different aspects of
the order,
like the shipping, the billing, et cetera,
et cetera.
So that, but I think this mapping is, is,
is, is one of the more challenging parts.
And
then finally, you now I've, then you quickly
run
into things like, oh, well what happens if
I
get the record. I get the record, I save
it, then I save it again. I have to
be able to keep track that I've saved it
once already.
So that I don't have multiple copies around.
And
this is an identity map. In fact, an identity
map was built into Rails and I think it
might, I don't, I don't work in Rails 4,
but I think it might be taken out or,
or changed slightly. But actually, on the
next slide
I'll show you what the identity map looks
like,
and this, actually, I stole from Rails and
made
it a bit simpler.
But all it's doing is just making sure that
there's a unique instance on, on a per-API
request
call for each entity object. So that I, I
can, and it actually serves as a really cheap
cache, but that's not what it's intent is.
So, so that's kind of where, where we're,
where
I'm going now, is most of those service object,
form object, request, those all kind of work
well
for us, and I'm not looking at what would
a repository pattern look like fleshed out.
And that
is, by far, the most challenging piece.
But I heard a question about where you put
things. You can really just put them anywhere.
You
know. That's the thing, like, Rails just seems
to
be, there's a part that's, I just felt constrained.
Like, Rails didn't give me any guidance on
where
do I put a service socket. Well, you can
really put it anywhere you like. You could
put
it on the auto load path or you can
put it where Rails might expect to see it.
But it, you can just create a services directory
and put your service objects there. Rails
will find
it. It's not hard. But Rails doesn't really
kind
of tell you what to do. So it doesn't
make it easy to do these, to think of
it, but it actually, it's not hard.
And but that's where we're going with that,
and
we'll, we'll give you a link to it, to,
I've started a Git Repo where I plan to
share some of these ideas, and if you're interested
in sharing ideas with us on that, we'll, we'll
be happy to talk to you or, or join
us on the, on the Git Repo.
E.R.: Right. So what's the point, again? At
the
start, we had these three things. Embrace
complexity. Getting,
getting the solution to work is only part
of
the fun, right? It's, it's your first draft,
as
DHH said, talking about writing. You get,
you do
it over and over. And getting things to work,
that ninety-line method I showed earlier,
that worked. But
does anyone want to go back and use that
again? It's no, it's no fun to revisit, unless
you're improving it, which, I did do eventually
and,
and now it's much nicer to work with. That,
that's fun. At least, we think that's fun.
Getting
beyond the problem and getting it to a level
where we actually don't mind going into our
code,
extending things, and changing things.
And we do that by breaking them up into
the smaller parts with the patterns that we've
talked
about.
Knowing where you're going. In Alice in Wonderland,
paraphrase,
they said if you don't know where you're going,
any road will take you there. We think it's
important to know where you're going and pick
a
road that you think will take you there. It
doesn't have to be our road. These are things
that helped us, and, and we think they're
good
ideas. But it's not gonna solve every problem
for
everyone. These aren't rules. Like Declan
said, they're not
science. You can't just take them and throw
them
on and expect that your code will magically
get
better by going on a diet.
And be more than just a Rails developer. Not
that it's bad to be a Rails developer, but
these things apply across, across languages
and stuff. Like,
don't just learn Ruby. Learn, learn beyond
that. So
as Declan mentioned, we have this GitHUb that
we've
set up, and right now it's just a readme.
We don't actually have the code in their yet.
We'll likely put some of the code we showed
today, especially the repository bits.
But we'd love to continue the discussion via
issues,
pull requests, whatever. That would be awesome.
And, and
we'd love to talk to you about some of
it throughout this week as well. In fact,
I'm
gonna also mentioned, we have reading. You'll
be able
to get these slides after, so I'm just gonna
gloss over them.
These are a couple of the books that we've
taken a look at to help us out with
these things.
D.W.: I think the crowd has spoken and it's
time to wrap up. If you want to come
up and talk, I'm happy to answer your questions.
Thank you.