Last active
December 2, 2022 17:23
-
-
Save noahc/0a98e19110138e391c5b1e1ad504aaff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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