$30 off During Our Annual Pro Sale. View Details »

A Brewer's Guide to Filtering out Complexity and Churn (RailsConf 2023 Edition)

A Brewer's Guide to Filtering out Complexity and Churn (RailsConf 2023 Edition)

RailsConf 2023 presentation by Alan Ridlehoover & Fito von Zastrow

Mechanical coffee machines are amazing! You drop in a coin, listen for the clink, make a selection, and the machine springs to life, hissing, clicking, and whirring. Then the complex mechanical ballet ends, splashing that glorious, aromatic liquid into the cup. Ah! C’est magnifique!

There’s just one problem. Our customers also want soup! And, our machine is not extensible. So, we have a choice: we can add to the complexity of our machine by jamming in a new dispenser with each new request; or, we can pause to make our machine more extensible before development slows to a halt.

Here's the video of the presentation: https://youtu.be/jE1Wp7-wnSM

Alan Ridlehoover
PRO

April 25, 2023
Tweet

More Decks by Alan Ridlehoover

Other Decks in Programming

Transcript

  1. Alan Ridlehoover & Fito von Zastrow
    RailsConf 2023
    The Coffee
    Machine Talk
    A Brewer’s Guide to Filtering out
    Complexity and Churn

    View Slide

  2. PRONOUNS
    he/him/his
    HOMETOWN
    Asunción

    RUBY EXPERIENCE
    12 years
    FAVORITE COFFEE
    Ghirardelli Dark
    Chocolate Mocha
    PRONOUNS
    he/him/his
    HOMETOWN
    Seattle

    RUBY EXPERIENCE
    12 years
    FAVORITE COFFEE
    New Orleans Cold
    Brew w/ Chicory

    View Slide

  3. View Slide

  4. 🤲

    View Slide

  5. View Slide

  6. View Slide

  7. 👍

    View Slide

  8. How did we get here?
    How complexity sneaks into a code base.

    View Slide

  9. class CoffeeMachine


    def vend


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispose_of_grounds


    end


    end
    ☕ Story: As a patron, I want coffee

    View Slide

  10. ☕ Story: As a patron, I want tea
    class CoffeeMachine


    def vend(drink: :coffee)


    if drink == :coffee


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispose_of_grounds


    elsif drink == :tea


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    end


    end


    end

    View Slide

  11. ☕ Refactor: DRY up the code
    class CoffeeMachine


    def vend(drink: :coffee)


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispose_of_grounds if drink == :coffee


    end


    end


    View Slide

  12. ☕ Story: As a patron, I want sweetener
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispense_sweetener if options[:sweet]


    dispose_of_grounds if drink == :coffee


    end


    end

    View Slide

  13. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    dispose_of_grounds if drink == :coffee


    end


    end


    ☕ Story: As a patron, I want cream

    View Slide

  14. ☕ Story: As a patron, I want cocoa
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_creamer if options[:creamy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end

    View Slide

  15. ☕ Story: As a patron, I want whipped cream
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_creamer if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end

    View Slide

  16. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_creamer if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end
    ☕ 7 commits: 9 conditionals
    1
    2
    3
    4 5
    6
    7 8
    9

    View Slide

  17. Do you smell that?
    How to recognize the problem before it
    becomes painful and frustrating.
    Rules by Sandi Metz Flog by Ryan Davis

    View Slide

  18. Score of Means


    - - - - - - - - - - - - - - - - - - - - - - - - -


    0-10 Awesome


    11-20 Good enough


    21-40 Might need refactoring


    41-60 Possible to justify


    61-100 Danger


    100-200 Whoop, whoop, whoop


    200 + Someone please think of the children


    (note: these are scores for an individual method)


    What's a good
    fl
    og score? by Jake Scruggs
    ☕ Measuring complexity with the flog gem

    View Slide

  19. ☕ Story: As a patron, I want coffee
    class CoffeeMachine


    def vend


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispose_of_grounds


    end


    end
    Churn

    1
    Complexity

    5.5
    10
    20
    30
    25
    15
    5
    0

    View Slide

  20. class CoffeeMachine


    def vend(drink: :coffee)


    if drink == :coffee


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispose_of_grounds


    elsif drink == :tea


    dispense_cup


    dispense_tea_bag


    heat_water


    dispense_water


    end


    end


    end
    Churn

    2
    Complexity

    14.6
    10
    20
    30
    25
    15
    5
    0
    ☕ Story: As a patron, I want tea

    View Slide

  21. ☕ Refactor: DRY up the code
    class CoffeeMachine


    def vend(drink: :coffee)


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    3
    Complexity

    10.9
    10
    20
    30
    25
    15
    5
    0

    View Slide

  22. ☕ Story: As a patron, I want sweetener
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispense_sweetener if options[:sweet]


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    4
    Complexity

    13.5
    10
    20
    30
    25
    15
    5
    0

    View Slide

  23. ☕ Story: As a patron, I want cream
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    end


    dispense_water


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    5
    Complexity

    16.0
    10
    20
    30
    25
    15
    5
    0

    View Slide

  24. ☕ Story: As a patron, I want cocoa
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    6
    Complexity

    21.3
    10
    20
    30
    25
    15
    5
    0

    View Slide

  25. ☕ Story: As a patron, I want whipped cream
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    7
    Complexity

    25.5
    10
    20
    30
    25
    15
    5
    0

    View Slide

  26. ☕ CoffeeMachine pain index over time
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    7
    Complexity

    25.5
    10
    20
    30
    25
    15
    5
    0

    View Slide

  27. ☕ CoffeeMachine pain index over time
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    Churn

    7
    Complexity

    25.5
    10
    20
    30
    25
    15
    5
    0
    Complexity

    25.5

    View Slide

  28. Can we turn it around?
    How to
    fi
    lter out complexity and churn.

    View Slide

  29. ☕ Refactor: Rehydrate to see missing abstractions
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    View Slide

  30. ☕ Refactor: Rehydrate to see missing abstractions

    View Slide

  31. ☕ Refactor: Rehydrate to see missing abstractions
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    dispense_cup


    heat_water


    if drink == :coffee


    prepare_grounds


    elsif drink == :tea


    dispense_tea_bag


    elsif drink == :cocoa


    dispense_cocoa_mix


    end


    dispense_water


    unless drink == :cocoa


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    end


    unless drink == :tea


    dispense_whipped_cream if options[:fluffy]


    end


    dispose_of_grounds if drink == :coffee


    end


    end


    View Slide

  32. ☕ Refactor: Rehydrate to see missing abstractions
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    if drink == :coffee


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    dispense_whipped_cream if options[:fluffy]


    dispose_of_grounds


    elsif drink == :tea


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if options[:sweet]


    dispense_cream if options[:creamy]


    elsif drink == :cocoa


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if options[:fluffy]


    end


    end


    end


    View Slide

  33. ☕ Refactor: Extract polymorphic objects
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    if drink == :coffee


    Coffee.new(options: options).prepare


    elsif drink == :tea


    Tea.new(options: options).prepare


    elsif drink == :cocoa


    Cocoa.new(options: options).prepare


    end


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end



    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  34. ☕ Refactor: Extract polymorphic objects
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    if drink == :coffee


    Coffee.new(options: options).prepare


    elsif drink == :tea


    Tea.new(options: options).prepare


    elsif drink == :cocoa


    Cocoa.new(options: options).prepare


    end


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end



    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  35. ☕ Refactor: Extract polymorphic objects
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(beverage, options)


    if beverage == :coffee


    Coffee.new(options)


    elsif beverage == :tea


    Tea.new(options)


    elsif beverage == :cocoa


    Cocoa.new(options)


    end


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end



    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  36. ☕ Refactor: Introduce the NullObject pattern
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(beverage, options)


    if beverage == :coffee


    Coffee.new(options)


    elsif beverage == :tea


    Tea.new(options)


    elsif beverage == :cocoa


    Cocoa.new(options)


    else


    NullBeverage.new(options)


    end


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end



    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  37. ☕ Refactor: Use convention to prevent churn
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(beverage, options)


    klass = beverage.capitalize


    if Module.const_defined?(klass)


    Module.const_get(klass).new(options)


    else


    NullBeverage.new(options)


    end


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end



    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  38. Self Registration
    How to create an open/closed factory class.
    🚧

    View Slide

  39. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(beverage, options)


    klass = beverage.capitalize


    if Module.const_defined?(klass)


    Module.const_get(klass).new(options)


    else


    NullBeverage.new(options)


    end


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  40. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  41. ☕ Refactor: Introduce self-registration
    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    View Slide

  42. ☕ Refactor: Introduce self-registration
    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    View Slide

  43. ☕ Refactor: Introduce self-registration
    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    View Slide

  44. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)

    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    ☕ Refactor: Introduce self-registration

    View Slide

  45. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    # {


    # :coffee => Coffee,

    # :tea => Tea,


    # :cocoa => Cocoa


    # )


    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    ☕ Refactor: Introduce self-registration

    View Slide

  46. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    ☕ Refactor: Introduce self-registration

    View Slide

  47. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    BeverageFactory.register(:coffee, self)


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    BeverageFactory.register(:tea, self)


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end
    class Cocoa < Beverage


    BeverageFactory.register(:cocoa, self)


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    ☕ Refactor: Introduce self-registration

    View Slide

  48. class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    ☕ Refactor: Introduce self-registration

    View Slide

  49. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    View Slide

  50. https://github.com/
    fi
    rst-try-software/manufacturable
    ☕ Manufacturable: A gem for self-registration

    View Slide

  51. Did that work?
    Were we able to reduce the complexity?

    View Slide

  52. ☕ Complexity of the CoffeeMachine class over time
    Rehydrate
    Extract Classes
    Introduce Factory
    Whipped Cream
    Cocoa
    Cream
    Sweetener
    DRY
    Tea
    Co
    ff
    ee
    10
    20
    30
    25
    15
    5
    0
    40
    35

    View Slide

  53. ☕ Complexity of the CoffeeMachine class over time
    Rehydrate
    Extract Classes
    Introduce Factory
    Whipped Cream
    Cocoa
    Cream
    Sweetener
    DRY
    Tea
    Co
    ff
    ee
    10
    20
    30
    25
    15
    5
    0
    40
    35

    View Slide

  54. ☕ Complexity of the CoffeeMachine class over time
    Rehydrate
    Extract Classes
    Introduce Factory
    Whipped Cream
    Cocoa
    Cream
    Sweetener
    DRY
    Tea
    Co
    ff
    ee
    10
    20
    30
    25
    15
    5
    0
    40
    35

    View Slide

  55. ☕ Complexity of the CoffeeMachine class over time
    Rehydrate
    Extract Classes
    Introduce Factory

    and self-registration
    Whipped Cream
    Cocoa
    Cream
    Sweetener
    DRY
    Tea
    Co
    ff
    ee
    10
    20
    30
    25
    15
    5
    0
    40
    35

    View Slide

  56. ☕ Complexity of the application
    Class Churn Complexity


    --------------------- ----- ----------


    CoffeeMachine 10 3.9


    BeverageFactory 3 1.8


    Beverage 2 2.2

    Coffee 1 16.6


    Tea 1 11.8


    Cocoa 1 8.1


    NullBeverage 1 0.0

    View Slide

  57. Now, who wants ?
    How to add functionality without modifying existing
    fi
    les.

    View Slide

  58. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end

    View Slide

  59. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class TomatoSoup < Beverage


    prepares :tomato_soup


    def prepare


    dispense_cup


    heat_water


    dispense_condensed_soup


    dispense_water


    dispense_croutons if crunchy?


    dispense_hot_sauce if spicy?


    end


    end

    View Slide

  60. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end

    View Slide

  61. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class TomatoSoup < Beverage


    prepares :tomato_soup


    def prepare


    dispense_cup


    heat_water


    dispense_condensed_soup


    dispense_water


    dispense_croutons if crunchy?


    dispense_hot_sauce if spicy?


    end


    end

    View Slide

  62. ☕ Refactor: Introduce self-registration
    class CoffeeMachine


    def vend(drink: :coffee, options: {})


    BeverageFactory.build(drink, options).prepare


    end


    end


    class BeverageFactory


    def self.build(key, options)


    beverage_class(key).new(options)


    end


    def self.register(key, klass)


    beverages[key] = klass


    end


    def self.beverage_class(key)


    beverages[key] || NullBeverage


    end


    def self.beverages


    @beverages ||= {}


    end


    end


    class NullBeverage < Beverage


    def prepare


    # no-op


    end


    end


    class Coffee < Beverage


    prepares :coffee


    def prepare


    dispense_cup


    heat_water


    prepare_grounds


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    dispense_whipped_cream if fluffy?


    dispose_of_grounds


    end


    end



    class Tea < Beverage


    prepares :tea


    def prepare


    dispense_cup


    heat_water


    dispense_tea_bag


    dispense_water


    dispense_sweetener if sweet?


    dispense_cream if creamy?


    end


    end


    class Beverage


    def self.prepares(key)


    BeverageFactory.register(key, self)


    end


    # initializer & other shared code


    end


    class Cocoa < Beverage


    prepares :cocoa


    def prepare


    dispense_cup


    heat_water


    dispense_cocoa_mix


    dispense_water


    dispense_whipped_cream if fluffy?


    end


    end


    class TomatoSoup < Beverage


    prepares :tomato_soup


    def prepare


    dispense_cup


    heat_water


    dispense_condensed_soup


    dispense_water


    dispense_croutons if crunchy?


    dispense_hot_sauce if spicy?


    end


    end


    class AppleCider < Beverage


    prepares :apple_cider


    def prepare


    dispense_cup


    heat_water


    dispense_cider_mix


    dispense_water


    dispense_cinnamon_stick


    end


    end


    View Slide

  63. Yeah, but does it scale?
    How to apply this to a large, complicated code base.

    View Slide

  64. ☕ Plotting complexity vs. churn
    Churn
    Complexity
    Getting Empirical about Refactoring by Michael Feathers

    View Slide

  65. ☕ Plotting complexity vs. churn
    Churn
    Complexity
    Pain Free

    View Slide

  66. ☕ Plotting complexity vs. churn
    Pain Free
    Painful
    Churn
    Complexity

    View Slide

  67. ☕ Plotting complexity vs. churn
    Pain Free
    Painful
    Churn
    Complexity
    Configuration?

    View Slide

  68. ☕ Plotting complexity vs. churn
    Pain Free
    Painful
    Churn
    Complexity
    Configuration?
    Omega Mess?
    Go Ahead, Make a Mess by Sandi Metz

    View Slide

  69. ☕ Plotting complexity vs. churn
    Churn
    Complexity

    View Slide

  70. ☕ Plotting complexity vs. churn
    Churn
    Complexity

    View Slide

  71. ☕ Plotting complexity vs. churn
    Churn
    Complexity

    View Slide

  72. Conclusion
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  73. Take Aways
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  74. Take Aways
    A Brewer’s Guide to Filtering out Complexity and Churn
    Complexity will sneak into your code.
    • Happens one commit at a time
    • Conditionals and DRY are often the culprits

    View Slide

  75. Take Aways
    A Brewer’s Guide to Filtering out Complexity and Churn
    Complexity will sneak into your code.
    • Happens one commit at a time
    • Conditionals and DRY are often the culprits
    You can recognize complexity before it becomes painful.
    • Keep methods short
    • Watch complexity
    • Pay attention to how it feels

    View Slide

  76. Take Aways
    A Brewer’s Guide to Filtering out Complexity and Churn
    Complexity will sneak into your code.
    • Happens one commit at a time
    • Conditionals and DRY are often the culprits
    You can recognize complexity before it becomes painful.
    • Keep methods short
    • Watch complexity
    • Pay attention to how it feels
    And, you can back away from painful complexity.
    • Polymorphism and factories are your friends
    • Overly DRY code can hide abstractions and couple algorithms in dangerous ways
    • Self-registration can take you across the no-churn
    fi
    nish line

    View Slide

  77. Homework
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  78. Homework
    What is the average method complexity for your application?
    • Use the Flog gem to measure complexity
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  79. Homework
    What is the
    fi
    le with the most churn in your application?
    • Use the Churn gem to measure churn
    What is the average method complexity for your application?
    • Use the Flog gem to measure complexity
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  80. Homework
    What is the
    fi
    le with the most churn in your application?
    • Use the Churn gem to measure churn
    Which class in your application needs the most attention?
    • Use the Ruby Critic gem to plot complexity vs. churn
    • Look for the
    fi
    le with the highest complexity and churn
    What is the average method complexity for your application?
    • Use the Flog gem to measure complexity
    A Brewer’s Guide to Filtering out Complexity and Churn

    View Slide

  81. Homework
    What is the
    fi
    le with the most churn in your application?
    • Use the Churn gem to measure churn
    Which class in your application needs the most attention?
    • Use the Ruby Critic gem to plot complexity vs. churn
    • Look for the
    fi
    le with the highest complexity and churn
    What is the average method complexity for your application?
    • Use the Flog gem to measure complexity
    A Brewer’s Guide to Filtering out Complexity and Churn
    Where might you use self-registration in your code?
    • Play with the technique
    fi
    rst to get a feel for it
    • Look at the Manufacturable gem for industrial-scale self-registration

    View Slide

  82. Get in touch!
    EMAIL
    fi
    [email protected]
    GITHUB
    @Fito
    TWITTER
    @
    fi
    to.vonzastrow
    MASTODON
    @[email protected]
    EMAIL
    [email protected]
    GITHUB
    @aridlehoover

    TWITTER
    @aridlehoover
    MASTODON
    @[email protected]
    BLOG
    the.codegardener.com
    GITHUB REPO
    https://github.com/
    fi
    rst-try-software/co
    ff
    ee

    View Slide

  83. Ruby Flog
    Ruby Flog
    Ruby Flog


    A VS Code extension that puts
    fl
    og scores in your status bar…

    View Slide

  84. We are the largest Rails shop you’ve never heard of.
    We build Internet machines for co
    ff
    ee lovers.
    Our 16 year old, 1.5 million line monolith supports:
    • A multi-billion dollar business
    • Billions of requests per day
    Our team is solving hard problems related to:
    • Code complexity
    • Rails at scale
    https://meraki.cisco.com/jobs

    View Slide

  85. Thanks for coming!
    A Brewer’s Guide to Filtering out Complexity and Churn
    Sandi Metz
    • Practical Object Oriented Design in Ruby (book)

    • 99 Bottles of OOP (book) (sample on the topic of measuring complexity)

    • Rules: https://youtu.be/npOGOmkxuio

    • Go Ahead, Make a Mess: https://youtu.be/mpA2F1In41w

    Ryan Davis
    • Flog: https://github.com/seattlerb/
    fl
    og
    Jake Scruggs
    • https://jakescruggs.blogspot.com/2008/08/whats-good-
    fl
    og-score.html

    Michael Feathers
    • Working E
    ff
    ectively with Legacy Code (book)

    • https://www.agileconnection.com/article/getting-empirical-about-refactoring

    Alan Ridlehoover & Fito von Zastrow
    • Manufacturable: https://github.com/
    fi
    rst-try-software/manufacturable

    • Ruby Flog: https://marketplace.visualstudio.com/items?itemName=
    fi
    rst-try-software.ruby-
    fl
    og

    View Slide