Coding Imperfectly (And Single-Responsibility Classes)

Malina Tran
Tech and the City
Published in
4 min readJun 7, 2016

Design is more the art of preserving changeability than it is the act of achieving perfection.

- Sandi Metz, Practical Object-Oriented Design in Ruby, Chapter 2

Image courtesy of #WOCinTech Chat

One of the hardest parts of writing code is in the beginning, when you are architecting your application. You, the coder, are creating a virtual world that will have a downstream effect on the existence of components and their interactions. You want to achieve a seemingly perfect world of purposeful classes and methods that are symbiotic in nature. You want to write in a way that feels elegantly succinct and smart.

And maybe you freeze up. After all, the pressure of achieving perfection can be a daunting task. (On a broader, but related, note: Reshma Saujani, founder of Girls Who Code, spoke about how the inclination to be perfect can negatively affect how girls approach coding. Clearly, this resonates with me).

Keeping this in mind, Sandi Metz writes about how the underlying design of an application is not about achieving a state of perfection–but rather, to make it amenable to future changes. She provides a guideline for how to organize code that can withstand change:

  • The consequences of the change should be obvious;
  • The change’s benefits should be comparable to the costs of making that change;
  • The code should be usable in new and unexpected ways;
  • The quality of the code should be so exemplary it encourages those who write it to perpetuate these qualities.

To further cement these guiding points, let’s talk specifically about classes. To code in an uncertain present and unforeseeable future, let’s zero in on what must be done.

A class is defined as a first-class object and represents the (noun) part of a domain that “should do the smallest possible useful thing; that is, it should have a single responsibility” according to Metz. Assigning more than one responsibility to a class would render it difficult to reuse. If it becomes so bloated with other responsibilities, it loses its universality and prohibits use in other contexts.

A class has variables and each instantiation of that class produces instance variables. Instance variables store data, which should be hidden from other methods and even the class, to avoid any manipulation. This can be done with `attr_reader` which enables reading, but not writing, access to the data; its virtual representation would look like this:

# Implementing attr_readerclass Person
attr_reader :name
def initialize(name)
@name = name
end
end
# Visual representation of attr_readerclass Person
def name
@name
end
end

This isolation is also defensible against future changes. Let’s assume that several methods are referencing the instance variable. If the data of the instance variable changes, it will only require change in a single location as changes in .

As a class develops and grows, there will be methods. Each method should do a single task. Just one. Refactor it, even if the ultimate design is not clear at the moment or if the method seems to require a two-step process. Consider `pluralizeFruits` which adds a single “s” to each fruit element in the array:

def pluralizeWords(words)
words.map { |word| word + "s" }
end

This is a two-step method that is very short, but can be further extracted into two methods. The purpose behind doing this is to expose hidden qualities, remove the need for comments, and encourage reuse or removal to a new class. Now, we can use the `iterate` method by generalizing its purpose.

def pluralizeWords(words)
words.map { |word| pluralize(word) }
end
def pluralize(word)
word + "s"
end

Writing code means not aiming for perfection, but settling for “good enough.” This may initially sound like the opposite of software craftsmanship, but Metz points out that some decisions cannot be made without additional information. And they shouldn’t be made prematurely. Design decisions can become a toss-up, and costs will be accrued if the decision later proves to be the wrong choice. Such design decisions in the midst of uncertainty (e.g. writing a Struct versus creating a new class) can wait. Methods will be written, dependencies will crop up, and reorganization will be determined.

Ask yourself: What is the future cost of doing nothing today?

In the era of failure as a badge of success, programmers will talk about breaking code with pride. Yes, error codes are abundant and a certain number of wrong decisions can guide you to the right answer. But what I have learned is that writing code is a practice of thoughtful experimentation. It is about progress, not perfection; it is about writing code with changeability and the future in mind.

--

--