Ruby on Rails
Doctrine of Useful Objects: Separate Fact from Fiction in OOD

Summarized using AI

Doctrine of Useful Objects: Separate Fact from Fiction in OOD

Scott Bellware • September 19, 2023 • Wrocław, Poland

In the presentation titled 'Doctrine of Useful Objects: Separate Fact from Fiction in OOD', Scott Bellware discusses the principles of object-oriented development (OOD) and how to effectively design software systems. He emphasizes the importance of understanding the differences between object-oriented programming and the creation of merely data structures. The video covers several key concepts including:

  • Introduction to Eventide: Scott introduces Eventide, an open-source toolkit for building evented systems, and its focus on community and education in software development.
  • Understanding Object Orientation: The discussion begins with the assertion that while object orientation can provide advantages, it does not guarantee them. A distinction is made between average capabilities in programming, often represented by a bell curve of the population's skill levels.
  • The Adoption Curve: Bellware highlights the 'crossing the chasm' phenomenon where early adopters of technology differ significantly from the early majority, creating challenges in knowledge transfer.
  • Fundamentals of Design: Key principles such as afferent and efferent coupling, single responsibility, and the 'tell, don't ask' principle are discussed to illustrate effective software design. Afferent coupling reflects how many objects depend on a certain object, with low afferent coupling being more advantageous for maintainability.
  • Critique of Common Practices: He critiques the 'fat model, skinny controller' design often seen in Rails applications, explaining how this can lead to confusion in responsibilities between models and controllers, ultimately leading to complex testing scenarios.
  • Creating Useful Objects: Scott introduces the doctrine of useful objects, which should be designed to limit dependencies and avoid nil reference errors. A useful object must have all dependencies instantiated at creation time.
  • Testing Strategies: Emphasis is placed on the clarity of tests, proposing a model where tests are cleanly separated from business logic and using dependency injection effectively to enhance maintainability.
  • Conclusion: The talk concludes by reiterating that successful software design focuses on reducing unnecessary afferent coupling and adopting clear principles that promote maintainability and effective testing practices.

Overall, the presentation acts as both a critique of common software design practices within object-oriented programming and a guide to more solid methodologies that encourage clarity and efficiency in development.

Doctrine of Useful Objects: Separate Fact from Fiction in OOD
Scott Bellware • September 19, 2023 • Wrocław, Poland

Doctrine of Useful Objects: Separate Fact from Fiction in Object-Oriented Development

wroclove.rb 2023

