00:00:14.080
All right, well hi everybody! I'm Jon. I heard you liked design patterns, so we're going to keep talking about those.
00:00:24.519
I got my first Ruby job in 2011, and I've seen various things about how people decide to organize their code. I live in Lyons, have a couple of kids and dogs, and work at Gusto. We're here sponsoring; talk to us!
00:00:36.719
This is Annie Maggie, and I would love to show you pictures of my dogs, but today we're going to talk about ways to organize code, the holy wars that accompany it, and why naming is the only thing that really matters.
00:00:52.320
So just to make sure everybody is on the same page, let's discuss the default organization for Rails applications. These are sort of the architectural components that Rails comes with, which include the model, view, and controller, or MVC.
00:01:06.119
A model wraps the database table and provides an abstraction for loading and persisting data. The view is the template that gets turned into HTML and is sent to the user, while the controller acts as a middleman that interprets the request from the browser, often performing actions on a model to get or persist data, and then presenting the results to the user via the view.
00:01:29.199
This structure is sufficient for many applications, but we're going to talk about ways to augment this foundation, which can help as your application grows in complexity, your team grows in size, and making changes becomes increasingly difficult and risky.
00:01:44.719
We'll start with something that most applications have to deal with: allowing users to sign up. We will begin with a basic approach and then look at some other options.
00:02:03.439
Here’s our controller. We have this create method where we get the user’s name, email, and password, then immediately create the record and sign them in. Great, that works and is easy to follow, but surprise, surprise, requirements are going to change.
00:02:25.720
We have spammers coming in, and we need to make users confirm their email before they can sign in. So instead, of signing them in immediately, we will now send them a confirmation email and redirect them to check their email.
00:02:38.519
That works well, but what if there’s a different endpoint from which we also want to create users? Now, we could copy and paste this code everywhere, but then maintenance quickly becomes a nightmare because changing requirements mean doing shotgun surgery; you'd have to change a lot of code.
00:02:44.879
Starting with everything in the controller is simple and easy to follow, but it becomes harder to test because controller tests involve more moving parts than just testing your plain old Ruby objects. They are not reusable, nor do they absorb complexity well.
00:03:02.360
What if we put it in the model instead? Now we have this after create hook on our model. Whenever we create a user, we will also send the confirmation email. Great! But now, every time we create a user, the tests will run a little slower because we’re sending this email.
00:03:17.920
It's guaranteed in software development that changing requirements will lead to increasing complexity, and it's rare for a project manager to come in and simplify things. Callbacks are powerful but dangerous tools, and venturing down that path can lead to madness.
00:03:34.439
As you continue to add callbacks to your models, your tests will become slower. More importantly, your code will become harder to reason about, as they add implicit behavior to your application, which complicates things. Explicit behavior is always easier to reason about.
00:03:56.480
Continuing on this path makes changes difficult and can lead to increasingly convoluted code. Thick models may seem simple, but they result in slower tests, forced reuse, and do not absorb complexity well.
00:04:15.480
While I’m emphasizing that this approach does not absorb complexity well, it’s challenging to demonstrate thoroughly with an example that fits on a slide. Let’s look at a real-world example of what can happen if you don’t proactively architect your application to handle added complexity.
00:04:35.920
Here’s a real-world user model, and I think this has gone beyond thick to being a full-fledged God model. Bear with me as I scroll through this model.
00:05:07.679
Alright, so apologies if you’ve worked on this kind of model—no judgment really, just sincere sympathy—but it's a demonstration of why addressing this is important. Rails ships with basic architectural components, which are great, but you don't have to stop there.
00:05:55.040
As software developers, we should be more than just individuals who run 'rails generate scaffold' commands. Ruby is great with classes and methods; we can utilize these fundamental building blocks to introduce new ways of encapsulating our business logic.
00:06:03.800
It would be beneficial if Rails included a business logic folder alongside models and controllers, but we have to create our own. So let’s extract a class. You know, it’s like when you’re watching a movie and they say the name of the movie in the movie—that’s this moment.
00:06:44.160
Here’s a basic example of an extracted class: a UserSigner class that takes in user parameters. It contains a 'do it' method that sends the confirmation email if we successfully save the user, and then returns the user.
00:07:05.360
With this extraction, our model goes back to knowing nothing about emails, and our controller gets simplified. This creates clear dividing lines: we only need to update the controller when we want to change something that the controller is responsible for, like the redirect after sign-up.
00:07:27.840
The UserSignup class is now in charge of all the details of what happens when a user signs up. Consequently, when we decide that we want to post new signups in Slack or email relevant administrators, this will absorb that complexity with minimal complaints.
00:07:54.200
It's still easy to reason about what's going on. Since I've named this talk the Single Responsibility Principle (SRP), let's emphasize that the core idea is that each class should only have one reason to change.
00:08:13.760
This requires us to think about code like humans who are aware of business requirements—that’s what is likely to change based on business needs. It involves making conscious choices regarding trade-offs of SRP, cohesion, and coupling.
00:08:44.720
Taking SRP too far might mean simple flows in your application require holding dozens of classes in your head, while not taking it far enough could mean that you have to reason about methods that are dozens of lines long or a user class that’s thousands of lines.
00:09:02.480
By establishing reasonable dividing lines for classes, you set yourself up to enable easier testing. It’s simpler to test something when it does not have a bunch of side effects, while promoting reuse, making it easier for new contributors to get up to speed, and making your application more pleasant to maintain.
00:09:29.200
Let’s look now at some common design patterns. You might have heard of a few today, but we’ll review them again. Design patterns are just standard, repeatable approaches to common problems that developers encounter.
00:09:50.000
They’re not magic, and there's seldom one correct pattern for any particular issue. Design patterns are useful tools to have at your disposal, and being familiar with them can make jumping into other codebases less painful.
00:10:11.600
For example, when you're just passing an instance of your ActiveRecord class to your view, you might be tempted to add presentation-specific methods to your ActiveRecord class or do too much in your view templates, which can lead to clutter.
00:10:47.040
Looking at this, you might think about adding full name or a display sign-up method to the user model. However, this will quickly fill your model with presentation-specific logic, potentially conflicting with the Single Responsibility Principle.
00:11:03.680
If we're changing the model, which should be concerned only with data fetching and persistence, but we're really just adjusting presentation-level concerns, then we might want to consider using a presenter, decorator, or view component. This way, we encapsulate presentation-specific logic.
00:11:27.000
This gives us a clear and simple view while keeping our presentation concerns isolated from the model in their own class. The only reason to change this presenter class would be if we needed to present something different for our user.
00:11:48.520
Form objects are another key pattern. They can be really powerful when creating multiple records from the same form. Imagine a scenario where you want to create both a user account and a waiver acceptance.
00:12:06.120
The Rails way might involve using nested attributes on your user model, which may work for simple cases. However, as your controller grows complex, switching to form objects keeps your model and controller clear of clutter, while allowing for custom validations across both objects.
00:12:30.760
Here’s how a form object example can work: we take in user attributes and waiver attributes. In our save method, we create both the user and the waiver within a transaction. This ensures that if one fails, the data doesn't get orphaned.
00:12:50.640
We now have a class where we can explicitly set up our associations and delegate validations to instances, ensuring that persistence is transactional.
00:13:02.720
Moving on to service objects, I've already shown a service object with the previous UserSignup class. The core idea behind service objects is to encapsulate functionality, making it explicit, easy to test, and reusable.
00:13:27.200
You can delve into strict forms like command objects, which require only a public perform method. This approach ensures a consistent interface but might limit your creativity—essentially restricting how you write your code.
00:13:50.560
Service objects generally solve the problem of keeping business logic centralized. They provide a single point of contact for your flow, promoting reusability and making your code more testable.
00:14:07.640
Policy classes encapsulate rules, such as determining if a user can access a certain post or if a post can be deleted. For instance, if only posts that are less than two days old, have no comments, or are owned by an administrator can be deleted, it’s helpful to encapsulate this logic in a policy class.
00:14:25.760
With a post deletion policy class, we pass in a user and a post to check permissions. This way, we encourage reuse across different areas of the application, avoiding repeated logic throughout various parts of the code.
00:14:50.760
Query objects are often just handled by scopes, but if you have a complicated set of scopes that frequently recur across your application, creating a nameable query object can help clarify its purpose.
00:15:09.320
For example, if 'sleepy puppies' is a central theme in our application, promoting it to a first-class citizen allows us to abstract away complexities and maintain clear communication in our codebase.
00:15:26.160
Value objects, also referred to as data objects or virtual domain objects, provide a defined interface for data management. They make your interfaces explicit, limiting the data passed around in a codebase. This is increasingly valuable as more developers contribute to a project.
00:15:47.680
If you return an ActiveRecord instance, the calling code can do anything with it; however, if you return a value object, they only receive the data you've intentionally provided. This approach enhances clarity and control within a codebase.
00:16:09.360
Repositories are the final pattern I'll discuss. They offer a structured way to retrieve data, primarily by limiting access to data that users are authorized to view and enabling eager loading to avoid performance pitfalls like N+1 queries.
00:16:24.120
We can create a repository that ensures users access only their pets or public pets, preventing unauthorized snooping through a neat wrapper around ActiveRecord’s interface.
00:16:42.240
Now, consider augmenting the information we retrieve with additional context, such as a pet's breed, which may involve calling an external API. By wrapping this logic in a repository, we can keep it isolated and easy to maintain.
00:17:05.760
To avoid performance issues, let's optimally fetch breed information for multiple pets in one call. Pulling this logic into a repository allows for optimizations and adjustments in a single place, improving maintainability.
00:17:28.720
While I apologize for possibly missing your favorite design pattern, I encourage discussion afterwards if you have suggestions. There are numerous design patterns, and each has its unique utility in making codebases easier to comprehend.
00:17:52.840
Remember that design patterns aren't a panacea—they should serve your application’s needs. The main takeaway from this talk is that if you can identify a concept that's currently hidden within another class, consider extracting it into its own class.
00:18:26.080
We’ve looked at common ways people tend to architect these extractions. If it doesn't fit into a specific design pattern, you can simply refer to it as a service class. However, take caution—instead of hurrying to extract code, it’s often best to do so once you've seen a concept repeat organically.
00:18:52.600
Preemptive refactoring can lead to extricating code that appears should belong to its own class but doesn’t represent an accurate abstraction. Recognizing true business concepts needs careful thought.
00:19:12.760
Strive to extract only once you've observed a concept or pattern consistently emerge multiple times. Dealing with naturally recurring code is generally more manageable than attempting to restructure concepts mistaken for business logic.
00:19:29.280
Don’t constrain yourself by trying to fit every element into a design pattern. A simply named class enhances encapsulation of your business logic, promoting reusability, testability, and ease of understanding.
00:19:48.960
This leads to a vital consideration in programming: naming. While a significant portion of your career might not involve thoughts about caching and validation, you cannot escape this challenging aspect of computer science.
00:20:05.480
Now, pivoting from code to people, how can you introduce a new design pattern to your codebase? Most of us don't work in isolation, and it’s essential to bring your team along as you propose architectural changes.
00:20:16.400
First, clarify in your mind why you want to introduce this design pattern. Identify the pain points motivating you, explain how the proposed solutions address these challenges.
00:20:37.440
There are countless ways to brew coffee, numerous routes to achieve a solution. Articulate why your proposal is superior compared to other options.
00:20:50.560
It’s vital that you and your team find a way forward, ensuring the application remains manageable for the long term. Avoid adopting an attitude of seeing yourself as a prophet with the only true path to maintainable code.
00:21:16.640
When discussing a proposal, consider having something tangible to reference. A quick proof of concept can help illustrate how the new approach functions in a targeted context.
00:21:44.960
It’s much simpler to discuss specific code rather than abstract concepts. Additionally, creating an architectural decision record (ADR) can be invaluable.
00:21:57.200
This involves documenting your motivations, alternatives considered, and the proposed solution. This gives tangible material for your team to analyze together.
00:22:22.680
If your company doesn't currently utilize an ADR process, introducing one can be beneficial, though it could warrant its own discussion later.
00:22:46.760
Having a record of past ADRs helps in preserving the reasoning behind decisions for future reference. Being able to revisit the motivations for architectural choices can be enlightening for new team members.
00:23:09.760
This practice can also encode these choices as guidelines for future development as your application evolves.
00:23:32.640
Here’s your checklist for change agents: a change agent is someone who seeks to introduce significant changes within your organization.
00:23:49.200
Draft your proposal, outlining motivations and alternatives, and carry out a proof of concept. Send out meeting invitations and encourage your team members to do pre-reading.
00:24:04.320
Let’s decide how to move forward and get right into it!
00:24:33.760
Thank you! That's about it.
00:24:53.000
Does anyone have questions?”
00:25:16.000
The question is about using Google Docs for ADRs. Yes, we do that; we have a template to fill out and post in Slack, soliciting feedback before a certain date.
00:25:22.120
If there’s a lot of discussion, let’s have a meeting to flesh it out.
00:25:29.360
So, the question is about prematurely extracting a class. If you extract a class too early, it might not reflect your domain accurately.
00:25:38.520
Yes, that can be dangerous because once it's part of your codebase, it tends to remain there. You've written tests for it and used it in multiple places.
00:25:50.120
There are methods to introduce the correct class gradually, but you might end up with two classes living in your codebase simultaneously.
00:25:56.880
It’s a challenge, no doubt.
00:26:06.040
Thank you.