00:00:00.240
Good afternoon, ladies and gentlemen of the jury. Welcome to today's trial: Use Cases versus the State of Rails Applications. I am your prosecutor, Siobhan Coker. When I'm not prosecuting software concepts, I'm the tech lead of the Purchase Team at Envato Market, which is a digital marketplace and also happens to be the scene of the crime.
00:00:08.800
Don't worry about taking notes today; I have a written transcript of the proceedings that I will share at the end. Now, as you should all know, we are here today to determine the guilt or innocence of the use case in the serious matter of improving the Envato Market Rails application. It has been charged with the following crimes: reducing coupling between the Rails framework and the business domain, increasing the visibility of the features within the codebase, and encapsulating complex business logic.
00:00:15.599
Now, I must warn you I will be showing you some code today, but I've kept them very simple and relevant, so please don't try to run them in production; they won't work. Before we delve into the evidence, let's establish some motive. So, who here has written or worked on a Ruby on Rails application before? Yeah, good. Okay, so the following story hopefully will be familiar to you. I've been writing Ruby on Rails apps for about seven years now, and they always seem to go a little something like this.
00:00:40.079
The business, looking ever so businessy, wants you to build a simple Ruby on Rails web application. And I'm sick to death of the classic Rails blog example, so instead today, let's go with, I don't know, maybe some kind of digital marketplace where you can buy photos. So first up, you gather the requirements, and luckily at this stage, there's just one: a buyer needs to be able to purchase a photo. So you map out the domain models: you have your Photo, your Buyer, and a Purchase, and you implement them. How well Rails makes it easy, right? Right off the bat, you get these three buckets where you can put your code; your models, your views, and your controllers.
00:02:05.439
So we create our models, just something like this. There's our Buyer model; same for our Photo, that one; and Purchase is the same. And for Purchase, we even get our associations pretty much for free. We then create our controller in a very similar way. Let's give it a create action, and inside, we'll create a purchase for that buyer and that photo and redirect to the purchase completed path, whatever that is, and we're done. Right? But look how happy the business is! Okay. But you all know what happens next.
00:02:49.760
Sometime later, your new requirements arrive. So the business comes back to you and says, 'Hey, now whenever buyers purchase photos, I also want that they get a purchase invoice, and I also want that we record the sales counts of these photos, and I also want that a buyer can review a purchased photo.' Okay, no problem! We can implement this pretty simply, right? We go to our controller where we're creating purchases, get the Buyer Mailer to send an invoice, increment the sales count of our photo, and enable the review of that photo for the buyer. Now the requirements are implemented, and the business is happy again.
00:03:22.320
But hang on a minute; it kind of looks like we've got a bit of a fat controller, and everyone knows this is bad, right? Rails 101. Okay, but there's a simple solution to this problem. We can go to our models; our Purchase model doesn't have much on it, so let's create a method called 'create_for_buyer'. We'll go back to our controller and rip all of that logic out, then go to our new method and chuck it in there. Now back in our controller, we just need to call that new method we've created, and look, our controller is nice and thin again! Look how nice it looks!
00:04:17.200
But hang on a minute; how's our model looking? Yes, it's a little fat. Is that a problem? Let's take a look. So this method that we've added to the Purchase model has added the responsibility of creating purchases and all of the associated business rules for creating a purchase. Now Purchase is already responsible for persistence because we're extending ActiveRecord::Base. And if you think, 'Hey, that's just one line; that's not a big responsibility,' have a look at this: an empty class that extends ActiveRecord::Base has 214 methods on it, and that is not counting the methods that you get from the Object class. So that's not insignificant.
00:05:15.840
On top of persistence, the Purchase class has additional reasons to change. It needs to know about the Buyer Mailer sending invoices, about incrementing sales counts, and about reviewing photos. This is just all in one method, so imagine if each method has its own responsibilities. Fat models have too many responsibilities. They’re already tightly coupled to your framework, and if you add your business logic to them, you're also coupling them to the business domain.
00:05:31.920
This means they have many reasons to change, which makes future changes harder, more prone to bugs, and merge conflicts. You'll end up with huge class files with many public and private methods in them. I'd like to compare reading a large class to looking up at a telephone pole in Vietnam, which I visited recently, and trying to follow a particular wire; it's almost impossible to see which methods are related, how, and where they’re being used. And considering that we write this code once, but we read it again and again, don't underestimate the value of having easy-to-read code.
00:06:36.880
Another problem is that we have obfuscated our business logic inside our ORM layer. So if you look at the source code structure of a typical Rails application, you see these nice MVC buckets that may reveal your domain models, but you don't see the use cases of the system and what it's actually meant to do. Okay, now that we've established some context, it's time to meet the use case.
00:07:09.440
What is a use case? Well, after some research, it turns out that like most other definitions in our industry, the term is a bit overloaded. However, I did find a definition on usability.gov that I quite like. It says a use case is a written description of how users will perform tasks on your website. It outlines, from a user's point of view, a system's behavior as it responds to a request. Each use case is represented as a sequence of simple steps beginning with a user's goal and ending when that goal is fulfilled.
00:07:45.720
Written use cases are a great tool for describing the behavior of a system because they provide a list of goals. But as soon as an application gets built, this documentation starts to drift from the implementation and becomes less relatable to the codebase. Simon Brown gives a great talk called 'The Essence of Software Architecture' where he talks about the importance of living architecture reflected in your code. So how can we reflect our use cases in our code?
00:08:25.999
I now call myself to the stand as a witness to the implementation of the use case pattern at Envato. At Envato, we have a large codebase with many developers working on it, so whatever we had, we needed it to be simple, consistent, and conventional to implement. A UML use case usually depicts an entire user flow, which may involve multiple actions. We decided to reduce the scope of this definition and define a use case as a plain Ruby class that defines one user action and the steps involved in completing that action.
00:08:56.000
Our classes are named after the action that the user takes, normally a noun combo like 'purchase_photo' or 'confirm_email'. We have a directory for the use cases to live under, which is named under their relevant domain so they're easy to find. We created a small module called UseCase, which defines a simple public interface for our use cases to implement. This module has a class method called perform that takes whatever arguments it needs, initializes the new instance, and calls perform on the instance.
00:09:44.640
You'll notice it's wrapped in a tap block; this ensures that the before method is a command method because any return value is ignored. I'll come back to that later. We also define the performances method, which the use cases can implement however they need to. Here’s a simple example of a use case: it's a plain Ruby object, it's named after the operation it carries out, it includes the UseCase module, it has an initializer that takes whatever arguments it needs, and it implements perform. Inside perform, there are methods that detail the steps needed to complete the action and achieve the goal, in this case, purchasing a photo.
00:10:30.280
Now, these methods are actually private method calls inside of the use case, and they each call out to another object to execute the logic. I'll come back to that later too. We also most of them have a success method which uses active model validations. This is so we can determine the success or failure of the use case and respond appropriately from our controller or other callers. So here’s a simple example of a controller calling a use case: it calls purchase_photo.perform.
00:11:16.400
It checks if it was a success; if it was, it goes to the purchase completed path, else it goes to the purchase fail path. I now call upon myself as the defense lawyer for the accused to recall what went wrong. I’ve covered the basic idea behind our current implementation; however, this design has had a few iterations as we've implemented and refactored more use cases. Although I'm sure there are more improvements still to be made, we’ve learned a few valuable lessons along the way.
00:11:58.160
Lesson one is command-query separation. This is an important concept in software design. The basic idea is that you split your methods into commands which have side effects and no return value, and queries which return value. Originally, we ignored this principle, and our perform method would produce side effects, but it also returned this result object that we would then query. But this made our use cases harder to test and harder to refactor.
00:12:35.680
Sandi Metz said we conflate commands and queries at our peril. If you haven’t seen her magic tricks of testing talk, I highly recommend it for advice on how to test objects that obey the command-query separation rule. By splitting our functionality into a command method performed and multiple queries such as success, our use cases now obey that separation rule and are much easier to test. Speaking of testing, we started unit testing our use cases as we normally do, but we quickly realized that this was actually a great layer to write our integration tests in.
00:13:09.440
This way, we can test the actual side effects that the before method produced and be confident our business logic is correct. So generally speaking, we unit test the classes and the layers below our use cases; we have integration tests for our use cases; and then we have a smaller layer of acceptance tests at the top to ensure the system works from the frontend all the way down. Here’s a quick example of an integration test.
00:13:44.000
So our subject is the perform method of the use case, and we can make expectations on the side effects of that perform command. For example, we expect perform to change the purchase count and we expect it to send an email with the subject 'purchase invoice'. Lesson three is: don't put your low-level logic in your use case. This introduces multiple levels of abstraction and gives the use case too much responsibility and knowledge of the rest of your system.
00:14:10.280
Instead, move the logic into specialized objects, calling them from the use case with high-level commands. This will increase the readability and changeability of your code. We did this by creating a number of classes, each implementing one step of a use case. For example, 'increment_sales_count' has its own object that knows how to increment a sales count of a photo. Lesson four is: don't make your use cases generic.
00:15:04.800
A key benefit of a use case is that it reflects a single use case of the system. If it's generic enough for multiple use cases, you lose clarity in context. We actually have multiple ways to complete the purchase depending on how the user paid for it, and initially, we tried to make one big use case that could handle all of the payment methods. This was just complex, confusing, and error-prone, so we eventually split it up into single-purpose use cases for each case, and now they're simple to read and understand.
00:15:43.440
Well, I've covered our implementation and what we got wrong. Now let's take a look at what the use case pattern actually provides for us. We get a consistent interface, so all our use cases look pretty similar; they have that main perform method, and they all include the UseCase module, so they're identifiable. We get self-documenting and readable code because each use case provides documentation of an important action in your system.
00:16:14.080
If another developer wants to know what happens when a purchase is completed, they can go straight to the perform method of that use case. And these methods are easy to read because each line is at the same level of abstraction. If they need the detail behind a particular step, they can go to that private method and see where the logic is. We get context and encapsulation, as each use case creates a contextual wrapper around a bunch of other objects that implement the low-level logic.
00:16:53.440
The inputs to a use case plus the sets inside of perform encapsulate everything you need to carry out that action. This also makes it easy to change or add new steps. Let’s say the business now wants us to send a purchase confirmation email to the buyer on a photo purchase; we just create a new private method that calls out to the right object for that behavior, and then we add it to our perform method, and that’s it.
00:17:12.000
We also get decoupling from our framework. So our controller has very little logic in it, and ActiveRecord models are empty, well apart from ActiveRecord. The business logic is in the use cases and in the plain Ruby classes that implement those use case steps. We get a codebase that reveals its features, so developers can look in one place to see all of the user features of the application.
00:17:41.600
Before we deliver our verdict, there are a couple of qualifications I would like to make. Now, I hate clichés so I hope I don’t have to spell this one out to you, but I just want to impress that the use case pattern is not some kind of shiny metallic projectile able to stop mythical creatures in its tracks. Just think of it as one more tool for your developer toolbox.
00:18:03.040
The name 'use case' is actually a bit of a debated topic at Envato. It's fair enough; the definition does encompass a broader context than we're applying it to here. So we have a few other options in the works, and I don't think the lead architect at Envato is a big fan of the term, but I can’t quite hear him from his ivory tower.
00:19:06.880
My colleague Steve Hodgkiss has just released a gem called 'Interaction' based on the use cases at Envato, and over time, hopefully, we'll be able to use that gem ourselves. I also came across the Interactor gem a few days ago, and it has a slightly different implementation, but the intent is virtually identical to our use cases, so that's really cool.
00:19:44.560
So, back to the case at hand. After reviewing all of the evidence, I think we have to conclude that implementing the use case pattern at Envato has helped us to reduce coupling, increase feature visibility, and encapsulate our complex business logic. The use case is guilty as charged. This court is adjourned.
00:20:04.880
And that's my blog.