rubyday 2016

How Programming In Other Languages Made My Ruby Code Better

How Programming In Other Languages Made My Ruby Code Better

by Simone Carletti

In the talk titled "How Programming In Other Languages Made My Ruby Code Better," Simone Carletti, a software engineer at Deal Simple, discusses the impact that diverse programming languages have had on his Ruby coding practices. The central theme revolves around the idea that exposure to different programming languages fosters growth and enhances coding styles, ultimately leading to improved code quality. Carletti highlights several key points throughout his presentation:

  • Importance of Cross-Pollination: Emphasizing the benefits of learning from various programming languages, Carletti relates this experience to personal growth akin to travel.
  • Diverse Language Influences: He shares his journey working with over 15 programming languages and presents examples such as Go, Elixir, and Crystal, illustrating how each has shaped his Ruby code.
  • Programming Paradigms: Carletti reviews significant programming paradigms including object-oriented, functional, and procedural programming, emphasizing the merits of immutability found in functional languages.
  • Concrete Examples: He showcases code snippets to explain how influences from Go and Elixir have transformed his approach to Ruby coding. For instance, Go's emphasis on simplicity and effective concurrency has encouraged him to adopt a more functional style in Ruby.
  • Service Objects and Dependency Injection: The concept of refactoring Ruby code to utilize service objects is presented as a way to improve design and testing through better separation of concerns.
  • Stateless Business Logic: Carletti advocates for introducing stateless logic that reduces side effects and enhances maintainability in Ruby applications.

Throughout the presentation, Carletti uses anecdotes and examples to underline the significant takeaway that Ruby developers can learn valuable techniques from other languages, ultimately improving their coding practices. He concludes by encouraging developers to not shy away from writing code and assertively adapt influences from diverse programming languages to bolster their skills in Ruby development.

