Posted about 1 month ago. Visible to the public. Repeats.

How to write modular code

Or: How to avoid and refactor spaghetti code

Please note that I tried to keep the examples small. The effects of the methods in this card are of course much more significant with real / more complex code.

What are the benefits of more modular code?

Code is written once but read often (by your future self and other developers who have to understand it in order to make changes for example). With more modular code you reduce the scope of what has to be understood in order to change something. Also, naming things gives you the opportunity to convey meaning which can be especially helpful for understanding.

Functions

When starting from scratch: write an outline first

When writing a new function, start with writing down what it has to do as functions - kind of the table of contents:

good:

Copy
def create_report data = gather_data warn_about_missing_data(data) cleaned_data = clean_data(data) generate_report(cleaned_data) end

Now try to implement each new function from your list. Do this by again by thinking about what the function has to do and writing an outline. Repeat until you have to actually implement something because you can no longer write outlines in a meaningful way.

However, if 90% of your functions only have one line of code you may have overdone it.

When refactoring code

This could be code from someone else or code you've just written.

Write a function instead of variable assignment

bad:

Copy
def my_function(employees) contact = employees['product owner'] || employees['lead developer'] || employees['trainee'] do_something(contact) # ... end

better:

Copy
def my_function(employees) contact = find_contact(employees) do_something(contact) # ... end private def find_contact(employees) employees['product owner'] || employees['lead developer'] || employees['trainee'] end

Write a function instead of adding a comment over a block

This also works well if you could think a comment over several lines of code.

bad:

Copy
def my_function # prepare report data customer_data = read(:customer_data, {}) customer_recommendations = read(:substituted_customer_recommendations, '') report_data = customer_data.merge({customer_recommendations: customer_recommendations}) json_data = { report_data: report_data } # write report data filename = 'report_data.json' result = Result.new( file: FileIO.new(json_data.to_json, filename) ) result.save! end

better:

Copy
def my_function report_data = prepare_report_data write_report_data(report_data) end def prepare_report_data customer_data = read(:customer_data, {}) customer_recommendations = read(:substituted_customer_recommendations, '') report_data = customer_data.merge({customer_recommendations: customer_recommendations}) { report_data: report_data } end def write_report_data(report_data) filename = 'report_data.json' result = Result.new( file: FileIO.new(report_data.to_json, filename) ) result.save! end

Write a function instead of repeating code (-> DRY, don't repeat yourself)

bad:

Copy
def preview(data) sanitized = sanitze(data) write(sanitized) navigate_to(:preview) end def next(data) sanitized = sanitze(data) write(sanitized) navigate_to(:back) end

better:

Copy
def preview(data) save_data(data) navigate_to(:preview) end def next(data) save_data(data) navigate_to(:back) end private def save_data(data) sanitized = sanitze(data) write(sanitized) end

Disclaimer: You should not try to overdo it with DRY. If you have to add many parameters to the new function to make it work for all use-cases it may be better to keep the code separate. Functions with lots of parameters are generally harder to understand.

Write a function instead of asking an object about all its interna: Tell don't ask

This is all about where code should live. If you're asking an object a lot about interna, that's a code smell. It would be better to ask the object to do the things you want for you. Try to refactor that out of your method into its own method in the class where it belongs.
bad:

Copy
class MyClass def create_target(network_profile) subnet = if network_profile.ip_configured? IPAddr.new("#{network_profile.ip_address}/#{network_profile.ip_netmask}").to_s end name = 'My Target' Target.create(subnet: subnet, name: name) end end

better:

Copy
class MyClass def create_target(network_profile) Target.create(subnet: network_profile.subnet, name: 'My Target') end end class NetworkProfile def subnet if ip_configured? IPAddr.new("#{ip_address}/#{ip_netmask}").to_s end end end

Classes

If you followed the advice in the first part of this card you now have lots of functions. Those should be structured with classes.

Write service classes

If you have several functions that could be grouped together (e.g. because they are operating on the same data or do something similar), consider adding a service class for them.

good:

Copy
class VersionParser def self.parse_version(version) version = remove_version_appendix(version) Versionomy.parse(version) end def self.major_version_change?(version_1, version_2) return false if version_1.blank? || version_2.blank? parse_major_version(version_1) != parse_major_version(version_2) end private def self.parse_major_version(version) parsed = version.is_a?(Versionomy::Value) ? version : parse_version(version) parsed.major end def self.remove_version_appendix(version) version.split('+').first if version end end

Write form models

Form models are not only a nice way to structure your code but also help you get away from callback hell.

Catchy examples for form models are things like user signup or password change forms (note: you should use a library for such things instead of building them yourself. But it is a good example).

Copy
class User < ApplicationRecord attr_accessor :new_password, :new_password_confirmation validates :new_password, enforce_password_policy: true validates :new_password_confirmation, presence: true validates :password, presence: true before_save :store_new_password def store_new_password # ... end # ... end

For most of the time you use the user model these validations and callbacks are useless because they are only used on one screen. So they should move to a form model:

Copy
class User::PasswordChange < ActiveType::Record[User] attr_accessor :new_password, :new_password_confirmation validates :new_password, enforce_password_policy: true validates :new_password_confirmation, presence: true validates :password, presence: true before_save :store_new_password def store_new_password # ... end end

Write classes for parameters that belong together

Sometimes you see multiple functions that always use the same parameters. This often happens if the parameters only make sense together - in this case you could add a new class.

bad:

Copy
class Geometry def draw_circle(x, y, radius) # ... end def move_object(start_x, start_y, end_x, end_y, object) # ... end #... end

better:

Copy
class Position attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def get_x @x end # ... end class Geometry def draw_circle(position, radius) # ... end def move_shape(start_position, end_position, shape) # ... end end

Namespaces

Use namespaces to structure your classes. Start early, for example when you have 3-4 classes that belong together.

Does your version of Ruby on Rails still receive security updates?
Rails LTS provides security patches for old versions of Ruby on Rails (3.2 and 2.3).

Owner of this card:

Avatar
Judith Roth
Last edit:
25 days ago
by Emanuel De
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Judith Roth to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more