00:00:08.040
Hello everyone, as Tim mentioned, I am Michael Fairley. If you think I say something stupid, feel free to tweet at me with angry tweets. I work at a company called Braintree, which is based out of the US. We handle online credit card payments. Over the past four years, we've been serving thousands of businesses, including some very impressive ones that are displayed on the screen. Four months ago, we also began working with merchants based in Australia. If you're interested in discussing payments, please come find me later.
00:00:39.160
Today, I'm going to talk about immutable Ruby. Mutability refers to changing the state of data in your code, while immutability means you do not change that state. Of course, your code will need to result in some changes, but utilizing immutability in small facets can help simplify your code and make it easier to test. I will also discuss the trade-offs associated with immutability.
00:01:05.200
I will begin by discussing some problems that arise from mutability. I will show you four examples of Ruby on Rails applications and some odd scenarios that can emerge, including bugs I've encountered in production. In the first example, we have a simple Person class with a first name and a last name, along with a method called full_name that concatenates the two. When we create a person named 'John Doe', his full name is correctly set as 'John Doe'. However, if we change his first name to 'Jim', his full name remains 'John Doe', creating confusion.
00:01:39.160
In the second example, a user can have multiple email addresses, which can be either verified or unverified. When a user signs up, we send them a verification email that they must click to verify their address. If they change their email address later, it might still show as verified. In the third example, there’s a person class with validation that requires the name to be at least four characters long. If we create a person named 'Bob' and he has a validation error due to a short name, and then change it to 'Robert', the errors on the Active Record object may still indicate that his name is too short, leading to confusion.
00:03:10.640
This next situation is more convoluted but quite confusing. We have a hash and an array where we use the array as a key in the hash. Unsurprisingly, the hash behaves as expected—however, when we try to look for values using both the literal and an old state, we can't find them. Yet, when we add or change a value in the array, we see it duplicated in the hash, which complicates things.
00:03:52.360
Now, let's discuss techniques, tools, and stylistic choices you can employ to take advantage of immutability and avoid the issues presented. Firstly, a value or value object is an object that contains attributes but has no identity. These are prevalent in programming; for example, two numbers are considered equal if they hold the same value, regardless of whether they are separate instances in the Ruby virtual machine. This same logic applies to dates and times, where their equality is defined by the internal data, not by any identity.
00:04:39.080
In Ruby, time has a method called UTC, which provides the UTC version of a time object. Importantly, it returns a new instance rather than altering the original object. However, strings in Ruby are not immutable, which can create confusion. For example, even if we have a string that contains the text of the King James Bible, changing the string does not affect the original text.
00:05:10.039
You can compose values out of other values to create domain objects, such as money. For instance, $1 is different from 10 Australian dollars despite their values being equivalent. There’s a remarkable gem called 'values' for working with value objects in Ruby, which I will show you examples of, demonstrating how it can help clean up your code. Value objects work similarly to structs; calling value.new allows you to pass attribute names and get a new class in return.
00:06:07.639
When you construct an instance of this class, it retains the expected data. Unlike structs, attempts to mutate this data will result in an error since value objects lack mutable methods. You can, however, add methods for formatting, such as to_string or a multiplication method for scaling. For example, if you have a user class with both a billing address and a shipping address, using a checkbox to signify using the billing address as the shipping address leads to cumbersome code.
00:06:36.919
Active Record has a method called composed_of for working with value objects embedded inside Active Record objects, which simplifies assigning a shipping address from a billing address without needing to copy it over. Value objects are incredibly useful for facilitating clear communication throughout your system and allow you to modify your code more easily.
00:07:11.199
Testing value objects is straightforward as well. You simply need to create a value object, call the relevant method, and assert against the result. Importantly, you can run these tests without requiring complex dependencies like Rails or factory libraries, leading to fast execution times. For instance, let's say we want to calculate shipping costs based on a user’s location. Instead of adding a calculate_shipping_price method directly to the user, I propose utilizing a shipping service that operates on values.
00:08:20.000
While this may initially seem convoluted, it simplifies testing, as such methods do not depend on Rails or a database. They also help you remain resilient to changes in requirements. For example, if we later introduce support for users with multiple addresses or ship them goods to businesses, having a structure based solely on values allows for easier modifications. When working with APIs, you often send values back and forth, reinforcing the practice of working with values as opposed to objects.
00:09:44.760
Immutability helps clarify assumptions in your code. It's easy to write code that assumes an object or an array will remain unchanged. However, returning six months later to modify that code can lead to confusion if you unintentionally alter something that was meant to remain static. To make these assumptions explicit, Ruby provides a freeze method for every object, which prevents changes to that object's instance variables. Yet, it doesn’t freeze nested elements—so arrays that are frozen can still have their contents adjusted.
00:10:54.000
At Braintree, we often deal with database records that we want to keep immutable. For example, we record transaction amounts that cannot be altered afterward. To enforce immutability, we have an immutable module that can be mixed into our Active Record classes, preventing updates when attempted. We can also enforce such constraints at the database level using PostgreSQL or MySQL to restrict updates or deletions on specific tables.
00:12:07.120
Event sourcing is another related concept that captures all changes to your application state as a sequence of events. This can either drive the change or serve as logs of what happened. For example, consider a bank account: you can track deposits and withdrawals as distinct events, and from these, derive the current balance. The derived state here is the balance, while the details of each transaction make up the observed state. Importantly, this approach allows for interesting inquiries.
00:12:48.640
You can query historical data, like determining a user's balance from a week ago, simply by reviewing the logged events up to that point. If a user returns a book, instead of removing the initial charge, we can insert a new event that negates the previous action, thereby preserving a complete history. Furthermore, events can be replayed; if a bug arises in production, developers can replay the event log to analyze the exact state of the application at the time of the error.
00:14:38.880
Git is an everyday example of an event-sourced system. In Git, each commit represents an event, and the state at any given time is the working directory. You can retrieve past states by checking out a specific SHA, effectively traveling through time. Git allows for event replays with functionalities like rebase, which re-applies a series of commits on a different branch. I once worked on a family history project where we used event sourcing for managing a family tree stored primarily in PostgreSQL. Given that querying graphs in PostgreSQL isn't efficient, we recorded the event log there while keeping the state of the tree referenced in a NoSQL graph database.
00:16:54.120
This dual approach provided us with data durability; even if the graph database failed, we retained a reliable source in PostgreSQL. In cases of vandalism or errors in the family tree, we could easily identify and revert changes by examining the event log. Following a significant learning experience with the graph system, we later chose to replay the logged events for rebuilding data back into PostgreSQL.
00:18:24.800
Beyond these advantages, event sourcing enables features like undo functionality similar to that found in applications like Microsoft Word. Each action you take can generate an event, so when you hit 'Command Z', the system restores the most recent state via its revert command. If you’re working on rich client applications, any feature that requires undo functionality might benefit from employing an event-sourced structure.
00:19:36.760
Regarding the implementation of event sourcing, you can also maintain derived state in memory. In environments where you're unsure a database will retain data, but you have a permanent data source, you could work with memory constructs like Redis. If you utilize mutable state, you break established software rules that assume data remains fixed. Notably, if you never alter data, the hassles of cache invalidation are significantly reduced.
00:20:42.480
This aspect connects with database normalization, which aims to isolate data to prevent duplication. However, with immutability, you are free from the obligation to normalize since updates will no longer apply to multiple locations. In terms of concurrency, a majority of threading safety issues arise from shared mutable state. To counteract this, you could implement private mutable states with libraries like Celluloid that utilize value objects to communicate.
00:22:25.110
Of course, there are trade-offs associated with adopting immutability—one being speed. Immutability typically leads to increased object allocation and copying, which can slow performance. Thus, we might need to use mutable state in scenarios where optimization is necessary. The fastest performance often lies outside the realm of aesthetically pleasing code.
00:23:34.560
You also lose certain freedoms and might face limitations based on library or framework assumptions involving mutable state. Ruby poses a challenge for immutability since it allows constant reassignment, lacking strict final variables found in other languages. The freeze method is Ruby’s core mechanism for achieving immutability. Additionally, deletion qualifies as a form of mutation. While tweets on Twitter may seem immutable, they can indeed be deleted, impacting cache and validation procedures.
00:24:50.840
Although we are a bit ahead of schedule, if you're interested in exploring further, I encourage you to learn one of three functional languages that emphasize immutability. These languages can make changing state challenging, but the concepts you gain from them will enhance your Ruby development. Rich Hickey, the creator of Clojure, advocates passionately for the dangers of mutable state, and I've included links to several of his insightful talks.
00:26:31.399
Some essential texts on value objects include 'Domain Driven Design', which discusses in depth how value objects interact with mutable objects, as well as the C2 Wiki debates among programming pioneers like Martin Fowler and Kent Beck about value object design. Additionally, Gary Bernhardt’s concept of the functional core with imperative shell can provide a valuable perspective as it emphasizes composing reliable, immutable methods and classes while integrating them with imperative code.
00:27:43.040
In Ruby, we encounter mutable data structures like hashes, arrays, and sets. However, at times, you may desire immutable structures—this is where persistent data structures come into play. When modifying a persistent structure, instead of changing the version you hold, you receive a new representation of that state. Libraries like Hamster provide a simple way to achieve this functionality.
00:29:01.320
With Hamster, for instance, if we create a set of symbols A, B, and C, we can add D to it and receive a new set containing all four elements while the original remains unchanged. This persistence applies to hashes as well. Updating or adding a value results in a new hash with the change, leaving the original intact. You can modify vectors similarly, ensuring full integrity of past versions.
00:30:18.080
In conclusion, persistent data structures can be efficiently implemented, sharing most of their structure with older versions to minimize copying overhead. As a demonstration, consider a vector of numbers where the underlying data structure remains intact unless modified, ensuring seamless transitions and optimized memory usage.
00:31:16.799
Thank you!
00:31:40.079
I am open to questions. One person asked if I could repeat their question and clarify how we transitioned the family tree data back into PostgreSQL.
00:32:10.960
To clarify, once we developed features allowing users to modify family tree records, we created event objects that executed their actions to modify the graph database, while also maintaining their states in PostgreSQL. When we decided to migrate back to PostgreSQL, we updated the code so that executing these event objects would affect a specific PostgreSQL table instead.
00:32:52.120
He also inquired about the database we used, which was Neo4j. Most of our challenges emerged from our handling of it, and yes, I do have a preference for Scala as a functional programming language, but it is easy to slip into object-oriented paradigms.
00:33:17.960
Towards the beginning of the talk, someone pointed out what I meant when I stated that values have no identity, illustrating that they indeed have an intrinsic identity expressed through their data. I appreciate that distinction—value objects are designed to determine their identity based on their content rather than external references.
00:33:54.879
In relation to integration with Active Record, a question was raised about the techniques available to facilitate interaction between these concepts. Specifically, how event sourcing employs value objects as its records, and how such systems maintain their data.
00:34:18.600
I noted that value objects hold intrinsic value within their structure, and despite the lack of methods to manipulate them, it is still feasible to maintain encapsulated state through referenced data. Finally, I was asked about experiences with mutable state within applications and measures taken to minimize its impact.
00:35:20.679
Here at Braintree, we are actively working on mutating parts of our objects, segregating immutable and mutable fields. This approach reinforces the importance of preserving core data integrity while allowing for necessary changes.
00:36:23.180
To conclude, difficulties with deep value object graphs arose, specifically in making changes to references. Since every modification requires traversing the hierarchy and copying states, maintaining a deeply nested structure could prove cumbersome.
00:37:07.760
Yet, in applications relying heavily on mutable state, considerations and designs must convey ease of modification without relinquishing value integrity.
00:37:30.800
Thank you all for your time! I'm delighted to engage in further discussion.