DEV Community

loading...
Cover image for Strategic - Painless Strategy Pattern in Ruby and Rails

Strategic - Painless Strategy Pattern in Ruby and Rails

Andy Maleh
RailsConf / RubyConf / AgileConf / EclipseCon / EclipseWorld Presenter. Software Engineering Expert. Glimmer Ruby GUI OSS Author. MS in SE DePaul University Chicago. Snowboarder/Drummer. Ex-Groupon.
Updated on ・3 min read

The Strategic Ruby gem (Painless Strategy Pattern for Ruby and Rails) just had a new release in 0.9.1 (as well as 0.9.0), adding the following features:

  • Strategy Matcher: custom matcher support for selecting strategies (e.g. implement a fuzz matcher for picking a strategy by full string name or any partial match in case it is typed by a user)
  • Strategy Alias: specify an alias for selecting a strategy (e.g. car strategy has "sedan" as an alias)
  • Strategy Exclusion: exclude a strategy from a matcher (e.g. partial match on 'USA' and 'US', but not 'U')

To give you some background, the Strategic Ruby gem was mostly born out of work I did last year at Chronogolf by Lightspeed, a golf course management web app built in Ruby on Rails, where we had countless of strategies for customizing models, especially in relation to quotes, pricing, payments, memberships, and golf course tee time reservations.

It is currently used in the DCR Programming Language, implementing language commands (Command Pattern) as a special case of Strategy Pattern, with auto-inference of strategy names from command file names by convention.

Strategic enables you to make any existing domain model "strategic", externalizing all logic concerning algorithmic variations into separate strategy classes that are easy to find, maintain and extend while honoring the Open/Closed Principle and avoiding conditionals.

In summary, if you make a class called TaxCalculator strategic by including the Strategic mixin module, now you are able to drop strategies under the tax_calculator directory sitting next to the class (e.g. tax_calculator/us_strategy.rb, tax_calculator/canada_strategy.rb) while gaining extra API methods to grab strategy names to present in a user interface, grab strategy classes to select, and/or instantiate TaxCalculator directly with a strategy from the get-go.

Example

Strategy UML Diagram

1- Include the Strategic module in the Class to strategize TaxCalculator:

class TaxCalculator
  include Strategic

  # strategies may implement a tax_for(amount) method
end
Enter fullscreen mode Exit fullscreen mode

2- Now, you can add strategies under this directory without having to modify the original class: tax_calculator

3- Add strategy classes having names ending with Strategy by convention (e.g. UsStrategy) under the namespace matching the original class name (TaxCalculator:: as in tax_calculator/us_strategy.rb representing TaxCalculator::UsStrategy) and including the module (Strategic::Strategy):

class TaxCalculator::UsStrategy
  include Strategic::Strategy

  def tax_for(amount)
    amount * state_rate(context.state)
  end
  # ... other strategy methods follow
end

class TaxCalculator::CanadaStrategy
  include Strategic::Strategy

  def tax_for(amount)
    amount * (gst(context.province) + qst(context.province))
  end
  # ... other strategy methods follow
end
Enter fullscreen mode Exit fullscreen mode

(note: if you use strategy inheritance hierarchies, make sure to have strategy base classes end with StrategyBase to avoid getting picked up as strategies)

4- In client code, set the strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):

tax_calculator = TaxCalculator.new(args)
tax_calculator.strategy = 'us'
Enter fullscreen mode Exit fullscreen mode

4a. Alternatively, instantiate the strategic model with a strategy to begin with:

tax_calculator = TaxCalculator.new_with_strategy('us', args)
Enter fullscreen mode Exit fullscreen mode

5- Invoke the strategy implemented method:

tax = tax_calculator.tax_for(39.78)
Enter fullscreen mode Exit fullscreen mode

Default strategy for a strategy name that has no strategy class is nil

You may set a default strategy on a strategic model via class method default_strategy

class TaxCalculator
  include Strategic

  default_strategy 'canada'
end

tax_calculator = TaxCalculator.new(args)
tax = tax_calculator.tax_for(39.78)
Enter fullscreen mode Exit fullscreen mode

Put to good use!

Learn more at:

Discussion (1)

Collapse
andyobtiva profile image
Andy Maleh Author

Version 1.0.0 was released today, improving the design to more authentically match the Gang of Four Strategy Design Pattern:
andymaleh.blogspot.com/2021/03/str...