state_design_pattern 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +4 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.txt +22 -0
- data/README.md +140 -0
- data/Rakefile +9 -0
- data/examples/case_changer.rb +45 -0
- data/examples/ceiling_fan.rb +55 -0
- data/examples/cursor.rb +83 -0
- data/examples/fsm.rb +67 -0
- data/lib/state_design_pattern/base_state.rb +29 -0
- data/lib/state_design_pattern/state_machine.rb +74 -0
- data/lib/state_design_pattern/version.rb +3 -0
- data/lib/state_design_pattern.rb +2 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/state_design_pattern_spec.rb +314 -0
- data/state_design_pattern.gemspec +27 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MzlmYTVlZmM1OTNmMjk4ODU4NTE2MmFlMzk1NDFhNTI1MjBhOTUzMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NGFmMWNkZTZiZjk4OGExOTMwYjlmMjQ2MmVhZmM0NGY5MjgwMGFlZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MTlmZTQ2MjAwZmU0YTUxYTBlMWEwZGQ4NmU2ZjBiNzdiYmUzNzM1YmI1ZjFj
|
10
|
+
OGJlMWY5YjRkMGE3ZTJhZjMzZjk3NDUzZjNlOGU1ZjVlYmQ4MDZmNzAxYWU4
|
11
|
+
YTFhNGRkYjlkYTEwMTJmNzIxNDBkZGNkMjBlNDU5YWRjZGJiZDM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MGM0OGNmYWQxNGUyNzUxMjk5MDI5NWEyYzkzNTRiMDNlZDljYjU2MTRiZWFh
|
14
|
+
OGRhMTNjN2ZhYzU4MTZhMmQ2MTM3YmEzYmMxNDBjNzY5NTU5YjhmZTc2NGQ4
|
15
|
+
YjllMTFlMDE3ODcwOWEwZTRjNjg2OGVlYjY5NjkwNmE1NWRhY2E=
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
state_design_pattern (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
coveralls (0.7.0)
|
10
|
+
multi_json (~> 1.3)
|
11
|
+
rest-client
|
12
|
+
simplecov (>= 0.7)
|
13
|
+
term-ansicolor
|
14
|
+
thor
|
15
|
+
docile (1.1.3)
|
16
|
+
mime-types (2.2)
|
17
|
+
minitest (5.3.4)
|
18
|
+
multi_json (1.10.1)
|
19
|
+
rake (10.3.2)
|
20
|
+
rest-client (1.6.7)
|
21
|
+
mime-types (>= 1.16)
|
22
|
+
simplecov (0.8.2)
|
23
|
+
docile (~> 1.1.0)
|
24
|
+
multi_json
|
25
|
+
simplecov-html (~> 0.8.0)
|
26
|
+
simplecov-html (0.8.0)
|
27
|
+
term-ansicolor (1.3.0)
|
28
|
+
tins (~> 1.0)
|
29
|
+
thor (0.19.1)
|
30
|
+
tins (1.3.0)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
coveralls (~> 0.7)
|
37
|
+
minitest (~> 5.3)
|
38
|
+
rake (~> 10.3)
|
39
|
+
state_design_pattern!
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Dwayne R. Crooks
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# state_design_pattern
|
2
|
+
|
3
|
+
[](https://travis-ci.org/dwayne/state_design_pattern)
|
4
|
+
[](https://coveralls.io/r/dwayne/state_design_pattern)
|
5
|
+
[](https://codeclimate.com/github/dwayne/state_design_pattern)
|
6
|
+
|
7
|
+
An implementation of the
|
8
|
+
[State Design Pattern](http://sourcemaking.com/design_patterns/state) in Ruby.
|
9
|
+
The State Design Pattern allows an object to alter its behavior when its
|
10
|
+
internal state changes.
|
11
|
+
|
12
|
+
## Example
|
13
|
+
|
14
|
+
Here's an example of the state design pattern in use. A light bulb is modeled.
|
15
|
+
It can be turned on and off. Every time it is turned on it uses 25% of its
|
16
|
+
energy. When it runs out of energy it cannot be turned on again.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class LightBulb < StateDesignPattern::StateMachine
|
20
|
+
def start_state
|
21
|
+
Off
|
22
|
+
end
|
23
|
+
|
24
|
+
def initial_context
|
25
|
+
Struct
|
26
|
+
.new(:energy, :times_on, :times_off)
|
27
|
+
.new(100, 0, 0)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Switch < StateDesignPattern::BaseState
|
32
|
+
def_actions :turn_on, :turn_off
|
33
|
+
end
|
34
|
+
|
35
|
+
class On < Switch
|
36
|
+
|
37
|
+
def turn_on
|
38
|
+
state_machine.send_event(:already_turned_on)
|
39
|
+
end
|
40
|
+
|
41
|
+
def turn_off
|
42
|
+
state_machine.times_off += 1
|
43
|
+
state_machine.transition_to_state_and_send_event(Off, :turned_off)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Off < Switch
|
48
|
+
|
49
|
+
def turn_on
|
50
|
+
if state_machine.energy >= 25
|
51
|
+
state_machine.energy -= 25
|
52
|
+
state_machine.times_on += 1
|
53
|
+
state_machine.transition_to_state_and_send_event(On, :turned_on)
|
54
|
+
else
|
55
|
+
state_machine.send_event(:out_of_energy)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def turn_off
|
60
|
+
state_machine.send_event(:already_turned_off)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class LightBulbObserver
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
@events = []
|
68
|
+
end
|
69
|
+
|
70
|
+
def last_event
|
71
|
+
@events.last
|
72
|
+
end
|
73
|
+
|
74
|
+
def update(event)
|
75
|
+
@events << event
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
light_bulb = LightBulb.new
|
80
|
+
light_bulb_observer = LightBulbObserver.new
|
81
|
+
|
82
|
+
light_bulb.add_observer(light_bulb_observer)
|
83
|
+
|
84
|
+
light_bulb.current_state #=> Off
|
85
|
+
|
86
|
+
light_bulb.energy #=> 100
|
87
|
+
light_bulb.times_on #=> 0
|
88
|
+
light_bulb.times_off #=> 0
|
89
|
+
|
90
|
+
light_bulb.turn_on
|
91
|
+
light_bulb_observer.last_event[:name] #=> :turned_on
|
92
|
+
|
93
|
+
light_bulb.current_state #=> On
|
94
|
+
|
95
|
+
light_bulb.energy #=> 75
|
96
|
+
light_bulb.times_on #=> 1
|
97
|
+
light_bulb.times_off #=> 0
|
98
|
+
|
99
|
+
light_bulb.turn_off
|
100
|
+
light_bulb.turn_on
|
101
|
+
|
102
|
+
light_bulb.turn_off
|
103
|
+
light_bulb.turn_on
|
104
|
+
|
105
|
+
light_bulb.turn_off
|
106
|
+
light_bulb.turn_on
|
107
|
+
|
108
|
+
light_bulb.energy #=> 0
|
109
|
+
light_bulb.times_on #=> 4
|
110
|
+
light_bulb.times_off #=> 3
|
111
|
+
|
112
|
+
light_bulb.turn_off
|
113
|
+
light_bulb.turn_on
|
114
|
+
|
115
|
+
light_bulb_observer.last_event[:name] #=> :out_of_energy
|
116
|
+
```
|
117
|
+
|
118
|
+
## Testing
|
119
|
+
|
120
|
+
You can run:
|
121
|
+
|
122
|
+
- All specs: `bundle exec rake`, or
|
123
|
+
- A specific spec: `bundle exec ruby -Ilib -Ispec spec/path_to_spec_file.rb`
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
If you'd like to contribute a feature or bugfix: Thanks! To make sure your
|
128
|
+
fix/feature has a high chance of being included, please read the following
|
129
|
+
guidelines:
|
130
|
+
|
131
|
+
1. Post a [pull request](https://github.com/dwayne/state_design_pattern/compare/).
|
132
|
+
2. Make sure there are tests! I will not accept any patch that is not tested.
|
133
|
+
It's a rare time when explicit tests aren't needed. If you have questions about
|
134
|
+
writing tests for state_pattern, please open a
|
135
|
+
[GitHub issue](https://github.com/dwayne/state_design_pattern/issues/new).
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
state_design_pattern is Copyright © 2014 Dwayne R. Crooks. It is free software,
|
140
|
+
and may be redistributed under the terms specified in the MIT-LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# See http://en.wikipedia.org/wiki/State_pattern#Java
|
4
|
+
|
5
|
+
require 'state_design_pattern'
|
6
|
+
|
7
|
+
class CaseChanger < StateDesignPattern::StateMachine
|
8
|
+
def start_state
|
9
|
+
Lowercase
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class CaseChangerState < StateDesignPattern::BaseState
|
14
|
+
def_actions :write
|
15
|
+
end
|
16
|
+
|
17
|
+
class Lowercase < CaseChangerState
|
18
|
+
def write(name)
|
19
|
+
puts name.downcase
|
20
|
+
state_machine.transition_to_state(MultipleUppercase)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class MultipleUppercase < CaseChangerState
|
25
|
+
def write(name)
|
26
|
+
puts name.upcase
|
27
|
+
state_machine.transition_to_state(Lowercase) if incr_count > 1
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def incr_count
|
32
|
+
@count = 0 unless @count
|
33
|
+
@count += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def main
|
38
|
+
changer = CaseChanger.new
|
39
|
+
|
40
|
+
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].each do |day|
|
41
|
+
changer.write(day)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
main
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# See http://sourcemaking.com/design_patterns/state/java/1
|
4
|
+
|
5
|
+
require 'state_design_pattern'
|
6
|
+
|
7
|
+
class CeilingFanPullChain < StateDesignPattern::StateMachine
|
8
|
+
def start_state
|
9
|
+
Off
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class CeilingFanState < StateDesignPattern::BaseState
|
14
|
+
def_actions :pull
|
15
|
+
end
|
16
|
+
|
17
|
+
class Off < CeilingFanState
|
18
|
+
def pull
|
19
|
+
puts "-> low speed"
|
20
|
+
state_machine.transition_to_state(Low)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Low < CeilingFanState
|
25
|
+
def pull
|
26
|
+
puts "-> medium speed"
|
27
|
+
state_machine.transition_to_state(Medium)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Medium < CeilingFanState
|
32
|
+
def pull
|
33
|
+
puts "-> high speed"
|
34
|
+
state_machine.transition_to_state(High)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class High < CeilingFanState
|
39
|
+
def pull
|
40
|
+
puts "-> turning off"
|
41
|
+
state_machine.transition_to_state(Off)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def main
|
46
|
+
chain = CeilingFanPullChain.new
|
47
|
+
|
48
|
+
loop do
|
49
|
+
print "Press "
|
50
|
+
gets.chomp
|
51
|
+
chain.pull
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
main
|
data/examples/cursor.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# See http://en.wikipedia.org/wiki/State_pattern#Pseudocode
|
4
|
+
|
5
|
+
require 'state_design_pattern'
|
6
|
+
|
7
|
+
class Cursor < StateDesignPattern::StateMachine
|
8
|
+
def start_state
|
9
|
+
PenTool
|
10
|
+
end
|
11
|
+
|
12
|
+
def use_pen_tool
|
13
|
+
transition_to_state(PenTool)
|
14
|
+
end
|
15
|
+
|
16
|
+
def use_selection_tool
|
17
|
+
transition_to_state(SelectionTool)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class AbstractTool < StateDesignPattern::BaseState
|
22
|
+
def_actions :move_to, :mouse_down, :mouse_up
|
23
|
+
end
|
24
|
+
|
25
|
+
class PenTool < AbstractTool
|
26
|
+
def move_to(point)
|
27
|
+
puts "pen: moving to #{point}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def mouse_down(point)
|
31
|
+
puts "pen: mouse down at #{point}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def mouse_up(point)
|
35
|
+
puts "pen: mouse up at #{point}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class SelectionTool < AbstractTool
|
40
|
+
def move_to(point)
|
41
|
+
if @mouse_button == :down
|
42
|
+
puts "selection: current selected rectangle is between #{@selection_start} and #{point}"
|
43
|
+
else
|
44
|
+
puts "selection: moving to #{point}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def mouse_down(point)
|
49
|
+
@mouse_button = :down
|
50
|
+
@selection_start = point
|
51
|
+
|
52
|
+
puts "selection: mouse down at #{point}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def mouse_up(point)
|
56
|
+
@mouse_button = :up
|
57
|
+
|
58
|
+
puts "selection: mouse up at #{point}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Point < Struct.new(:x, :y)
|
63
|
+
end
|
64
|
+
|
65
|
+
def main
|
66
|
+
cursor = Cursor.new
|
67
|
+
|
68
|
+
# Draw a line from (1, 1) to (5, 5)
|
69
|
+
cursor.use_pen_tool
|
70
|
+
cursor.move_to(Point.new(1, 1))
|
71
|
+
cursor.mouse_down(Point.new(1, 1))
|
72
|
+
cursor.move_to(Point.new(5, 5))
|
73
|
+
cursor.mouse_up(Point.new(5, 5))
|
74
|
+
|
75
|
+
# Select part of the line
|
76
|
+
cursor.use_selection_tool
|
77
|
+
cursor.move_to(Point.new(2, 2))
|
78
|
+
cursor.mouse_down(Point.new(2, 2))
|
79
|
+
cursor.move_to(Point.new(4, 4))
|
80
|
+
cursor.mouse_up(Point.new(4, 4))
|
81
|
+
end
|
82
|
+
|
83
|
+
main
|
data/examples/fsm.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# See http://sourcemaking.com/design_patterns/state/java/6
|
4
|
+
|
5
|
+
require 'state_design_pattern'
|
6
|
+
|
7
|
+
class FSM < StateDesignPattern::StateMachine
|
8
|
+
def start_state
|
9
|
+
A
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class FSMState < StateDesignPattern::BaseState
|
14
|
+
def_actions :on, :off, :ack
|
15
|
+
end
|
16
|
+
|
17
|
+
class A < FSMState
|
18
|
+
def on
|
19
|
+
puts "A + on = C"
|
20
|
+
state_machine.transition_to_state(C)
|
21
|
+
end
|
22
|
+
|
23
|
+
def off
|
24
|
+
puts "A + off = B"
|
25
|
+
state_machine.transition_to_state(B)
|
26
|
+
end
|
27
|
+
|
28
|
+
def ack
|
29
|
+
puts "A + ack = A"
|
30
|
+
state_machine.transition_to_state(A)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class B < FSMState
|
35
|
+
def on
|
36
|
+
puts "B + on = A"
|
37
|
+
state_machine.transition_to_state(A)
|
38
|
+
end
|
39
|
+
|
40
|
+
def off
|
41
|
+
puts "B + off = C"
|
42
|
+
state_machine.transition_to_state(C)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class C < FSMState
|
47
|
+
def on
|
48
|
+
puts "C + on = B"
|
49
|
+
state_machine.transition_to_state(B)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def main
|
54
|
+
fsm = FSM.new
|
55
|
+
|
56
|
+
[2, 1, 2, 1, 0, 2, 0, 0].each do |msg|
|
57
|
+
begin
|
58
|
+
fsm.on if msg == 0
|
59
|
+
fsm.off if msg == 1
|
60
|
+
fsm.ack if msg == 2
|
61
|
+
rescue StateDesignPattern::IllegalStateException
|
62
|
+
puts "error"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
main
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module StateDesignPattern
|
2
|
+
|
3
|
+
class BaseState
|
4
|
+
|
5
|
+
attr_reader :state_machine
|
6
|
+
|
7
|
+
def initialize(state_machine)
|
8
|
+
@state_machine = state_machine
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.def_actions(*actions)
|
12
|
+
define_method(:actions) do
|
13
|
+
actions
|
14
|
+
end
|
15
|
+
|
16
|
+
actions.each do |action|
|
17
|
+
def_action(action)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.def_action(action_name)
|
22
|
+
define_method(action_name) do |*args|
|
23
|
+
raise IllegalStateException
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class IllegalStateException < StandardError; end
|
29
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'observer'
|
3
|
+
|
4
|
+
module StateDesignPattern
|
5
|
+
|
6
|
+
class StateMachine
|
7
|
+
extend Forwardable
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
initialize_context
|
12
|
+
transition_to_start_state
|
13
|
+
setup_delegation
|
14
|
+
end
|
15
|
+
|
16
|
+
def initial_context
|
17
|
+
end
|
18
|
+
|
19
|
+
def start_state
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def current_state
|
24
|
+
@state.class
|
25
|
+
end
|
26
|
+
|
27
|
+
def transition_to_state_and_send_event(state_class, name, message = {})
|
28
|
+
transition_to_state(state_class)
|
29
|
+
send_event(name, message)
|
30
|
+
end
|
31
|
+
|
32
|
+
def transition_to_state(state_class)
|
33
|
+
@state = state_class.new(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_event(name, message = {})
|
37
|
+
event = { name: name, source: self }.merge(message)
|
38
|
+
|
39
|
+
changed
|
40
|
+
notify_observers(event)
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def initialize_context
|
48
|
+
@ctx = initial_context
|
49
|
+
end
|
50
|
+
|
51
|
+
def transition_to_start_state
|
52
|
+
transition_to_state(start_state)
|
53
|
+
end
|
54
|
+
|
55
|
+
def setup_delegation
|
56
|
+
setup_context_delegation
|
57
|
+
setup_action_delegation
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_context_delegation
|
61
|
+
if @ctx
|
62
|
+
methods = @ctx.members.map { |reader| [reader, "#{reader}=".to_sym] }.flatten
|
63
|
+
self.class.def_delegators :@ctx, *methods
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def setup_action_delegation
|
68
|
+
if @state.respond_to?(:actions)
|
69
|
+
methods = @state.actions
|
70
|
+
self.class.def_delegators :@state, *methods
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "the operation of the state design pattern with an example" do
|
4
|
+
|
5
|
+
TIMESTAMP = Time.now
|
6
|
+
|
7
|
+
class LightBulb < StateDesignPattern::StateMachine
|
8
|
+
def start_state
|
9
|
+
Off
|
10
|
+
end
|
11
|
+
|
12
|
+
def initial_context
|
13
|
+
Struct
|
14
|
+
.new(:energy, :times_on, :times_off)
|
15
|
+
.new(100, 0, 0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Switch < StateDesignPattern::BaseState
|
20
|
+
def_actions :turn_on, :turn_off, :toggle
|
21
|
+
end
|
22
|
+
|
23
|
+
class On < Switch
|
24
|
+
|
25
|
+
def turn_on
|
26
|
+
state_machine.send_event(:already_turned_on, when: TIMESTAMP)
|
27
|
+
end
|
28
|
+
|
29
|
+
def turn_off
|
30
|
+
state_machine.times_off += 1
|
31
|
+
state_machine.transition_to_state_and_send_event(Off, :turned_off, when: TIMESTAMP)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Off < Switch
|
36
|
+
|
37
|
+
def turn_on
|
38
|
+
if state_machine.energy >= 25
|
39
|
+
state_machine.energy -= 25
|
40
|
+
state_machine.times_on += 1
|
41
|
+
state_machine.transition_to_state_and_send_event(On, :turned_on, when: TIMESTAMP)
|
42
|
+
else
|
43
|
+
state_machine.send_event(:out_of_energy, when: TIMESTAMP)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def turn_off
|
48
|
+
state_machine.send_event(:already_turned_off, when: TIMESTAMP)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class LightBulbObserver
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@events = []
|
56
|
+
end
|
57
|
+
|
58
|
+
def last_event
|
59
|
+
@events.last
|
60
|
+
end
|
61
|
+
|
62
|
+
def update(event)
|
63
|
+
@events << event
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe LightBulb do
|
68
|
+
|
69
|
+
let(:light_bulb) { LightBulb.new }
|
70
|
+
let(:light_bulb_observer) { LightBulbObserver.new }
|
71
|
+
|
72
|
+
before do
|
73
|
+
light_bulb.add_observer(light_bulb_observer)
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "the initial state of the bulb" do
|
77
|
+
it "is off" do
|
78
|
+
light_bulb.current_state.must_equal Off
|
79
|
+
end
|
80
|
+
|
81
|
+
it "is full of energy" do
|
82
|
+
light_bulb.energy.must_equal 100
|
83
|
+
end
|
84
|
+
|
85
|
+
it "has been turned on 0 times" do
|
86
|
+
light_bulb.times_on.must_equal 0
|
87
|
+
end
|
88
|
+
|
89
|
+
it "has been turned off 0 times" do
|
90
|
+
light_bulb.times_off.must_equal 0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "the actions that can be performed on the light bulb" do
|
95
|
+
|
96
|
+
it "can be turned on" do
|
97
|
+
light_bulb.must_respond_to :turn_on
|
98
|
+
end
|
99
|
+
|
100
|
+
it "can be turned off" do
|
101
|
+
light_bulb.must_respond_to :turn_off
|
102
|
+
end
|
103
|
+
|
104
|
+
it "can be toggled" do
|
105
|
+
light_bulb.must_respond_to :toggle
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "how the actions work" do
|
110
|
+
|
111
|
+
describe "in the off state" do
|
112
|
+
|
113
|
+
describe "#turn_on" do
|
114
|
+
|
115
|
+
before do
|
116
|
+
light_bulb.turn_on
|
117
|
+
end
|
118
|
+
|
119
|
+
it "turns on the bulb" do
|
120
|
+
light_bulb.current_state.must_equal On
|
121
|
+
end
|
122
|
+
|
123
|
+
it "causes the bulb to use 25% of its energy" do
|
124
|
+
light_bulb.energy.must_equal 75
|
125
|
+
end
|
126
|
+
|
127
|
+
it "has been turned on 1 time" do
|
128
|
+
light_bulb.times_on.must_equal 1
|
129
|
+
end
|
130
|
+
|
131
|
+
it "has been turned off 0 times" do
|
132
|
+
light_bulb.times_off.must_equal 0
|
133
|
+
end
|
134
|
+
|
135
|
+
it "sends a :turned_on event" do
|
136
|
+
event = light_bulb_observer.last_event
|
137
|
+
|
138
|
+
event[:name].must_equal :turned_on
|
139
|
+
event[:source].must_equal light_bulb
|
140
|
+
event[:when].must_equal TIMESTAMP
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "#turn_off" do
|
145
|
+
|
146
|
+
before do
|
147
|
+
light_bulb.turn_off
|
148
|
+
end
|
149
|
+
|
150
|
+
it "keeps the bulb turned off" do
|
151
|
+
light_bulb.current_state.must_equal Off
|
152
|
+
end
|
153
|
+
|
154
|
+
it "doesn't use any energy" do
|
155
|
+
light_bulb.energy.must_equal 100
|
156
|
+
end
|
157
|
+
|
158
|
+
it "has been turned on 0 times" do
|
159
|
+
light_bulb.times_on.must_equal 0
|
160
|
+
end
|
161
|
+
|
162
|
+
it "has been turned off 0 times" do
|
163
|
+
light_bulb.times_off.must_equal 0
|
164
|
+
end
|
165
|
+
|
166
|
+
it "sends an :already_turned_off event" do
|
167
|
+
event = light_bulb_observer.last_event
|
168
|
+
|
169
|
+
event[:name].must_equal :already_turned_off
|
170
|
+
event[:source].must_equal light_bulb
|
171
|
+
event[:when].must_equal TIMESTAMP
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "#toggle" do
|
176
|
+
|
177
|
+
it "doesn't work" do
|
178
|
+
proc { light_bulb.toggle }.must_raise StateDesignPattern::IllegalStateException
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "in the on state" do
|
184
|
+
|
185
|
+
before do
|
186
|
+
light_bulb.turn_on
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "#turn_on" do
|
190
|
+
|
191
|
+
before do
|
192
|
+
light_bulb.turn_on
|
193
|
+
end
|
194
|
+
|
195
|
+
it "keeps the bulb turned on" do
|
196
|
+
light_bulb.current_state.must_equal On
|
197
|
+
end
|
198
|
+
|
199
|
+
it "doesn't use any energy" do
|
200
|
+
light_bulb.energy.must_equal 75
|
201
|
+
end
|
202
|
+
|
203
|
+
it "has been turned on 1 time" do
|
204
|
+
light_bulb.times_on.must_equal 1
|
205
|
+
end
|
206
|
+
|
207
|
+
it "has been turned off 0 times" do
|
208
|
+
light_bulb.times_off.must_equal 0
|
209
|
+
end
|
210
|
+
|
211
|
+
it "sends an :already_turned_on event" do
|
212
|
+
event = light_bulb_observer.last_event
|
213
|
+
|
214
|
+
event[:name].must_equal :already_turned_on
|
215
|
+
event[:source].must_equal light_bulb
|
216
|
+
event[:when].must_equal TIMESTAMP
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe "#turn_off" do
|
221
|
+
|
222
|
+
before do
|
223
|
+
light_bulb.turn_off
|
224
|
+
end
|
225
|
+
|
226
|
+
it "turns off the bulb" do
|
227
|
+
light_bulb.current_state.must_equal Off
|
228
|
+
end
|
229
|
+
|
230
|
+
it "doesn't use any energy" do
|
231
|
+
light_bulb.energy.must_equal 75
|
232
|
+
end
|
233
|
+
|
234
|
+
it "has been turned on 1 time" do
|
235
|
+
light_bulb.times_on.must_equal 1
|
236
|
+
end
|
237
|
+
|
238
|
+
it "has been turned off 1 time" do
|
239
|
+
light_bulb.times_off.must_equal 1
|
240
|
+
end
|
241
|
+
|
242
|
+
it "sends a :turned_off event" do
|
243
|
+
event = light_bulb_observer.last_event
|
244
|
+
|
245
|
+
event[:name].must_equal :turned_off
|
246
|
+
event[:source].must_equal light_bulb
|
247
|
+
event[:when].must_equal TIMESTAMP
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
describe "#toggle" do
|
252
|
+
|
253
|
+
it "doesn't work" do
|
254
|
+
proc { light_bulb.toggle }.must_raise StateDesignPattern::IllegalStateException
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe "how the bulb works when it is out of energy" do
|
261
|
+
|
262
|
+
before do
|
263
|
+
# at 100%
|
264
|
+
light_bulb.turn_on
|
265
|
+
light_bulb.turn_off
|
266
|
+
# at 75%
|
267
|
+
light_bulb.turn_on
|
268
|
+
light_bulb.turn_off
|
269
|
+
# at 50%
|
270
|
+
light_bulb.turn_on
|
271
|
+
light_bulb.turn_off
|
272
|
+
# at 25%
|
273
|
+
light_bulb.turn_on
|
274
|
+
light_bulb.turn_off
|
275
|
+
# at 0%
|
276
|
+
end
|
277
|
+
|
278
|
+
it "is off" do
|
279
|
+
light_bulb.current_state.must_equal Off
|
280
|
+
end
|
281
|
+
|
282
|
+
it "is out of energy" do
|
283
|
+
light_bulb.energy.must_equal 0
|
284
|
+
end
|
285
|
+
|
286
|
+
it "has been turned on 4 times" do
|
287
|
+
light_bulb.times_on.must_equal 4
|
288
|
+
end
|
289
|
+
|
290
|
+
it "has been turned off 4 times" do
|
291
|
+
light_bulb.times_off.must_equal 4
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "when you try to turn it on" do
|
295
|
+
|
296
|
+
before do
|
297
|
+
light_bulb.turn_on
|
298
|
+
end
|
299
|
+
|
300
|
+
it "remains off" do
|
301
|
+
light_bulb.current_state.must_equal Off
|
302
|
+
end
|
303
|
+
|
304
|
+
it "sends an :out_of_energy energy event" do
|
305
|
+
event = light_bulb_observer.last_event
|
306
|
+
|
307
|
+
event[:name].must_equal :out_of_energy
|
308
|
+
event[:source].must_equal light_bulb
|
309
|
+
event[:when].must_equal TIMESTAMP
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'state_design_pattern/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'state_design_pattern'
|
8
|
+
spec.version = StateDesignPattern::VERSION
|
9
|
+
spec.author = 'Dwayne R. Crooks'
|
10
|
+
spec.email = ['me@dwaynecrooks.com']
|
11
|
+
spec.summary = %q{An implementation of the State Design Pattern in Ruby.}
|
12
|
+
spec.description = %q{An implementation of the State Design Pattern in Ruby. The State Design Pattern allows an object to alter its behavior when its
|
13
|
+
internal state changes.}
|
14
|
+
spec.homepage = 'https://github.com/dwayne/state_design_pattern'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.required_ruby_version = '>= 1.9.3'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.3'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.3'
|
26
|
+
spec.add_development_dependency 'coveralls', '~> 0.7'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: state_design_pattern
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dwayne R. Crooks
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: coveralls
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.7'
|
55
|
+
description: ! "An implementation of the State Design Pattern in Ruby. The State Design
|
56
|
+
Pattern allows an object to alter its behavior when its\n internal state changes."
|
57
|
+
email:
|
58
|
+
- me@dwaynecrooks.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- .gitignore
|
64
|
+
- .travis.yml
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- examples/case_changer.rb
|
71
|
+
- examples/ceiling_fan.rb
|
72
|
+
- examples/cursor.rb
|
73
|
+
- examples/fsm.rb
|
74
|
+
- lib/state_design_pattern.rb
|
75
|
+
- lib/state_design_pattern/base_state.rb
|
76
|
+
- lib/state_design_pattern/state_machine.rb
|
77
|
+
- lib/state_design_pattern/version.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/state_design_pattern_spec.rb
|
80
|
+
- state_design_pattern.gemspec
|
81
|
+
homepage: https://github.com/dwayne/state_design_pattern
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.9.3
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.2.2
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: An implementation of the State Design Pattern in Ruby.
|
105
|
+
test_files:
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- spec/state_design_pattern_spec.rb
|