00:00:04.259 Thank you. So, did anybody hang out with Nathan last night?
00:00:09.599 Yeah, so what’s the bet? Does anybody have a bet on what time he's going to wake up today based on the state he was in when he left?
00:00:14.700 My bet is that he's in Berlin right now, as Andre said.
00:00:22.320 I'm Scott Bellware. I'm the co-founder of Eventide, which is a toolkit for evented autonomous components.
00:00:29.220 Like I said yesterday, we're finally able to take the words 'services' and 'microservices' out of the tool name now that those terms are no longer popular.
00:00:35.280 Eventide is a toolkit for building evented systems using Pub/Sub autonomous components and event sourcing.
00:00:40.860 We just came back from our first summit, a mini-conference held in British Columbia, Canada, where several of us gathered to plan the next major version of Eventide. We also held a contributor boot camp, inviting anyone interested in becoming an open-source developer to participate and receive training to get started.
00:01:05.640 We work on a product called Neuron, which is a legal process automation toolkit for one of the leading venture capital law firms in the world. This firm has locations in San Francisco, London, Sydney, Hong Kong, and other major cities. All the core logic is built on Eventide, while the front end is developed using Rails.
00:01:19.380 So, what is Eventide? It's an open-source project, and our community plays a significant role in everything we do.
00:01:24.840 It's a mindset, a methodology, and also an educational platform. When we talk about the Eventide framework, we're not just referring to software tools; we consider that a toolkit.
00:01:30.300 The framework informs our thinking, and a major aspect of this is not only building software but also teaching.
00:01:36.480 One thing we're very proud of is winning the Ruby Award in 2019 for social impact, which acknowledged our engagement with the community and our commitment to education.
00:01:42.180 Mostly, when we discuss software development, we're focused on object-oriented development. We also cover methodology, management, UI/UX, testing, and other areas. However, at its core, we emphasize object-oriented programming.
00:01:52.560 There's an old saying that object orientation allows for advantages but doesn't provide advantages. This will be an important point to keep in mind as we go through today's presentation, which will center on design and the fundamentals of design.
00:02:00.180 Let's talk a bit about people, specifically programmers. This shape should hopefully look familiar; it's a normal distribution curve, or a bell curve.
00:02:06.600 A bell curve can be divided into standard deviations. One common way to analyze a group of people is to look at six standard deviations, or six sigmas. Observations show that typically about 70 percent of the population falls within Sigma 3 and Sigma 4, which represent the average. If you take average height, weight, income, or any other measurable attribute, you will usually find that the middle part of the bell curve contains the largest number of people.
00:02:22.920 This same curve is used to describe adoption. Roger's adoption curve explores how any technology develops within human society, from its invention to its later stages. The numbers roughly align, with about 70 percent of the population residing in the middle.
00:02:36.240 In both cases, we observe averages or common characteristics in the middle while reserving the edges for exceptional cases, which typically are oriented from low to high on a left-to-right axis. In the adoption curve, skills or capabilities can be represented similarly, appearing low on the left and high on the right, with most people clustering in the middle.
00:02:51.480 Returning to the adoption curve, an interesting phenomenon is the chasm. This has been a topic of significant discussion in recent decades. The term 'crossing the chasm' often relates to the difficulties of getting technology adopted by the majority outside early adopters.
00:03:07.560 Innovators are the inventors; they easily adopt their technology, and early adopters follow suit. However, moving to the early majority or the majority can present challenges. The chasm appears to consume knowledge and understanding.
00:03:19.440 People in the early majority are separate from the early adopters—different social groups that don't communicate. This lack of understanding makes it challenging for the majority to integrate knowledge from early adopters.
00:03:31.740 Instead, the majority often follow even more exceptional individuals—the inventors and innovators. This gap in communication creates a huge challenge.
00:03:43.860 This leads to a somewhat shocking assertion: Active Record objects are simply data structures that are built in an object-oriented language. While any language will require data structures, that doesn't inherently make them object-oriented. Each programming paradigm influences the design of data structures.
00:03:57.300 For those learning software development through Rails, it's possible you haven't grasped the principles of object orientation yet. This contributes to the difficulties many Rails systems and applications face.
00:04:11.520 Object orientation allows for advantages, but it doesn't inherently provide them. You might question if this matters if your company is progressing and your team is functioning well. However, the underlying issue is that development is supposed to be easier than it often is in practice.
00:04:27.900 Let’s discuss fundamentals that will help clarify why challenges arise and what strategies you can implement in the future. First, consider afferent and efferent coupling, as well as generalization and specialization.
00:04:39.900 These concepts address how abstract or concrete a unit of design may be. We will delve into these topics deeper as we proceed, but for now, I'll present the following principles.
00:04:51.240 In software development, understanding single responsibility is crucial. A single unit of software should focus on one task or area rather than many. This connects to the principle of 'tell, don't ask,' a manifestation of encapsulation that helps prevent software entanglement.
00:05:06.240 Next, let's review the coupling dimension. Afferent coupling refers to calls into an object, while efferent coupling refers to calls out of an object.
00:05:12.880 For example, an object with high afferent coupling means many other objects rely on this object, while an object with low afferent coupling experiences less reliance.
00:05:20.640 To illustrate, if we have two scenarios, which one is easier to change? Presumably, the one with lower afferent coupling would be less troublesome to modify.
00:05:32.040 When you change a highly dependent unit of software, you must also investigate and potentially validate all the software pieces entangled with it. Thus, as we review the coupling dimension as a continuum, we will evaluate how afferent and efferent coupling impact our software.
00:05:49.560 Moreover, let's think of our conceptual dimension as a vertical continuum. I'll provide examples of afferent and efferent components. A Rails model, for instance, is quite central to an application, placing it in the afferent category.
00:06:07.740 An example of efferent could be a controller—it typically only interacts with other objects while remaining independent. No other code invokes it directly except through the front controller or web framework.
00:06:16.680 By positioning these components, we arrive at four quadrants. The upper section contains components expected to change rarely, while those below are anticipated to change frequently.
00:06:31.860 This creates a happy quadrant of afferent and generalized objects, like a system object in Java or C#. It has high afferent coupling since everything tends to derive from it—these objects are generic and don’t serve specific purposes. Conversely, the other happy quadrant consists of efferent and specialized elements, like a controller class.
00:06:49.440 These controller classes orchestrate other objects, initiating outbound calls specific to their operations.
00:07:06.240 We find other quadrants less beneficial as the first describes a scenario where a general class possesses all methods from other classes—this becomes an absurdity. Such a model class would result in an inversion of abstraction.
00:07:20.340 Similarly, the last quadrant features an efferent component with many inbound calls but also specific functionality. This configuration can lead to absurd outcomes.
00:07:39.900 Now, considering the 'fat model, skinny controller' ideology in Rails software design, which quadrant does this fall into? It is indeed one of the absurd categories.
00:07:52.920 Our experience shows that 'fat model, skinny controller' leads to fundamental mistakes in software design. This practice places specificity in the model objects that should only be present in controller classes.
00:08:07.140 If your model object contains code only utilized by a single controller, it indicatively suggests crossed responsibilities, as it blurs the line between model and controller roles.
00:08:16.740 Additionally, this configuration undermines testing as Rails controllers are challenging to test properly. If test-driven development (TDD) had been applied appropriately from the start, this issue could've been easily avoided.
00:08:30.840 Retrofitting software to adhere to a mistaken paradigm feels like a last-ditch effort to validate a non-ideal design. To clarify this further, it's helpful to remember a handy acronym representing the desired design quadrant and process.
00:08:48.600 I have a blog write-up discussing the 'distance from the main sequence' metric. The term originally coined was somewhat abstract but represents a fundamental design principle that everything should serve to restrict dependencies and reduce the risks associated with changes.
00:09:07.740 Essentially, a useful object performs a specific task and should change only in rare circumstances. The meaning behind 'tell, don't ask' conveys the importance of encapsulating responsibilities correctly.
00:09:22.740 To recap: our focus is on reducing afferent coupling as a way to maintain manageable change within our software architecture.
00:09:38.460 What constitutes a useful object? This doctrine shapes how we approach all of our software development and object-oriented systems here at Eventide. All our tools and customer projects follow this paradigm. Adhering to this methodology simplifies nearly 40 years of object-oriented design.
00:09:56.520 A useful object must have all its dependencies instantiated upon creation. None should pose a risk of being nil, otherwise, it results in runtime errors.
00:10:09.600 This is a fundamental design issue because nil reference errors render components unusable—thus, leading to frustration.
00:10:23.880 Moreover, we further differentiate an object's initializer from logic. Initializers should only accept and record primitive values they receive.
00:10:39.840 In doing this, we isolate the primary creation concern from other potential complications.
00:10:46.380 Additionally, we ensure the object doesn’t require an excessive structure like an inversion of control container just to function properly.
00:10:59.520 As a result of these strategies, we diminish the need for test doubles or mock objects and instead depend more on telemetry, embracing a cleaner design.
00:11:10.200 The objective is that our test code remains sacred, avoiding unnecessary complication, which leads to enhanced clarity.
00:11:22.680 Now, let's preview a code snippet showcasing a simplistic test case. This case will sign up a user through an HTTP client, albeit using a custom protocol instead of JSON.
00:11:36.000 The 'sign up' class should feel intuitive, using dependency injection to facilitate user registration.
00:11:48.600 However, a complication appears in the test setup—the infrastructure setups obscure the intent of the test, making it less scannable and readable, which hinders the overall clarity.
00:12:04.380 When test code is cluttered with irrelevant structures, it disrupts the flow of knowledge retrieval from that code, resulting in reduced efficiency.
00:12:22.680 In this scenario, passing nil values during initialization leads to predictable errors. Instead, our design objectives dictate that these objects need to be usable from the moment they are instantiated.
00:12:40.680 Additionally, we'd prefer not needing to consider the HTTP client during a test, properly abstracting it away from the code.
00:12:56.280 Using overrideable methods or optional arguments can even warrant the warning that our design may not be optimal.
00:13:12.840 Rather, we seek the initializer to express conceptually essential knowledge, not just dependency mechanics. That’s our end goal.
00:13:25.080 In that vein, the 'sign up' class should cleanly receive user data, streamlining direct comprehension without convoluted reasoning. By converting the dependency into an attribute, we enhance simplicity.
00:13:40.560 However, if that attribute defaults to nil, it can induce nil reference errors. We must not only address this but also ensure that a strict adherence to healthy design practices prevails.
00:14:00.840 Using null coalescing patterns allows for a graceful handling of potentially nil instances, enabling seamless usability.
00:14:17.380 Yet, we impose a creative constraint; we need to maintain our ability to manage and control our dependencies effectively.
00:14:32.880 Introducing a specific library, 'mimic', can officially establish null objects—allowing an instruction to be implemented while ensuring that no real HTTP calls are necessary during tests.
00:14:46.640 Through defining parameters properly, we avoid the risk of triggering live services elsewhere. Instead, we can attach diagnostic purposes without compromising our design.
00:15:00.000 The diagnostic aspect of our design allows gathering telemetry and analyzing interactions effectively, ensuring better transparency.
00:15:21.100 Now, I will refine the design further by implementing a dependency macro library that creates easy-to-read imbued dependencies.
00:15:37.500 This library allows leveraging specialized methods through defined interfaces—upholding a certain level of abstraction while avoiding excessive complexity.
00:15:55.140 Utilizing these constructs allows our classes to maintain clarity across both convenience and mechanics, leading to a solid implementation.
00:16:10.700 When invoking the initializer for dependencies, we must acknowledge how they interact within the context of our application.
00:16:28.540 To wrap up, the classes we design seek to provide both clarity and control—allowing them to operate as intended without interference from external structures.
00:16:44.640 We have painstakingly adopted an approach that promises high degrees of maintainability without convoluting the core logic with dependencies.
00:17:02.320 Moving further, we operationally separate tests from core logic, encapsulating them distinctly and assigning designated structures to each concern.
00:17:19.120 With that, let’s open the floor to questions, comments, or critiques. If any objections arise, please feel free to present them.
00:17:39.960 Yes, I have a question. Yesterday you mentioned that one can gauge the quality of a test based on its dependency setup. Could you elaborate on how this relates to the design of our sign up process?
00:17:58.920 In our situation, the HTTP client is essential for sign up, yet we rely primarily on its interface rather than its implementation.
00:18:10.680 Yes, the nature of this test examines interactions. The purpose is to confirm proper communication with the HTTP client during the sign-up process.
00:18:27.120 In cases where the HTTP client returns values, we would implement specialized methods to facilitate this requirement.
00:18:44.040 Essentially, we would encapsulate expectations for return values while providing checks for interaction quality.
00:18:56.640 So essentially, we navigate through testing practices with caution by emphasizing direct interactions to gauge functionality.
00:19:09.840 Correct.
00:19:20.960 This leads to a discussion on whether the dependencies and design structures we use truly influence the clarity versus complexity in our implementations.
00:19:31.640 While employing dependency injection containers can offer convenience, we must emphasize the autonomy of our classes to configure their dependencies without reliance on external frameworks.
00:19:47.780 Thus, the core principle ensures minimal external entanglements while enhancing the maintainability of our designs.
00:20:00.520 Thank you, everyone! It’s been great sharing these insights with you all. If there are any further queries, feel free to reach out!
00:20:11.240 So, it looks like we’re out of time. Thank you for your attention!
Explore all talks recorded at wroclove.rb 2023
+14