00:00:14.690 Thank you, thank you everybody for taking the time to be here with me today. I hope you're still fresh enough to follow this talk. It's quite dense, and I hope you like code; otherwise, it's going to be quite an annoying talk. I have to ask you one favor: since this talk will be really dense, I encourage you to not use your notebooks and just follow the presentation. There will be a lot of code, and if you just listen to me without looking at the code, chances are you'll end up at the door thinking, 'What did he talk about?'
00:00:40.590 So, a quick introduction: I work at a company called Deal Simple. We provide domain management services for many people, including domain registration and necessary certificates automation via API. These are a few of our customers, including services for Ruby gems. So, if you have installed a gem before, you have actually used our services. Now, why am I here at another Ruby conference talking about different languages? This chart here somehow summarizes my relationship with programming languages over the last ten years. I've been writing production code in more than 15 different programming languages. I love programming languages and have been emphasizing in my presentations in recent years the importance of being influenced by other programming languages.
00:01:16.049 I mentioned that this is a slide from one of my talks at Ruby conferences in 2015, where I encouraged cross-pollination from different languages. Today, I want to explain why I am pursuing this idea. To me, learning a new programming language is like starting a new journey. When you pack for the trip, you don't know what to expect. You don't know what you will see or which problems you will encounter. But no matter what, when you return, you will have grown as a person. Whether you become better or worse is a completely different topic, but you will change your perspective on many subjects.
00:01:42.180 In fact, for me, traveling is not only one of the most fun and interesting experiences, but it is also one of the most effective ways to improve myself. Being in contact with different cultures and trying new experiences is invaluable. Let me show you a few of them. I don't know how many of you actually knew that pineapples grow on bushes. I thought they grew on trees! How that made me a better person, I have no idea, but it's something that I learned.
00:02:10.320 This is another mind-blowing experience I had while traveling and living in Indonesia. This is a meme showing how they stop by a gasoline station; they have no traditional gasoline stations. Instead, there are grocery stores selling gasoline in vodka bottles. You can see here, these are all vodka bottles filled with gasoline. When you stop to fill up your motorbike, they pour the gasoline into the tank using the bottle. Now, how does that relate to my talk? Well, I would say it illustrates survival skills. It's just something that changed my perspective.
00:02:38.660 Today, I want to show you different types of influences. These can be either traditional languages that you are all familiar with, such as C, C++, C#, Java, and Ruby, or a bunch of emerging languages. It's important to note that 'emerging' doesn't necessarily mean a new language. For instance, Erlang and Haskell have been around for years, but they have gained notable traction lately. As we've seen in other presentations, programming languages encapsulate how computers are evolving, which in turn influences which programming languages are suitable.
00:03:05.140 Some languages that may not have been ideal for use twenty years ago are now perfect, given the changes in the computing world. We also have bridging languages. For example, JRuby is one of the most notable examples in our community, allowing Ruby code to run under the Java virtual machine. This represents an amazing influence derived from a different language. We have Rust and Ruby working together, and we also have Erlang running in the Java Virtual Machine.
00:03:28.440 You may wonder why I'm saying all of this. Well, it's an experience, much like the one I had with pineapples. It’s interesting to observe how new languages come and go. I don’t expect you to read the code here, but I want you to note this website that fetches information from GitHub to show how code adoption among different programming languages is changing today.
00:03:56.740 Looking at code contributions on GitHub, I want to quickly go through some programming language paradigms. As you probably know, we have object-oriented programming, where we essentially design models around objects. Then we have functional languages. Today, I'll mention two functional languages. One key aspect of functional programming is immutability, which we will discuss later. Also, we have procedural languages, where there exists a series of structured procedures. You read the code from top to bottom, executing steps and performing changes on the data you have.
00:04:29.440 Now, which languages have influenced my way of working? Specifically, I think Java still influences my code style. I will show some code examples today, as well as Ruby, which influences how I write other Ruby code, as well as Crystal. Today, I want to focus on three different languages.
00:04:55.680 The first one is, of course, Go. Go focuses on simplicity and readability and has an amazing concurrency pattern and design architecture. Its design focuses on the composition of elements. It has an excellent set of tooling and a great ecosystem. Developed by Google, it compiles extremely fast and is well-suited for network libraries, with a high-quality standard library.
00:05:15.071 Here are some Go code examples for you. I added some code here; I don’t expect you to read it. This will be available for anyone who downloads the slides later to check out. Now, why is Go interesting for Ruby developers? It’s an excellent alternative for some network operations or background jobs, demonstrating a high level of attention to detail and expressiveness—more than you might expect from such a language. Go also changed my perception of quality. In Ruby, we might think that a method with thirty lines of code is bad code, while in Go, a thirty or forty-line method can be perfectly acceptable.
00:06:09.560 Next, we have Elixir. Many of you probably know about Elixir; it’s a programming language that runs on the Erlang Virtual Machine. It’s well-designed for concurrency and has an amazing feature called pattern matching. Elixir is immutable and offers excellent tooling. You can consider it a drop-in replacement for Ruby or other tools. It works exceptionally well with Erlang, is very effective for building scalable and maintainable applications, and has a lot of similarities with Ruby's syntax.
00:06:33.920 Elixir has a web framework called Phoenix, which has many similarities to Rails in some ways. I also find that many Elixir developers are former Ruby developers, making it easier for Rubyists to transition to Elixir due to its short learning curve. Finally, we have Crystal. Crystal is almost a drop-in replacement for Ruby that compiles to native code and offers type inference and compile-time evaluation.
00:07:02.121 This is an example of some Crystal code; it looks quite similar to Ruby. Now, why is Crystal interesting for Ruby developers? Firstly, its syntax is very close to Ruby, making it easy to adopt. However, its type inference can be incredibly useful for large projects. There was an attempt by Mike, who made an effort to port Sidekiq to Crystal, and it was done in a remarkably short amount of time.
00:07:31.920 Now, what I want to show you today, and right now, we’ve not seen much code so far, is how these languages have influenced my way of writing Ruby code. First of all, I call this approach 'strikes to the rescue.' Let’s take a look at this class. We have a very simple class with a refresh method that fetches information from the outside and changes the state of the object.
00:08:01.200 I’ve seen this approach everywhere. The problem with this approach is that it’s extremely hard to test because you have to set up objects and potentially mock external dependencies. Let’s examine this code, which is even worse. This code combines state changes and static methods. For example, a method called 'renew' performs operations on a domain; it does not mutate any state but changes external data.
00:08:25.710 Let’s focus on class methods, which do not change any state and just return a domain object. Then we have the 'refresh' method inside this context that changes the object’s state. In this example, the 'renew' method is not even a static function; it’s actually an instance method. Part of the code comprises instance methods, while part comprises static methods, which creates a confusing design in this class.
00:08:54.482 In this specific case, to use the 'renew' method, you actually have to construct an instance of the class. Thus, to trigger two different code paths, one for the external service and one for the state change, you need to create an object first. What can we do to improve upon this? We can use an approach where every class method that includes operations doesn’t require instances unless it specifically deals with an instance.
00:09:21.000 Additionally, we can forget that Ruby has structs. This influence is quite common in languages like Go, where structs are used everywhere to handle data. I’m not speaking about the specific struct class we have in Ruby; I am discussing the concept of a struct as a mere container for data. In Go, we use a struct to represent various elements, and struct can easily define types.
00:09:49.550 Let’s analyze how we can change this code to refactor it using structs. We can create a new struct object that consists solely of methods to handle our data more effectively. Instead of returning an instance of itself, we can structure the Registrar code to return new struct objects. The primary advantage is that in larger implementations, we can improve a class significantly by separating data manipulation and operations.
00:10:14.480 This separation of concerns makes testing much more manageable. Today, most of my code, particularly when dealing with third-party services, is designed around this principle. We employ structs solely for representing and passing data rather than relying heavily on objects with operations. This leads to a cleaner separation of concerns, helping to achieve immutability for the data we manipulate.
00:10:41.790 This introduces the idea of stateless business logic, which aligns closely with functional languages. In functional programming, we have minimal side effects due to data immutability. By incorporating this into our code, we can write methods that will always return the same result regardless of external factors. I’m sure many of you have experienced the issue of passing a hash as an option to a specific method and having unexpected changes reflect back on the original hash. This scenario can be challenging to debug.
00:11:12.180 Let’s look at another example. Here we have a processing class with an account model. This model uses a method to fetch data from an external service. It’s not good practice to have connections to services within your model as it mixes persistence with third-party integration. To enhance this code, we can introduce an API, creating a custom method to define the actions we want to perform on the data.
00:11:37.800 After that, the next step is to have another object called service objects that will appropriately handle business logic concerning this data. This method I’ll introduce is empty for now due to changes, but it will be injected as a dependency in the original method. Next, we’ll move the body of that method into the service I created, replacing the original call to the account's 'score' with the service call. This way, we can segregate operation from persistence.
00:12:05.520 Let’s observe more operations within our services. We can introduce an 'order service' to manage the processing of orders. This service will handle the business logic surrounding the order itself. So, let’s transfer the processing body into the order service, and now our order model only manages data related to itself. Testing becomes significantly easier because we can simply pass a lightweight object rather than a full order with items for testing.
00:12:31.070 This way, we can introduce dependency injection into our account service during initial digitization, allowing other service dependencies to be injected at runtime when we are testing. When testing the order service, we don’t need to run through the entire account service's functionality—just pass a double object that simulates an account service. We can focus specifically on the order service.
00:12:56.800 Next, we can reflect further on our operations by introducing additional services. For instance, a payment service that manages payments exclusively. This will allow us to test the payment service independently, ensuring smooth processing without complications from the order processing itself. By using this structure, we can monitor how we pass around data as parameters, which simplifies the process and avoids heavy coupling.
00:13:25.380 What benefits are we witnessing now? Our models are designed to be significantly cleaner. The views we have for both of them are straightforward; anytime we need to test either model, we can do so in isolation without mocking dependencies from third-party services. In turn, these tests run more efficiently and with less overhead.
00:13:49.899 If you’re asking yourself whether writing more code is a bad thing, the answer is no. As developers, we should not fear writing code; otherwise, we could find ourselves in the wrong profession. This philosophy can be seen in many other programming languages, including Elixir, where you pass dependencies explicitly as method arguments instead of relying on the internal state.
00:14:14.500 How has my code changed today? I use a functional-oriented service object approach to define our business logic, while the model serves only for data manipulation and exposes custom functions. I do not simply call save or update methods, which are common in model classes. Instead, I expose a well-designed and tested API. I ensure that any side effects caused by state are simplified, making testing easier.
00:14:38.200 By reducing unnecessary context, I can make the model objects leaner by eliminating third-party interactions. We expose business logic in a single, centralized location where all operations can be easily seen and accessed.
00:15:01.460 Let me take a short break. Let’s try to be a bit more functional! Why do we want to approach coding this way? We know that functional code minimizes side effects, meaning we can see clearly what will happen when we execute it. All ingredients contributing to our final output are right there in front of us. If something goes wrong, we know precisely what we passed and can trace back our steps.
00:15:25.220 I’ve adopted a much more functional style in parts of my Ruby code. Let’s construct a simple example from our orders controller where we have to process an order. Instead, we will implement a registry pattern to manage our services. Though I won’t go into every detail, bear with me as we call our order service and process it.
00:15:54.280 We will call the process order passing in the instance and reassign the result to a new instance. This leads us to question why we didn’t just change the state of the original instance within the process order method. While nothing forbids us from doing so, I want to illustrate how we can set expectations on the returning object that was processed.
00:16:18.469 If our service creates a new instance and we want to test that, how can the updated object be used in our tests to meet expectations? For instance, we can create a 'stubbed' order as the return value to our expectations. This way, we could formulate tests—such as expecting the controller to redirect to a specific URL—by providing the order ID from the returned object.
00:16:41.900 We take the processing of the service out of the equation, allowing us to focus our tests solely on the order controller itself. This is a significant topic within the discussion of dependency injection in the Ruby community. Many react with surprise, asking, 'What's that?'
00:17:01.230 Dependency injection can help mitigate tight coupling with your code and dependencies. If you access the object you’re testing through 'any instance of' in RSpec, it will lead to complications. Imagine testing a model where you use 'any instance of account.' You could test anything and have little visibility into what's happening.
00:17:25.570 The absence of easy access to the specific instance you’re testing is a sign of strong coupling, indicating potential issues for code changes in the future. Not being able to interact with the object during tests highlights the need for improvement. Additionally, it has been shown that it's challenging to effectively write tests when you use 'expect any instance of' in your testing.
00:17:51.530 Now, let’s discuss the implementation of a library. We primarily utilize a simple library to streamline common tasks such as manipulating and sanitizing data. We do not make use of default structures from the standard library, as they do not serve us well. Instead, we have a custom library which is a simplified hash-like object for setting and getting attributes.
00:18:16.260 This structure does not perform any coercion or validation. We've developed additional requirements atop the base object to perform validations as needed. For instance, we have an object called 'param objective' that acts as a parameter race, functioning almost like a hash but with added attributes.
00:18:38.720 We successfully minimize dependencies by limiting how we implement structs. In past experiences, we relied on structs predominantly for libraries, leading to a convoluted dependency structure as they became rooted in our application.
00:19:01.000 Therefore, we created a simplified module; it's no more than fifty lines of code, intended to facilitate quick access while ensuring we can build other objects that fulfill specific requirements. If we have validation needs, the library operates independently, allowing struct objects to encapsulate the data required.
00:19:30.080 Thank you so much for being here today!
00:19:50.240 If you have any questions or objections, I will be happy to respond. During discussions around testing, I emphasize that using 'any instance of' is a very poor practice because you have minimal visibility of what’s happening. This can be detrimental when testing specific behavior of your models, as it could lead to tight coupling in your code.
00:20:30.550 Ultimately, I appreciate the mix of influences I’ve gleaned from the languages discussed earlier today. Ruby is a fantastic language, and learning from others can only improve our Ruby skills. Thank you!