Strategic - Painless Strategy Pattern in Ruby and Rails
Tags: ruby, rails, oop, designpatterns
Published: 2021-03-20 22:57:00

The [Strategic Ruby gem](https://github.com/AndyObtiva/strategic/) (Painless [Strategy Pattern](https://www.journaldev.com/1754/strategy-design-pattern-in-java-example-tutorial) 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](https://github.com/AndyObtiva/strategic/) was mostly born out of work I did last year at [Chronogolf](https://www.chronogolf.ca/?redirected=en-US) by [Lightspeed](https://www.lightspeedhq.com/), a golf course management web app built in [Ruby on Rails](https://rubyonrails.org/), 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](https://github.com/AndyObtiva/dcr), implementing language commands ([Command Pattern](https://www.journaldev.com/1624/command-design-pattern)) as a special case of [Strategy Pattern](https://www.journaldev.com/1754/strategy-design-pattern-in-java-example-tutorial), with auto-inference of strategy names from command file names by convention.

[Strategic](https://github.com/AndyObtiva/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](https://github.com/AndyObtiva/strategic/raw/master/strategic-example.png)

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

```ruby
class TaxCalculator
  include Strategic

  # strategies may implement a tax_for(amount) method
end
```

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`):

```ruby
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
```

(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'):

```ruby
tax_calculator = TaxCalculator.new(args)
tax_calculator.strategy = 'us'
```

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

```ruby
tax_calculator = TaxCalculator.new_with_strategy('us', args)
```

5- Invoke the strategy implemented method:

```ruby
tax = tax_calculator.tax_for(39.78)
```
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

```ruby
class TaxCalculator
  include Strategic
  
  default_strategy 'canada'
end

tax_calculator = TaxCalculator.new(args)
tax = tax_calculator.tax_for(39.78)
```

[Put to good use!](https://github.com/AndyObtiva/strategic)

Learn more at:
- https://andymaleh.blogspot.com/2021/03/strategic-100.html
- https://andymaleh.blogspot.com/2021/03/strategic-091-strategy.html
- https://github.com/AndyObtiva/strategic