Andrzej Krzywda

Business Logic in Ruby

wroc_love.rb 2019

00:00:14.650 All right, I think I'll just introduce myself. My name is Andrzej Krzywda. Today I am in two roles: I am one of the organizers here, and I will also have my talk. As an organizer, I would like to welcome all of you. I am very grateful and happy that you have come here and that we can meet with so many passionate Ruby programmers. I think this shows the power of Ruby. Thank you very much for coming. My talk is kind of like a story about my journey in programming—my hobby, passion, and programming career. I want to show you how I think about code, particularly business logic, and I would like to present some examples of the code. I would also like to invite you to share some of your ideas with me.
00:00:44.170 Just to give you some context, I should warn you that the next slide might be offensive in the Ruby community because it contains Java. This is my coming-out moment: I was a Java developer and a part of the Java community before I started doing Ruby. I was involved in a specific part of the Java community that focused on Agile, but Agile 20 years ago didn't mean meetings; it meant caring about the code and expressing business rules in the codebase nicely, along with unit testing and so on. It was really a code-specific focus. I'm mentioning my Java background because it had a significant influence on my approach, so with that warning in mind, let's look at some Java code.
00:01:24.730 Java, can anyone tell me what this code is about? It's an implementation of a post class, similar to what DJ has shown in the initial Rails applications. What’s specific here is that this Java code doesn't use any framework or library. In the Java community, we call those kinds of classes 'POJO'—plain old Java objects. This concept was essential in the Java community because the idea was to have business classes that were not bogged down by framework extensions. Twenty years ago, there wasn't much in terms of frameworks in programming communities; it was more about libraries, and it was considered a design smell or a bad practice if your business classes imported any outside extensions. Business classes were meant to be pure and free from any external library influences.
00:02:18.489 When Rails appeared in 2004, a significant part of the Java community, including myself, was excited. We were drawn to Ruby because the community was deeply focused on testing, unit testing, and code quality. The language's syntax was so elegant that it felt like we wanted to write Ruby in Java all the time without realizing that Ruby even existed. However, when the Rails community enticed us with its features, we were also unknowingly adopting some challenges. One of the features that many of us accepted was the practice of inheriting from ActiveRecord base. When DHH showed this code, which was shorter than traditional Java, we thought, 'Oh, this is better!' Now, our communication with business requirements and business people became easier because they could even look at the code and somewhat understand it, apart from the ActiveRecord base part.
00:02:46.930 However, what DHH very nicely concealed from us was the fact that this code does have some hidden complexities. This is what happens when we inherit from ActiveRecord: we have readers and setters, a constructor, and eventually we end up with tightly coupled code to the database. This was indeed a big deal for initial Rails developers, as it seemed so easy to use with the database already part of ActiveRecord. We embraced it, thinking it was not a significant issue but instead a great feature. But then we gradually realized we could write even nicer code. For instance, if a post has many comments, we can express it that way. The validations are incredibly simple to implement, and we were excited that everything was there and working. However, we didn't realize that we were tightly coupling ourselves to those objects. Each ActiveRecord object poses complications, especially when they grow and become complex.
00:03:46.510 This leads to one of the biggest realizations of my journey. I don't want to dive too deep on separating reads and writes, but my next topic is focused solely on business logic. This means making decisions directly in the codebase. I do not want to be concerned with readers—this is what ActiveRecord is about; it has two roles: one for writing data and one for reading it. Writing data involves making decisions—do we accept this post as a new title for the blog post? If validations are passed, then it's fine. We accept this. Reading data involves using associations, such as has many comments, which are usually used for displaying information later, rather than making decisions. You don’t concern yourself with changing the title or knowing the count of comments when you're not making decisions based on that input.
00:04:14.560 With has many comments, you now introduce a significant dependency and coupling to comments. As Marcus, who conducted a mutation testing workshop, once told me, ActiveRecord has an infinite API. Once you inherit from ActiveRecord base, your model is endowed with a plethora of available methods, leading to a vast interface. How can we alleviate this issue? One approach is to split ActiveRecord into two objects: one focused on reading operations, where readers would not be part of it, and the other concentrating on writing operations. This way, we only retain our business logic without the extraneous dependencies or coupling that comes with it. The idea is to introduce read objects purely for the purpose of displaying data.
00:08:24.740 Now let me show you how this approach would manifest in code. You start with the initial implementation and then layer the business logic into separate objects. Note that there is now no ActiveRecord dependency, and you can implement logic, such as raising exceptions when a post does not have a title. Additionally, we need mechanisms for publishing events since that's the only way these two parts can connect seamlessly. The rich object can then accept published events. Employing such a method, we can retain ActiveRecord’s advantages for data display purposes while mitigating coupling, allowing us to retain clarity and separation of concerns throughout the codebase. The algorithm of splitting responsibilities leads to cleaner, smaller ActiveRecord models by avoiding the excess baggage caused by read configurations.
00:12:30.780 To explore more complex scenarios, our team conducted extensive discussions over the years on how to implement business logic effectively. Following many conversations, we adopted Domain-Driven Design (DDD) principles and referred to our business logic classes as aggregates. This talk isn't about DDD specifically; however, during our race architect masterclass, we developed a repository internally at our company. This repository, created by my colleague Pavel, focused on implementing different business logic implementations—from discussions to practical applications. We ventured into how aggregates might look and whether specific requirements exist for various facets of business logic implementation. I also discovered a crucial requirement that now guides our approach to non-trivial situations which includes examples pertinent to commonly-used structures like JIRA. Implementing business logic and state machines often aligns closely with practical organizational tasks like tracking issues and their lifecycles through different statuses.