How to Improve Your Software Design With Code That Feels Wrong

You know what you want to do, but your code just isn’t cooperating. Maybe it has a few too many levels of indentation, or chains a half dozen methods, or looks asymmetrical. Whatever it is, something just feels off. You could ignore it – I mean, you have a backlog full of features you still want to write, and it’s really not that bad. But that would be a mistake: Your code is trying to tell you something, and you don’t want to miss it.

If you can learn to tell when your code feels strange, you will quickly and drastically improve your software design skills. This intuition is hard to build, since it comes from experience, mentorship, and code reviews. But you can get some help from libraries that use syntactic vinegar to make bad code feel bad.

What does syntactic vinegar look like?

Here’s an example of syntactic vinegar using minitest/mock, a small mocking and stubbing library that ships with Ruby:

require 'minitest/mock'

class CartTest < MiniTest::Test
  def test_error_message_set_on_charge_failure
    cart = Cart.new(items)
    cart.stub(:charge!, false) do
      cart.checkout!
      assert_equal "The credit card could not be charged", cart.credit_card_error
    end
  end
end

When you run the test, the charge! method on Cart is stubbed, so the test won’t hit the payment processor. The block syntax is nice for making sure you only stub exactly when you want to. But what happens when you want to stub a bunch of methods?

require 'minitest/mock'

class CartTest < MiniTest::Test
  def test_error_message_set_on_charge_failure
    payment_processor = PaymentProcessor.new
    cart = Cart.new(items, processor: payment_processor)

    payment_processor.stub(:charge!, false) do
      payment_processor.stub(:login!, true) do
        payment_processor.stub(:logout!, true) do
          cart.checkout!
          assert_equal "The credit card could not be charged", cart.credit_card_error
        end
      end
    end
  end
end

Ew. That’s a lot of indentation. And that’s just in a single test – you can imagine this code being repeated in a lot of other tests.

You could wrap all this nesting into a test helper method. But if you’re really listening to your code, it’s telling you that you should find a better way. It might be time to look into using a Test Double instead:

class TestPaymentProcessor < PaymentProcessor
  def login!(account_id, key)
    true
  end

  def charge!(amount, credit_card)
    credit_card.can_be_charged?
  end

  def logout!
    true
  end
end

class CartTest < MiniTest::Test
  def test_error_message_set_on_charge_failure
    test_payment_processor = TestPaymentProcessor.new
    cart = Cart.new(items, processor: test_payment_processor)
    
    cart.credit_card = failing_credit_card
    cart.checkout!
    assert_equal "The credit card could not be charged", cart.credit_card_error
  end
end

Now your test is more readable. Plus, you have a TestPaymentProcessor that can be used in a lot of other places. You could even use it in development mode, if you don’t want to hit a real server!

Bad code should feel bad

By using opinionated libraries that make bad code obvious, you’ll start to notice bad code much faster and more reliably. And that’ll make your future code cleaner, easier to read, and less painful to work with.

What are your favorite opinionated libraries, and how do they help you find and fix bad code? Let me know in the comments below!

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments