Metaprogramming

Summarized using AI

Mapping Concepts Into Code

Chris Oliver • April 11, 2024 • Sydney, Australia

In this talk titled "Mapping Concepts Into Code" delivered by Chris Oliver at RubyConf AU 2024, the speaker explores the complexities involved in designing a notifications feature for applications. The intention is to reveal the often hidden challenges that developers face when transitioning from simple ideas to concrete implementations in code.

Key points discussed include:
- Intuitive Code: Oliver begins by defining intuitive code, highlighting Ruby's readability and the satisfaction of discovering existing methods within frameworks like Rails. However, he underscores that readability alone does not guarantee intuitiveness in coding.
- Complexity of Notifications: Using notifications as a focal point, Oliver argues that implementing them is far more complicated than it initially appears. Each application tends to handle notifications differently, often leading to user frustration.
- Mapping Out Logic: To illustrate the complexity, he reflects on a tweet regarding Slack's notification logic, which considers various factors such as user preferences and priority levels. Oliver emphasizes the importance of context in delivering notifications effectively.
- Creating a Notifications Gem: He outlines the design process of a new Ruby gem called Noti, modeled after Rails' Action Mailer and Active Job, to create an intuitive notification system. This system allows for the definition of various delivery methods, such as emails or push notifications, based on user settings.
- Refactoring and Simplifying: As Oliver digs into code, he discusses the necessity of organizing the notification logic to avoid confusion, explaining the balance between abstraction and readability. He introduces the concept of "ordered options" to streamline delivery methods.
- Job Processing: The talk guides through the lifecycle of notification processing, from queuing jobs to ensuring that delivery methods are clearly defined without over-complexifying the architecture.
- Best Practices: Oliver emphasizes the importance of clarity in code and the avoidance of redundancy to maintain clean, maintainable designs. He warns against the pitfalls of overly complicated implementations that can arise from poor structuring.
- Future Implications: Closing his talk, he asserts that the principles discussed can lead to enhanced notifications systems in future Rails versions, fostering a clearer, more user-friendly coding experience.

The audience is left with a significant takeaway: a well-organized approach to handling notifications can lead to better user engagement and development efficiency. Oliver encourages developers to think critically about their designs and strive for simplicity and clarity in their code.

Mapping Concepts Into Code
Chris Oliver • April 11, 2024 • Sydney, Australia

Implementing a feature like "notifications" in an app sounds simple, right? As you dig in to problems like this, you'll realize the complexity that lies below the surface.

In this talk, we'll walk through designing a feature like Notifications and how naming, DSLs, metaprogramming, and a bunch of other small decisions can make code feel delightful to use. Plus, we'll take a look at some of the decisions along the way that didn't turn out so well, analyze why they didn't work, and how we can improve them.

RubyConf AU 2024

