00:00:13.480
Hi everyone, thank you so much for having me and for being here for my talk, "Go Pro with PORO." One of my big motivations for this talk is that I personally have a penchant for words. I love learning new words and finding the right word, which is really helpful in coding. I resonate deeply with Ruby because it is so expressive and word-friendly.
00:00:24.960
Today, I wanted to put together a talk where I could build and promote a vocabulary around different design patterns in Ruby. Many of these terms will likely be familiar to you, but hopefully they will inspire you to push the boundaries. I would love to have more conversations about them later today, as I know there are wonderful ways to write Ruby code. These examples are just a few, but I hope you can take something away from this discussion.
00:00:59.519
A little bit more about me: my name is Ifat Ribon, and I'm a principal architect at an agency called Launchpad Lab, based out of Chicago. We have developers from all over North and South America, and working at an agency has provided me with the wonderful opportunity to see a lot of different codebases, work with a lot of different developers, learn from them, and also explore various patterns that I've enjoyed or learned from others.
00:01:20.960
With that, I'm going to walk through a few themes that I want you to keep in mind as I present this talk. The first theme is encapsulation. When we talk about code design, we're really discussing how you organize your code and the boundaries you draw around the business logic. There are many ways to approach this. You can keep everything in one file or split everything into small pieces; both are perfectly valid forms of encapsulation.
00:01:42.240
Next, I want you to consider clarity. There are so many different ways to write Ruby, and they are all wonderful. At the end of the day, choose patterns and writing styles that will be clear to you and other developers. As the old adage goes, your future self is another developer. When you return to a codebase after months or years, and a bug appears, you want to quickly find and reason about the code.
00:02:03.880
Finally, the third theme or goal here is simplicity. This ties in closely with clarity, but keep things simple. If a pattern doesn't resonate with you or feels like it's adding overhead, consider taking a step back and trying a different approach. Find what works for you.
00:02:31.680
With that, I’m going to go through some common design patterns. I know some of you are thinking that there are a couple here that aren’t even POROs, but bear with me as I set a foundation. We will discuss database wrappers, modules, services, API wrappers, virtual domain models, and request and presentation objects. There are far more ways to categorize these, but this is the perspective I’ve come up with in the last few years.
00:02:42.840
We’ll start with database wrappers. So, what are they? Database wrappers create a nice interface for working with your database tables. If you’re using Rails or similar frameworks, you are likely familiar with object-relational mapping (ORM), which helps simplify database interactions. Rails, in particular, offers a wonderful DSL for making queries simpler and defining relationships.
00:03:02.680
However, while using the ORM is helpful, it can quickly become unwieldy. Your model classes may start to include elements that don't relate directly to the database but pertain more to the business logic. I've found it beneficial to focus your classes on what you need to know about being a database table and use other patterns to encapsulate business logic.
00:03:29.280
For example, here's a classic model in a public schema, which focuses on relationships and constraints. It contains simple instance methods to assist with slightly more complex relationships. Conversely, at Launchpad, we often work with Salesforce integrations through Heroku Connect, which creates a Salesforce schema for you. Building an active record wrapper around that simplifies the reasoning process.
00:03:42.420
Next, moving on to modules. Modules in Ruby are straightforward; they are collections of code. You might encounter instances where people use modules purely for namespacing. Essentially, grouping related classes without needing to combine them all into one file provides clarity.
00:04:01.280
It’s worth noting that modules cannot be instantiated. They are an instance of the module class, which means you can't create an instance of a module directly. However, you can use modules directly by exposing methods with the self notation or extend other bundles of code using prepend, include, or extend.
00:04:24.560
A common use case for modules that you might be familiar with is model concerns. This is an excellent starting point for keeping model classes simpler by encapsulating additional responsibilities. Similar concepts apply to controller concerns and view helpers, which act as modules that get included in the view layer.
00:04:49.760
I've been exploring the idea of using modules to represent functional programming in Ruby. At its most basic, functional programming allows you to provide the same input and receive the same output every time, without managing state at all. This perspective has led me to adopt a practice of reaching for a module first when performing operations, rather than relying on object-oriented programming.
00:05:16.960
Now, let's discuss some common examples reflecting what we’ve talked about. With two tables housing address fields, I could define a model concern for those fields. On the controller side, we often use controller concerns for exception handling in our APIs, consolidating the logic for both sending exceptions and capturing them with services like Sentry.
00:05:40.960
Lastly, view helpers are fantastic. They allow you to encapsulate messy display or presentation logic in a helper that gets loaded automatically. Similarly, for calculations, if the database table holds raw values but I want to have calculated outputs, I could establish methods that always yield the same output for given inputs.
00:06:03.200
Now, I realize I haven’t even defined what a PORO is, which is quite bad for presentations. So, let's clarify: a PORO, which stands for Plain Old Ruby Object, is just any Ruby class distinct from a database wrapper. This concept allows for better distinctions, and I've gathered a few categories of examples: services, API wrappers, virtual domain models, and request presentation objects.
00:06:34.640
To start with, service classes are essentially POROs that encapsulate business logic, particularly for calculations, transactions, or a series of steps in procedural coding. While they may seem procedural and not entirely leveraging object-oriented principles, services still allow you to expose methods and share common functionality among service classes.
00:07:00.960
Many of my teammates expose a public method, typically named "perform" or "call," to execute the main functionality. Frequently, these methods are defined with verbs or nouns indicating their actions, such as "generate PDF." Over time, I’ve focused more on using reader methods only for the key attributes I wish to expose, rather than for every attribute, as exposing unnecessary ones can be misleading.
00:07:24.520
The next pattern is API wrappers. These are POROs representing reusable classes for interacting with external APIs or gems that do similar work. They abstract away the internal details of making requests, whether you’re using HTTP Party or Net HTTP. When approaching this, I generally define methods for RESTful routes, allowing subclasses to provide variations when necessary.
00:07:49.120
An example would be interacting with the Under Armour API. By creating a defined method that organizes functionality and keeps a clean interface, I can easily manage requests and responses from various endpoints while ensuring that shared logic is reusable and easy to modify.
00:08:13.960
Continuing, we have virtual domain models—often considered the quintessential POROs. They do not persist in a database state but exist to represent transient data in your application. You may define these when dealing with API results or other types of data needed immediately in your view layer.
00:08:36.520
Their structure looks akin to how you might design a schema. Public methods correspond to model attributes, allowing you to display data cleanly. I've observed this pattern in various projects, such as working with the Contentful API, where I created methods that mimic Active Record functionality without actually persisting data.
00:09:02.560
This brings us to request and presentation objects, which aim to simplify the structure of your apps, particularly in your controller and view layers. These POROs advance the single-responsibility principle and ease testing efforts, as they allow you to interact with simpler class objects compared to a more complex MVC pattern.
00:09:27.320
Presentation objects can be generically categorized into various classes, including presenters, decorators, and serializers. Libraries like Draper provide straightforward interfaces for implementing decorators, easily allowing you to modify data presentation for your views while keeping the underlying object intact.
00:09:53.120
Additionally, serializers enable you to format data for client responses, ensuring it retains the needed structure for effective application use. Various aspects in serialization can help transform incoming data into usable formats, enhancing application robustness.
00:10:15.880
As we bring the talk to a close, I've shared the patterns I've explored and some terms I've developed. I'm eager to hear about how each of you applies these patterns differently or what vocabulary you've come up with. Feel free to come chat with me afterward; I appreciate you all being here today!
00:10:41.520
Thank you!