Skip to content

Instantly share code, notes, and snippets.

@noahc
Last active December 2, 2022 17:23
Show Gist options
  • Select an option

  • Save noahc/0a98e19110138e391c5b1e1ad504aaff to your computer and use it in GitHub Desktop.

Select an option

Save noahc/0a98e19110138e391c5b1e1ad504aaff to your computer and use it in GitHub Desktop.
#original example from Dave Copeland (@davetron5000) sourced from https://twitter.com/davetron5000/status/1598696425675788290
# I deviate from Dave's example by adding a refund method below that I think typifies the Condition Crazy Developer Pattern
class PaymentWithConditions
def charge(price, customer)
if customer.payment_method == :credit_card
charge_credit_card(price, customer.credit_card)
elsif customer.payment_method == :venmo
charge_venmo(price, customer.venmo)
else
raise "BUG: Don't know how to deal with customer payment method"
end
end
def refund(price, reason=:customer_return)
if customer.payment_method == :credit_card
if reason == :customer_return && not_more_than_30_days_ago
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
elsif customer.payment_method == :venmo
if reason == :customer_return && not_labeled_as_fraud_account
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago && includes_only_returnable_items
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
else
raise "BUG: Don't know how to deal with customer refund request"
end
end
end
# A solution to this condition is often to create classes that implement a common method
class PaymentWithCard
def charge(price, customer)
# whatever was in charge_credit_card in PaymentWithConditions
end
def refund(price, reason=:customer_return)
if reason == :customer_return && not_more_than_30_days_ago
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
end
end
class PaymentWithVenmo
def charge(price, customer)
# whatever was in charge_venmo in PaymentWithConditions
end
def refund(price, reason=:customer_return)
if reason == :customer_return && not_labeled_as_fraud_account
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago && includes_only_returnable_items
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
end
end
# But, you still need some logic to figure out which of these classes to use.
# With `if` statements:
payment = nil
if customer.payment_method == :credit_card
payment = PaymentWithCard.new
elsif customer.payment_method == :venmo
payment = PaymentWithVenmo.new
else
raise "BUG: Don't know how to deal with customer payment method"
end
# If all we had was just the charge conditional, it doesn't feel like much of a win.
# You've replaced the single conditional with a conditional at the call site instead of inside the method.
# But once we add more methods, we notice a pattern.
# The goal is not to be conditional free, but to decrease the cost of code production and maintence.
#With a single conditional at the call site, we remove all outer most conditionals and are able to reduce the code complexity.
#However, if we examine a single example, there's more we can do.
#Prior version from above with refund method also extracted
class PaymentWithVenmo
def charge(price, customer)
# whatever was in charge_venmo in PaymentWithConditions
end
def refund(price, reason=:customer_return)
if reason == :customer_return && not_labeled_as_fraud_account
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago && includes_only_returnable_items
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
end
end
# We can continue this pattern and change the refund method to call new classes.
class PaymentWithVenmo
def charge(price, customer)
# whatever was in charge_venmo in PaymentWithConditions
end
def refund(price, reason=:customer_return)
RefundWithVenmo.new(price, reason).process
end
end
class RefundWithVenmo < ApplicationRecord
attr_accessor :price, :reason
validate :not_labeled_as_fraud_account, if: customer_return?
validate :not_more_than_90_days_ago, if: our_mistake?
validate :includes_only_returnable_items, if: our_mistake?
def initilize(price, reason)
@price = price
@reason = reason
end
def process
if valid?
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
end
private
def customer_return?
reason == :customer_return
end
def our_mistake?
reason == our_mistake
end
def not_labeled_as_fraud_account # and other validator methods....
end
end
# Conditionals in and of them selves aren't evil, but they have a cost that must pay to continue to exist in the code.
# When they don't pull their own weight, we are better served by finding other ways to express ourselves in the code.
# Our new `RefundWithVenmo` class certainly contains conditionals in the validators, but these serve as guard clauses.
# Guard clauses are one way of reducing the cost of conditionals, but what do more expensive conditionals look like?
#Here are few examples of what I consider conditonals that are on the more expensive side.
#Nested Conditionals like we saw earlier:
def refund(price, reason=:customer_return)
if customer.payment_method == :credit_card
if reason == :customer_return && not_more_than_30_days_ago
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
elsif customer.payment_method == :venmo
if reason == :customer_return && not_labeled_as_fraud_account
return_to_customer(price, customer.payment_method)
elsif reason == :our_mistake && not_more_than_90_days_ago && includes_only_returnable_items
return_to_customer(price, customer.payment_method)
else
handle_invalid_refund_request
end
else
raise "BUG: Don't know how to deal with customer refund request"
end
end
# multiple conditions associated with a single if/else branch from earlier
def refund(price, reason=:customer_return)
if reason == :customer_return && not_labeled_as_fraud_account
.
elsif reason == :our_mistake && not_more_than_90_days_ago && includes_only_returnable_items
.
else
handle_invalid_refund_request
end
end
# similiarly shaped methods. Notice I added a baz branch to refund, which means that baz does the default when we call charge, but does the baz thing when we do refund.
def charge
if foo
# do foo
elseif bar
#do bar
else
#do default thing
end
end
def refund
if foo
# do foo
elseif bar
#do bar
elseif baz
#do baz thing
else
#do default thing
end
end
# We can change the way we write our code to prefer single line conditionals that serve as guard clauses where possible.
# We can also extract similiarly shaped code to classes that provide both a context via class name and reduce the cost.
# There are no right ansewers here. But the goal is to drive the cost of conditionals and by extension the cost of change lower.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment