Plain Old Ruby Object (PORO)

Summarized using AI

Go Pro with POROs

Ifat Ribon • January 01, 2024 • Asheville, NC

In the talk titled "Go Pro with PORO," Ifat Ribon explores Ruby design patterns and best practices aimed at enhancing code clarity, encapsulation, and simplicity. As a principal architect at Launchpad Lab, Ifat shares insights derived from extensive exposure to various codebases and collaboration with developers across America. She presents a framework to discuss different design patterns, emphasizing three key themes:

  • Encapsulation: Ifat describes how to organize code and the boundaries of business logic, highlighting that there are valid approaches to encapsulation, whether in a single file or spread across multiple components.
  • Clarity: She stresses the importance of writing clear code, which helps not only the current developer but also future ones returning to the codebase. This involves choosing patterns that are easily understandable.
  • Simplicity: If a design pattern feels overly complex, Ifat encourages stepping back to identify a simpler solution that aligns better with the developer's style and needs.

Throughout her talk, Ifat delves into several common design patterns, introducing them in an organized manner, including:

  • Database Wrappers: These provide a simplified interface for database interactions, often using Object-Relational Mapping (ORM) to abstract complexity.
  • Modules: Ifat explains how modules can encapsulate related methods and functionalities without instantiating objects. This is notably useful for organizing concerns in models, controllers, and views.
  • Plain Old Ruby Objects (POROs): Ifat defines POROs as any Ruby class that does not directly interface with a database. She categorizes them into:
    • Service Classes: Focus on encapsulating business logic, often with a method named "perform" to execute functionality.
    • API Wrappers: Reusable classes that handle interactions with external APIs, ensuring clean and manageable code.
    • Virtual Domain Models: Represent transient data without persisting it in a database, suitable for handling data from APIs.
    • Request and Presentation Objects: Aim to simplify the application structure, enhancing single-responsibility principles and easing testing.

Ifat concludes her talk by highlighting the diverse ways developers can apply these patterns in their projects and invites attendees for further discussion. The overarching takeaways emphasize the importance of vocabulary in coding practices and the need for discussions around improving code architecture.

Go Pro with POROs
Ifat Ribon • January 01, 2024 • Asheville, NC

Ifat explores several common Ruby design patterns and best practices, and how they can make your code more encapsulated, clear and simple to work with.

Blue Ridge Ruby 2023

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!
Explore all talks recorded at Blue Ridge Ruby 2023
+2