Rodrigo Urubatan

Refactoring for Rails - Using Deodorant to Prevent Code Smells

A talk from RubyConfTH, held in Bangkok, Thailand on December 9-10, 2022.

Find out more and register for updates for our 2023 conference at https://rubyconfth.com/

RubyConfTH 2022 videos are presented by Cloud 66. https://cloud66.com

RubyConf TH 2022

00:00:00.120 foreign
00:00:15.000 Good afternoon, everyone.
00:00:17.180 After starting to work with Ruby, one of my main objectives in life is to help people write better code and have more free time.
00:00:22.560 With that in mind, we need to take care of our code.
00:00:25.199 There is no magic trick to achieve this.
00:00:29.820 One of the best books I've read on the subject is 'Refactoring' by Martin Fowler.
00:00:34.260 It's not Ruby-specific, but after I read it, I decided to talk about it for the Ruby community.
00:00:36.360 There are many different refactoring patterns across various languages.
00:00:39.780 Sometimes, people refer to them by different names.
00:00:44.700 I prefer the names from Martin Fowler's book.
00:00:45.480 These names make sense in many languages, not just Ruby.
00:00:48.000 For example, you can change a function declaration, or collapse your class hierarchy.
00:00:51.440 If you have a superclass that shouldn't be a superclass of a certain class, you can modify that.
00:00:55.440 You can also extract a module, combining many functions into a module.
00:00:58.920 In Ruby, you can combine a lot of functions into a large class or decompose a conditional.
00:01:01.980 The names of these practices are pretty self-explanatory.
00:01:05.519 However, there is one type of refactoring I find challenging: the extract method.
00:01:09.320 I always feel the need to use an IDE, which I don't usually have when working with Ruby.
00:01:15.960 But I've discovered there's a plugin for VIM and one for Sublime Text that help with this.
00:01:20.420 The VIM plugin is much better, but both work.
00:01:25.860 Besides that, you basically copy, paste, and change variables, and that’s it.
00:01:29.280 For most refactoring tasks, this is sufficient.
00:01:31.140 But the challenging part is remembering when to do things and identifying problems.
00:01:35.640 People began using the term 'code smells' a few years ago to help identify issues in our code.
00:01:39.060 I think this idea is quite useful.
00:01:42.479 Let's start with this analogy.
00:01:44.700 Imagine visiting a marvelous country with beautiful beaches, but then you look at your code.
00:01:47.580 You check a new codebase and see there are no tests, leading you to think the original developers loved adventure.
00:01:51.840 Something is likely to break down the line.
00:01:55.319 There are lesser risks, like a simple case statement without an 'else', which can lead to code breaking in six months.
00:02:01.019 I have an example from my company where we had an enumeration for defining tag types.
00:02:06.840 All the current values were included in a case statement describing them with human-friendly names.
00:02:11.879 Six months later, when a reporter asked for a new tag type, we added an item to the DNM.
00:02:15.420 However, nobody remembered to increase the test cases to account for that.
00:02:19.200 As a result, we ended up with many unknown and new values exploding because there was no 'else' in the case.
00:02:21.920 So, that's one example.
00:02:28.260 Just a quick comment: if there are no tests, stop thinking about refactoring and focus on writing tests.
00:02:30.780 You won't be able to do anything effectively without tests.
00:02:33.420 To start, I want to list a few good practices for refactoring.
00:02:40.200 After completing refactoring, all your tests should still pass; otherwise, you may have broken something.
00:02:45.979 Refactoring, by definition, should not change the behavior of your code; you are improving it while keeping everything working as before.
00:02:50.700 In non-critical scenarios, you might change a class exposed as an API, but that’s not the ideal case.
00:02:55.800 Refactoring is about maintaining code quality, so you should allocate time for it in your daily work.
00:03:03.000 Each iteration, you should separate time for refactoring; if you take too long to improve your code, it will cost you much more later.
00:03:09.700 The time saved by improving code quality will also save your vacation time and reduce the number of bugs.
00:03:16.300 Since everyone likes to take time off, it makes sense to take care of your code.
00:03:20.740 As a golden rule, anything that doesn't simply improve the code should be handled in its own pull request.
00:03:23.340 If your code isn't properly organized, the people reviewing it will find it difficult to go through.
00:03:26.280 Instead of reviewing a hundred lines of feature-related code, they will review five thousand changed lines.
00:03:30.300 It's important to handle small refactorings always and leave the code you are touching in a better state than you found it.
00:03:35.979 Some problems seem simple but can lead to big issues.
00:03:40.000 For example, broken chains of 'safe' calls in Ruby may work, but they can break over time.
00:03:42.240 Similarly, a chain of three or four ternary operators might indicate that you missed one of the outcomes.
00:03:48.120 In general, this makes the code vulnerable to breaking.
00:03:53.579 Additionally, look out for things that don’t feel right.
00:03:57.999 This image references a show from a few years ago, illustrating how unexpected variables can appear.
00:04:00.300 If you encounter chemical case variables in your code, they probably don’t belong there.
00:04:05.400 For the first scenarios, several tools can help you address these issues.
00:04:10.000 For example, I suggest running RoboCop before you commit your code.
00:04:14.520 RoboCop verifies your code and can automatically handle quick fixes.
00:04:17.400 If you're using JavaScript, you can use 'lint-staged' to run it automatically before committing.
00:04:22.080 There are other code smells that are tolerable in an Italian restaurant but not in your code.
00:04:27.900 If your code looks like spaghetti, you really need to stop and fix something before it breaks.
00:04:31.500 Spaghetti code is usually problematic and doesn't look clean; it's often difficult to follow how it works.
00:04:36.000 In the best-case scenario, you'll notice that some classes are managing too many responsibilities.
00:04:39.000 When you have methods that are too large and do too many things, it’s time to refactor.
00:04:43.680 You may also face incomplete class libraries that could lead to circular dependencies.
00:04:48.180 Often the changes you make in one part of the code lead to unexpected breaks elsewhere.
00:04:52.740 This often resembles the IMAX code example where there's a warning for modifying code.
00:04:56.680 If you think you know what you're doing in such areas, beware and leave them alone.
00:05:01.200 These are examples of very well tested spaghetti code.
00:05:05.240 A significant problem associated with spaghetti code is the presence of 'whale classes'.
00:05:07.440 These are usually large classes with hundreds or even thousands of lines of code.
00:05:10.500 Such classes often indicate that they are performing too many functions.
00:05:14.160 Additionally, you may encounter 'doughnut methods' that are significant but not as big.
00:05:17.880 They possess high cyclomatic complexity and most of the time, they can be simplified.
00:05:21.060 For whale classes, if you identify multiple that have shared code, it’s possible to extract a superclass.
00:05:26.160 You can share that superclass across multiple classes or collapse the hierarchy to improve clarity.
00:05:31.200 If the class still feels like it should be doing more, consider combining functions into a module.
00:05:35.700 This will help isolate tests and keep your code cleaner and easier to read.
00:05:40.020 You can also move methods from larger classes to the new superclass or module.
00:05:44.760 Extract methods to reduce the size and complexity of the doughnut methods.
00:05:49.620 Turning variables into arguments simplifies code and helps to share code among methods.
00:05:54.000 This often occurs when the methods have small different variations.
00:05:56.760 To further improve readability, you can rename methods and variables for better clarity.
00:06:01.140 Sometimes, your code might feel like a fruit salad—lacking coherence.
00:06:05.940 A class with too many instance variables can lead to confusion.
00:06:10.000 If changing one variable breaks another property, that’s a red flag.
00:06:13.620 A class with too many methods can usually be solved by extracting a module.
00:06:16.920 You can simplify methods with excessive arguments by creating value objects.
00:06:20.760 Alternatively, you can pass a hash with possible values to the method.
00:06:24.480 This facilitates testing individual cases and achieves a cleaner layout.
00:06:30.360 Moreover, examining your code should not feel claustrophobic or overly complicated.
00:06:35.820 There are aspects in code that feel out of place.
00:06:37.740 For instance, unneeded comments like 'this code removes an item' can be misleading.
00:06:42.600 Instead, aim to explain the business rationale for the method's existence.
00:06:47.520 If you find yourself needing to explain exactly what a method does, reconsider the naming.
00:06:53.760 Comments that explain the business rationale are extremely useful.
00:06:56.880 I've encountered unnecessary libraries.
00:06:58.920 Once, I found an npm module that compared a value to itself—it seemed like a joke.
00:07:02.800 However, such modules unfortunately proliferate.
00:07:05.280 It's crucial to address situations where a method has multiple return statements.
00:07:08.400 Having excessive return points within a method leads to confusion and potential errors.
00:07:13.860 If you see numerous exit points in a method, consider breaking it into smaller functions.
00:07:17.400 Improving logic by using early conditions can also streamline your code.
00:07:22.020 This should result in simpler, more linear logic.
00:07:25.740 As I mentioned earlier, creating value objects in Ruby is infrequent.
00:07:29.340 You can often use a simple hash or create a struct.
00:07:31.320 You don't need to define a class with many accessors for variables.
00:07:35.340 structs create classes that grant basic functionality without additional overhead.
00:07:40.680 All these extraneous elements should simply be removed from your code.
00:07:44.980 You will notice areas of code that induce a sense of claustrophobia.
00:07:48.060 You feel it should be easier to make changes.
00:07:51.100 This tension arises frequently in spaghetti code or large classes.
00:07:56.160 For instance, having a single user class duplicated across multiple applications leads to complications.
00:08:00.140 Every modification necessitates updating all instances, which is impractical.
00:08:05.080 Encapsulating such shared logic into a separate service is usually a better approach.
00:08:10.980 You may find yourself needing to change various points in the code.
00:08:13.420 This increases complexity and can generate more problems than it resolves.
00:08:17.920 Next, let's discuss classic error pointers, such as fat controllers and thin models.
00:08:22.760 When your controllers are bloated with database-related code, it's a sign of trouble.
00:08:26.700 If a controller method has more than 50 or 100 lines, refactoring is necessary.
00:08:31.560 Controllers should delegate responsibilities to models, which also should not become bloated.
00:08:35.820 For instance, if you have a song model, it should only define its core attributes.
00:08:39.960 If it starts to include file conversion methods, that indicates a design flaw.
00:08:44.460 Such functionality should reside in separate services or libraries.
00:08:48.700 Fat models violate the principles of the Rails framework.
00:08:52.600 Ideally, all database-related interactions should occur within the models.
00:08:56.560 Controllers are meant to pass parameters into models and retrieve data for views.
00:09:00.280 This streamlined approach enhances testability and delineates responsibilities.
00:09:04.800 Moreover, certain elements may simply feel wrong or raise red flags.
00:09:09.000 For example, if modules are used in Rails migrations.
00:09:12.839 This creates complications should you need to reapply migrations across different databases.
00:09:16.680 I’ve faced this problem multiple times.
00:09:20.420 Migrations missing a down method complicate rollbacks.
00:09:23.520 Without the ability to revert migrations, you can’t update effectively.
00:09:27.179 These are clear indicators that problems will arise soon.
00:09:31.600 There are scenarios where certain practices lead to confusion.
00:09:35.880 For instance, if you have views overloaded with query logic or repetitive syntax.
00:09:40.020 In such cases, refactor your views by transferring complex code to view helpers.
00:09:44.420 This will yield cleaner views and simplify testing.
00:09:48.000 To summarize, tools like RoboCop or similar can greatly assist you.
00:09:52.020 By automating your process, your code quality will improve.
00:09:55.080 You'll also gain insights into areas that need improvement.
00:09:58.020 Separating refactoring from feature implementations leads to clearer pull requests.
00:10:01.560 Organizing your classes properly, utilizing superclass extractions, and restructuring methods can enhance clarity.
00:10:05.840 Using tools like RoboCop will highlight circular dependencies.
00:10:10.020 Pay attention to the size of controller and model class definitions and conduct thoughtful validation.
00:10:14.160 Always use view helpers judiciously.
00:10:16.560 Keep your views focused; avoid embedding complex queries.
00:10:20.220 Cleaner code results in fewer bugs and more free time to enjoy your vacation.
00:10:25.060 Thank you for your attention, and if you would like my contact information, it's available in the QR code.