Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Two Trains and Other Refactoring Analogies

Two Trains and Other Refactoring Analogies

Have you ever heard someone say, 'This code sucks. We need to rewrite it.'?

I've been there and it usually doesn't end well. We'll discuss analogies around continual improvement and how to avoid declaring technical bankruptcy.

Presented at a SoCal Python meetup: http://www.meetup.com/socalpython/events/233187442/

Kevin London

August 30, 2016
Tweet

More Decks by Kevin London

Other Decks in Programming

Transcript

  1. “Any fool can write code that a computer can understand.

    Good programmers write code that humans can understand.” Martin Fowler
  2. def get_tax(order): if order.state == 'CA': return order.subtotal * .0875

    elif order.state == 'PA': return order.subtotal * .08 else: raise NotImplementedError()
  3. def get_tax(order): if order.state == 'CA': return order.subtotal * .0875

    elif order.state == 'PA': return order.subtotal * .08 elif order.state == 'TX': return order.subtotal * .0825 else: raise NotImplementedError()
  4. def get_tax(order): if order.state == 'CA': return order.subtotal * .0875

    elif order.state == 'PA': return order.subtotal * .08 else: raise NotImplementedError()
  5. import pytest from taxes import get_tax, Order def test_get_tax_for_ca(): order

    = Order(subtotal=10, state='CA') assert get_tax(order) == 10 * .0875 def test_get_tax_for_pa(): order = Order(subtotal=10, state='PA') assert get_tax(order) == 10 * 0.08 def test_it_raises_error_for_others(): with pytest.raises(NotImplementedError): order = Order(subtotal=1, state='EQ') assert get_tax(order) == 0
  6. TAXES = { 'CA': .0875, 'PA': .08, } def get_tax(order):

    try: return order.subtotal * TAXES[order.state] except KeyError: raise NotImplementedError()
  7. def tax_equals_percent(state, percent): subtotal = 10 order = Order(subtotal, state)

    return get_tax(order) == subtotal * percent . . . def test_get_tax_for_texas(): assert tax_equals_percent(‘TX', .0825) . . .
  8. TAXES = { 'CA': .0875, 'PA': .08, 'TX': .0825, }

    def get_tax(order): try: return order.subtotal * TAXES[order.state] except KeyError: raise NotImplementedError()
  9. “For each desired change: make the change easy (warning: this

    may be hard), then make the easy change.” Kent Beck
  10. class User: def __init__(self, data): self.data = data def validate(self):

    if '@' not in self.data['email']: raise ValueError('Invalid email provided.') if len(self.data['first_name']) > 50: raise ValueError('First name too long.') if len(self.data['last_name']) > 100: raise ValueError('Last name too long.')
  11. import pytest def assert_raises_error(data, expected_message): user = User(data=data) with pytest.raises(ValueError)

    as excinfo: user.validate() assert expected_message in str(excinfo.value) class TestValidateUser: def test_invalid_email_raises_error(self): data = { 'first_name': 'foo', 'email': 'bademail', 'last_name': 'lee', } assert_raises_error(data, 'Invalid email provided') . . .
  12. class User: def __init__(self, data): self.data = data def validate(self):

    self.validate_email() self.validate_first_name() self.validate_last_name() def validate_email(self): if '@' not in self.data['email']: raise ValueError('Invalid email provided.') def validate_first_name(self): if len(self.data['first_name']) > 50: raise ValueError('First name too long.') def validate_last_name(self): if len(self.data['last_name']) > 100: raise ValueError('Last name too long.')
  13. class User: _fields = [ 'email', 'first_name', 'last_name' ] def

    __init__(self, data): self.data = data def validate(self): for field in self._fields: method_name = 'validate_{}'.format(field) validate_field = getattr(self, method_name) validate_field() def validate_email(self): . . .
  14. class TestValidateUser: . . . def test_invalid_phone_number_raises_error(self): data = {'phone_number':

    'no_phone'} user = User(data) with pytest.raises(ValueError): user.validate_phone_number()
  15. class User: _fields = [ 'email', 'first_name', 'last_name', 'phone_number', ]

    PHONE_LENGTH = 10 . . . def validate_phone_number(self): if len(self.data['phone_number']) != User.PHONE_LENGTH: raise ValueError('Phone number is wrong length.')
  16. <?php if (isset($_GET['fahrenheit'])) { $fahrenheit = $_GET['fahrenheit']; } else {

    die('No degrees in Fahrenheit provided.'); } $celsius = ($fahrenheit - 32) * 5 / 9; echo floor($celsius);
  17. import requests API_URL = 'http://localhost:8888/celsius' def test_api_converts_fahrenheit(): degrees_f = 32

    degrees_c = 0 path = '{}?fahrenheit={}'.format(API_URL, degrees_f) response = requests.get(path) assert int(response.content) == degrees_c def test_api_rounds_down(): degrees_f = 100 degrees_c = 37 # rounded down path = '{}?fahrenheit={}'.format(API_URL, degrees_f) response = requests.get(path) assert int(response.content) == degrees_c def test_api_returns_error_if_no_degrees(): response = requests.get(API_URL) assert 'No degrees' in response.content
  18. from math import floor from flask import Flask, request app

    = Flask(__name__) @app.route('/celsius') def celsius(): try: fahrenheit = int(request.args.get('fahrenheit')) except TypeError: return 'No degrees in Fahrenheit provided.' celsius = (fahrenheit - 32) * 5 / 9; return str(floor(celsius))
  19. References: Refactoring - Martin Fowler & Kent Beck https://www.amazon.com/Refactoring-Improving-Design-Existing- Code/dp/B007WTFWJ6/ref=sr_1_1?

    s=books&ie=UTF8&qid=1472533198&sr=1-1&keywords=refactori ng+martin+fowler#nav-subnav Working with Legacy Code - Michael Feathers https://www.amazon.com/Working-Effectively-Legacy-Michael- Feathers/dp/0131177052 Forget Technical Debt — Here's How to Build Technical Wealth http://firstround.com/review/forget-technical-debt-heres-how-to- build-technical-wealth/
  20. Credits: IMG_5163 - Justin Henry, https://flic.kr/p/ebaYR The sky is the

    limit #2 - Mark Dries, https://flic.kr/p/eeq2eQ The way it should be! - Patrick Dirden, https://flic.kr/p/ 8Yp4mf 0001_zoriah_photojournalist_war_photographer_20120719 _0631 - Zoriah, https://flic.kr/p/dkScaB Freaktography - Ravaged, https://flic.kr/p/nh72Tp Aachen - Aachener Dom Pala d’oro Antependium - Daniel Mennerich, https://flic.kr/p/pBmHyw