behaves 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a306c547822ce551aa1e2c6f8073eca5ba9c9e91526bfad490368845af6e5185
4
- data.tar.gz: c31eaf7f25270f52bcce238b0bd6a601e1a4c96a38ada2f22546aa149f32af28
3
+ metadata.gz: cd22ae5f7d6af9cc2a76a8d7b73d7ec25af7c759afac262731ef56c79f1d9a3b
4
+ data.tar.gz: b5abbc034aa95a5067bed0ce7eb2f5472c6b3e89333d8128806499ea25d38fdc
5
5
  SHA512:
6
- metadata.gz: 6d0a66ad83736a1c0bae7dfdbdca4bb7f9af48c6b8043f7cc0066e2dea68b16e9e776bc603272e15c063912dee3fa6972ccc0a178098fd891158f366b38421ed
7
- data.tar.gz: 2dae6fcdc1e832fa8e2e2f4e6d9485f4925f6ba355b92b5a601288d379afe466b8c2e4062ae8d8c66c71d18561bc9bead9ef50bfad13b128376519d2768b28aa
6
+ metadata.gz: a809170ad8c43554ee5a6845ec558ce803a766ec216a088c7fb21b6b92c355dc5d618b340a48fdf5eb4cb80079d40c429622d8a344998a3a278454aa84414129
7
+ data.tar.gz: 1ed4e8d27bd2171cc0e2020c1fa869ff099c887ebaeb2ef35c4d639c715d5b5e3787910e272a99b7a5c0b5fe0e31fc0af29715f1de40364652bd32bca113c4ac
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- behaves (0.1.0)
4
+ behaves (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,56 +1,57 @@
1
- ## Installation
2
-
3
- Add this line to your application's Gemfile:
4
-
5
- ```ruby
6
- gem 'behaves'
7
- ```
8
-
1
+ ![CircleCI Badge](https://img.shields.io/circleci/build/github/edisonywh/behaves.svg)
2
+ ![RubyGems Badge](https://img.shields.io/gem/v/behaves.svg)
3
+ ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/edisonywh/behaves.svg)
9
4
 
10
5
  # Behaves
11
6
 
12
- Behaves is a gem that helps you maintain contracts between different classes. This is especially useful for dealing for adapter patterns by making sure that all of your adapters define the required behaviors.
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
- Let's dive into the cons of implementing behaviors through `Inheritance`
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
- First, I think this is a very opaque implementation - at a quick glance, there's no real way to know if there are any behaviors required. The only way to be sure is to dive into the parent class and look for any methods that does nothing but raises a `NotImplementedError`. This gets cascadingly worse if you have multiple hierachy of inheritance.
11
+ *Detailed explanations in the sections below.*
25
12
 
26
- Secondly, with inheritance, the behavioral contract is dependent upon the method lookup chain - this poses a few issues:
13
+ ## Installation
27
14
 
28
- 1) `Unused code` - You now have a stub method on your parent class that does nothing but raise an error, and it won't be used any longer after it's your behaviors are adhered to.
15
+ Add this line to your application's Gemfile:
29
16
 
30
- 2) `Fragile implementation` - You are reliant on the ancestor chain not being intercepted. For example, if someone else on your team defines a method that has the same name as your behavior, but sits higher up the chain (through `prepending` for example), your stub method is now useless and won't ever catch if a behavior is not implemented.
17
+ ```ruby
18
+ gem 'behaves'
19
+ ```
31
20
 
32
- 3) `Runtime errors` - There are possibility for runtime errors. If a child did not adhere to the required behaviors, your code won't actually know that until it tries to call the method on child class. Error on production? Not good.
21
+ ## Usage
33
22
 
34
- ## How does `Behaves` solve this problem?
23
+ This is how you define behaviors with `behaves`.
35
24
 
36
- Behaves aim to solve this problem by being **explicit** and **upfront**:
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
- - A very clear `behaves_like Animal` that indicates that this class has a certain behaviors to adhere to, as implemented by `Animal`.
30
+ implements :speak, :eat
31
+ end
32
+ ```
39
33
 
40
- - Guarantee to catch implementation deviation regardless of the ancestor chains.
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
- - With the way Behaves is written, your code will fail to even load if you don't adhere to the behaviors upfront - **no more runtime errors in production**.
39
+ behaves_like Animal
40
+ end
41
+ ```
43
42
 
44
- - No need to define a stub method that does nothing - behaviors checking are done through symbols, as defined in `implements` by the behaviorial class.
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
- See below for more examples about how `Behaves` work.
45
+ ```diff
46
+ - NotImplementedError: Expected `Dog` to behave like `Animal`, but `speak, eat` are not implemented.
47
+ ```
47
48
 
48
- ## Usage
49
+ This is in stark contrast to defining behaviors with inheritance. Let's take a look.
49
50
 
50
- Let's take a look how to define behaviors using `Inheritance`.
51
+ ### Inheritance-based behaviors
51
52
 
52
53
  ```ruby
53
- # Inheritance Behaviors
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
- You have now defined `Animal#speak` and `Animal#eat` which are not useful at all, along with all the issues I pointed out earlier.
72
+ 1) It is unclear that `Dog` has a certain set of behaviors to adhere to.
76
73
 
77
- Now let's take a look at how `Behaves` work.
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
- class Animal
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 :speak, :eat
96
+ implements :hunt
84
97
  end
85
98
 
86
- class Dog
99
+ class Prey
87
100
  extend Behaves
88
101
 
89
- behaves_like Animal
102
+ implements :run, :hide
103
+ end
90
104
 
91
- def speak
92
- "woof"
93
- end
105
+ class Shark
106
+ extend Behaves
94
107
 
95
- def eat
96
- "chomp"
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
- With `Behaves`, it is immediately obvious that Dog should behave like a certain class -> `Animal`, and you do not have to implement stub methods on `Animal`.
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
- 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.
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).
@@ -1,3 +1,3 @@
1
1
  module Behaves
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
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
- at_exit do
11
- required = defined_behaviors(klass)
14
+ add_injected_behaviors(klass)
15
+ at_exit { check_for_unimplemented(klass) }
16
+ end
12
17
 
13
- implemented = Set.new(self.instance_methods - Object.instance_methods)
18
+ private
14
19
 
15
- unimplemented = required - implemented
20
+ def check_for_unimplemented(klass)
21
+ required = defined_behaviors(klass)
16
22
 
17
- exit if unimplemented.empty?
23
+ implemented = Set.new(self.instance_methods - Object.instance_methods)
18
24
 
19
- raise NotImplementedError, "Expected `#{self}` to behave like `#{klass}`, but `#{unimplemented.to_a.join(', ')}` are not implemented."
20
- end
21
- end
25
+ unimplemented = required - implemented
22
26
 
23
- private
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.1.1
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-10 00:00:00.000000000 Z
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
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.3.1
7
- before_install: gem install bundler -v 1.17.1