state_pattern 2.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +276 -0
- data/examples/rails_2_3_8_button_example/config/environment.rb +1 -1
- data/examples/rails_2_3_8_button_example/db/development.sqlite3 +0 -0
- data/examples/rails_3_button_example/Gemfile +1 -1
- data/examples/rails_3_button_example/Gemfile.lock +2 -2
- data/examples/rails_3_button_example/db/development.sqlite3 +0 -0
- data/examples/rails_3_button_example/log/development.log +95 -1600
- data/lib/state_pattern/active_record.rb +1 -1
- data/test/state_pattern/active_record/test_helper.rb +0 -2
- metadata +10 -25
- data/README.rdoc +0 -264
- data/examples/rails_3_button_example/db/test.sqlite3 +0 -0
data/README.md
ADDED
@@ -0,0 +1,276 @@
|
|
1
|
+
[![Build Status](https://secure.travis-ci.org/dcadenas/state_pattern.png?branch=master)](http://travis-ci.org/dcadenas/state_pattern)
|
2
|
+
|
3
|
+
# state_pattern
|
4
|
+
|
5
|
+
A Ruby state pattern implementation.
|
6
|
+
|
7
|
+
This library intentionally follows the classic state pattern implementation (no mixins, classical delegation to simple state classes, etc.) believing that it increases flexibility (internal DSL constraints vs plain object oriented Ruby power), simplicity and clarity.
|
8
|
+
|
9
|
+
The gem is ready for Rails active record integration (see below and the examples folder).
|
10
|
+
|
11
|
+
## Usage and functionality summary
|
12
|
+
|
13
|
+
* Define the set of states you want your stateful object to have by creating a class for each state and inheriting from `StatePattern:State`.
|
14
|
+
* All public methods defined in this state classes, except `enter` and `exit` (see below), are then available to the stateful object and their behaviour will depend on the current state .
|
15
|
+
* If this automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.
|
16
|
+
* Inside each state instance you can access the stateful object through the +stateful+ method.
|
17
|
+
* Inside each state instance you can access the previous state through the +previous_state+ method.
|
18
|
+
* Define `enter` or `exit` methods to hook any behaviour you want to execute whenever the stateful object enters or exits the state.
|
19
|
+
* An event is just a method that calls `transition_to` at some point.
|
20
|
+
* If you want guards for some event just use plain old ifs before your `transition_to`.
|
21
|
+
* In the stateful object you must `set_initial_state`.
|
22
|
+
|
23
|
+
## Examples
|
24
|
+
|
25
|
+
So here's a simple example that mimics a traffic semaphore
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'state_pattern'
|
29
|
+
|
30
|
+
class Stop < StatePattern::State
|
31
|
+
def next
|
32
|
+
sleep 3
|
33
|
+
transition_to(Go)
|
34
|
+
end
|
35
|
+
|
36
|
+
def color
|
37
|
+
"Red"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Go < StatePattern::State
|
42
|
+
def next
|
43
|
+
sleep 2
|
44
|
+
transition_to(Caution)
|
45
|
+
end
|
46
|
+
|
47
|
+
def color
|
48
|
+
"Green"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Caution < StatePattern::State
|
53
|
+
def next
|
54
|
+
sleep 1
|
55
|
+
transition_to(Stop)
|
56
|
+
end
|
57
|
+
|
58
|
+
def color
|
59
|
+
"Amber"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class TrafficSemaphore
|
64
|
+
include StatePattern
|
65
|
+
set_initial_state Stop
|
66
|
+
end
|
67
|
+
|
68
|
+
semaphore = TrafficSemaphore.new
|
69
|
+
|
70
|
+
loop do
|
71
|
+
puts semaphore.color
|
72
|
+
semaphore.next
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Let's now use one nice example from the AASM documentation and translate it to state_pattern.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
require 'state_pattern'
|
80
|
+
|
81
|
+
class Dating < StatePattern::State
|
82
|
+
def get_intimate
|
83
|
+
transition_to(Intimate) if stateful.drunk?
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_married
|
87
|
+
transition_to(Married) if stateful.willing_to_give_up_manhood?
|
88
|
+
end
|
89
|
+
|
90
|
+
def enter
|
91
|
+
stateful.make_happy
|
92
|
+
end
|
93
|
+
|
94
|
+
def exit
|
95
|
+
stateful.make_depressed
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Intimate < StatePattern::State
|
100
|
+
def get_married
|
101
|
+
transition_to(Married) if stateful.willing_to_give_up_manhood?
|
102
|
+
end
|
103
|
+
|
104
|
+
def enter
|
105
|
+
stateful.make_very_happy
|
106
|
+
end
|
107
|
+
|
108
|
+
def exit
|
109
|
+
stateful.never_speak_again
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Married < StatePattern::State
|
114
|
+
def enter
|
115
|
+
stateful.give_up_intimacy
|
116
|
+
end
|
117
|
+
|
118
|
+
def exit
|
119
|
+
stateful.buy_exotic_car_and_wear_a_combover
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Relationship
|
124
|
+
include StatePattern
|
125
|
+
set_initial_state Dating
|
126
|
+
|
127
|
+
def drunk?; @drunk; end
|
128
|
+
def willing_to_give_up_manhood?; @give_up_manhood; end
|
129
|
+
def make_happy; end
|
130
|
+
def make_depressed; end
|
131
|
+
def make_very_happy; end
|
132
|
+
def never_speak_again; end
|
133
|
+
def give_up_intimacy; end
|
134
|
+
def buy_exotic_car_and_wear_a_combover; end
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
## Enter and exit hooks
|
139
|
+
|
140
|
+
Inside your state classes, any code that you put inside the enter method will be executed when the state is instantiated.
|
141
|
+
You can also use the exit hook which is triggered when a successful transition to another state takes place.
|
142
|
+
|
143
|
+
## Overriding automatic delegation
|
144
|
+
|
145
|
+
If the automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class TrafficSemaphore
|
149
|
+
include StatePattern
|
150
|
+
set_initial_state Stop
|
151
|
+
|
152
|
+
def color
|
153
|
+
# some great code here
|
154
|
+
# now we call the current state implementation
|
155
|
+
super
|
156
|
+
# more cool hacking here
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
## Rails
|
162
|
+
|
163
|
+
To use the state pattern in your Rails models you need to:
|
164
|
+
|
165
|
+
* Add a state column for your model table of type string
|
166
|
+
* Include StatePattern::ActiveRecord in your model file
|
167
|
+
* Use the state pattern as you would do in a plain Ruby class as shown above
|
168
|
+
|
169
|
+
Please see the examples folder for a Rails 3 example.
|
170
|
+
|
171
|
+
### Example
|
172
|
+
|
173
|
+
Remember to put each class in its correct file following Rails naming conventions.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
module BlogStates
|
177
|
+
#we can put common state behaviour into a base state class or we could have implemented it inside the model with methods that call super, your choice
|
178
|
+
class StateBase < StatePattern::State
|
179
|
+
def submit!
|
180
|
+
end
|
181
|
+
|
182
|
+
def publish!
|
183
|
+
end
|
184
|
+
|
185
|
+
def reject!
|
186
|
+
transition_to(Rejected)
|
187
|
+
stateful.save!
|
188
|
+
end
|
189
|
+
|
190
|
+
def verify!
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class Published < StateBase
|
195
|
+
end
|
196
|
+
|
197
|
+
class Pending < StateBase
|
198
|
+
def publish!
|
199
|
+
transition_to(Published) if stateful.valid?
|
200
|
+
stateful.save!
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class Unverified < StateBase
|
205
|
+
def submit!
|
206
|
+
if stateful.submitter.manager?
|
207
|
+
if stateful.profile_complete?
|
208
|
+
transition_to(Published)
|
209
|
+
else
|
210
|
+
transition_to(Pending)
|
211
|
+
end
|
212
|
+
|
213
|
+
stateful.save!
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def verify!
|
218
|
+
transition_to(Pending)
|
219
|
+
stateful.save!
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
class Rejected < StateBase
|
224
|
+
def publish!
|
225
|
+
transition_to(Published) if stateful.valid?
|
226
|
+
stateful.save!
|
227
|
+
end
|
228
|
+
|
229
|
+
def enter
|
230
|
+
Notifier.notify_blog_owner(stateful)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class Blog < ActiveRecord::Base
|
236
|
+
include StatePattern::ActiveRecord
|
237
|
+
set_initial_state Unverified
|
238
|
+
|
239
|
+
.
|
240
|
+
.
|
241
|
+
.
|
242
|
+
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
### The state attribute
|
247
|
+
|
248
|
+
By default `StatePattern::ActiveRecord` expects a column named `state` in the model. If you prefer to use another attribute do:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
set_state_attribute :state_column
|
252
|
+
```
|
253
|
+
|
254
|
+
### How do I decide? state_pattern or [AASM](http://github.com/rubyist/aasm)?
|
255
|
+
|
256
|
+
* Lot of state dependent behavior? Lot of conditional logic depending on the state? => state_pattern
|
257
|
+
* Not much state dependent behavior? => AASM
|
258
|
+
|
259
|
+
## Thanks
|
260
|
+
|
261
|
+
* [Alvaro Gil](http://github.com/zevarito) for being the first using this gem in a real Rails project.
|
262
|
+
* [Nicolás Sanguinetti](http://github.com/foca) for his great feedback.
|
263
|
+
|
264
|
+
## Installation
|
265
|
+
|
266
|
+
```bash
|
267
|
+
gem install state_pattern
|
268
|
+
```
|
269
|
+
|
270
|
+
## Collaborate
|
271
|
+
|
272
|
+
[http://github.com/dcadenas/state_pattern](http://github.com/dcadenas/state_pattern)
|
273
|
+
|
274
|
+
## Copyright
|
275
|
+
|
276
|
+
Copyright (c) 2009 Daniel Cadenas. See LICENSE for details.
|
@@ -15,7 +15,7 @@ Rails::Initializer.run do |config|
|
|
15
15
|
# config.load_paths += %W( #{RAILS_ROOT}/extras )
|
16
16
|
|
17
17
|
# Specify gems that this application depends on and have them installed with rake gems:install
|
18
|
-
config.gem "state_pattern"
|
18
|
+
config.gem "state_pattern", :version => "~>2.0.0"
|
19
19
|
# config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
|
20
20
|
# config.gem "sqlite3-ruby", :lib => "sqlite3"
|
21
21
|
# config.gem "aws-s3", :lib => "aws/s3"
|
Binary file
|
@@ -60,7 +60,7 @@ GEM
|
|
60
60
|
thor (~> 0.14.0)
|
61
61
|
rake (0.8.7)
|
62
62
|
sqlite3-ruby (1.3.1)
|
63
|
-
state_pattern (
|
63
|
+
state_pattern (2.0.0)
|
64
64
|
thor (0.14.3)
|
65
65
|
treetop (1.4.8)
|
66
66
|
polyglot (>= 0.3.1)
|
@@ -72,4 +72,4 @@ PLATFORMS
|
|
72
72
|
DEPENDENCIES
|
73
73
|
rails (= 3.0.0)
|
74
74
|
sqlite3-ruby
|
75
|
-
state_pattern (
|
75
|
+
state_pattern (~> 2.0.0)
|
Binary file
|