Ivan Nemytchenko

Counterintuitive Rails Pt. 2

wroc_love.rb 2018

00:00:13.680 Okay, we've heard a lot about all this awesome sourcing stuff. Now, let's get back to earth and back on Rails.
00:00:21.820 As I promised, we start with these pictures, and eighty-two people left their responses. I want to say thanks to all of them.
00:00:30.009 Now we have this data, and we have some interesting points to think about. Surprisingly, 66% of the people are happy with Rails.
00:00:39.550 This either means that you're much less critical of your code than I am, or that you know how to manage complexity much better than I do. I don't know why you're not in my position instead.
00:00:54.220 It's also possible that you already know how to deal with Rails and manage complexity within it.
00:01:02.770 This is fairly obvious. If you tell people every year what to expect, they should at least be familiar with DDD and how people manage complexity.
00:01:10.180 Almost eighty-one to eighty-two percent of people do additional layers and building blocks, while only twenty percent use frameworks on top of Rails.
00:01:24.249 Nick, for short, could improve his marketing here.
00:01:31.240 What surprised me is that someone actually thinks that if they use Rails, they are managing complexity. The two last options they gave were actually fake; you can't manage complexity simply by moving stuff around.
00:01:44.350 I mean, the numbers are real, but the options are not. Nearly everyone who answered said they use services. If you look at the data, a lot of users reported using Posit or Ease.
00:01:56.490 I would really love to see how their Posit services look in Rails applications.
00:02:01.659 The most notable love-hate relationship seems to be with Devise, where only 40% of people think it's a good thing, while 36% think it's evil, and the rest find it all too complicated.
00:02:14.319 Alright, we can now get back to the topics we wanted to discuss. I would like to start with callbacks.
00:02:20.349 When it comes to models, we need to consider how people use callbacks. Half of the respondents do not use them.
00:02:27.000 I'm going to start with a small example that I found on the internet yesterday. I thought it would be funny to share.
00:02:34.830 The intention behind it is good; the idea is to have a default value for gender. But the code is quite strange. I create a new object and assign a gender, then I check if the object is valid, and suddenly the gender value changes.
00:02:49.689 What kind of separation of concerns does this represent? What overall design are we talking about here?
00:02:56.589 If we write code like this, we will never be able to use models effectively because we rely on the expectation of an object being valid and ready to use only after it has been saved.
00:03:10.629 There are two states of an object: before it's saved and after it's saved. We explicitly use this in our code, so this is problematic, as demonstrated in more complex applications.
00:03:25.869 So, what can we do here? Let's look at it again.
00:03:28.880 We know how to place defaults in Rails. We use migrations to put defaults there, but the issue is that this is not necessarily the best practice.
00:03:41.169 Defaults are part of your business logic, and if you place them in two separate locations, like the database and the model, you end up maintaining the same information in different places.
00:03:56.589 The change is trivial; you can use constructors in Active Record models to set defaults, which simply works. This allows you to reduce at least some of the callbacks in your models.
00:04:02.769 Another example I found online involves a gem for validating external services.
00:04:07.909 The code appears simple, yet when we check if the room is valid, it makes a request to an external service. We wait for the service's response, and then the state of the object suddenly changes.
00:04:21.440 We're simply asking if the object is valid, assuming it should behave as expected.
00:04:26.729 The external service's documentation suggests putting this logic in the model, which makes it hard to resist the urge to comply.
00:04:32.839 But the bad news is you can't blindly trust anyone on the internet. Just because a service has thousands of stars on GitHub or a beautiful landing page doesn't mean it's sound.
00:04:48.279 The good news is it's possible to learn how to make your own decisions, which is what we’re trying to do here.
00:04:55.699 So, how do we handle the need to interact with external services? We have a service layer just for that.
00:05:01.250 We call the geocoder explicitly, pass the address, and get the data out of this service, which we then place into our database.
00:05:14.649 Let's analyze what’s wrong with this setup. Services are about business logic, but the second line in this service involves persistence.
00:05:27.509 It handles the details of how we put data into Active Record, which makes it convenient to work with but isn't quite business logic.
00:05:40.710 This is where another layer comes in, which we will call mutators. This layer serves a single purpose: to handle operations involving creation, editing, and deletion.
00:05:54.990 We take the line of code and place it into a class and a method within that mutator class, keeping the services clean.
00:06:01.310 Now, the service contains only business logic; it interacts with external services and transfers data to the database, while all the persistence logic is managed by the mutator.
00:06:09.780 At this level of abstraction, we aren't worried about storing technical details; we just want it stored correctly, and we need someone else to ensure that.
00:06:16.279 Is it acceptable to use some callbacks? Yes, you can use callbacks like this if you want to capture counters or handle denormalization during creation.
00:06:22.570 However, when things become more complicated, consider moving them to the mutator layer as well.
00:06:35.490 Previously, we had services doing a lot of work to store something in the database. However, this is merely Active Record work and can be completely transferred to the mutator.
00:06:43.949 What I love about this schema is that it allows you to be lazy. In simple instances, you can do everything in the controller.
00:06:51.360 We create an object, and if it's valid, we save it. If it's not valid, we render a form.
00:07:01.520 In more complex situations, we create a service to manage the logic or, if it only pertains to data persistence, we create a mutator and call it from the controller.
00:07:14.250 If everything goes awry, we can create a service that can invoke different mutators, call various external services, or trigger jobs.
00:07:22.440 Here's an example of such a service. However, I’m not a fan of Hanami; in Hanami, you must prepare all the boilerplate code upfront, which doesn’t allow for laziness.
00:07:30.790 The purpose of layers is significant. First of all, we try to treat models as domain models, which contain relationships and business rules.
00:07:47.220 It's vital to remember that in our main models, there shouldn’t be any references to IDs because IDs are implementation details. The only place IDs should be passed is when our system triggers a job.
00:08:02.690 Mutators handle all creation, editing, and deletion logic, ensuring we operate with objects in their correct state and that these operations are atomic.
00:08:09.080 Services handle business logic and interactions with external services, while controllers manage application logic, including additional form fields, sessions, and flash messages.
00:08:13.640 Now, let's discuss form objects. The picture from the survey indicates that most people use them, while others use them occasionally.
00:08:23.740 2023% of people use them frequently, especially when additional parameters don't integrate seamlessly with the default Active Record conventions.
00:08:31.860 For example, when registering a user, we may also want to create a company. In such cases, the form becomes complex, and we must ensure everything is handled properly.
00:08:47.800 It's not apparent how to achieve this using Rails' default methods. People are incredibly creative with form objects.
00:09:00.350 I have a list of links pointing to various gems and approaches for creating form objects. It shows that there is no single correct way to build them.
00:09:13.080 A well-known article discusses multiple ways to decompose Active Record models, although many of these strategies struggle with nested attributes.
00:09:25.930 Some believe that you shouldn't use nested attributes at all because they can cause a lot of issues.
00:09:34.890 One method I found involves constructing form objects from scratch rather than trying to fit everything into Active Record, which can be an inconvenience.
00:09:41.040 In controllers, using form objects feels like using models. We simply call the form object and save it, which simplifies the process.
00:09:50.049 If we are dealing with different contexts, such as ensuring a publication date is set in one context but not in another, relying on conditionals in our models can be problematic.
00:10:01.430 We should avoid cluttering our models with validations related to form conditions. One way to simplify this approach is by creating simple form objects.
00:10:10.600 We define validators and other needed methods in the form object, allowing Rails to recognize it as a valid model.
00:10:21.990 In this example, we create a moderation article form. Although it isn't a typical form, it operates similarly.
00:10:28.770 We include additional validations and attributes, and it simply works when we use it in our controller. This method surprised me the first time I saw it.
00:10:42.230 Form objects can be incredibly easy to implement, providing numerous advantages without maintaining extensive logic.
00:10:48.840 They cost virtually nothing to maintain, and we should use Rails defaults wherever suitable.
00:10:58.240 This design aligns well with standard controller architecture, creating a visually appealing structure.
00:11:05.720 However, as we discussed, what we want to build relates to hierarchies. Thus, we have the moderation article form linked to its corresponding controller.
00:11:17.390 Of course, there's a degree of dirty hack involved, but as long as it helps us build a robust system, we shouldn't mind.
00:11:26.650 Next, our attention turns to implicit states. I experienced this while designing the subscription page.
00:11:37.620 Subscriptions can get complicated; you need to acknowledge credit card details, read the monthly passes, handle payments, and potentially cancel subscriptions.
00:11:50.590 At a certain point, I was unsure what was occurring in my code. What was troubling was using random methods while trying to decipher it.
00:12:00.820 Using flagging methods makes it challenging to identify what's happening, generating a convoluted structure.
00:12:11.720 Now there are gems to standardize states; for instance, flag sheet allows you to define your flags in a model and provides valuable methods.
00:12:19.370 However, sometimes employing flags this way can create issues, particularly when defining states in your objects.
00:12:35.150 For example, if we use flags to determine states such as 'trial expired' or 'subscription canceled,' it can complicate the logic.
00:12:41.810 Therefore, when introducing flags like this, we can unintentionally create a significant combination explosion of states.
00:12:54.590 Our expectations will fall short when we realize there are many combinations that our models must handle.
00:13:01.290 This complexity explosion is why we must avoid using flags to manage object states. We should instead consider state machines.
00:13:09.020 State machines explicitly define system behavior in various states, making implicit states more manageable.
00:13:19.900 When a system's behavior varies based on its state, state machines can be an excellent solution.
00:13:27.200 Almost everything in software engineering can be modeled with state machines.
00:13:33.150 We can describe complex systems through transitions, states, and rules, similar to Docker.
00:13:39.390 As developers, we can specify acceptable states in our models and ensure our system behaves correctly.
00:13:48.080 Explicitly defined states and transitions help prevent convenience of RoR while allowing the possibility of having multiple state machines per model when needed.
00:14:02.330 Next, let's examine some survey results on whether people use state machines.
00:14:08.310 Surprisingly, half of the respondents do not use them.
00:14:20.570 Next, let's dive into the conversation about statements throughout the application.
00:14:31.750 An article by a writer from Toptal discussed too much logic in views, which led to a solution that increased complexity.
00:14:43.190 The proposed solution involved creating a fake object if there were no current user, moving complexity from one place to another.
00:14:55.890 The better solution, surprisingly, is to create null objects that define potential roles in our application.
00:15:03.360 For instance, we can create a plain Ruby object called ‘guest’ that defines all necessary methods of a user object, giving us the expected behavior without complexity.
00:15:12.860 In the controller, we look for a real user. If we don't find one, we create a guest object, simplifying our view logic.
00:15:25.020 This approach leverages polymorphic behavior and provides new ways of establishing user interactions.
00:15:32.530 It appears that there is a considerable gap since many people aren’t applying this strategy.
00:15:39.970 Returning to models, good models are central in our applications.
00:15:48.520 Clean models foster happiness, while mishandled models augur misfortune and frustration.
00:15:54.940 There should be more focus on understanding models' intricacies over other misguided complexities.
00:16:00.560 I want to draw your attention to the historical perspectives of heliocentric and geocentric models of the solar system.
00:16:08.756 Nicholas Copernicus promoted a heliocentric view, but Aristarchus of Samos conceptualized it much earlier.
00:16:17.820 This highlights that even if the community continually places everything into models, we need not follow suit blindly.
00:16:25.200 Ultimately, I encourage you to be mindful of your reasoning when approaching dependencies in Rails.
00:16:32.950 Understanding the mechanisms beneath Rails will empower you to make better decisions.
00:16:40.060 A thought-provoking conclusion might arise that we should not engage with Rails unless we truly grasp how it works.
00:16:49.160 The reality, however, is often different, and we must navigate the terrain accordingly.
00:16:57.140 I hope this talk stimulates conversation within your teams and explores these ideas further.
00:17:05.240 If you have no questions, I’ll consider my job successfully done since this indicates you're getting more answers than inquiries.
00:17:15.780 However, I am open to inquiries should they arise. Personally, I'm pleased to state that I'm finally finishing my book.
00:17:24.620 Your feedback and emails remain crucial, and I hope to deliver small tasks focusing on creating service layers and mutators in the future.
00:17:32.410 This crafting will solidify your understanding of these concepts. That's all from me, and thank you.