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.