behaves 0.1.1 → 0.2.0
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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +105 -54
- data/lib/behaves/version.rb +1 -1
- data/lib/behaves.rb +23 -9
- metadata +2 -3
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd22ae5f7d6af9cc2a76a8d7b73d7ec25af7c759afac262731ef56c79f1d9a3b
|
4
|
+
data.tar.gz: b5abbc034aa95a5067bed0ce7eb2f5472c6b3e89333d8128806499ea25d38fdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a809170ad8c43554ee5a6845ec558ce803a766ec216a088c7fb21b6b92c355dc5d618b340a48fdf5eb4cb80079d40c429622d8a344998a3a278454aa84414129
|
7
|
+
data.tar.gz: 1ed4e8d27bd2171cc0e2020c1fa869ff099c887ebaeb2ef35c4d639c715d5b5e3787910e272a99b7a5c0b5fe0e31fc0af29715f1de40364652bd32bca113c4ac
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,56 +1,57 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
gem 'behaves'
|
7
|
-
```
|
8
|
-
|
1
|
+

|
2
|
+

|
3
|
+

|
9
4
|
|
10
5
|
# Behaves
|
11
6
|
|
12
|
-
Behaves is a gem that helps you
|
13
|
-
|
14
|
-
For example, you can specify that class `Dog` and class `Cat` should both behave the same as `Animal`, or that your `ApiClientMock` should behave the same as the original `ApiClient` (more explanation below)
|
15
|
-
|
16
|
-
The idea for `Behaves` stemmed from my research into [adapter pattern in Ruby](https://www.sitepoint.com/using-and-testing-the-adapter-design-pattern/) and José Valim's article on [Mocks and explicit contracts](http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/).
|
17
|
-
|
18
|
-
I found that the current idiom to achieve `behaviors` in Ruby is through `Inheritence`, and then subsequently defining a "required" (I put quotation marks around it, because it's not exactly `required` until you run it) method, which does nothing except raising a `NotImplementedError`. While I don't necessarily think it's *bad*, I do think that there could be an alternative that's **more explicit**, **less boilerplate**, **cleaner ancestors hierachy**, thus the birth of `Behaves`.
|
19
|
-
|
20
|
-
## Cons of inheritance
|
7
|
+
Behaves is a gem that helps you define behaviors between classes. **Say goodbye to runtime error when defining behaviors.**
|
21
8
|
|
22
|
-
|
9
|
+
Behaves is especially useful for dealing with adapter patterns by making sure that all of your adapters define the required behaviors. See [usage below](https://github.com/edisonywh/behaves#usage) for more examples.
|
23
10
|
|
24
|
-
|
11
|
+
*Detailed explanations in the sections below.*
|
25
12
|
|
26
|
-
|
13
|
+
## Installation
|
27
14
|
|
28
|
-
|
15
|
+
Add this line to your application's Gemfile:
|
29
16
|
|
30
|
-
|
17
|
+
```ruby
|
18
|
+
gem 'behaves'
|
19
|
+
```
|
31
20
|
|
32
|
-
|
21
|
+
## Usage
|
33
22
|
|
34
|
-
|
23
|
+
This is how you define behaviors with `behaves`.
|
35
24
|
|
36
|
-
|
25
|
+
First, define required methods on the `Behavior Object` with the `implements` method, which take a list of methods.
|
26
|
+
```ruby
|
27
|
+
class Animal
|
28
|
+
extend Behaves
|
37
29
|
|
38
|
-
|
30
|
+
implements :speak, :eat
|
31
|
+
end
|
32
|
+
```
|
39
33
|
|
40
|
-
|
34
|
+
Then, you can turn any object (the `Behaving Object`) to behave like the `Behavior Object` by using the `behaves_like` method, which takes a `Behavior Object`.
|
35
|
+
```ruby
|
36
|
+
class Dog
|
37
|
+
extend Behaves
|
41
38
|
|
42
|
-
|
39
|
+
behaves_like Animal
|
40
|
+
end
|
41
|
+
```
|
43
42
|
|
44
|
-
|
43
|
+
Voilà, that's all it takes to define behaviors! Now if `Dog` does not implement `speak` and `eat`, your code will then throw error **on file load**, instead of **at runtime**.
|
45
44
|
|
46
|
-
|
45
|
+
```diff
|
46
|
+
- NotImplementedError: Expected `Dog` to behave like `Animal`, but `speak, eat` are not implemented.
|
47
|
+
```
|
47
48
|
|
48
|
-
|
49
|
+
This is in stark contrast to defining behaviors with inheritance. Let's take a look.
|
49
50
|
|
50
|
-
|
51
|
+
### Inheritance-based behaviors
|
51
52
|
|
52
53
|
```ruby
|
53
|
-
# Inheritance
|
54
|
+
# Inheritance - potential runtime error.
|
54
55
|
class Animal
|
55
56
|
def speak
|
56
57
|
raise NotImplementedError, "Animals need to be able to speak!"
|
@@ -65,44 +66,98 @@ class Dog < Animal
|
|
65
66
|
def speak
|
66
67
|
"woof"
|
67
68
|
end
|
68
|
-
|
69
|
-
def eat
|
70
|
-
"chomp"
|
71
|
-
end
|
72
69
|
end
|
73
70
|
```
|
74
71
|
|
75
|
-
|
72
|
+
1) It is unclear that `Dog` has a certain set of behaviors to adhere to.
|
76
73
|
|
77
|
-
|
74
|
+
2) Notice how `Dog` does not implement `#eat`? Inheritance-based behaviors have no guarantee that `Dog` adheres to a certain set of behaviors, which means you can run into runtime errors like this.
|
78
75
|
|
79
76
|
```ruby
|
80
|
-
|
77
|
+
corgi = Dog.new
|
78
|
+
corgi.eat
|
79
|
+
# => NotImplementedError, "Animals need to be able to eat!"
|
80
|
+
```
|
81
|
+
|
82
|
+
3) Another problem is you have now defined `Animal#speak` and `Animal#eat`, two stub methods of which they do nothing but raise an undesirable `NotImplementedError`.
|
83
|
+
|
84
|
+
The power of `Behaves` does not stop here either.
|
85
|
+
|
86
|
+
## Features
|
87
|
+
|
88
|
+
### Multi-behaviors
|
89
|
+
|
90
|
+
`Behaves` allow you to define multiple behavior for a single behaving object. **This is not possible with inheritance**.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class Predator
|
81
94
|
extend Behaves
|
82
95
|
|
83
|
-
implements :
|
96
|
+
implements :hunt
|
84
97
|
end
|
85
98
|
|
86
|
-
class
|
99
|
+
class Prey
|
87
100
|
extend Behaves
|
88
101
|
|
89
|
-
|
102
|
+
implements :run, :hide
|
103
|
+
end
|
90
104
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
105
|
+
class Shark
|
106
|
+
extend Behaves
|
94
107
|
|
95
|
-
|
96
|
-
|
108
|
+
# Shark is both a `Predator` and a `Prey`
|
109
|
+
behaves_like Predator
|
110
|
+
behaves_like Prey
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### Inject Behaviors
|
115
|
+
|
116
|
+
When someone decides to use `behaves` to define behaviors, they in turn lose the ability to utilize some other aspect of inheritance, one of it being inheriting methods.
|
117
|
+
|
118
|
+
So, `Behaves` now ship with a feature called `inject_behaviors` for that need!
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class Dad
|
122
|
+
extend Behaves
|
123
|
+
|
124
|
+
implements :speak, :eat
|
125
|
+
|
126
|
+
inject_behaviors do
|
127
|
+
def traits; "Dad's traits!"; end
|
97
128
|
end
|
98
129
|
end
|
130
|
+
|
131
|
+
class Child
|
132
|
+
extend Behaves
|
133
|
+
|
134
|
+
behaves_like Dad
|
135
|
+
|
136
|
+
def speak; "BABA"; end
|
137
|
+
def eat; "NOM NOM"; end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Child.new.traits #=> "Dad's traits!"
|
99
141
|
```
|
100
142
|
|
101
|
-
|
143
|
+
This extends to more than just method implementation too, you can do anything you want! That's because the code inside `inject_behaviors` run in the context of the `Behaving Object`, also `self` inside `injected_behaviors` refers to the `Behaving Object`.
|
144
|
+
|
145
|
+
*Do note that if you use this extensively, you might be better off using inheritance, since this will create more `Method` objects than inheritance.*
|
146
|
+
|
147
|
+
## Tips
|
148
|
+
If you do not want to type `extend Behaves` every time, you can monkey patch `Behaves` onto `Object` class, like so:
|
149
|
+
|
150
|
+
> Object.send(:extend, Behaves)
|
102
151
|
|
103
152
|
## Thoughts
|
104
153
|
|
105
|
-
|
154
|
+
The idea for `Behaves` stemmed from my research into [adapter pattern in Ruby](https://www.sitepoint.com/using-and-testing-the-adapter-design-pattern/) and José Valim's article on [Mocks and explicit contracts](http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/).
|
155
|
+
|
156
|
+
I found that the current idiom to achieve `behaviors` in Ruby is through inheritence, and then subsequently defining 'required' methods, which does nothing except raising a `NotImplementedError`. This approach is fragile, as it **does not guarantee behaviors**, runs the **risk of runtime errors**, and has an **opaque implementation**.
|
157
|
+
|
158
|
+
Thus with this comes the birth of `Behaves`.
|
159
|
+
|
160
|
+
Also referring to the article by José Valim, I really liked the idea of being able to use Mock as a noun. However, while the idea sounds good, you've now introduced a new problem in your codebase -- your Mock and your original Object might deviate from their implementation later on. Not a good design if it breaks. Elixir has `@behaviors` & `@callback` built in to keep them in sync. `Behaves` is inspired by that.
|
106
161
|
|
107
162
|
## Development
|
108
163
|
|
@@ -117,7 +172,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/edison
|
|
117
172
|
## License
|
118
173
|
|
119
174
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
120
|
-
|
121
|
-
## Code of Conduct
|
122
|
-
|
123
|
-
Everyone interacting in the Behaves project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/behaves/blob/master/CODE_OF_CONDUCT.md).
|
data/lib/behaves/version.rb
CHANGED
data/lib/behaves.rb
CHANGED
@@ -6,21 +6,28 @@ module Behaves
|
|
6
6
|
@behaviors ||= Set.new(methods)
|
7
7
|
end
|
8
8
|
|
9
|
+
def inject_behaviors (&block)
|
10
|
+
@inject_behaviors ||= block
|
11
|
+
end
|
12
|
+
|
9
13
|
def behaves_like(klass)
|
10
|
-
|
11
|
-
|
14
|
+
add_injected_behaviors(klass)
|
15
|
+
at_exit { check_for_unimplemented(klass) }
|
16
|
+
end
|
12
17
|
|
13
|
-
|
18
|
+
private
|
14
19
|
|
15
|
-
|
20
|
+
def check_for_unimplemented(klass)
|
21
|
+
required = defined_behaviors(klass)
|
16
22
|
|
17
|
-
|
23
|
+
implemented = Set.new(self.instance_methods - Object.instance_methods)
|
18
24
|
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
25
|
+
unimplemented = required - implemented
|
22
26
|
|
23
|
-
|
27
|
+
return if unimplemented.empty?
|
28
|
+
|
29
|
+
raise NotImplementedError, "Expected `#{self}` to behave like `#{klass}`, but `#{unimplemented.to_a.join(', ')}` are not implemented."
|
30
|
+
end
|
24
31
|
|
25
32
|
def defined_behaviors(klass)
|
26
33
|
if behaviors = klass.instance_variable_get("@behaviors")
|
@@ -29,4 +36,11 @@ module Behaves
|
|
29
36
|
raise NotImplementedError, "Expected `#{klass}` to define behaviors, but none found."
|
30
37
|
end
|
31
38
|
end
|
39
|
+
|
40
|
+
def add_injected_behaviors(klass)
|
41
|
+
injected_behaviors = klass.instance_variable_get("@inject_behaviors")
|
42
|
+
if injected_behaviors
|
43
|
+
self.class_eval &injected_behaviors
|
44
|
+
end
|
45
|
+
end
|
32
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: behaves
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Edison Yap
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-06-
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|
@@ -92,7 +92,6 @@ files:
|
|
92
92
|
- ".circleci/config.yml"
|
93
93
|
- ".gitignore"
|
94
94
|
- ".rspec"
|
95
|
-
- ".travis.yml"
|
96
95
|
- CODE_OF_CONDUCT.md
|
97
96
|
- Gemfile
|
98
97
|
- Gemfile.lock
|