00:00:11
Our next speaker is a Rails developer, plant mother, and beginner wobbler. I won’t spoil the surprise, so ask her about it.
00:00:18
She enjoys uncovering valuable insights from seemingly simple places when approached with curiosity.
00:00:24
She enables creators to bring their dreams to life through crowdfunding at BackerKit. Please welcome Sweta Sanghavi.
00:00:41
A couple of years ago at BackerKit, we set out to build a crowdfunding platform.
00:00:47
We had been working behind the scenes with creators for over a decade, yet we had never enabled them to run campaigns on our software.
00:00:53
Hence, we aimed to build a product that would compete with an established first-to-market competitor.
00:01:01
Lucky for us, we already had a creator interested in experimenting with us.
00:01:06
However, this also meant we had to be up and running within a few months by the time they wanted to launch.
00:01:14
We needed to build everything from a new payment processing method to routing within a new engine in our Rails monolith.
00:01:20
At that time, we were transitioning BackerKit from a B2B to a B2C product.
00:01:26
This felt audacious, scary, and stressful, but it was also exciting.
00:01:34
It was a chance to build in a new space, solve new problems for new customers, and leave behind our technical debt.
00:01:40
While there were unexpected hiccups—like the entire team catching COVID at the onsite where we started building—Rails supported us in putting up new user flows relatively quickly.
00:01:46
Soon, we launched our first customer on crowdfunding by BackerKit as scheduled, and the project was funded.
00:01:53
Shortly after, other customers lined up to launch on BackerKit, each with new requirements and bugs to fix.
00:02:01
We needed to iterate on features we had already rolled out, while also expanding our engagement methods with backers.
00:02:07
However, while building out this platform, we started hitting some roadblocks.
00:02:12
We were trying to push many new features out as fast as we could to get user feedback, but soon the features took much longer to implement.
00:02:24
The initial patterns that had served us well at the start began to falter as time moved on.
00:02:31
As developers, we were constantly balancing production pressures, weighing speed to release against the time needed to write well-factored code.
00:02:37
Today, we’ll discuss how to maintain speed in development by better utilizing a simple tool: our familiar friend, the Plain Old Ruby Object.
00:02:48
Welcome to 'Plain Old But Mighty: Leveraging POROs in Greenfield and Legacy Code'. My name is Sweta Sanghavi, and I’m a New York transplant living in sunny Oakland, California.
00:02:59
And just to settle the debate, I believe pineapple does belong on pizza.
00:03:05
As a tech lead at BackerKit, I work to build tools that help creators achieve their best crowdfunding campaigns.
00:03:11
We also support them in fulfilling these campaigns and distributing rewards to backers.
00:03:17
Also living in Oakland is my firstborn, Morty, who today will double as my co-pilot.
00:03:23
Morty is a staunch proponent of the PORO in his code, and I hope we can all learn something from him today.
00:03:31
We'll begin today by discussing what a PORO is, how to use them to grow Greenfield code, and how to leverage them in legacy code.
00:03:38
In the next half hour, I aspire for us all to become better at spotting opportunities to leverage POROs and ship more extendable code.
00:03:46
Whether it’s for new or existing features, let’s start by defining what a PORO is.
00:03:54
And I hear Morty meowing for attention. Morty, what is it?
00:04:00
Morty has an answer for us: a Plain Old Ruby Object is an object that does not inherit any functionality from Rails.
00:04:07
We gain many great features from Rails itself, but today, we are focused on code that does not inherit from Rails.
00:04:14
We can write better Rails code by leveraging POROs to encapsulate data and methods within our Rails application.
00:04:21
So simply put, we are talking about simple objects with no additional bells and whistles from Rails.
00:04:28
It’s really just Ruby. And while POROs may seem simple, let’s not be misled into thinking they are unimportant.
00:04:34
They help us factor our code into a more object-oriented design, provide us flexibility in building out our code, give us a usable API, and support faster testing.
00:04:41
Let’s see how we can use POROs to grow Greenfield code.
00:04:48
When building new features, we often encounter high-turn code because many of the requirements are still unknown.
00:04:53
This code is still in development, and POROs allow us to continue to grow this code.
00:05:00
So, everyone put on your BackerKit hats, and let’s look at an example from BackerKit.
00:05:06
First, a little background on the app we are building on today: BackerKit is a crowdfunding platform.
00:05:13
Creators launch projects like a new fantasy role-playing game, which have funding goals. Backers can support these projects in exchange for rewards.
00:05:21
If enough backers are excited about a project and invest in it, the project gets funded.
00:05:29
The funds are collected, and the creator can use that money for production.
00:05:38
Now let's imagine that BackerKit is going to launch 100 pin projects at the same time for an event called Pentopia.
00:05:45
If all the projects get funded, backers who supported these projects will receive commemorative pins.
00:05:52
To celebrate this event and track its progress, we will have a progress bar on the project pages.
00:06:00
This will let the backers know what percentage of projects have been funded, along with a funded count displayed at the bottom left.
00:06:06
In this example, for instance, 89 out of 100 projects were funded.
00:06:12
Let’s start sketching this feature into our codebase.
00:06:20
All right, Morty, your turn! You’re going to write us a red test.
00:06:27
Morty is pulling up our project controller spec and writing a new test in the show action, as this new feature will be visible on project pages.
00:06:35
We expect this progress bar to be displayed there.
00:06:42
We’ll start with a straightforward spec, covering both funded and non-funded Pentopia projects.
00:06:49
When we make a request to the show page, we expect the body to contain the funded count we visualized earlier.
00:06:56
Let’s get Morty’s tests to pass.
00:07:02
We will open up this show action and find a way to represent this new event.
00:07:09
We need to determine the funded count for our new component.
00:07:16
Since this is a one-off feature for Pentopia, I resist committing to anything concrete for now.
00:07:22
So, I think I’ll temporarily store the projects participating in this event in a constant array.
00:07:29
The thermometer should only display for projects involved in Pentopia.
00:07:36
Therefore, I will check if the project we are trying to display is included in the event.
00:07:42
Next, we’re going to need a progress component to show the progress.
00:07:48
Looking through our code, I find a view component called progress component.
00:07:54
The progress component takes three parameters: a title, the progress, and the total.
00:08:01
We can think of it as a numerator and denominator represented by progress and total.
00:08:07
So, I will set the title and total based on our constant representing the Pentopia projects.
00:08:14
To find the total, we can just get the length of that constant.
00:08:22
The last piece is to get the count of funded projects.
00:08:30
I’m working towards a green test without overthinking how to accomplish this.
00:08:36
I will iterate through the projects participating in Pentopia and use the scope method I found, fully funded, to count the number.
00:08:43
Then, I’ll add that to our controller code.
00:08:48
So, if our constant includes the current project, we instantiate an instance of our event progress component.
00:08:56
This component will show what percent of funded projects exist for the event.
00:09:02
Let's render this view component conditionally in our show action.
00:09:08
We’ll also add a little stub for our constant so we can assert that the two projects we’ve added are returned by our Pentopia project stub.
00:09:14
Let's run our test.
00:09:22
All right, we got two greens! Amazing!
00:09:28
Now let’s examine the trade-offs we’ve introduced in our show action.
00:09:33
Firstly, we now have a test, which validates the functionality required by this feature.
00:09:40
Getting to a green test illuminated the unknowns and requirements we need.
00:09:47
Until we’re green, we can’t guarantee that we have accomplished the task.
00:09:54
Thus, refractoring prematurely is not an option.
00:10:02
With this green test, we know the requirements for this new domain concept.
00:10:08
We need the number of funded projects, a way to check if the current project is included in this event, and the denominators.
00:10:13
Consider that we have added significant code to this controller.
00:10:20
If this event does not occur again, this code will remain in this controller indefinitely for all future developers.
00:10:28
If this is early in the controller's lifecycle, the impact of this code may not be noticeable.
00:10:35
However, as the codebase grows, this added complexity can become an obstacle.
00:10:43
How can we instead utilize our PORO to support this new feature?
00:10:52
We understand that this new domain concept introduces the idea of multiple projects launching simultaneously.
00:11:00
What if we represent that with a new object? I will name it EventProject.
00:11:06
This will represent the fact that it's an event.
00:11:13
Let’s move our specs to a new spec for this new PORO.
00:11:19
We essentially already have the interface we need because we saw it in action in the controller code.
00:11:26
The funded projects method will return the funded projects and the projects method will return the projects.
00:11:34
Through these tests, we can determine that the name should simply return the string 'Pentopia' for our view component.
00:11:42
Now, let’s see what it's like to pass that test for this PORO.
00:11:50
Notice this exercise is largely moving controller code into a new file, a new object called EventProject.
00:11:56
We taking our constants and gave them a name that's hardcoded for now, while also relocating the projects method.
00:12:02
This method will iterate over the constant to grab the projects.
00:12:09
We also use that same scope method we talked about for funded projects.
00:12:17
One last essential aspect we outlined is a way to check if the current project we're displaying belongs to this event.
00:12:23
So, we will accept a project and add a method to return true or false if that project is participating in Pentopia.
00:12:30
By leveraging our previously written projects method, we can ensure the event's progress view component renders appropriately.
00:12:38
We can then simply return the component or nil based on the project’s status.
00:12:44
As I’m coding, I realize the only requirement for this feature is conditionally rendering this progress bar.
00:12:50
Additionally, I’ll move the other methods to the private scope and expose only the event progress view component.
00:12:57
Let's take a closer look at the PORO.
00:13:05
It’s still quite simple. It takes a project and has just one public method.
00:13:12
However, it offers a place for future changes to the EventProject to take form.
00:13:19
We can foresee future use cases for even those private methods, and some of this code may evolve.
00:13:26
Let’s see how the controller looks with this new object.
00:13:32
It’s now much simpler—essentially just one line where we set an instance variable to the return value of the event progress view component.
00:13:39
Our view code remains unchanged; our instance variable can either be nil or the event progress component we want.
00:13:47
This code now adheres better to the single responsibility principle.
00:13:54
Our show action stays focused on the components needed to display the project page.
00:14:00
In contrast, the EventProject is now responsible for knowing when to render this component and for supplying all the information needed.
00:14:06
This refactoring has also granted us much greater flexibility.
00:14:14
With this adjustment, we introduce a new seam into our code.
00:14:20
The term 'seam,' as I’m using it, refers to a place in which we can alter our program's behavior without modifying the actual code.
00:14:28
This aligns with the open-closed principle, which states that software should be open for extension but closed for modification.
00:14:35
In our case, team members should be able to add new functionalities to our system without altering the existing code.
00:14:42
We could modify the internals of EventProject without requiring any changes to the controller code.
00:14:49
Let’s say there's a new requirement regarding whether we show or don't show that component.
00:14:56
We won’t have to alter the controller; the EventProject can handle all new conditional logic.
00:15:03
Leveraging POROs yields seams that introduce simplicity to our design.
00:15:09
The controller remains straightforward, while the EventProject can continuously evolve.
00:15:16
This allows our code to remain inexpensive, easy to extend, and modify as needed.
00:15:24
POROs also provide a means to wrap our uncertainties, allowing our codebase to evolve.
00:15:31
In Greenfield code, POROs play a crucial role when wrapping business logic to represent new domain concepts.
00:15:39
This helps contain uncertainties and hard-coded bits in one place.
00:15:45
Those hard-coded strings and arrays can reside in our PORO, whereas outside the project, we don’t care how funded projects gets evaluated.
00:15:52
Within the PORO, we can develop the code we need as new requirements emerge.
00:15:59
Sometimes we need to tolerate uncertainties while waiting for the right abstractions to appear in our code.
00:16:06
Having a boundary around this code mitigates pain as we wait for future examples.
00:16:12
For instance, when we first received the feature requirement, we could save it as a model.
00:16:20
However, we don’t yet know the attributes that EventProject will require.
00:16:27
We expect to encounter unknowns during this initial phase, including changing table names and managing downtime with deployed changes.
00:16:33
None of these are concerns I want to deal with when adding a new feature.
00:16:41
This PORO helps us get the API we want from a model while alleviating the costs of waiting.
00:16:48
This PORO is designed with the current use case in mind, while allowing any interface changes as needed.
00:16:55
Moreover, our half-baked PORO, with its constants and hard-coded strings, signals to the reader where this object is in its development.
00:17:02
It’s not as set in stone as a model might imply.
00:17:08
Until we receive a requirement that can’t be fulfilled without a new table, I’m happy to wait.
00:17:15
Our POROs also foster faster and simpler testing.
00:17:22
When we refactored our tests around our PORO, we no longer needed to load our Rails app or its gems just to test our code.
00:17:30
Previously, while in controller specs, we would make requests to actions and assert on responses, which are expensive tests.
00:17:37
However, in our PORO, we can create better unit tests that don’t demand as many resources.
00:17:43
Our tests are now faster, as we transition integration tests from the controller into unit tests.
00:17:50
Furthermore, our controller specs will be more straightforward and won't require any test data setup.
00:17:57
They’ll simply expect to receive different responses from EventProject.
00:18:05
In addition, we have an ergonomic, easy-to-use API.
00:18:12
POROs can provide ergonomic features based on how we utilize and compose methods.
00:18:18
Even the public and private scopes we’ve devised contribute to this.
00:18:25
We might also add some syntactic sugar if calling certain methods frequently.
00:18:32
This keeps our code uncluttered and easy to understand.
00:18:37
If we need to wrap APIs, for example, we can use POROs with more semantic names and methods pertaining to our domain.
00:18:44
Using POROs allows us to design code in flexible, composable bits.
00:18:50
It was not as challenging to shift from fitting our code into the controller to developing a new object.
00:18:57
The most difficult part is simply deciding to form a new object during the quest for extendable code.
00:19:05
By continuing this practice long term, we will cultivate more flexible code.
00:19:12
Now, let’s talk about legacy code, which can have various definitions.
00:19:19
For instance, code that the original developer no longer maintains, or code that's no longer engineered but continually patched or untested.
00:19:26
The first bullet reflects how one developer hands the code off to another.
00:19:33
At one point, someone knew the purpose of this code, but they are no longer here for various reasons.
00:19:39
Show of hands: who has ever encountered a legacy code they had a hand in creating only to find it hard to understand?
00:19:47
I find it interesting how such code is never simple to patch without clear abstractions or design for extending.
00:19:53
This often puts us in a challenging position, especially if the code is untested.
00:20:02
It’s intimidating to change code we know has crucial functionality, yet we can’t quite comprehend.
00:20:08
How did we get here?
00:20:15
As new feature requests come in, we search our codebase for adjacent code and stick new code there.
00:20:21
We often add more code where code already exists.
00:20:28
If there’s a complex conditional, what's one more else-if statement?
00:20:36
Unfortunately, the natural trend for code is to grow larger and larger until it tips.
00:20:43
The complexity can become so overwhelming that imagining putting code somewhere else becomes impossible.
00:20:50
Poorly factored legacy code, which is harder to utilize than you might expect, is often criticized.
00:20:57
Yet, frequently this code persists because it continues to fulfill a purpose.
00:21:04
It reflects the constraints applicable during the time this code was written, when requirements were still emerging.
00:21:11
If we find ourselves extending this code, it is likely high-turn and worth an investment.
00:21:17
Refactoring this code can be intimidating due to the lack of tools available for untangling it.
00:21:25
This is where POROs shine.
00:21:32
They offer small, manageable steps within the feature delivery lifecycle, helping us begin disentangling legacy code.
00:21:39
Let’s look back at our app at BackerKit to explore how we can lasso our legacy code with our mighty POROs.
00:21:46
We will examine our pledge controller, as making a pledge is a crucial aspect of our app.
00:21:53
The complexity here is not surprising; in fact, this code does not fit on a single slide.
00:22:00
Prior to this code excerpt, we set some attributes on a pledge.
00:22:07
Pledge.confirm essentially saves the pledge, and, if successful, carries out some actions.
00:22:13
If unsuccessful, we’ll execute an alternate set of actions.
00:22:20
This presents a wall of text—so, I don’t expect you to read everything here.
00:22:27
We need to address numerous tasks once we've successfully saved our pledge.
00:22:35
As I scan this code, it becomes challenging to understand what's crucial for saving a pledge.
00:22:42
Putting on my archaeologist hat, I can speculate how we got here.
00:22:49
A feature request came in stating we want to show star backer status after someone pledges.
00:22:56
A reasonable developer looks for a seam after creating a pledge and sticks this additional functionality in.
00:23:03
I recognize a variety of service objects instantiated within this create method; design exists here.
00:23:09
While assessing this code, I notice two different activities occurring.
00:23:16
Setting instance variables for the next action or user view, and side effects or data modifications to support user flows.
00:23:23
I’m going to capture much of this procedural code that isn’t vital for the redirect logic and move it into a Plain Old Ruby Object.
00:23:30
As I review the method calls, I can see that the arguments for these methods often involve project, user, and pledge.
00:23:37
Let’s create a new service object called PledgeCreationService with user, project, and pledge as parameters.
00:23:44
I’m simply transferring much of the code into this new service object with a call method.
00:23:52
Looking back at the controller, I must retain some code within the conditional.
00:23:59
We've successfully resolved a substantial portion of the conditional branch.
00:24:06
I would argue that even the simple act of crafting a new service object that isn't fully factored produces a positive change in our code’s structure.
00:24:12
We can utilize POROs for small, incremental changes that enhance the code a bit better than how we found it.
00:24:20
We’ve provided a breadcrumb for the next developer to come in and enhance this further.
00:24:27
With this change in the controller, its primary purpose is now clearer.
00:24:35
It focuses on creating the pledge and redirecting the user to the pledge show.
00:24:42
By following the single responsibility principle, we can clarify the intended function of the action.
00:24:49
Hence, when someone extends the code, they can focus on side effects only if they need to.
00:24:56
We also gain a more usable API.
00:25:04
After examining my service object, I can categorize the operations into four distinct groups: creating followers, backfilling data, creating badges, and sending notifications.
00:25:12
Once we narrow the scope of the code we’re reviewing, it's much easier to reason about it.
00:25:20
We can gracefully name these tasks in a more usable manner, making the post-pledge service easy to understand.
00:25:28
Introducing POROs also presents us with opportunities for decoupling.
00:25:35
As I move the code into a service, I gain better insight into its dependencies.
00:25:41
Specifically, when I look at the mailer being sent, I notice it’s triggered when the created pledge funds the project.
00:25:48
This realization occurred as I attempted to decouple that worker into the service, which clarified its requirements.
00:25:57
This approach provides us a stronger foundation for refactoring complexity and decoupling objects.
00:26:04
I considered setting a variable called newly funded, which evaluates to true or false.
00:26:11
This would simplify that gnarly logic being passed into our service.
00:26:18
Decoupling that into a simple boolean will enhance usability.
00:26:25
While performing these refactors, we want our inner objects to be aware of as little as possible.
00:26:31
By saving this as just a true or false variable and passing that to the service, we can support separation of concerns.
00:26:38
Creating more reusable code will increase efficiency in the future.
00:26:45
If the manner we evaluate the last parameter alters, the service object will not require adjustments.
00:26:51
This speeds up our change process significantly.
00:26:58
We’re now going to accept 'newly funded' into the service object, assessing whether to send that email.
00:27:05
I also noticed we can base a lot off 'pledge,' so I’ll encapsulate 'pledge' within the service object.
00:27:12
We can instantiate the backer and project right inside the service.
00:27:19
By introducing this new object for future extensions or modifications, we create a more manageable landscape.
00:27:26
The next developer will appreciate having a clearer platform for extending this code.
00:27:32
They won't have to contribute to the unwieldy complexity already present in this controller action.
00:27:39
This technique is beneficial for untested code.
00:27:46
We can develop a new class or method from existing code and test just that within its block.
00:27:54
Once we separate dependencies from the existing code, we can introduce tests.
00:28:01
After breaking dependencies, we write tests and subsequently implement changes.
00:28:08
Yet, there are times we lack the time or context to break dependencies.
00:28:15
In such cases, leveraging POROs can help us escape a problematic dependency situation.
00:28:22
Let’s suppose we’ve received a request to enhance an existing feature, enabling backers to pay an installment to settle their remaining balance.
00:28:29
Underneath, we need to execute data updates managed by the scheduled payment manager.
00:28:38
The new feature incorporates logging to maintain a record of changes made to payment schedules.
00:28:46
The challenge here is that the manager is complex and untested, making it hard to uncover all its components.
00:28:53
Thus, let’s introduce a new class for this behavior, specifically the LoggingScheduledPaymentManager.
00:29:00
We will require it to respond to all the methods originally defined in the scheduled payment manager.
00:29:09
Therefore, I will extend the scheduled payment manager.
00:29:15
This will allow it to respond to all the same method calls.
00:29:22
Now we can incorporate the existing scheduledPaymentManager object into our newly created logging class.
00:29:29
We’ve successfully created a new class that we can use to maintain the behavior of the scheduled payment manager.
00:29:36
But in this new class, we can integrate that extra functionality and develop it using TDD.
00:29:43
Let’s add a logScheduledPaymentChange method and test drive this new feature.
00:29:50
We’ve now carved out a space to add this new method without adding to the former dependency complexities.
00:29:58
The next step is to call our new method in place of the function we’re trying to extend in the scheduled payment manager.
00:30:05
Ultimately, we would call the existing method, consolidateDueBalance.
00:30:12
Now our wrapper class, the LoggingScheduledPaymentManager, can behave like the scheduled payment manager while sharing its API.
00:30:18
This means it will execute the consolidateDueBalance method yet also incorporate the needed logging behavior.
00:30:25
Finally, we need to replace calls to our existing scheduled payment manager with this new wrapper class.
00:30:33
This transition should work seamlessly as it maintains the same API as the original object.
00:30:39
This method ensures that the code calling it won't recognize it as a different object.
00:30:47
What does this demonstrate?
00:30:54
This technique is called the wrap class technique, specifically aimed at addressing dependency situations.
00:31:02
We can’t decouple these dependencies due to a lack of time or context, making it easier to create a temporary solution.
00:31:10
This is helpful in situations where we can't afford to expand an existing class that already has independent behavior.
00:31:18
Or we’re faced with a large class that we want to keep from becoming even larger.
00:31:25
While this may seem like a significant change, it’s crucial to recognize the complexity already lurking in existing methods.
00:31:33
We could easily have added to that complexity instead of managing it.
00:31:40
This new procedure enables us to cultivate a narrow slice of tested code.
00:31:47
Often, the biggest hurdle to refactoring difficult code is the complexity we face.
00:31:55
Introducing incremental improvements empowers us to outline a roadmap for the rest of the team.
00:32:02
It provides initial tools to help tackle that complex, untested code.
00:32:09
Sometimes undertaking these changes inevitably adds complexity to our code.
00:32:16
While implementing incremental improvements, we must exercise patience with temporary challenges.
00:32:22
We are steadily charting a path toward breaking dependencies and refactoring legacy code.
00:32:30
This is a quote from a talk entitled 'All the Small Things' by Sandy Metz.
00:32:39
In this talk, Sandy walks through refactoring and simplifying specific code in interim steps.
00:32:46
Throughout these steps, it often seems the code grows more complex before it gets simpler.
00:32:54
However, if we’re committed to an object-oriented design, we should have faith that we’re moving towards greater simplicity.
00:33:02
We must recognize that the complexity faced today is worth the trade-off for that clearer end state.
00:33:10
The reality is we don’t always have time to accomplish everything in one ticket.
00:33:17
But we exist in a developer ecosystem, and sometimes taking one step will set the path for the next developer.
00:33:24
They may then continue to the next milestone on their journey of small steps.
00:33:31
POROs are our mightiest tool.
00:33:38
The beauty of POROs is that they create a clear delineation or seam in our code.
00:33:45
They offer strategies for growing Greenfield code in evolving classes.
00:33:52
They allow us to extract domain concepts that are easily mutable.
00:33:59
By leveraging POROs, we can break large complex classes into smaller objects with minimal coupling.
00:34:06
When dealing with legacy code, they let us refactor objects we can then use to build more flexible and faster-deployable code.
00:34:12
POROs grant new beginnings—whether literally in a new file or metaforically at a decision point, we create a new seam for modification.
00:34:19
The next time we grapple with new or existing code, I hope we turn to our simple, plain POROs.
00:34:26
Let’s strive to leave our codebase a little better than we found it, inviting seams of simplicity into our design.
00:34:32
Thank you so much for attending 'Plain Old But Mighty.' I don't have the time to answer questions here.
00:34:38
However, I will be around in the hallway if you have thoughts or inquiries.
00:34:44
Feel free to email or DM me; all complaints or critiques will be managed by Morty!
00:34:50
Here are some inspirations for my talk, and I highly recommend Michael Feathers’ book.
00:34:56
It outlines various strategies for tackling dependencies in legacy code.
00:35:03
I want to extend a special thanks to everyone who offered feedback on my talk, including my dad.
00:35:09
He’s attending his second RailsConf, so let’s give him a round of applause!
00:35:16
Thank you for coming!