duckpond 1.0.2 → 1.1.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/.travis.yml +4 -3
- data/Gemfile +1 -1
- data/Gemfile.lock +23 -2
- data/README.md +34 -28
- data/docs/.the_solution.txt.swp +0 -0
- data/docs/the_problem.txt +98 -0
- data/docs/the_solution.txt +56 -0
- data/duckpond.gemspec +1 -1
- data/lib/duckpond/contract.rb +13 -7
- data/lib/duckpond/inspection.rb +36 -0
- data/lib/duckpond/version.rb +2 -2
- data/lib/duckpond.rb +2 -3
- data/spec/contract_spec.rb +76 -0
- data/spec/{sighting_spec.rb → inspection_spec.rb} +8 -9
- data/spec/spec_helper.rb +3 -0
- metadata +10 -14
- data/lib/duckpond/binoculars.rb +0 -57
- data/lib/duckpond/sighting.rb +0 -37
- data/spec/binoculars_spec.rb +0 -52
- data/spec/duck_spec.rb +0 -53
- data/spec/the_problem_spec.rb +0 -120
- data/spec/the_solution_spec.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49a48ab6dd27f5acfb06ff204a3679299aabab0d
|
4
|
+
data.tar.gz: adeae633ba2f56c289cff8b0f3ae902e2f63e2d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21d67910df3b44f7aac195706ab565b3e20b23549d73251f8ba7dba36f31d033c2e293333d3c17b33b83555b23a345b6b73e97be7ba3cf49b9c19338d64b6c3a
|
7
|
+
data.tar.gz: 2cbf35d78a704430210fff5e7fe78aea5da7c04aba15d484e420501a8b5e4ec26948f6682e019687fed20fd32ed04964d9ea5d7615dcfd5b091b8f5a32184c36
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -6,8 +6,19 @@ PATH
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
+
coveralls (0.7.0)
|
10
|
+
multi_json (~> 1.3)
|
11
|
+
rest-client
|
12
|
+
simplecov (>= 0.7)
|
13
|
+
term-ansicolor
|
14
|
+
thor
|
9
15
|
diff-lcs (1.2.5)
|
10
|
-
|
16
|
+
docile (1.1.5)
|
17
|
+
mime-types (2.3)
|
18
|
+
multi_json (1.10.1)
|
19
|
+
rake (0.9.6)
|
20
|
+
rest-client (1.6.7)
|
21
|
+
mime-types (>= 1.16)
|
11
22
|
rspec (3.0.0)
|
12
23
|
rspec-core (~> 3.0.0)
|
13
24
|
rspec-expectations (~> 3.0.0)
|
@@ -20,12 +31,22 @@ GEM
|
|
20
31
|
rspec-mocks (3.0.1)
|
21
32
|
rspec-support (~> 3.0.0)
|
22
33
|
rspec-support (3.0.0)
|
34
|
+
simplecov (0.8.2)
|
35
|
+
docile (~> 1.1.0)
|
36
|
+
multi_json
|
37
|
+
simplecov-html (~> 0.8.0)
|
38
|
+
simplecov-html (0.8.0)
|
39
|
+
term-ansicolor (1.3.0)
|
40
|
+
tins (~> 1.0)
|
41
|
+
thor (0.19.1)
|
42
|
+
tins (1.3.0)
|
23
43
|
|
24
44
|
PLATFORMS
|
25
45
|
ruby
|
26
46
|
|
27
47
|
DEPENDENCIES
|
28
48
|
bundler (~> 1.3)
|
49
|
+
coveralls
|
29
50
|
duckpond!
|
30
|
-
rake
|
51
|
+
rake (~> 0)
|
31
52
|
rspec
|
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
[](http://badge.fury.io/rb/duckpond)
|
2
2
|
[](https://travis-ci.org/mikeyhogarth/duckpond)
|
3
|
+
[](https://coveralls.io/r/mikeyhogarth/duckpond)
|
3
4
|
|
4
5
|
# Duckpond
|
5
6
|
|
@@ -12,13 +13,9 @@ Duck Typing can make code confusing and unreadable, particularly
|
|
12
13
|
when multiple developers are working on the same project or when
|
13
14
|
projects are inherited by new developers.
|
14
15
|
|
15
|
-
|
16
|
-
in spec form. This would be a good place to start, although there is a crash
|
17
|
-
course below.
|
16
|
+
* [the_problem](docs/the_problem.txt) - This outlines the problems with duck typing.
|
18
17
|
|
19
|
-
* [
|
20
|
-
|
21
|
-
* [the_solution_spec](spec/the_solution_spec.rb) - This outlines how duckpond gets around these problems.
|
18
|
+
* [the_solution](docs/the_solution.txt) - This outlines how duckpond gets around these problems.
|
22
19
|
|
23
20
|
|
24
21
|
## Installation
|
@@ -38,35 +35,34 @@ Or install it yourself as:
|
|
38
35
|
|
39
36
|
## Usage
|
40
37
|
|
41
|
-
Usage is demonstrated in '[
|
42
|
-
"contract" classes by inheriting from DuckPond::Contract.
|
43
|
-
|
44
|
-
|
38
|
+
Usage is demonstrated in '[the_solution](docs/the_solution.txt)', but in a nutshell you
|
39
|
+
create "contract" classes by inheriting from DuckPond::Contract. These classes should
|
40
|
+
be commented extensively.
|
41
|
+
|
42
|
+
The "has_method" method is used to specify which methods the contract expects to see. The
|
43
|
+
following contract describes classes which respond to #length and #to_s
|
45
44
|
|
46
45
|
class MyContract < DuckPond::Contract
|
47
|
-
has_method :length
|
46
|
+
has_method :length
|
48
47
|
has_method :to_s
|
49
48
|
end
|
50
49
|
|
51
|
-
Once you've declared a contract, you can
|
52
|
-
|
53
|
-
|
54
|
-
obj = "Hello World"
|
55
|
-
sighting = DuckPond::Binoculars.identify(obj)
|
56
|
-
sighting.quacks_like? MyContract
|
57
|
-
=> true
|
58
|
-
|
59
|
-
There are other syntaxes:
|
50
|
+
Once you've declared a contract, you can compare objects to it to see if the contract is
|
51
|
+
fulfilled by the object:
|
60
52
|
|
61
|
-
|
62
|
-
|
63
|
-
|
53
|
+
MyContract.fulfilled_by? "Hello"
|
54
|
+
=> true
|
55
|
+
MyContract.fulfilled_by? 12
|
56
|
+
=> false
|
64
57
|
|
65
|
-
|
66
|
-
|
58
|
+
There is also a "bang" version of the fulfilled_by method, that raises an error instead
|
59
|
+
of returning false.
|
67
60
|
|
61
|
+
MyContract.fulfilled_by! 12
|
62
|
+
=> RAISES ERROR!!
|
68
63
|
|
69
|
-
|
64
|
+
Contracts can be combined into composite "super contracts" - contracts which are made up of
|
65
|
+
various other contracts. This ties in with the reccomendation of preferring composition over inheritance:
|
70
66
|
|
71
67
|
class MyCompositeConrtact < DuckPond::Contract
|
72
68
|
include_methods_from MyContract
|
@@ -74,7 +70,7 @@ Ducks can be combined into composite "super contracts" - contracts which are mad
|
|
74
70
|
end
|
75
71
|
|
76
72
|
|
77
|
-
|
73
|
+
In the real world, a contract might look like this:
|
78
74
|
|
79
75
|
class IEmailable < DuckPond::Contract
|
80
76
|
#send: should send the results of :message via email to :to
|
@@ -91,12 +87,22 @@ And then be implemented in a method like this:
|
|
91
87
|
|
92
88
|
class Emailer
|
93
89
|
def send(email)
|
94
|
-
|
90
|
+
IEmailable.fulfilled_by! email
|
95
91
|
email.send
|
96
92
|
end
|
97
93
|
end
|
98
94
|
|
99
95
|
|
96
|
+
## Compatibility
|
97
|
+
|
98
|
+
CI tests exist for the following rubies, so they definately work and there's no reason all 1.9.x rubies wouldnt' work either:
|
99
|
+
|
100
|
+
- 1.9.3
|
101
|
+
- 2.0.0
|
102
|
+
- 2.1.1
|
103
|
+
- jruby-19mode
|
104
|
+
|
105
|
+
|
100
106
|
## Contributing
|
101
107
|
|
102
108
|
1. Fork it
|
Binary file
|
@@ -0,0 +1,98 @@
|
|
1
|
+
#
|
2
|
+
# Take this class for instance...
|
3
|
+
#
|
4
|
+
# It has a single method which;
|
5
|
+
#
|
6
|
+
# * Calls an instance method on its parameter.
|
7
|
+
# * Sends its parameter off to some other object.
|
8
|
+
# * Creates an instance variable with the result.
|
9
|
+
#
|
10
|
+
|
11
|
+
class Foo
|
12
|
+
def self.bar(obj)
|
13
|
+
obj.foo!
|
14
|
+
fooinator = Test::Fooinator.new
|
15
|
+
@fooified_obj = fooinator.foo(obj)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# From an architectural perspective, there is really nothing
|
21
|
+
# wrong with this class/method. Even from a design perspective
|
22
|
+
# the only thing that you can really criticize is that the
|
23
|
+
# method is a little vague in terms of what it does, and that's just
|
24
|
+
# because we don't know what "fooing" is.
|
25
|
+
#
|
26
|
+
# But there are a few other problems.
|
27
|
+
#
|
28
|
+
# You will often hear rubyists talking about the wonder of
|
29
|
+
# "duck typing". Developers can pass anything they want to the "bar"
|
30
|
+
# method above and it'll still work so long as the parameter responds
|
31
|
+
# to certain methods etc.
|
32
|
+
#
|
33
|
+
# You will almost as often hear programmers freaking out about how
|
34
|
+
# chaotic and crazy this approach is! Arguments about such matters usually
|
35
|
+
# eventually gravitate towards the problem being that ruby is not
|
36
|
+
# strongly typed. The knowledge about what "ducks" can be sent to the
|
37
|
+
# :bar method is not stored anywhere: it's implicit in obj's usage. Great if
|
38
|
+
# you were the developer of the method and you understand it completely,
|
39
|
+
# but what if it's someone else?
|
40
|
+
#
|
41
|
+
# In the above example, in order to see which type of object they can
|
42
|
+
# pass to the "bar" method, a developer would at the very least need
|
43
|
+
# to:
|
44
|
+
#
|
45
|
+
# * Grep for "foo!" to see what the hell it does
|
46
|
+
# * See what the hell a "Fooinator" does
|
47
|
+
# * Look at the fooinator#foo method to see what the hell THAT does
|
48
|
+
# * Look for all instances of @fooified_obj and see what the hell THOSE DO
|
49
|
+
#
|
50
|
+
# And even if they do all that, there's a chance they might miss something.
|
51
|
+
# If they do miss something, this will likely manifest itself in an error
|
52
|
+
# further up the stack, such as method missing exceptions etc., in places
|
53
|
+
# OTHER than where the actual mistake was made.
|
54
|
+
#
|
55
|
+
# Nightmare!
|
56
|
+
#
|
57
|
+
# Wouldn't it be nice if the "bar" method did a check or two, or even raised
|
58
|
+
# an exception when it was passed an object that wasn't compatible with its
|
59
|
+
# functionality? You could do this yourself manually:
|
60
|
+
#
|
61
|
+
|
62
|
+
class Foo
|
63
|
+
def self.bar(obj)
|
64
|
+
raise TypeError unless obj.respond_to?(:foo!) && obj.respond_to?(:chunky_bacon)
|
65
|
+
|
66
|
+
obj.foo!
|
67
|
+
fooinator = Test::Fooinator.new
|
68
|
+
@fooified_obj = fooinator.foo(obj)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# But now you've got more problems!
|
74
|
+
#
|
75
|
+
# * The method is now becoming longer and messier.
|
76
|
+
# * The "knowledge" about what :bar can receive is stored within the
|
77
|
+
# :bar method itself, but that knowledge may be needed elsewhere.
|
78
|
+
#
|
79
|
+
# Wouldn't it be nice if there was a neat way to store knowledge about all
|
80
|
+
# our ducks in one place, and to be able to reference them whenever we need to.
|
81
|
+
#
|
82
|
+
# Although duck typing is seen as a ruby technique, it is actually
|
83
|
+
# used in other languages too but the contract in strongly typed languages
|
84
|
+
# is made explicit through the use of interfaces. In c#, for example:
|
85
|
+
#
|
86
|
+
# public string foo(IMyInterface interface)
|
87
|
+
# {
|
88
|
+
# //your implementation here. A confused developer
|
89
|
+
# //could go off and look in IMyInterface to see what
|
90
|
+
# //this "duck" is meant to do.
|
91
|
+
# }
|
92
|
+
#
|
93
|
+
# As ruby is not strongly typed, you don't have this reference and the code
|
94
|
+
# becomes impossible for other developers to use!
|
95
|
+
#
|
96
|
+
# This is the problem duckpond hopes to solve.
|
97
|
+
#
|
98
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
# First, declare a contract.
|
3
|
+
#
|
4
|
+
|
5
|
+
class ObjContract < DuckPond::Contract
|
6
|
+
has_method :to_s
|
7
|
+
has_method :length
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# Then use it to check stuff!
|
12
|
+
#
|
13
|
+
|
14
|
+
ObjContract.fulfilled_by? "Hello"
|
15
|
+
=> true (or false if not fulfilled)
|
16
|
+
|
17
|
+
#
|
18
|
+
# The "bang" version will raise an error if the object isn't the duck.
|
19
|
+
#
|
20
|
+
|
21
|
+
ObjContract.fulfilled_by! "Hello"
|
22
|
+
=> true (or error if not fulfilled)
|
23
|
+
|
24
|
+
#
|
25
|
+
# So, going back to that example from the problem doc...
|
26
|
+
#
|
27
|
+
|
28
|
+
class Foo
|
29
|
+
def self.bar(obj)
|
30
|
+
ObjContract.fulfilled_by! obj
|
31
|
+
|
32
|
+
obj.foo!
|
33
|
+
fooinator = Test::Fooinator.new
|
34
|
+
@fooified_obj = fooinator.foo(obj)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
#
|
40
|
+
# Viola!
|
41
|
+
#
|
42
|
+
# Final Tips:
|
43
|
+
#
|
44
|
+
# * Contracts should be commented liberally. They form the descripiton
|
45
|
+
# of a contract just as interfaces would in strongly typed languages.
|
46
|
+
# Developers will be prompted to consult the duck when they see a pair
|
47
|
+
# of binoculars in your code, so make sure they find what they're looking
|
48
|
+
# for.
|
49
|
+
#
|
50
|
+
# * Rather than writing the duckpond functionality directly within the methods
|
51
|
+
# themselves, you could easily put them in hooks or method aliases. If you do this
|
52
|
+
# though, remember that the point of this app is to make things *more* visible, not
|
53
|
+
# less, so don't go tucking these duck checks too far away from where they are needed
|
54
|
+
# or leave an obvious trail if you do!
|
55
|
+
#
|
56
|
+
|
data/duckpond.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'duckpond/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "duckpond"
|
8
|
-
spec.version =
|
8
|
+
spec.version = DuckPond::VERSION
|
9
9
|
spec.authors = ["Mikey Hogarth"]
|
10
10
|
spec.email = ["mikehogarth20@hotmail.com"]
|
11
11
|
spec.description = %q{Explicit duck-typing for ruby}
|
data/lib/duckpond/contract.rb
CHANGED
@@ -7,6 +7,8 @@
|
|
7
7
|
#
|
8
8
|
module DuckPond
|
9
9
|
class Contract
|
10
|
+
class ContractInfringementError < TypeError;end
|
11
|
+
|
10
12
|
class << self
|
11
13
|
|
12
14
|
def clauses
|
@@ -22,7 +24,6 @@ module DuckPond
|
|
22
24
|
clauses << method_name
|
23
25
|
end
|
24
26
|
|
25
|
-
|
26
27
|
#
|
27
28
|
# include_clauses_from
|
28
29
|
#
|
@@ -34,16 +35,21 @@ module DuckPond
|
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
-
#
|
39
|
-
# quacks_like?
|
40
38
|
#
|
41
|
-
#
|
39
|
+
# fulfilled_by?
|
42
40
|
#
|
43
|
-
def
|
44
|
-
|
41
|
+
def fulfilled_by?(obj)
|
42
|
+
inspection = DuckPond::Inspection.new(obj)
|
43
|
+
return false unless inspection.fulfilled_by? self
|
44
|
+
true
|
45
45
|
end
|
46
46
|
|
47
|
+
#
|
48
|
+
# fulfilled_by!
|
49
|
+
#
|
50
|
+
def fulfilled_by!(obj)
|
51
|
+
raise ContractInfringementError unless fulfilled_by?(obj)
|
52
|
+
end
|
47
53
|
end
|
48
54
|
end
|
49
55
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# DuckPond::Inspection
|
3
|
+
#
|
4
|
+
# The duckpond analysis class is essentially just a wrapper for the inspected
|
5
|
+
# object, used here to avoid un-nessecary monkey patching.
|
6
|
+
#
|
7
|
+
module DuckPond
|
8
|
+
class Inspection
|
9
|
+
|
10
|
+
# the wrapped object
|
11
|
+
attr_reader :subject
|
12
|
+
|
13
|
+
#
|
14
|
+
# initialize
|
15
|
+
#
|
16
|
+
# Construct with any ruby object.
|
17
|
+
#
|
18
|
+
def initialize(subject)
|
19
|
+
@subject = subject
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# fulfilled_by?
|
24
|
+
#
|
25
|
+
# receives exactly one contract, and returns true/false if
|
26
|
+
# the inspected object responds to the same methods indicated
|
27
|
+
# by the contract's clauses.
|
28
|
+
#
|
29
|
+
def fulfilled_by?(contract)
|
30
|
+
contract.clauses.each do |clause|
|
31
|
+
return false unless @subject.respond_to? clause
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/duckpond/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "1.0
|
1
|
+
module DuckPond
|
2
|
+
VERSION = "1.1.0"
|
3
3
|
end
|
data/lib/duckpond.rb
CHANGED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DuckPond
|
4
|
+
describe Contract do
|
5
|
+
describe 'attributes' do
|
6
|
+
context '.clauses' do
|
7
|
+
it 'responds to and memoizes the result' do
|
8
|
+
expect(Contract).to respond_to :clauses
|
9
|
+
expect(Contract.clauses).to be_an Array
|
10
|
+
expect(Contract.clauses).to be_empty
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class LengthContract < DuckPond::Contract
|
16
|
+
has_method :length
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'fulfilled_by?' do
|
20
|
+
context 'when the object is fulfilled by the contract' do
|
21
|
+
it 'returns true' do
|
22
|
+
expect(LengthContract.fulfilled_by?("Hello")).to eq true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
context 'when the object is not fulfilled by the contract' do
|
26
|
+
it 'returns false' do
|
27
|
+
expect(LengthContract.fulfilled_by?(2)).to eq false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'fulfilled_by!' do
|
33
|
+
context 'when the object is fulfilled by the contract' do
|
34
|
+
it 'returns true' do
|
35
|
+
expect{LengthContract.fulfilled_by!("Hello")}.to_not raise_error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
context 'when the object is not fulfilled by the contract' do
|
39
|
+
it 'returns false' do
|
40
|
+
expect{LengthContract.fulfilled_by!(2)}.to raise_error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class MyContract < Contract
|
47
|
+
has_method :foo
|
48
|
+
has_method :bar
|
49
|
+
end
|
50
|
+
|
51
|
+
describe MyContract do
|
52
|
+
context '.clauses' do
|
53
|
+
it 'quacks like foo and bar' do
|
54
|
+
clauses = MyContract.clauses
|
55
|
+
expect(clauses.length).to eq 2
|
56
|
+
expect(clauses).to include :foo
|
57
|
+
expect(clauses).to include :bar
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class MySimilarContract < Contract
|
63
|
+
include_clauses_from MyContract
|
64
|
+
has_method :chunky_bacon
|
65
|
+
end
|
66
|
+
|
67
|
+
describe MySimilarContract do
|
68
|
+
it 'retains its parents quackings' do
|
69
|
+
clauses = MySimilarContract.clauses
|
70
|
+
expect(clauses.length).to eq 3
|
71
|
+
expect(clauses).to include :foo
|
72
|
+
expect(clauses).to include :bar
|
73
|
+
expect(clauses).to include :chunky_bacon
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
require 'ostruct'
|
2
2
|
|
3
3
|
module DuckPond
|
4
|
-
describe
|
5
|
-
|
4
|
+
describe Inspection do
|
6
5
|
describe '#initialize' do
|
7
6
|
it 'is constructed using any ruby object' do
|
8
7
|
obj = Object.new
|
9
|
-
|
10
|
-
expect(
|
8
|
+
analysis = Inspection.new(obj)
|
9
|
+
expect(analysis.subject).to be obj
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
14
|
-
describe '#
|
13
|
+
describe '#fulfilled_by?' do
|
15
14
|
|
16
15
|
class MyChunkyBaconContract < Contract
|
17
16
|
has_method :chunky_bacon
|
@@ -20,16 +19,16 @@ module DuckPond
|
|
20
19
|
context 'when the sighted object quacks like the duck' do
|
21
20
|
it 'returns true' do
|
22
21
|
obj = OpenStruct.new(:chunky_bacon => :mmm)
|
23
|
-
|
24
|
-
expect(
|
22
|
+
inspection = Inspection.new(obj)
|
23
|
+
expect(inspection.fulfilled_by? MyChunkyBaconContract).to be true
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
28
27
|
context 'when the sighted object does not quack like the duck' do
|
29
28
|
it 'returns false' do
|
30
29
|
obj = OpenStruct.new(:vegan_fallafal => :yuck)
|
31
|
-
|
32
|
-
expect(
|
30
|
+
inspection = Inspection.new(obj)
|
31
|
+
expect(inspection.fulfilled_by? MyChunkyBaconContract).to be false
|
33
32
|
end
|
34
33
|
end
|
35
34
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duckpond
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mikey Hogarth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -53,19 +53,18 @@ files:
|
|
53
53
|
- LICENSE
|
54
54
|
- README.md
|
55
55
|
- Rakefile
|
56
|
+
- docs/.the_solution.txt.swp
|
57
|
+
- docs/the_problem.txt
|
58
|
+
- docs/the_solution.txt
|
56
59
|
- duckpond.gemspec
|
57
60
|
- lib/duckpond.rb
|
58
|
-
- lib/duckpond/binoculars.rb
|
59
61
|
- lib/duckpond/contract.rb
|
60
|
-
- lib/duckpond/
|
62
|
+
- lib/duckpond/inspection.rb
|
61
63
|
- lib/duckpond/version.rb
|
62
|
-
- spec/binoculars_spec.rb
|
63
64
|
- spec/classes/fooinator.rb
|
64
|
-
- spec/
|
65
|
-
- spec/
|
65
|
+
- spec/contract_spec.rb
|
66
|
+
- spec/inspection_spec.rb
|
66
67
|
- spec/spec_helper.rb
|
67
|
-
- spec/the_problem_spec.rb
|
68
|
-
- spec/the_solution_spec.rb
|
69
68
|
homepage: https://github.com/mikeyhogarth/duckpond
|
70
69
|
licenses:
|
71
70
|
- MIT
|
@@ -91,10 +90,7 @@ signing_key:
|
|
91
90
|
specification_version: 4
|
92
91
|
summary: Explicit duck-typing for ruby
|
93
92
|
test_files:
|
94
|
-
- spec/binoculars_spec.rb
|
95
93
|
- spec/classes/fooinator.rb
|
96
|
-
- spec/
|
97
|
-
- spec/
|
94
|
+
- spec/contract_spec.rb
|
95
|
+
- spec/inspection_spec.rb
|
98
96
|
- spec/spec_helper.rb
|
99
|
-
- spec/the_problem_spec.rb
|
100
|
-
- spec/the_solution_spec.rb
|
data/lib/duckpond/binoculars.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# DuckPond::Binoculars
|
3
|
-
#
|
4
|
-
# Binoculars are the users interface to the ducks themselves and
|
5
|
-
# are responsible for building "sightings" out of objects. There
|
6
|
-
# are also a couple of convinience methods on here to perform
|
7
|
-
# common duck-spotting tasks.
|
8
|
-
#
|
9
|
-
module DuckPond
|
10
|
-
class Binoculars
|
11
|
-
class ContractInfringementError < TypeError;end
|
12
|
-
|
13
|
-
# The sighted object
|
14
|
-
attr_reader :sighting
|
15
|
-
|
16
|
-
#
|
17
|
-
# identify (class and instance level version)
|
18
|
-
#
|
19
|
-
# receives any ruby object, returns a "sighting" of that object
|
20
|
-
#
|
21
|
-
def identify(obj)
|
22
|
-
@sighting = DuckPond::Sighting.new(obj)
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.identify(obj)
|
26
|
-
DuckPond::Sighting.new(obj)
|
27
|
-
end
|
28
|
-
|
29
|
-
#
|
30
|
-
# confirm
|
31
|
-
#
|
32
|
-
# receives any ruby object and a duck class
|
33
|
-
# returns true if the object quacks like the duck,
|
34
|
-
# otherwise false.
|
35
|
-
#
|
36
|
-
def self.confirm(obj, duck)
|
37
|
-
binoculars = DuckPond::Binoculars.new
|
38
|
-
sighting = binoculars.identify(obj)
|
39
|
-
return false unless sighting.quacks_like?(duck)
|
40
|
-
true
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# confirm!
|
45
|
-
#
|
46
|
-
# does exactly the same as the standard confirm
|
47
|
-
# method, but raises an exception if the object
|
48
|
-
# does not quack like the duck.
|
49
|
-
#
|
50
|
-
def self.confirm!(obj, duck)
|
51
|
-
raise ContractInfringementError unless confirm(obj, duck)
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
|
data/lib/duckpond/sighting.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# DuckPond::Sighting
|
3
|
-
#
|
4
|
-
# The duckpond sighting class is essentially just a wrapper for the sighted
|
5
|
-
# object, used here to avoid un-nessecary monkey patching.
|
6
|
-
#
|
7
|
-
module DuckPond
|
8
|
-
class Sighting
|
9
|
-
|
10
|
-
# the wrapped object
|
11
|
-
attr_reader :sighted_object
|
12
|
-
|
13
|
-
#
|
14
|
-
# initialize
|
15
|
-
#
|
16
|
-
# Construct with any ruby object.
|
17
|
-
#
|
18
|
-
def initialize(sighted_object)
|
19
|
-
@sighted_object = sighted_object
|
20
|
-
end
|
21
|
-
|
22
|
-
#
|
23
|
-
# quacks_like?
|
24
|
-
#
|
25
|
-
# receives exactly one duck, and returns true/false if
|
26
|
-
# the sighted object responds to the same methods indicated
|
27
|
-
# by the duck's quacks.
|
28
|
-
#
|
29
|
-
def quacks_like?(duck)
|
30
|
-
duck.clauses.each do |clause|
|
31
|
-
return false unless @sighted_object.respond_to? clause
|
32
|
-
end
|
33
|
-
true
|
34
|
-
end
|
35
|
-
alias :looks_like? :quacks_like?
|
36
|
-
end
|
37
|
-
end
|
data/spec/binoculars_spec.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module DuckPond
|
4
|
-
describe Binoculars do
|
5
|
-
|
6
|
-
let(:obj) { Object.new }
|
7
|
-
|
8
|
-
describe '#identify' do
|
9
|
-
it 'returns any ruby object, in "sighting" form' do
|
10
|
-
bins = Binoculars.new
|
11
|
-
sighting = bins.identify(obj)
|
12
|
-
expect(sighting).to be_a Sighting
|
13
|
-
expect(sighting.sighted_object). to be obj
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
describe ".identify" do
|
18
|
-
it 'returns any ruby object, in "sighting" form' do
|
19
|
-
sighting = Binoculars.identify(obj)
|
20
|
-
expect(sighting).to be_a Sighting
|
21
|
-
expect(sighting.sighted_object). to be obj
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
describe '.confirm' do
|
26
|
-
|
27
|
-
class StringContract < Contract
|
28
|
-
has_method :to_s
|
29
|
-
has_method :length
|
30
|
-
end
|
31
|
-
|
32
|
-
context 'when given a ruby object and a duck class that it quacks like' do
|
33
|
-
it 'returns true' do
|
34
|
-
expect(DuckPond::Binoculars.confirm("Hello World", StringContract)).to be true
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
context 'when given a ruby object and a duck class that it doesnt quacks like' do
|
39
|
-
it 'returns false' do
|
40
|
-
expect(DuckPond::Binoculars.confirm(Object.new, StringContract)).to be false
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
describe '.confirm!' do
|
46
|
-
it 'behaves like the un-banged version, but raises an error if it doesnt quack right' do
|
47
|
-
expect {DuckPond::Binoculars.confirm!(Object.new, StringContract)}.to raise_error
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
end
|
52
|
-
end
|
data/spec/duck_spec.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module DuckPond
|
4
|
-
|
5
|
-
describe Contract do
|
6
|
-
describe 'attributes' do
|
7
|
-
context '.clauses' do
|
8
|
-
it 'responds to and memoizes the result' do
|
9
|
-
expect(Contract).to respond_to :clauses
|
10
|
-
expect(Contract.clauses).to be_an Array
|
11
|
-
expect(Contract.clauses).to be_empty
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class MyContract < Contract
|
18
|
-
has_method :foo
|
19
|
-
has_method :bar
|
20
|
-
end
|
21
|
-
|
22
|
-
describe MyContract do
|
23
|
-
context '.clauses' do
|
24
|
-
it 'quacks like foo and bar' do
|
25
|
-
clauses = MyContract.clauses
|
26
|
-
expect(clauses.length).to eq 2
|
27
|
-
expect(clauses).to include :foo
|
28
|
-
expect(clauses).to include :bar
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
describe '.quacks_like?' do
|
33
|
-
it 'returns true if the argument is in the quackings' do
|
34
|
-
expect(MyContract.quacks_like? :foo).to be true
|
35
|
-
expect(MyContract.quacks_like? :chunky_bacon).to be false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class MySimilarContract < Contract
|
41
|
-
include_clauses_from MyContract
|
42
|
-
has_method :chunky_bacon
|
43
|
-
end
|
44
|
-
|
45
|
-
describe MySimilarContract do
|
46
|
-
it 'retains its parents quackings' do
|
47
|
-
expect(MySimilarContract.quacks_like? :chunky_bacon).to be true
|
48
|
-
expect(MySimilarContract.quacks_like? :foo).to be true
|
49
|
-
expect(MySimilarContract.quacks_like? :bar).to be true
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
data/spec/the_problem_spec.rb
DELETED
@@ -1,120 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Duckpond
|
4
|
-
describe 'The Problem' do
|
5
|
-
context 'when a method is called' do
|
6
|
-
it 'is often difficult to see what parameters are allowed' do
|
7
|
-
|
8
|
-
#
|
9
|
-
# Take this class for instance...
|
10
|
-
#
|
11
|
-
# It has a single method which;
|
12
|
-
#
|
13
|
-
# * Calls an instance method on its parameter.
|
14
|
-
# * Sends its parameter off to some other object.
|
15
|
-
# * Creates an instance variable with the result.
|
16
|
-
#
|
17
|
-
|
18
|
-
class Foo
|
19
|
-
def self.bar(obj)
|
20
|
-
obj.foo!
|
21
|
-
fooinator = Test::Fooinator.new
|
22
|
-
@fooified_obj = fooinator.foo(obj)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
#
|
27
|
-
# From an architectural perspective, there is really nothing
|
28
|
-
# wrong with this class/method. Even from a design perspective
|
29
|
-
# the only thing that you can really criticize is that the
|
30
|
-
# method is a little vague in terms of what it does, and that's just
|
31
|
-
# because we don't know what "fooing" is.
|
32
|
-
#
|
33
|
-
# But there are a few other problems.
|
34
|
-
#
|
35
|
-
# You will often hear rubyists talking about the wonder of
|
36
|
-
# "duck typing". Developers can pass anything they want to the "bar"
|
37
|
-
# method above and it'll still work so long as the parameter responds
|
38
|
-
# to certain methods etc.
|
39
|
-
#
|
40
|
-
# You will almost as often hear programmers freaking out about how
|
41
|
-
# chaotic and crazy this approach is! Arguments about such matters usually
|
42
|
-
# eventually gravitate towards the problem being that ruby is not
|
43
|
-
# strongly typed. The knowledge about what "ducks" can be sent to the
|
44
|
-
# :bar method is not stored anywhere: it's implicit in obj's usage. Great if
|
45
|
-
# you were the developer of the method and you understand it completely,
|
46
|
-
# but what if it's someone else?
|
47
|
-
#
|
48
|
-
# In the above example, in order to see which type of object they can
|
49
|
-
# pass to the "bar" method, a developer would at the very least need
|
50
|
-
# to:
|
51
|
-
#
|
52
|
-
# * Grep for "foo!" to see what the hell it does
|
53
|
-
# * See what the hell a "Fooinator" does
|
54
|
-
# * Look at the fooinator#foo method to see what the hell THAT does
|
55
|
-
# * Look for all instances of @fooified_obj and see what the hell THOSE DO
|
56
|
-
#
|
57
|
-
# And even if they do all that, there's a chance they might miss something.
|
58
|
-
# If they do miss something, this will likely manifest itself in an error
|
59
|
-
# further up the stack, such as method missing exceptions etc., in places
|
60
|
-
# OTHER than where the actual mistake was made.
|
61
|
-
#
|
62
|
-
# Nightmare!
|
63
|
-
#
|
64
|
-
# Wouldn't it be nice if the "bar" method did a check or two, or even raised
|
65
|
-
# an exception when it was passed an object that wasn't compatible with its
|
66
|
-
# functionality? You could do this yourself manually:
|
67
|
-
#
|
68
|
-
|
69
|
-
class Foo
|
70
|
-
def self.bar(obj)
|
71
|
-
raise TypeError unless obj.respond_to?(:foo!) && obj.respond_to?(:chunky_bacon)
|
72
|
-
|
73
|
-
obj.foo!
|
74
|
-
fooinator = Test::Fooinator.new
|
75
|
-
@fooified_obj = fooinator.foo(obj)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
#
|
80
|
-
# But now you've got more problems!
|
81
|
-
#
|
82
|
-
# * The method is now becoming longer and messier.
|
83
|
-
# * The "knowledge" about what :bar can receive is stored within the
|
84
|
-
# :bar method itself, but that knowledge may be needed elsewhere.
|
85
|
-
#
|
86
|
-
# Wouldn't it be nice if there was a neat way to store knowledge about all
|
87
|
-
# our ducks in one place, and to be able to reference them whenever we need to.
|
88
|
-
#
|
89
|
-
# Although duck typing is seen as a ruby technique, it is actually
|
90
|
-
# used in other languages too but the contract in strongly typed languages
|
91
|
-
# is made explicit through the use of interfaces. In c#, for example:
|
92
|
-
#
|
93
|
-
# public string foo(IMyInterface interface)
|
94
|
-
# {
|
95
|
-
# //your implementation here. A confused developer
|
96
|
-
# //could go off and look in IMyInterface to see what
|
97
|
-
# //this "duck" is meant to do.
|
98
|
-
# }
|
99
|
-
#
|
100
|
-
# As ruby is not strongly typed, you don't have this reference and the code
|
101
|
-
# becomes impossible for other developers to use!
|
102
|
-
#
|
103
|
-
# This is the problem duckpond hopes to solve.
|
104
|
-
#
|
105
|
-
|
106
|
-
class Reader
|
107
|
-
def intrigued?
|
108
|
-
true
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
the_reader = Reader.new
|
113
|
-
expect(the_reader).to be_intrigued
|
114
|
-
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
|
data/spec/the_solution_spec.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
module DuckPond
|
4
|
-
describe "The Solution" do
|
5
|
-
context 'when a method is called' do
|
6
|
-
it 'pays to use duckpond' do
|
7
|
-
|
8
|
-
#
|
9
|
-
# First, declare a duck.
|
10
|
-
#
|
11
|
-
|
12
|
-
class ObjContract < DuckPond::Contract
|
13
|
-
has_method :to_s
|
14
|
-
has_method :length
|
15
|
-
end
|
16
|
-
|
17
|
-
#
|
18
|
-
# Grab a pair of binoculars...
|
19
|
-
#
|
20
|
-
|
21
|
-
binoculars = DuckPond::Binoculars.new
|
22
|
-
|
23
|
-
#
|
24
|
-
# Now, lets say you have an object...
|
25
|
-
#
|
26
|
-
|
27
|
-
obj = "Hello World"
|
28
|
-
|
29
|
-
#
|
30
|
-
# You can "spot" it with your binoculars...
|
31
|
-
#
|
32
|
-
|
33
|
-
sighting = binoculars.identify(obj)
|
34
|
-
|
35
|
-
#
|
36
|
-
# And now you can compare your sightings with your ducks!
|
37
|
-
#
|
38
|
-
|
39
|
-
result = sighting.quacks_like?(ObjContract)
|
40
|
-
expect(result).to be true
|
41
|
-
|
42
|
-
#
|
43
|
-
# You could check all of this within a method:
|
44
|
-
#
|
45
|
-
|
46
|
-
binoculars = DuckPond::Binoculars.new
|
47
|
-
sighting = binoculars.identify(obj)
|
48
|
-
raise TypeError unless sighting.quacks_like?(ObjContract)
|
49
|
-
|
50
|
-
#
|
51
|
-
# But because this trio of lines are so common, a
|
52
|
-
# convinience method exists to perform all three at
|
53
|
-
# once:
|
54
|
-
#
|
55
|
-
|
56
|
-
result = DuckPond::Binoculars.confirm(obj, ObjContract)
|
57
|
-
expect(result).to be true
|
58
|
-
|
59
|
-
#
|
60
|
-
# The "bang" version will raise an error if the object isn't the duck.
|
61
|
-
#
|
62
|
-
|
63
|
-
expect { DuckPond::Binoculars.confirm!(Object.new, ObjContract) }.to raise_error TypeError
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
# So, going back to that example from the problem_spec
|
68
|
-
#
|
69
|
-
|
70
|
-
class Foo
|
71
|
-
def self.bar(obj)
|
72
|
-
DuckPond::Binoculars.confirm!(obj,ObjConrtact)
|
73
|
-
|
74
|
-
obj.foo!
|
75
|
-
fooinator = Test::Fooinator.new
|
76
|
-
@fooified_obj = fooinator.foo(obj)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
|
81
|
-
#
|
82
|
-
# Viola!
|
83
|
-
#
|
84
|
-
# Final Tips:
|
85
|
-
#
|
86
|
-
# * Contracts should be commented liberally. They form the descripiton
|
87
|
-
# of a contract just as interfaces would in strongly typed languages.
|
88
|
-
# Developers will be prompted to consult the duck when they see a pair
|
89
|
-
# of binoculars in your code, so make sure they find what they're looking
|
90
|
-
# for.
|
91
|
-
#
|
92
|
-
# * Rather than writing the duckpond functionality directly within the methods
|
93
|
-
# themselves, you could easily put them in hooks or method aliases. If you do this
|
94
|
-
# though, remember that the point of this app is to make things *more* visible, not
|
95
|
-
# less, so don't go tucking these duck checks too far away from where they are needed
|
96
|
-
# or leave an obvious trail if you do!
|
97
|
-
#
|
98
|
-
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|