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

Conditional Code Smells

Conditional Code Smells

And not like roses. If/then/else is one of the first things a new programmer learns. But, like any tool, it can be overused. Too much conditional logic makes your methods hard to understand and reason about.

Conditionals can often indicate that we are missing important abstractions in our code. You may be familiar with using polymorphism to eliminate conditionals, but there are many other ways. This talk goes through a few of those ways and focuses on rewriting a piece of complicated, conditional logic to use no conditionals at all.

Materials mentioned:

Sandi Metz: All the Little Things - https://www.youtube.com/watch?v=8bZh5LMaSmE

Sandi Metz: Practical Object Oriented Design in Ruby - http://www.poodr.com/

Katrina Owen: Therapeutic Refactoring - https://www.youtube.com/watch?v=J4dlF0kcThQ

Avdi Grimm: Confident Ruby - http://www.confidentruby.com/­

Avdi Grimm: Confident Code (the talk that preceded the book) - https://www.youtube.com/watch?v=T8J0j2xJFgQ

Eric Roberts

August 19, 2014
Tweet

More Decks by Eric Roberts

Other Decks in Programming

Transcript

  1. Conditional statements are features of a programming language which perform

    different computations or actions depending on whether a programmer-specified boolean condition evaluates to true or false. Apart from the case of branch predication, this is always achieved by selectively altering the control flow based on some condition. — Wikipedia
  2. Instead, they indicate weaknesses in design that may be slowing

    down development or increasing the risk of bugs in the future. — Wikipedia
  3. if thing puts 'hello' elsif other_thing puts 'auf wiedersehen' elsif

    another_thing puts 'bonjour' else puts 'goodbye' end
  4. if thing puts 'hello' elsif other_thing if going puts 'auf

    wiedersehen' else puts 'hallo' end elsif another_thing puts 'bonjour' else puts 'goodbye' end
  5. def pricing_range_fold_and_save(new_range) pos = 0 pricings = self.pricings other_range =

    Pricing.new if pricings.length == 0 pricings.push(new_range) else pricings.each_with_index do |e, i| if i == 0 && new_range.start_date < e.start_date pricings.unshift(new_range) if new_range.end_date <= e.end_date e.start_date = new_range.end_date + 1.day break else while pricings[1].present? && new_range.end_date >= pricings[1].end_date pricings[1].destroy pricings.delete pricings[1] end if pricings[1].present? pricings[1].start_date = new_range.end_date + 1.day end break end elsif new_range.start_date <= e.end_date && new_range.start_date >= e.start_date if new_range.end_date < e.end_date other_range = e.dup other_range.start_date = new_range.end_date + 1.day if new_range.start_date == e.start_date e.destroy pricings.delete e i -= 1 else e.end_date = new_range.start_date - 1.day end pricings.insert(i+1, new_range) pos = i+1 pricings.insert(i+2, other_range) break else if new_range.start_date == e.start_date e.destroy pricings.delete e i -= 1 else e.end_date = new_range.start_date - 1.day end
  6. pricings.insert(i+1, new_range) pos = i+1 while pricings[i+2].present? && new_range.end_date >=

    pricings[i+2].end_date pricings[i+2].destroy pricings.delete pricings[i+2] end if pricings[i+2].present? pricings[i+2].start_date = new_range.end_date + 1.day end break end elsif i == pricings.size-1 pricings[i].end_date = new_range.start_date-1.day pricings.push(new_range) break end end end pricings.each_with_index do |pricing, i| if i != pricings.size-1 && pricing.price ==pricings[i+1].price pricing.end_date = pricings[i+1].end_date end if i != 0 && pricing.end_date == pricings[i-1].end_date pricing.destroy pricings.delete pricing end if pricing.end_date < pricing.start_date pricing.destroy pricings.delete pricing end end pricings.each do |pricing| if pricing != pricings[pos] pricing.currency = pricings[pos].currency pricing.save end end pricings[pos].save return pricings end
  7. Conditional code can be like reading a choose your own

    adventure book from front to back.
  8. class Company has_many :campaigns has_many :mailings, as: :owner end class

    Campaign belongs_to :company has_many :mailings, as: :owner end
  9. company = Company.new mailing = Mailing.new(owner: company) mailing.company campaign =

    Campaign.new(company: company) mailing = Mailing.new(owner: campaign) mailing.company
  10. class GildedRose attr_reader :name, :quality, :days_remaining def initialize(name, quality, days_remaining)

    @name, @quality, @days_remaining = name, quality, days_remaining en def tick [...] end end
  11. def tick days_remaining -= 1 if name == 'Aged Brie'

    quality -= 1 elsif name == 'Sulfuras, Hand of Ragnaros' quality += 1 end end
  12. class GildedRose attr_reader :name, :quality, :days_remaining def initialize(name, quality, days_remaining)

    @name, @quality, @days_remaining = name, quality, days_remaining en def tick days_remaining -= 1 end end
  13. class GildedRose [...] def self.klass_for(name) { 'Aged Brie' => AgedBrie,

    'Sulfuras, Hand of Ragnaros' => Sulfuras } end end
  14. def pricing_range_fold_and_save(new_range) pos = 0 pricings = self.pricings other_range =

    Pricing.new if pricings.length == 0 pricings.push(new_range) else pricings.each_with_index do |e, i| if i == 0 && new_range.start_date < e.start_date pricings.unshift(new_range) if new_range.end_date <= e.end_date e.start_date = new_range.end_date + 1.day break else while pricings[1].present? && new_range.end_date >= pricings[1].end_date pricings[1].destroy pricings.delete pricings[1] end if pricings[1].present? pricings[1].start_date = new_range.end_date + 1.day end break end elsif new_range.start_date <= e.end_date && new_range.start_date >= e.start_date if new_range.end_date < e.end_date other_range = e.dup other_range.start_date = new_range.end_date + 1.day if new_range.start_date == e.start_date e.destroy pricings.delete e i -= 1 else e.end_date = new_range.start_date - 1.day end pricings.insert(i+1, new_range) pos = i+1 pricings.insert(i+2, other_range) break else if new_range.start_date == e.start_date e.destroy pricings.delete e i -= 1 else e.end_date = new_range.start_date - 1.day end
  15. def pricing_range_fold_and_save(new_range) pos = 0 pricings = self.pricings other_range =

    Pricing.new if pricings.length == 0 pricings.push(new_range) else
  16. pricings.each_with_index do |e, i| if i == 0 && new_range.start_date

    < e.start_date pricings.unshift(new_range) if new_range.end_date <= e.end_date e.start_date = new_range.end_date + 1.day break else while pricings[1].present? && new_range.end_date >= pricings[1].end_date pricings[1].destroy pricings.delete pricings[1] end if pricings[1].present? pricings[1].start_date = new_range.end_date + 1.day end break end
  17. 1 - If the new price completely covers an old

    price, delete the old price.
  18. class PricingRange attr_reader :property def initialize(property) @property = property end

    def add(pricing) delete_complete_overlaps(pricing) adjust_overlaps(pricing) split_containing(pricing) expand_to_meet(pricing) coalesce_pricings end end
  19. def adjust_overlaps_of_start(new) pricings.each do |old| if new.start_date <= old.start_date &&

    new.end_date >= old.start_date old.start_date = new.end_date + 1.day old.save! end end end
  20. def adjust_overlaps_of_start(new) to_adjust = pricings.select do |old| new.start_date <= old.start_date

    && new.end_date >= old.start_date end to_adjust.each do |pricing| pricing.start_date = new.start_date + 1.day pricing.save! end end
  21. def adjust_overlaps_of_start(new) to_adjust = pricings.select { |old| new.overlaps_start_of? old }

    to_adjust.each do |pricing| pricing.start_date = new.start_date + 1.day pricing.save! end end
  22. def adjust_overlaps_of_start(new) overlapped = pricings.find { |old| new.overlaps_start_of? old }

    overlapped.start_date = new.start_date + 1.day overlapped.save! end
  23. def adjust_overlaps_of_start(new) overlapped = pricings.find { |old| new.overlaps_start_of? old }

    return unless overlapped overlapped.start_date = new.start_date + 1.day overlapped.save! end
  24. def adjust_overlaps_of_start(new) to_adjust = pricings.select { |old| new.overlaps_start_of? old }

    to_adjust.each do |pricing| pricing.start_date = new.start_date + 1.day pricing.save! end end
  25. def adjust_overlaps_of_end(new) pricings.each do |old| if new.start_date <= old.end_date &&

    new.end_date >= old.end_date old.end_date = new.start_date - 1.day old.save! end end end
  26. def adjust_overlaps_of_end(new) to_adjust = pricings.select { |old| new.overlaps_end_of? old }

    to_adjust.each do |pricing| pricing.end_date = new.start_date - 1.day pricing.save! end end
  27. def split_containing(new) pricings.each do |old| if old.start_date <= new.start_date &&

    old.end_date >= new.end_date old_end = old.end_date old.end_date = new.start_date - 1.day old.save! other_side = old.dup other_side.start_date = new.end_date + 1.day other_side.end_date = old_end pricings << other_side end end end
  28. def split_containing(new) to_split = pricings.select { |old| old.contains? new }

    to_split.each do |left_side| right_side_end = left_side.end_date left_side.end_date = new.start_date - 1.day left_side.save! pricings << build_right_side(left_side, new.end_date + 1.day, right_side_end) end end
  29. def expand_to_meet_before(new) pricings.each do |old| if old == pricings.order('start_date ASC').first

    && old.start_date > new.end_date old.start_date = new.end_date + 1.day old.save! end end end
  30. def expand_to_meet_before(new) first_pricing_after_new = pricings.select do |old| old == pricings.order('start_date

    ASC').first && old.start_date > new.end_date end first_pricing_after_new.each do |old| old.start_date = new.end_date + 1.day old.save! end end
  31. def expand_to_meet_before(new) first_pricing_after_new = pricings.select do |old| old == first_pricing

    && old.is_after?(new) end first_pricing_after_new.each do |old| old.start_date = new.end_date + 1.day old.save! end end
  32. def coalesce_pricings pricings.each do |pricing| previous = pricings.where(end_date: pricing.start_date -

    1.day).first if previous && pricing.price == previous.price pricing.start_date = previous.start_date previous.destroy pricing.save! end end end
  33. def coalesce_pricings delete_before = pricings.select do |pricing| previous = pricings.where(end_date:

    pricing.start_date - 1.day).first previous && pricing.price == previous.price end delete_before.each do |pricing| previous = pricings.where(end_date: pricing.start_date - 1.day).first pricing.start_date = previous.start_date previous.destroy pricing.save! end end
  34. def coalesce_pricings delete_before = pricings.select do |pricing| previous = previous_pricing_for(pricing)

    previous && pricing.price == previous.price end delete_before.each do |pricing| previous = previous_pricing_for(pricing) pricing.start_date = previous.start_date previous.destroy pricing.save! end end
  35. ▸ Confident Ruby by Avdi Grimm ▸ All the Little

    Things by Sandi Metz ▸ Practical Object Oriented Design in Ruby by Sandi Metz ▸ Therapeutic Refactoring by Katrina Owen