Rocky Mountain Ruby 2023

Go Pro with POROs

Go Pro with POROs

by Ifat Ribon

In the talk titled "Go Pro with POROs" at the Rocky Mountain Ruby 2023 event, Ifat Ribon discusses the significance of Plain Old Ruby Objects (POROs) in modern Ruby/Rails development. Through this presentation, she aims to clarify various design patterns and encourage developers to consider clarity, encapsulation, and simplicity in their coding practices.

Key Points Discussed:
- Importance of Naming: Ifat emphasizes that naming conventions enhance communication and understanding among developers, thereby fostering creativity and effective problem solving.
- Encapsulation: She highlights the need for defining boundaries in code and organizing business logic to follow SOLID principles, which leads to maintainable and understandable code.
- Clarity: With many ways to write Ruby code, Ifat stresses the importance of writing code that is clear not only for others but also for one’s future self, facilitating easier maintenance and onboarding processes.
- Simplicity: While clever coding can be tempting, keeping code simple helps reinforce clarity and makes collaboration easier over time.

Design Patterns Explored:
- Database Wrappers: Ribon discusses using wrappers for database interactions and insists on keeping Active Record models focused on database-related tasks by avoiding unrelated logic, which helps maintain clean architecture.
- Modules: She introduces modules as versatile entities that enhance code organization by allowing shared logic to be easily named and reused across different models and controllers.
- Services: Ifat defines services as POROs that encapsulate business logic and often generate a single public method aimed at achieving specific tasks, thereby improving code organization.
- API Wrappers: These reusable classes simplify interactions with external APIs, ensuring clarity and manageability in handling requests and responses.
- Virtual Domain Models: These are transient actors that help represent domain objects without requiring persistence, useful particularly in API scenarios.
- Request and Presentation Objects: By employing these POROs, developers can effectively separate concerns, thereby clarifying data handling and presentation logic, which aids in easier testing and maintenance.

Conclusions:
- Ifat encourages developers to explore these design patterns, adapting and implementing them based on their projects’ needs. She underscores that embracing POROs and the outlined practices can enhance their coding discipline and overall project organization. The goal is to inspire developers to utilize these insights practically, leading to better project outcomes and collaborative environments.

