strategic 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +86 -67
  5. data/lib/strategic.rb +67 -9
  6. metadata +31 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5b79636eeccc8c6b813d95c05e0aba4e864d8cbc
4
- data.tar.gz: 4fb40967c29ffa5c788db5432c0a80444ab5c3db
2
+ SHA256:
3
+ metadata.gz: d4031c3d360c08e12ac58e42d6e56b97c177c342e64a1680275f50ad0add7e08
4
+ data.tar.gz: 5b4b783eca5301aa05f6043729531c7458b40806ed3b768c7b341b283d77ccda
5
5
  SHA512:
6
- metadata.gz: 654d0e603dbe10f748ba3025664bf0480a4833d62be091ffb3f6b807754487a8284f2687f7f9c40658d499e99342f408c2c282c8590962664c16cf6504453aa9
7
- data.tar.gz: 74f1272d40abb8e22520914614bf99a80455f99480007f4b7bc54b0b785065afcf8762822cf34bae977108e42895971c4bf9ac822d18f81620f9914925f461bd
6
+ metadata.gz: b5edee9e7e0f491e5ecda04465372bb9e45274ecc906d07798e627a4485f8776f1625c8175ff403b3f8a25a8e16781f20dd6d02c76b0600cc41b7a9c4354b920
7
+ data.tar.gz: 1c7ebfb4ce224c80d3181b55e4c9137ac7d1015733211592ec95218a581cedafd56abe235fab07de92c2cdf2343873eb2ad5a7789ebc18d97f05f84b88528c13
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Change Log
2
+
3
+ ## 0.9.0
4
+
5
+ - `strategy_matcher` block support that enables any strategy to specify a custom matcher (or the superclass of all strategies instead)
6
+ - `strategy_exclusion` class method support that enables any strategy to specify exclusions from the custom `strategy_matcher`
7
+ - `strategy_alias` class method support that enables any strategy to specify extra aliases (used by superclass's `strategy_class_for` method)
8
+
9
+ ## 0.8.0
10
+
11
+ - Initial version with `strategy_class_for`, `new_strategy`, `strategies`, and `strategy_names`
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017 Andy Maleh
1
+ Copyright (c) 2020-2021 Andy Maleh
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,18 +1,23 @@
1
- # Strategic (Painless Strategy Pattern in Ruby and Rails)
1
+ # Strategic 0.9.0
2
+ ## Painless Strategy Pattern in Ruby and Rails
2
3
  [![Gem Version](https://badge.fury.io/rb/strategic.svg)](http://badge.fury.io/rb/strategic)
4
+ [![Build Status](https://travis-ci.com/AndyObtiva/strategic.svg?branch=master)](https://travis-ci.com/AndyObtiva/strategic?branch=master)
5
+ [![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/strategic/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/strategic?branch=master)
3
6
 
4
- if/case conditionals can get really hairy in highly sophisticated business domains.
5
- Domain model inheritance can help remedy the problem, but dumping all
6
- logic variations in the same domain models can cause a maintenance nightmare.
7
- Thankfully, Strategy Pattern as per the Gang of Four solves the problem by externalizing logic variations to
7
+ (Note: this gem is a very early alpha work in progress and may change API in the future)
8
+
9
+ `if`/`case` conditionals can get really hairy in highly sophisticated business domains.
10
+ Object-oriented inheritance helps remedy the problem, but dumping all
11
+ logic variations in subclasses can cause a maintenance nightmare.
12
+ Thankfully, the Strategy Pattern as per the [Gang of Four book](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612) solves the problem by externalizing logic to
8
13
  separate classes outside the domain models.
9
14
 
10
- Still, there are a number of challenges with repeated implementation of Strategy Pattern:
15
+ Still, there are a number of challenges with "repeated implementation" of the Strategy Pattern:
11
16
  - Making domain models aware of newly added strategies without touching their
12
17
  code (Open/Closed Principle).
13
- - Fetching the right strategy without use of conditionals.
18
+ - Fetching the right strategy without the use of conditionals.
14
19
  - Avoiding duplication of strategy dispatch code for multiple domain models
15
- - Have different strategies mirror an existing domain model hierarchy
20
+ - Have strategies mirror an existing domain model inheritance hierarchy
16
21
 
17
22
  `strategic` solves these problems by offering:
18
23
  - Strategy Pattern support through a Ruby mixin and strategy path/name convention
@@ -21,56 +26,16 @@ code (Open/Closed Principle).
21
26
  - Ability to fetch a strategy by name or by object type to mirror
22
27
  - Plain Ruby and Ruby on Rails support
23
28
 
24
- `strategic` enables you to make any existing domain model "strategic",
29
+ `Strategic` enables you to make any existing domain model "strategic",
25
30
  externalizing all logic concerning algorithmic variations into separate strategy
26
- classes that are easy to find, maintain and extend.
27
-
28
- ## Instructions
29
-
30
- ### Option 1: Bundler
31
-
32
- Add the following to bundler's `Gemfile`.
33
-
34
- ```ruby
35
- gem 'strategic', '~> 0.8.0'
36
- ```
37
-
38
- ### Option 2: Manual
39
-
40
- Or manually install and require library.
41
-
42
- ```bash
43
- gem install strategic -v0.8.0
44
- ```
45
-
46
- ```ruby
47
- require 'strategic'
48
- ```
49
-
50
- ### Usage
51
-
52
- Steps:
53
- 1. Have the original class you'd like to strategize include Strategic
54
- 2. Create a directory matching the class underscored file name minus the '.rb' extension
55
- 3. Create a strategy class under that directory, which:
56
- - Lives under the original class namespace
57
- - Extends the original class to strategize
58
- - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
59
- 4. Get needed strategy class using `strategy_class_for` class method taking strategy name (any case) or related object/type (can call `strategy_names` class method to obtain strategy names)
60
- 5. Instantiate strategy with needed constructor parameters
61
- 6. Invoke strategy method needed
62
-
63
- Alternative approach:
64
-
65
- Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
66
- and constructor parameters
67
-
68
- Passing an invalid strategy name to `strategy_class_for` returns original class as the default
69
- strategy.
31
+ classes that are easy to find, maintain and extend while honoring the Open/Closed Principle.
70
32
 
71
33
  ### Example
72
34
 
73
- 1. Class to strategize is: `TaxCalculator`
35
+ <img src="strategic-example.png"
36
+ alt="Strategic Example" />
37
+
38
+ 1. Include `Strategic` module in the Class to strategize: `TaxCalculator`
74
39
 
75
40
  ```ruby
76
41
  class TaxCalculator
@@ -82,9 +47,9 @@ class TaxCalculator
82
47
  end
83
48
  ```
84
49
 
85
- 2. Directory to create strategies under: `tax_calculator`
50
+ 2. Now, you can add strategies under this directory without having to modify the original class: `tax_calculator`
86
51
 
87
- 3. Strategy class:
52
+ 3. Add strategy classes under the namespace matching the original class name (`TaxCalculator`) and extending the original class (`TaxCalculator`) just to take advantage of default logic in it:
88
53
 
89
54
  ```ruby
90
55
  class TaxCalculator::UsStrategy < TaxCalculator
@@ -108,32 +73,32 @@ class TaxCalculator::CanadaStrategy < TaxCalculator
108
73
  end
109
74
  ```
110
75
 
111
- 4. Get needed strategy:
76
+ 4. In client code, obtain the needed strategy by underscored string reference minus the word strategy (e.g. UsStrategy becomes simply 'us'):
112
77
 
113
78
  ```ruby
114
79
  tax_calculator_strategy_class = TaxCalculator.strategy_class_for('us')
115
80
  ```
116
81
 
117
- 5. Instantiate strategy:
82
+ 5. Instantiate the strategy object:
118
83
 
119
84
  ```ruby
120
85
  tax_calculator_strategy = strategy_class.new('IL')
121
86
  ```
122
87
 
123
- 6. Invoke strategy method:
88
+ 6. Invoke the strategy overridden method:
124
89
 
125
90
  ```ruby
126
91
  tax = tax_calculator_strategy.tax_for(39.78)
127
92
  ```
128
93
 
129
- **Alternative approach using `new_strategy`:**
94
+ **Alternative approach using `new_strategy(strategy_name, *initializer_args)`:**
130
95
 
131
96
  ```ruby
132
97
  tax_calculator_strategy = TaxCalculator.new_strategy('US', 'IL')
133
98
  tax = tax_calculator_strategy.tax_for(39.78)
134
99
  ```
135
100
 
136
- **Default strategy for a strategy name that has no strategy class is TaxCalculator**
101
+ **Default strategy for a strategy name that has no strategy class is the superclass: `TaxCalculator`**
137
102
 
138
103
  ```ruby
139
104
  tax_calculator_strategy_class = TaxCalculator.strategy_class_for('France')
@@ -141,13 +106,66 @@ tax_calculator_strategy = tax_calculator_strategy_class.new
141
106
  tax = tax_calculator_strategy.tax_for(100.0) # returns 9.0 from TaxCalculator
142
107
  ```
143
108
 
144
- ## Release Notes
109
+ ## Setup
145
110
 
146
- **0.8.0:** Initial version with `strategy_class_for`, `new_strategy`, `strategies`, and `strategy_names`
111
+ ### Option 1: Bundler
112
+
113
+ Add the following to bundler's `Gemfile`.
114
+
115
+ ```ruby
116
+ gem 'strategic', '~> 0.9.0'
117
+ ```
118
+
119
+ ### Option 2: Manual
120
+
121
+ Or manually install and require library.
122
+
123
+ ```bash
124
+ gem install strategic -v0.9.0
125
+ ```
126
+
127
+ ```ruby
128
+ require 'strategic'
129
+ ```
130
+
131
+ ### Usage
132
+
133
+ Steps:
134
+ 1. Have the original class you'd like to strategize include Strategic
135
+ 2. Create a directory matching the class underscored file name minus the '.rb' extension
136
+ 3. Create a strategy class under that directory, which:
137
+ - Lives under the original class namespace
138
+ - Extends the original class to strategize
139
+ - Has a class name that ends with `Strategy` suffix (e.g. `NewCustomerStrategy`)
140
+ 4. Get needed strategy class using `strategy_class_for` class method taking strategy name (any case) or related object/type (can call `strategy_names` class method to obtain strategy names)
141
+ 5. Instantiate strategy with needed constructor parameters
142
+ 6. Invoke strategy method needed
143
+
144
+ Alternative approach:
145
+
146
+ Combine steps 4 and 5 using `new_strategy` method, which takes both strategy name
147
+ and constructor parameters
148
+
149
+ Passing an invalid strategy name to `strategy_class_for` returns original class as the default
150
+ strategy.
151
+
152
+ ## API
153
+
154
+ - `StrategicSuperClass::strategy_class_for(string_or_class_or_object)`: selects a strategy class based on a string (e.g. 'us' selects USStrategy) or alternatively a class/object if you have a mirror hierarchy for the strategy hierarchy
155
+ - `StrategicSuperClass::new_strategy(string_or_class_or_object, *args, &block)`: instantiates a strategy based on a string/class/object and strategy constructor args
156
+ - `StrategicSuperClass::strategies`: returns list of strategies discovered by convention (nested under a namespace matching the superclass name)
157
+ - `StrategicSuperClass::strategy_names`: returns list of strategy names (strings) discovered by convention (nested under a namespace matching the superclass name)
158
+ - `StrategicSuperClass::strategy_matcher`: custom match (e.g. `strategy_matcher {|string| string.start_with?('C') && string.end_with?('o')}`)
159
+ - `StrategicSuperClass::strategy_exclusion`: exclusion from custom matcher (e.g. `strategy_exclusion 'Cio'`)
160
+ - `StrategicSuperClass::strategy_alias`: alias for strategy in addition to strategy's name derived from class name by convention (e.g. `strategy_alias 'USA'` for `UsStrategy`)
147
161
 
148
162
  ## TODO
149
163
 
150
- None
164
+ [TODO.md](TODO.md)
165
+
166
+ ## Change Log
167
+
168
+ [CHANGELOG.md](CHANGELOG.md)
151
169
 
152
170
  ## Contributing
153
171
 
@@ -161,7 +179,8 @@ None
161
179
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
162
180
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
163
181
 
164
- ## Copyright
182
+ ## License
183
+
184
+ [MIT](LICENSE.txt)
165
185
 
166
- Copyright (c) 2020 Andy Maleh. See LICENSE.txt for
167
- further details.
186
+ Copyright (c) 2020-2021 Andy Maleh.
data/lib/strategic.rb CHANGED
@@ -1,3 +1,24 @@
1
+ # Copyright (c) 2020-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
1
22
  module Strategic
2
23
  def self.included(klass)
3
24
  klass.extend(ClassMethods)
@@ -5,6 +26,30 @@ module Strategic
5
26
  end
6
27
 
7
28
  module ClassMethods
29
+ def strategy_alias(alias_string_or_class_or_object)
30
+ strategy_aliases << alias_string_or_class_or_object
31
+ end
32
+
33
+ def strategy_aliases
34
+ @strategy_aliases ||= []
35
+ end
36
+
37
+ def strategy_exclusion(exclusion_string_or_class_or_object)
38
+ strategy_exclusions << exclusion_string_or_class_or_object
39
+ end
40
+
41
+ def strategy_exclusions
42
+ @strategy_exclusions ||= []
43
+ end
44
+
45
+ def strategy_matcher(&matcher_block)
46
+ if block_given?
47
+ @strategy_matcher = matcher_block
48
+ else
49
+ @strategy_matcher
50
+ end
51
+ end
52
+
8
53
  def require_strategies
9
54
  klass_path = caller[1].split(':').first
10
55
  strategy_path = File.expand_path(File.join(klass_path, '..', Strategic.underscore(self.name), '**', '*.rb'))
@@ -14,17 +59,30 @@ module Strategic
14
59
  end
15
60
 
16
61
  def strategy_class_for(string_or_class_or_object)
17
- if string_or_class_or_object.is_a?(String)
18
- strategy_class_name = string_or_class_or_object.downcase
19
- elsif string_or_class_or_object.is_a?(Class)
20
- strategy_class_name = string_or_class_or_object.name
62
+ strategy_class = nil
63
+ if strategy_matcher
64
+ strategy_class = strategies.detect do |strategy|
65
+ match = strategy&.strategy_matcher&.call(string_or_class_or_object)
66
+ match ||= strategy.instance_exec(string_or_class_or_object, &strategy_matcher)
67
+ match unless strategy.strategy_exclusions.include?(string_or_class_or_object)
68
+ end
21
69
  else
22
- strategy_class_name = string_or_class_or_object.class.name
70
+ if string_or_class_or_object.is_a?(String)
71
+ strategy_class_name = string_or_class_or_object.downcase
72
+ elsif string_or_class_or_object.is_a?(Class)
73
+ strategy_class_name = string_or_class_or_object.name
74
+ else
75
+ strategy_class_name = string_or_class_or_object.class.name
76
+ end
77
+ begin
78
+ class_name = "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
79
+ strategy_class = class_eval(class_name)
80
+ rescue NameError
81
+ # No Op
82
+ end
23
83
  end
24
- class_name ||= "::#{self.name}::#{Strategic.classify(strategy_class_name)}Strategy"
25
- class_eval(class_name)
26
- rescue NameError
27
- self
84
+ strategy_class ||= strategies.detect { |strategy| strategy.strategy_aliases.include?(string_or_class_or_object) }
85
+ strategy_class ||= self
28
86
  end
29
87
 
30
88
  def new_strategy(string_or_class_or_object, *args, &block)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strategic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-27 00:00:00.000000000 Z
11
+ date: 2021-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -56,14 +56,14 @@ dependencies:
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '1.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
@@ -86,42 +86,56 @@ dependencies:
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 0.8.5
89
+ version: 0.8.23
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - '='
95
95
  - !ruby/object:Gem::Version
96
- version: 0.8.5
96
+ version: 0.8.23
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.10.0
103
+ version: 0.16.1
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.10.0
110
+ version: 0.16.1
111
111
  - !ruby/object:Gem::Dependency
112
- name: puts_debuggerer
112
+ name: simplecov-lcov
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 0.8.0
117
+ version: 0.7.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 0.8.0
124
+ version: 0.7.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: puts_debuggerer
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 0.8.1
125
139
  description: |
126
140
  if/case conditionals can get really hairy in highly sophisticated business domains.
127
141
  Domain model inheritance can help remedy the problem, but you don't want to dump all
@@ -139,9 +153,11 @@ email: andy.am@gmail.com
139
153
  executables: []
140
154
  extensions: []
141
155
  extra_rdoc_files:
156
+ - CHANGELOG.md
142
157
  - LICENSE.txt
143
158
  - README.md
144
159
  files:
160
+ - CHANGELOG.md
145
161
  - LICENSE.txt
146
162
  - README.md
147
163
  - lib/strategic.rb
@@ -149,7 +165,7 @@ homepage: http://github.com/AndyObtiva/strategic
149
165
  licenses:
150
166
  - MIT
151
167
  metadata: {}
152
- post_install_message:
168
+ post_install_message:
153
169
  rdoc_options: []
154
170
  require_paths:
155
171
  - lib
@@ -164,9 +180,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
180
  - !ruby/object:Gem::Version
165
181
  version: '0'
166
182
  requirements: []
167
- rubyforge_project:
168
- rubygems_version: 2.6.10
169
- signing_key:
183
+ rubygems_version: 3.2.3
184
+ signing_key:
170
185
  specification_version: 4
171
186
  summary: Painless Strategy Pattern for Ruby and Rails
172
187
  test_files: []