00:00:04.080 Hello, everybody. Sorry, let me fix my slides. I wrote these in the United States, so you can read them here.
00:00:10.240 I was thinking about what I wanted to talk about, and I recently did a bunch of refactoring on some projects.
00:00:18.680 This experience gave me an idea about how to take an idea and implement it in code.
00:00:24.160 There are many times when the challenges are hidden below the surface.
00:00:29.439 I wanted to walk you through a refactoring I did in the past few months.
00:00:37.640 First off, let’s talk about what intuitive code is.
00:00:42.800 When you use something like Rails and you wonder if a method exists, you just try it.
00:00:49.120 It’s a wonderful feeling when you discover that someone else has already thought of that method and implemented it.
00:00:56.719 Ruby is a language that naturally reads kind of like English.
00:01:02.680 We even have question marks at the ends of methods, which is quite awesome.
00:01:08.280 However, this is not enough to make code feel intuitive.
00:01:13.600 Naming things is difficult; architecting them is also a challenge.
00:01:19.840 Overall, it can be quite hard.
00:01:25.240 Let's look at an example: notifications.
00:01:30.600 If I asked you to implement notifications, your instinct might be that it's easy.
00:01:35.720 After all, you might just think you can send an SMS or a message to Slack, right?
00:01:42.560 However, it’s really not that simple.
00:01:49.079 We've implemented notifications in all of our applications differently every time.
00:01:56.399 In the end, they all end up being a bit annoying in one way or another.
00:02:02.680 So I thought, why not try to map this out?
00:02:08.840 I saw a tweet from about five or six years ago.
00:02:14.519 It discussed how Slack decides to send a notification.
00:02:23.000 There are many factors to consider: user preferences, Do Not Disturb mode, priority issues, and so on.
00:02:30.440 There are many decisions that come into play when considering how to deliver a notification.
00:02:36.519 For instance, they might send you a push notification.
00:02:43.159 If you read it, they know they don't need to send a notification later via email.
00:02:49.560 However, if you don't read it, they may escalate it to your phone, followed by an email.
00:02:55.640 This illustrates the complexity of the logic involved.
00:03:01.959 Some examples of notifications include a notification bar that pulls records from the database.
00:03:08.720 These records might also need to be broadcast in real-time via WebSocket.
00:03:15.159 If you are on the page and someone comments on something, we want to show that immediately.
00:03:20.840 You might want to send messages to Discord, Slack, or Teams.
00:03:26.159 This could be done via webhooks or API calls.
00:03:31.280 Of course, we also have SMS through services like Twilio or Apple's Push Notification Service.
00:03:38.599 And let's not forget about email.
00:03:43.799 So I was thinking: surely we can figure out how to do this in Ruby, making it feel intuitive.
00:03:50.239 I wanted to create a new project for this process.
00:03:55.920 I started building a gem called Noti, seeking inspiration for its design.
00:04:01.239 I wanted it to feel like Rails because it would be used in Rails applications.
00:04:07.599 I looked at Action Mailer and Active Job to model the behavior.
00:04:13.519 We can define the logic in a class and provide context via a block.
00:04:19.160 We can specify what to send, such as the new comment email, and prompt to deliver it in the background.
00:04:24.560 Notifications generally operate in the background, making it a suitable model.
00:04:31.320 My vision included a class that would allow the definition of multiple delivery methods.
00:04:36.479 We could specify delivery options such as email, Action Cable, and the database.
00:04:42.520 We might want to send an email only if the user has opted to receive emails.
00:04:49.759 Additionally, we could delay the emailing for five minutes, checking if the notification was read.
00:04:56.479 We wanted a flexible interface that establishes all of these rules.
00:05:01.639 Making a complex decision tree similar to Slack's, if needed, would be possible.
00:05:07.440 This would allow us to implement helper methods, ensuring clarity for users.
00:05:13.600 So, how do we implement something like this?
00:05:19.120 We can start by creating a parent class from which our notification classes can inherit.
00:05:25.400 We merely need to collect the delivery method details, which can be stored in an array.
00:05:32.680 This initial step is quite simple, perhaps even too easy.
00:05:40.120 Now, if we add from the previous example, say, iOS push notifications,
00:05:46.840 we end up needing quite a bit of context to send a notification.
00:05:52.919 We require certificates, key IDs, team IDs, connection pool size, and other elements.
00:06:00.440 This makes the class harder to understand since the iOS specifics are not well contained.
00:06:08.120 This could be similar if you've set up associations or custom validations in Active Record.
00:06:14.440 You can specify a symbol that calls a method of the same name.
00:06:21.039 But Rails configuration provides a config block that gives you a config object.
00:06:27.919 Here, you can set options and allow passage of a block for better organization.
00:06:38.560 Thus, the prior code can be condensed and reorganized.
00:06:44.480 This enables hiding iOS details when not needed.
00:06:50.440 We need a high-level understanding of what this notification is destined to do.
00:06:56.440 We can then update our code appropriately.
00:07:02.440 If a block is provided, we’ll return the object for adjustments.
00:07:09.680 We save this as ordered options instead of raw hash.
00:07:16.440 Now, ordered options serve as our basic context.
00:07:23.599 To build our deliveries for comments, we can animate our previously defined deliveries.
00:07:31.039 The delivery method collects recipients for notifications.
00:07:38.920 Each recipient allows us to queue up jobs, sending emails and updating the database.
00:07:46.079 However, once we go through each recipient, we must queue up those jobs.
00:07:51.280 To do this, we need to find jobs with the right symbol, connecting them with relevant classes.
00:07:57.840 So we need to lookup in the namespace to convert symbols into corresponding classes.
00:08:03.400 We'll use constant tools to fetch the Ruby object and capture it.
00:08:11.120 Now, we can set the method variable for these Active Jobs.
00:08:16.960 Here we can introduce delays, allowing Sidekiq to manage these effectively.
00:08:25.000 This part of the code begins to grow complex and messy.
00:08:31.960 At this stage, we should consider extracting potentially confusing classes.
00:08:38.000 We want to avoid code that’s only piecing together configurations.
00:08:45.280 If we analyze the job functions, we can detect a degree of over-extraction.
00:08:53.080 The current storage mechanism is simply a hash of hashes.
00:08:58.080 This setup does not afford any internal functionality.
00:09:06.680 Thus, we should posit a delivery-by-object to consolidate required functions.
00:09:10.680 Ultimately, this object needs to know how to set options.
00:09:18.480 The job needs specifications as such, knowing the sender and method.
00:09:24.160 We can delineate these from each other efficiently.
00:09:30.720 Now we have simplistic objects that simplify our process.
00:09:39.560 This shift enables us to specify how delivery works as we see fit.
00:09:46.760 We no longer need to externally understand the technical architecture.
00:09:54.720 This closure showcases the concept of ordered options and their benefits.
00:10:03.520 For Rails application development, these parameters allow expansion.
00:10:10.080 While a hash is fine for developers, it lacks friendly interaction in Ruby.
00:10:15.400 Thus, we can build within code maintainability and effortless communication.
00:10:22.399 For delivering messages, we'll gather the email delivery constants.
00:10:30.679 Upon verification, we’ll validate the five-minute wait option.
00:10:38.320 If you examine Rails source code, it tends to be surprisingly simple.
00:10:42.400 All it does is instantiate a minor object that takes your job and stores the options.
00:10:52.200 This can be accomplished in relatively few lines.
00:10:59.480 It creates a surface layer that remains hidden in the process.
00:11:05.680 This approach is advantageous to not require intrusive modifications.
00:11:11.480 We can implement the external methods without altering Active Job's core.
00:11:18.080 You might overlook tiny concepts that lead to messy implementations.
00:11:24.760 You can additionally structure the database and deliveries more efficiently.
00:11:30.960 Certainly, database delivery methods turned out to be more unique than anticipated.
00:11:38.560 These must lead priority as we manage broadcasts and emails.
00:11:44.320 To send notifications, we need to confirm data in our database.
00:11:50.639 Tracking advance requires to ensure clicking mechanisms function.
00:11:56.399 We will begin routing these priorities logically.
00:12:00.520 Upon execution, we can check each database delivery and set its reliance.
00:12:06.599 With each step, we assess interplay between collected parameters.
00:12:12.480 Now that we've segmented deliveries, you may think it’s manageable.
00:12:18.880 We needed to derive functionality, not solely interpretations.
00:12:26.839 As a note, we accomplish database storage effectively and simply.
00:12:34.919 The notification models can now flourish in derivatives of objective designs.
00:12:44.799 Conversions allow for a dynamic approach regarding filtering oddities.
00:12:51.959 In doing so, we're capable of deriving contextual attributes in our class structures.
00:12:58.920 This allows us to guide pathing expectations through clearer structures.
00:13:07.440 Efficiency surges arise while managing notifications in active record.
00:13:14.920 This results in orderly procedures grounded in well-structured code.
00:13:22.400 Redirecting complicated notions enhances the capacity to maintain effective design patterns.
00:13:29.920 The dagger of duplicated parameters continues at unwanted moments.
00:13:36.320 Effectively managing these ties prevents a convoluted mess from future rewrites.
00:13:44.200 Next, we build variations on event processing, mapping out visible responses.
00:13:52.960 Different events trigger notifiers to formulate external notifications.
00:14:00.920 As we understand what events guide, those in return open further pathways.
00:14:09.199 Tracking all communications can provoke rapid inquiry into redundant areas.
00:14:16.600 Understanding boundaries is integral; clarity signifies improvement.
00:14:22.720 Separating notification records simplifies confusion within future implementation.
00:14:30.639 We should build towards more manageable scenarios while optimizing processes.
00:14:39.200 Ultimately, our goal is clear communication between events and notifiers.
00:14:46.720 Soon, renaming options will clarify purpose and process further.
00:14:53.440 Redirecting systems opens up virtual styles for manual implementation.
00:15:01.320 Dynamic job methods deliver affordances enabled through databases.
00:15:05.520 Efficiency maintains integrity guiding methods of response.
00:15:12.240 Foundational organization becomes necessary.
00:15:19.919 We seek out a holistic perspective involving redefined roles.
00:15:26.639 To this end, notifiers hold everything in sync and cohesive.
00:15:32.000 Thus, they challenge conventional design methods by achieving clarity.
00:15:41.040 This union of structures allows you to manage internal identifiers.
00:15:48.360 Code itself becomes extensible without entail vulnerable complexities.
00:15:56.760 Enhanced future-proof notifications aim for lower friction communication paths.
00:16:04.640 Ongoing changes serve to clarify user feedback in responsive environments.
00:16:12.640 Upon reshaping designs, larger frameworks will naturally support granularity.
00:16:20.000 The next generation of Rails will solidify and build upon the foundation laid here.
00:16:28.000 This will culminate in a comprehensive understanding of notification systems.
00:16:36.000 As we move forward with implementations, the insights gathered here can guide the future.
00:16:44.000 Thank you, and I hope you learned something valuable today!
Explore all talks recorded at RubyConf AU 2024
+14