00:00:13.799 Cool, thank you, and thank you all for being here! It's so fun to meet new people within the community and learn new things, or perhaps talk about the things that we all do and love so much.
00:00:20.560 Hopefully, my talk continues to embody that spirit as well. This talk, "Go Pro with POROs," is based on one of the big motivations or emphases I have.
00:00:27.519 I have a penchant for words; I love learning new vocabulary and finding just the right word for a given situation. I genuinely believe in the power of naming things. Being able to communicate clearly ensures that everyone is on the same page, which leads to greater creativity and problem-solving.
00:00:41.239 So, I wanted to put together a talk where I could start providing some names and vocabulary to illustrate how I organize my code and discuss some design patterns I've learned along the way from other developers or have simply explored.
00:00:54.039 I hope that some of these patterns may be familiar to you. Perhaps you have different names for them or even different patterns that I didn’t touch on, and I'd really love to hear about those later if you'd like to come up and chat with me.
00:01:11.799 A little bit more about me: my name is Ifat, and I work at a digital product agency called Launchpad Lab. We are headquartered in Chicago, but we have developers from all over North and South America.
00:01:26.799 This has been a tremendous benefit, as I have learned from many different developers while working in a project-oriented environment, collaborating with diverse teams, learning different styles, and exploring various patterns together.
00:01:40.880 We've had the chance to solve novel problems and observe how some challenges arise repeatedly, allowing us to gain mastery over solutions or even take chances on new explorations.
00:01:54.079 One of the big themes I want to emphasize throughout this talk is the idea of encapsulation. We heard a bit about it in the last talk by Mark, which resonated with me. This concept involves defining boundaries in your code and determining what domain models or aspects of business logic you want to encapsulate and keep in a specific place, making them easier to reason about.
00:02:18.280 This approach allows you to follow the SOLID principles we talked about earlier. You'll see that I will likely bring this up repeatedly in connection to different patterns because, at their core, they are all different ways to encapsulate your code.
00:02:32.080 The second big theme I want to focus on is clarity. There are so many different ways to write Ruby code, which is part of its beauty, but that flexibility can also be a double-edged sword. The guiding principle I often use in my code is: Is this clear for another developer?
00:02:49.879 This consideration includes your future self, be it in six months, twelve months, or five years from now. Can someone looking at this code, whether onboarding or trying to address an unexpected bug, easily find it, understand it, and continue working with it?
00:03:07.479 The final theme I'll keep in mind during this talk is simplicity. It can be enjoyable to devise clever solutions, often resulting in beautifully complex code. However, when working with others—or even your future self—keeping things simple reinforces the first two themes, making life easier for both you and anyone else you might collaborate with.
00:03:43.799 With that said, I'll now touch on several categories of design patterns. I understand the first two might not even fall under the PORO category, but I ask for your patience as they provide foundational concepts that we will build upon—starting from the most familiar to many in this room and advancing toward possibly novel or different ideas.
00:04:03.319 We'll explore database wrappers, modules, services, API wrappers, virtual domain models, and request and presentation objects.
00:04:24.440 Let’s get started with database wrappers. This should be the most familiar concept for everyone here. Database wrappers are classes typically part of an Object-Relational Mapping (ORM) framework, like Rails. Other frameworks may use similar concepts, or you might roll your own.
00:04:54.280 These wrappers provide a user-friendly interface for working with your database tables. Rails specifically offers a nice Domain-Specific Language (DSL) for constructing queries without needing extensive SQL knowledge, as well as for defining relationships, associations, and validations.
00:05:20.400 One pattern I've converged on as I've gained experience as a developer, which was also evident in the last talk, focuses on maintaining a single responsibility for your Active Record models. I've aimed to keep the model classes closely tied to what's relevant to the database.
00:05:43.680 This approach ensures that we avoid the common pattern of cluttering model classes with unrelated logic, which can lead to messy, spaghetti-like code. Instead, I strive to be intentional about where I place that logic, leveraging other patterns we’ll discuss to find more appropriate homes for it.
00:06:05.720 An important point I want to highlight, which may be obvious to many in the room but was an 'aha' moment for me early in my Ruby on Rails journey, is that ORM can indeed be used for any database table across any schema.
00:06:29.159 So while it's a common understanding, recognizing how powerful ORM is was enlightening. Like all tools, we must learn them and use them wisely.
00:06:49.760 As an example, here you can see a standard class for a workout plan table in our database. I've limited it to associations, constraints, and maybe a few defined enum types and some queries with scopes. It also features a simple instance method wrapping a more complex query.
00:07:11.720 One of the key things I want to stress is to be intentional in keeping your model classes focused on database-related tasks rather than drowning them in business logic.
00:07:34.400 Another comparable example is how we can work with various schemas. At Launchpad Lab, we frequently integrate with clients using Salesforce, which often entails utilizing a tool called Heroku Connect. This tool allows us to build Active Record wrappers around Salesforce schemas, treating them just like any other database table.
00:07:51.600 Next up are modules, which are fascinating, and I've been experimenting with them increasingly. In simple terms, a module is simply a collection of code. Namespacing is a common use case for modules, as it bundles code together, making it easier to manage.
00:08:09.360 That being said, a major benefit is the opportunity to assign meaningful names to code segments, which improves readability. I often encourage teams to extract functionality into modules that can be easily named.
00:08:23.239 One thing to note about modules is that while they can't be instantiated, they are incredibly versatile. They can be utilized directly by defining public methods or indirectly by including them in classes. As someone beginning their Ruby journey, you'll likely use modules regularly, particularly for model concerns.
00:08:41.359 When different models share logic, extracting that logic into a module (commonly referred to as a concern in Rails) becomes practical. This also applies to controller concerns that help keep your controllers lean. Helpers for views allow you to separate presentation code from business logic seamlessly.
00:09:03.000 An insightful perspective that my mentor at Launchpad introduced me to is that modules are a fantastic way to apply functional programming principles in Ruby. For instance, when writing a calculator for basic arithmetic operations, it’s a great example of how modules can effectively encapsulate input and output functionalities.
00:09:25.840 Despite their flexibility, it is essential to maintain clarity and consistency in your codebases. A pattern I have leaned into is encouraging consistent naming for modules, which allows developers to quickly recognize their purpose.
00:09:50.960 So, a few examples of modules might begin with a model concern for shared fields across models, a controller concern to handle error notifications or request management, or view helpers to format data before presentation.
00:10:13.680 One instance might involve a workout metrics module that takes in specific attributes and calculates outputs like pace. These methods can be tested independently while providing clarity and organization within your application.
00:10:42.760 Now, I've made a significant oversight in this presentation by repeatedly referring to POROs without defining it. I sincerely apologize; let's clarify that now: a PORO, or Plain Old Ruby Object, is a term used to distinguish any Ruby class not tied to an Active Record class backed by a database.
00:11:00.720 As I previously mentioned, the rest of the categories we'll discuss fall under the PORO umbrella. We'll start with Services, which have generated substantial discussion and diverse opinions recently.
00:11:23.200 My perspective on services which might lean towards a hot take is that while they often become a catch-all term for any PORO performing business logic, I believe we can come up with clearer names and definitions for the logic types we work with.
00:11:41.920 To me, services encapsulate business logic to perform calculations or a series of steps, following a procedural approach. Admittedly, instantiating a class may not leverage the advantages of object-oriented programming fully, but utilizing classes provides convenience in methods and public attributes.
00:12:06.760 In our agency, we tend to create services that expose a single public method, often labeled 'run' or 'call', and we name the class after its intended function. While there’s no right or wrong way to do this, it's a pattern I've adopted for consistency.
00:12:30.480 An example of a service could be a factory that executes a sequence of steps to create objects. For instance, I might create a School Factory that accepts parameters to generate additional objects or trigger notifications as needed.
00:13:03.440 Similarly, another practical example involves interacting with APIs. I once worked on an app where I tweeted my progress during my first half-marathon so I would fetch my logged workout every few miles.
00:13:22.079 This API wrapper service handled the intricacies of fetching data, which could be tailored to facilitate further processing. I ensured to expose an errors variable to inform other components when something went awry.
00:13:46.799 Transitioning to API wrappers, these are reusable classes that help interface with external APIs. They generate a cleaner abstraction for your interactions, which is useful whether the APIs are owned or third-party.
00:14:08.319 When we write API wrappers, we often implement interfaces for making requests, abstracting away some details to provide a more straightforward implementation for API endpoints.
00:14:27.360 These wrappers allow us to handle variations in requests, such as overriding headers or payloads for specific needs. This way, if details need to change, we can handle those adjustments in one place.
00:14:51.840 Next, we can discuss virtual domain models, which can help keep our model class focused on database-centered operations while still representing domain objects that aren't backed by a database.
00:15:12.480 They're often transient, with a focus on immediate use cases without persisting them in a database. This is particularly useful for APIs where we can capture incoming data into virtual domain models for processing.
00:15:34.799 A practical example could involve financial models where we address complex calculations based on prior line items and current values, utilizing POROs to facilitate this pluralism.
00:15:54.240 Finally, we'll cover request and presentation objects that help clarify requests and responses. These POROs allow us to separate concerns, reducing complexity in controllers, models, and views.
00:16:15.520 By adopting this approach, we can document, test, and maintain our code more easily, aligning with established patterns and libraries like presenters, decorators, and form objects.
00:16:38.560 For instance, presenters simply encapsulate presentation logic, while decorators enhance existing objects with presentation-focused methods. Form objects, on the other hand, streamline complex forms by abstracting away interdependencies for ease of use.
00:17:11.199 We can also utilize serializers to format API data as needed for clients, ensuring the right shape is returned. Moreover, transformers can help with incoming parameters to normalize and prepare data ahead of persistence.
00:17:33.680 An example might involve using a workout view component to encapsulate how we present workout data without needing to intertwine that with business logic. This separation promotes clarity and maintainability.
00:18:01.039 With that in mind, I hope these patterns resonate with you or inspire new approaches in your work. I'd be delighted to hear your perspectives or engage in discussions afterward.