Talks
A Case for Use Cases
Summarized using AI

A Case for Use Cases

by Shevaun Coker

In her talk titled "A Case for Use Cases" delivered at RubyConf AU 2015, Shevaun Coker argues for the implementation of use cases as a crucial design pattern to enhance the structure of Ruby on Rails applications. The session is framed as a trial, with use cases being put on trial for their ability to improve the quality and maintainability of Rails applications. Coker highlights several key issues commonly encountered in Rails development, particularly related to the separation of concerns and reduction of complexity.

Key Points Discussed:

- Introduction to Use Cases: Coker defines a use case as a contextual, well-defined interface between controllers and domain entities, aimed at encapsulating complex business logic.

- The Rails Development Pain Points: She recounts a familiar scenario where business requirements evolve, leading developers to overburden controllers, models, and views, causing the infamous "fat controller" and "fat model" phenomena that make code difficult to manage.

- Consequences of Fat Models: A detailed analysis of the drawbacks of fat models, which include excessive responsibilities and high coupling, ultimately complicating future maintenance and introducing bugs.

- Implementation of Use Cases at Envato: Coker shares her experience in implementing the use case pattern at Envato, detailing how it provides simplicity and consistency in code. Each use case is implemented as a plain Ruby class focusing on a single action. These classes encapsulate a series of steps necessary for completing user actions, promoting better organization and readability.

- Command-Query Separation: A critical lesson learned was the importance of separating command methods (which modify state) and query methods (which return values) to enhance testability and maintainability of code.

- Refactorings and Best Practices: Coker emphasizes the importance of keeping use cases specific, avoiding generic designs that blur the clarity of single user actions. She also discusses the creation of specialized objects to handle low-level logic, thereby reducing the responsibility of the use case classes.

- Benefits of Use Cases: The use case pattern leads to more readable code, better encapsulation of business logic, and greater visibility of features within the codebase. Coker concludes that the use case pattern is a powerful tool for improving the structure of Rails applications.

Conclusion: In summary, the talk concludes with the assertion that the use case pattern has proven beneficial in reducing coupling, increasing feature visibility, and encapsulating complex business logic in Rails applications, advocating for broader adoption of this design pattern among developers.

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.
Explore all talks recorded at RubyConf AU 2015
+14