00:00:10.120
Welcome to the presentation titled "From Variables to Domain-Driven Design."
00:00:12.400
Our speaker today is Karynn Ikeda. Karynn is a lover of language, inspired by ancient Latin and modern German, as well as the language we all use—Ruby.
00:00:17.400
Currently a staff engineer at Bist, Karynn leverages semiotics within a Rails codebase to design systems that are easily readable and self-explanatory. Let’s give a warm welcome to Karynn!
00:00:36.160
Thank you so much! It’s wonderful to be here. This is my first time at RailsConf, so I’m very excited to be joining you as a speaker today.
00:00:39.040
As mentioned, my name is Karynn and I work as a staff engineer at Bist. However, I haven't always been a software engineer. My fascination with language led me to study semiotics, linguistics, and philosophy in college.
00:00:49.280
So what is semiotics? It is the study of signs and their use and interpretation. Initially, I thought semiotics was purely academic until I became a software engineer. One of our core jobs as software engineers is to be builders of sign systems.
00:01:06.640
Natural language is the primary sign system we use daily to communicate the thoughts in our minds, just as I am speaking to you now. As software engineers, we use programming languages, such as Ruby, to articulate the behavior of our technical systems.
00:01:20.720
When I say that we are builders of sign systems, I mean that we are inventing our own language. Every variable and class is a creation of developers, unique to the repositories and applications they occupy.
00:01:32.920
As builders of sign systems, it's vital that we grasp how signs work to design better systems. Now, what is a sign? A sign comprises two parts: the signifier and the signified. To illustrate this relationship, I’d like to share an art piece by René Magritte that exemplifies this concept.
00:01:52.280
In this piece, a pipe extends from the frame, and the message reads, "This is not a pipe." While this may seem obvious, the cursive lines of the word "pipe" constitute the signifier, while the physical pipe jutsting out represents the signified.
00:02:00.239
Together, the cursive word and the physical object form a sign. Upon seeing or hearing the word "pipe," our minds invoke the literal concept of a pipe.
00:02:09.840
In relating this back to language, the letters for "pipe" are not universal signifiers. Here is "pipe" represented in a few different languages, all of which should invoke the physical object to speakers of those languages. We can deduce that signs are invented; each language develops its own symbol or sign for "pipe." This brings me to my first point about names: signs are arbitrary.
00:02:29.680
If names can be anything, what determines if one name is superior to another? Today, my talk will focus on the declared name—the signifier—and its role as a sign within an application.
00:02:41.360
Naming is considered one of the hardest challenges in computer science. Choosing and utilizing a name extends a sign system that we want to be comprehensible to other individuals. We need to understand how a name functions within the system to utilize it effectively.
00:03:03.680
Let's take a look at the humble variable.
00:03:04.840
Variable declaration and assignment originated as mechanisms for the machine to know where to store pieces of data, so they could be read or overwritten later in the program. We, as programmers, communicate with the machine. However, if variables are an abstraction for machine language, why is it essential to have meaningful names in our codebases?
00:03:23.680
Couldn’t we use X to serve the purpose? Indeed, if we were only communicating with the machine, we could stop here. But in today’s collaborative codebases, variable names must communicate intent to fellow programmers.
00:03:35.120
A programming language serves two functions: communicating with the machine and with other programmers. Particularly in an untyped language like Ruby, there are no guarantees about what is being signified.
00:03:49.920
Names act as signs, conveying what is signified to other humans. The human aspect is crucial; the machine doesn't care that you've assigned zero to X, but the human does. The variable X only holds meaning if you understand the context.
00:04:03.440
You can see the variable assignment in the code, and in that context, the relationship between X and what it signifies is clear. But what happens when your code is viewed out of context? This scenario occurs often.
00:04:17.760
For example, consider controller instance variables in Rails. Rails copies controller instance variables to the view initializer, leading programmers to think that these are semantically identical variables. Thus, when we see these variables in the view, we read them out of context and rely on the strength of the variable's name to convey what is signified, since we cannot directly see the variable declaration in the view.
00:04:43.320
Returning to our earlier example, let’s assign a more meaningful name. Is "bit" a better name than "X"? I assert that it is only better because it draws on external context. In English, a bit is defined as a unit of information represented as either zero or one in binary notation.
00:04:57.760
Thus, we are slightly cheating in making this comparison. Names, therefore, possess no inherent meaning; they derive significance from their differences compared to other names.
00:05:09.360
Consider the two names: "bit" and "bits." The addition of an 's' conveys certain assumptions about each. If you guessed that one is a scalar and the other a collection, you would be correct.
00:05:22.920
Pluralization instantly communicates a lot to us in both natural language and in code. Rails utilizes singular and plural throughout its MVC framework to dictate naming conventions.
00:05:34.520
This presents a comfortable framework for us as Rails developers. What about differences where the characters remain the same but capitalization varies?
00:05:40.000
We know that if we use all caps, conventionally this signifies a constant in Ruby rather than a variable. This leads us to certain assumptions about whether or not we expect the value to change.
00:05:50.160
What happens when we introduce punctuation? For instance, adding a question mark often indicates a method in Ruby that might return a Boolean value. Thus, pluralization, capitalization, and punctuation each possess meanings in Ruby distinct from how we use these differences in natural language.
00:06:05.000
We’ve already begun generating a significant amount of meaning based on subtle differences. Let’s return to X. This variable could represent anything, but do you perceive it as different now?
00:06:12.120
Even if we have no prior knowledge of X, can we make assumptions based on our discussions? We can leverage our understanding of coding conventions to aid our comprehension.
00:06:29.360
Another way to consider difference is through the concept of binary opposition, the idea that two concepts cannot coexist simultaneously because they are defined in relation to one another. In binary notation, this is expressed as ones and zeros, or on and off. In Ruby and other languages, we express this through booleans—true and false.
00:06:49.360
We can also define these two constants as exact opposites. For instance, if I change "stop" to "true," "go" would immediately become "false." This provides a prime example of how meaning within a sign system is interdependent.
00:07:01.600
As we continue to evolve sign systems, like a codebase, we must reconcile how the introduction of a new sign or name alters the meaning of existing signs. Here, for example, we’ve introduced "slow," defining it as a false class and a true class.
00:07:17.760
What does "slow" equal in this example? We retain the names—the signifiers—while altering the values that are signified for it to make sense. This offers flexibility, but you must be cautious when considering binary oppositions.
00:07:30.160
English provides us with tools to create similar types of opposition through prefixes and suffixes, such as "inaccessible" versus "accessible" or "fearless" and "fear." Thus, signs are not static; their meanings will evolve as other names are added or removed from a system.
00:07:48.120
What happens if we eliminate differences? Fortunately, in the global scope, a variable or method name cannot be reused without overwriting its definition. However, names can still be reused when considering different scopes, which leads us to classes.
00:08:07.480
Returning to our art piece, it references an earlier work by René Magritte where the message beneath the pipe translates to "This is not a pipe." Here, we see that the name "pipe" signifies both a plumbing pipe and a smoking pipe.
00:08:23.760
Within the context of each art piece, this is internally consistent. However, when comparing the two pieces, the semantic meaning becomes less clear. Classes act similarly in our code; when attributes and methods are private, we only need to be concerned with the semantic relationships within the class.
00:08:39.120
Imagine we find a wheel for steering and tires for motion. When attributes and methods are public, we need to consider the semantic interactions between different objects.
00:08:55.680
In this instance, we also have a unicycle, which has a wheel. Is the car's wheel the same as the unicycle's wheel? Unlike art, we cannot easily see inside the frame to determine if "wheel" signifies the same concept; we must look at the unicycle class to find out.
00:09:10.880
Here, the unicycle wheel is more similar to the car's tires, while the handle correlates more closely with the car's wheel. When the same signifiers are utilized together but signify different concepts, it can create cognitive dissonance, complicating our understanding of the code.
00:09:29.760
Names of signifiers can be identical but signify different concepts, resulting in potential confusion. However, it is important to note that difference is not inherently negative; as long as the terms signify the same concept, everything is okay.
00:09:45.880
We can leverage this through duck typing; if "go" conveys a similar concept, even if implemented slightly differently across classes, we can create cognitive similarity.
00:10:02.560
Reflecting on the artwork, consider how both pieces symbolize the act of signification within their respective frames, becoming signs themselves while drawing connections to the broader realm of art.
00:10:20.920
Signs are not one-dimensional; they can encompass other signs. A further example of this phenomenon is found in compound words within English—such as "automobile," which when literally interpreted means "self-moving." This vehicle represents something distinct from its components.
00:10:36.760
Classes encapsulate variable and method names, acting as their own unique signs. Similarly, the introduction of new named classes will alter the semantic relationships of existing names, affecting both higher and lower-level signs.
00:10:52.079
Imagine we are Victorian developers considering a semantic model for a Victorian vehicle with wheels, a chassis, and horses. Upon introducing the "horseless carriage," how does this transform our sign relationships?
00:11:10.720
Initially, it may seem little has changed. We can accommodate this new concept easily, as it shares many attributes with a carriage. We could even devise a method to differentiate between a carriage and a horseless carriage based on whether it has horses before considering the engine.
00:11:25.920
However, if we want to introduce buses, cars, or other types of moving objects, we need to rethink our decision to use the term "horseless carriage." In fact, this term was merely another name for the automobile.
00:11:40.560
When we transitioned to "automobile," we altered not just the name of the horseless carriage but the very way we conceptualized "horse"—shifting its definition towards "engine" and transforming "horsepower" into an abstract unit of power.
00:11:53.120
Consider how many "horseless carriages" you may still carry in your codebase. We have discussed various strategies for structuring our sign systems through classes. However, as codebases expand, we may need to consider if classes are sufficient.
00:12:06.680
We’ve witnessed how microservices break down code into manageable components, allowing individual teams to own specific portions. The computer doesn’t care how we organize our files as long as they’re accessible—this is primarily a human problem associated with scaling our code as engineering organizations grow.
00:12:20.439
This necessitates an organizational structure to understand how different pieces relate, thereby enabling task breakdowns and ownership assignment. Another principle gaining traction in the Ruby on Rails community is the concept of the modular monolith.
00:12:34.880
Rails provides organizing principles based on the Model-View-Controller design pattern, functioning as an organizational approach. While Rails is not particularly opinionated, it introduces the idea of namespaces, allowing for organized frameworks.
00:12:49.440
However, within this MVC model, we lack a holistic view of all that pertains to a given namespace, splitting the information across different hierarchies in our directory structure.
00:13:06.319
This segregation leads to multiple frames. Fortunately, numerous tools now assist in organizing our code. One such tool is Pax modularization, where each pack represents a modularized domain package.
00:13:20.720
We can describe these packages using tools like Packwork to define dependencies, ultimately helping us create a hierarchy that makes sense when we want to aggregate classes and files.
00:13:38.960
Instead of having your namespace buried amongst each model's views and controllers, using packed Rails helps us position our packages closer to the root so we can view all of our code in the context of that MVC model.
00:13:51.360
However, to do this effectively, we need to consider what constitutes a good package. Robert Martin's guidelines promote strong functional cohesion, emphasizing that we should group together classes likely to be reused in tandem.
00:14:06.760
Conversely, we should avoid grouping classes that aren't functionally related. This principle makes logical sense, and as one of the earliest Paxwork adopters, Shopify has conducted retrospectives on this approach.
00:14:20.799
Developers tend to cluster code into packages based on semantic clues that might have little relation to how the code actually runs. This approach seems to contradict Martin’s emphasis on grouping code by function.
00:14:34.880
It is tempting to cluster similar-sounding classes together for semantic clarity—an instinct we’ve established nurtures difference. By violating this notion of difference, we risk creating cognitive dissonance.
00:14:47.760
If I divide two files with manufacturing-related names, I must address the question of what "manufacturing" truly represents. This leads us to the interplay of functional coupling and semantic coupling, which may not always align.
00:15:00.000
We often make tough decisions prioritizing one aspect over the other. For Shopify, focusing on functional cohesion means setting aside semantic coupling for a future consideration.
00:15:12.199
As software engineers, we may notice these tensions, yet we often overlook semantic debt as a subset of technical debt. Semantic issues don't typically emerge from coding shortcuts, but rather from the system's evolution over time.
00:15:26.339
As we add new names, classes, and variables, we might not recognize that the sign system is evolving. It’s crucial to understand that renaming can be a significant investment, and this outline isn't a suggestion to rename every file or class.
00:15:40.000
Instead, it sheds light on how these semantic contradictions arise in our codebases. Treating it as any other form of technical debt requires prioritization, as there are costs to consider.
00:15:55.760
These costs include longer onboarding times for new team members as they navigate semantic contradictions, potentially introducing bugs due to misinterpretations of how specific code operates.
00:16:10.920
Is there a better approach? Most of us manage systems mirroring the real world. Therefore, modeling our code on names used in the real world can help mitigate semantic issues since business domains generally change at a slower pace.
00:16:25.720
This principle represents the core of domain-driven design, which aligns the design of our software with our comprehension of the real-world business domain.
00:16:40.000
Domain-driven design starts with a focus on language, rooted in semantics, and observes how that language is utilized outside of code before embedding these names back into the software system.
00:16:55.040
The concept of ubiquitous language implies that by modeling our signs after the real world, we can reduce the mental burden required for semantic understanding since we will also inherit natural associations.
00:17:08.080
This shared language can facilitate clearer communication between different stakeholders, fostering discussions regarding how our systems should evolve.
00:17:20.560
This is particularly relevant for established business domains, however, our innovation-driven field may produce coding that creates entirely new domains.
00:17:38.440
Consider social media—a domain that originated with no direct real-world counterpart. In this case, how should we choose our names? Reflect on the power of metaphor.
00:17:58.560
We find ourselves in Detroit today, the birthplace of the first mass-produced automobile and a fascinating concept called the assembly line. Yet, it's crucial to note that Henry Ford didn't invent the assembly line; he appropriated the idea from the meat-packing industry.
00:18:12.640
Ford’s innovation lay in reframing how the assembly line applied to automobile manufacturing rather than merely inventing the concept.
00:18:27.420
In meat packing, butchers cut parts from the animal as it moved along the line—an operation that Ford imagined in reverse. Instead of removing parts, they would add them.
00:18:41.440
This is not a perfect analogy, but it highlights the significant similarities that can be leveraged when designing systems based on language from other industries.
00:18:56.000
Consider the completed car, which occupies a similar conceptual space as the animal's body. We see here our metaphor stretches across to connect our horses and combustible engines.
00:19:09.000
As we conclude, remember that names establish a framework for defining problems within our systems. We’re not just presenting our fellow developers with a glossary of terms but with a comprehensive sign system.
00:19:24.000
Understanding a name's role within this system allows us to collaborate effectively and construct superior systems together.
00:19:40.000
I'd like to leave you with one final thought. When the creators of Unix sought to express how to connect the output of one function to the input of another, they aptly turned to the metaphor of pipes.
00:19:56.960
This metaphor simplifies our understanding of what the symbol indicates and how it should function. It serves as a classic example of a lasting name that continues to resonate today.
00:20:11.280
Thank you!