loading...

Ruby devs, how do you work?

hiddewie profile image Hidde Wieringa ・2 min read

This post is more like a request for discussion thread than a normal 'informational' post. First some history.

Daily work

In the company I work at, we use many different languages, but mostly based on the Java (JVM) ecosystem (Java, Groovy, Kotlin, etc.) and Ruby ecosystem (Rails, etc.). Most of the teams work with one language/ecosystem, but once in a while we collaborate on some code work in a different language than our daily work. That is fun, challenging and very good for learning new patterns and code styles.

Observations

I usually work within the JVM ecosystem (mostly Kotlin), and feel very confortable there. The standard library is great, there is a lot of learning content available for such a new language, and it is possible to build on the shoulders of a giant (Java) in terms of libraries and performance.

Whenever I collaborate and write Ruby code, I feel a little bit powerless. The first thing I do when I open a project which is not my daily code base, I scroll around the directories and files, and look around.

Then, when we start working on the problem at hand, we open the corresponding files and start editing. The first thing I do is Ctrl + click two times:

  1. Find all the usages of the class/function we are working in (get to know the context of the work);
  2. Find the definition of a referenced class or variable (what are we working with).

This does not work in Ruby (and many other interpreted languages), as any piece of code can call any other piece of code. Even methods (Ruby does not seem to have properties) can be added dynamically. Things like dependency injection are not really a thing (http://weblog.jamisbuck.org/2008/11/9/legos-play-doh-and-programming).

This leads me to the following set of questions I would like to ask the community.

Questions

  • How do you find out about usages or definitions of models in code that you did not write, know little about but have to refactor for some reason?
  • Apart from test coverage, how do you make sure you do not break anything when changing something else? Even with test coverage: do you unit test every property call?
  • How do you store knowledge in the code about some model, like which properties mean what, which are required during construction, which must exist together etcetera? Ruby is object-oriented to its core (https://www.ruby-lang.org/en/about), so encapsulation is important. I fail to understand how to encapsulate something that any other piece of code can extend or modify.

Hopefully this lets me get a better feeling on how developers can work with interpreted languages like Ruby. Thanks in advance for the discussion!

Posted on Mar 9 by:

hiddewie profile

Hidde Wieringa

@hiddewie

Programmer and mathematician. Problem solver. Lover of clean code, coffee, cycling, running and Italian food. Kotlin, Java, Spock (Groovy), PHP, Angular, MariaDB, ...

Discussion

markdown guide
 

Whenever I collaborate and write Ruby code, I feel a little bit powerless

Eh eh I can see that if you're used to IDEs plus statically typed languages. I'm used to no IDE and dynamically typed languages which tend to rely on best practices and "code hygiene" on bigger code bases.

I code in Ruby all day lately, I'm going to comment adding notes about my workflow

The first thing I do when I open a project which is not my daily code base, I scroll around the directories and files, and look around.

The first thing I do in a new codebase is either try to fix a bug or read tests (or both). Reading code around, at least for me, makes it really hard to understand what fits together, mostly because code in editors is listed file by file alphabetically, so it doesn't really help :D

Another technique which I find very helpful is to set a breakpoint, hit it and then use step debugging.

Find all the usages of the class/function we are working in (get to know the context of the work);

Whenever I have to answer the question "where does this method come from" I open the terminal and grep with ripgrep which is super fast.

This does not work in Ruby (and many other interpreted languages), as any piece of code can call any other piece of code.

To be fair "calling any other piece of code" (not sure I understand the full meaning) is probably not a property of being interpreted but likely of being dynamically typed and Ruby having open classes. Ruby has encapsulation though, you can mark methods private and not be able to call them from outside without raising an exception.

Things like dependency injection are not really a thing

Ruby supports it, as all languages do. Maybe the issue here is that developers tend not to use it much and rely on mixins to inject behavior, but you can absolutely pass functions, classes and whatever using DI

How do you find out about usages or definitions of models in code that you did not write, know little about but have to refactor for some reason?

I grep for the methods, as you can see I use ripgrep quite a lot, whatever the language:

history | grep rg | wc -l
     174

Apart from test coverage, how do you make sure you do not break anything when changing something else? Even with test coverage: do you unit test every property call?

Mmm aside from test coverage? You can do manual testing but testing (be it integration, functional or unit) is still the best course of action if you want to shield from regressions.

BTW Ruby kinda has properties (if you mean the getter/setter combo). See attr_accessor. You can also customize its behavior, like custom property accessors in other languages by using meta programming, see define_method

How do you store knowledge in the code about some model, like which properties mean what, which are required during construction, which must exist together etcetera

I'm not sure I understand the question. Is it related to the concept of Java property or property of the application? In the latter case you have tests and documentation.

I fail to understand how to encapsulate something that any other piece of code can extend or modify.

The philosophy is different but you can still use access modifiers to "protect" methods. Ruby supports protected and private and they work fine. You can still call them from outside if you really need to, but at least you need to be explicit about it.

Hope this helps!

 

It's odd you say "things like dependency injection are not really a thing" and link to an article talking about how someone made a DSL-type dependency injection system. Ruby does a lot of things through dependency injection, it's a fairly common pattern. Many different behaviours can be layered on using that approach and you see it in various forms in different Ruby projects.

One good example is Rails Engines, a way of completely encapsulating a component of a Rails application in a way that's modular. You can plug in an Engine and have new models, views, controllers, and all the supporting code for that Engine, and also a clean way to remove it if it becomes obsolete or unnecessary.

When it comes to finding out about definitions, many modern editors, Visual Studio Code included, have a fairly capable method discovery and backtracking system. This is done through tools like Solargraph that can dig into your Ruby code and make a map of method definitions.

This ties into your question about method arguments. Solargraph can parse out documentation that's in the source and show you how to use any given method. If you write comments in a form Solargraph understands, which is also useful when automatically exporting documentation, it'll give you all the help you need.

As for testing, that's something of an art more than a science. When working on Ruby code you should be aware of the contract that method is fulfilling, implied or express, and work to avoid violating that. This generally means if a method accepts data in a particular form you continue to accept that form, you don't arbitrarily change the rules, and if it emits results in a particular form or forms you commit to do the same.

A good way to verify that this continues to be the case is unit tests, but these can only test so much. If you want to be a sneaky jerk, intentionally or unintentionally, you can change how the method works in a subtle, yet important way. Like you might mangle your arguments with in-place modifiers like gsub! or delete that the test might not notice, but which is against the spirit of the original "contract" where you normally do not "own" arguments you're given, you must treat them gently and with respect, unless you've made it clear in the documentation that you're assuming control of them.

A lot of Ruby boils down to the "Duck Typing" principle, and by extension, the "Duck Method" principle. If your method does exactly what it says on the tin, reliably and predictably, under all the advertised use cases, then it's a valid implementation. It doesn't matter how you do things internally, there's a lot of latitude there. You just need to pay attention to your obligations and work to adhere to those boundaries.

 

Thank you for your comment.

I meant "DI is not really a thing" as in "in Ruby, DI is often not needed in the form of an entire framework as the backbone of your application".

Solargraph seems interesting, I will definitely look into that for code analysis!

 

Ruby doesn't lean too heavily on the dependency injection pattern, but it's still something you can see occur in many forms.

In other languages with a more formal implementation of this where it occurs is much more obvious, but in Ruby there's a very smooth gradient between light applications of this, like mixin methods from modules, to entire frameworks built around it, like Rails, and everything in-between.

So basically dependency injection is rarely forced on you, but it's there if you need it, and you can do it however you want.

 

I see that your company likes to utilize the Java ecosystem... To your knowledge, has your company ever made use of JRuby or JRuby on Rails?

 

As far as I know, no use is being made of JRuby or similar. The only thing I know is that people say it's the worst of both worlds (JVM and Ruby). I cannot confirm or deny that opinion... Seems worth looking into!