duckpond 1.0.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 +7 -0
- data/.gitignore +34 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +17 -0
- data/LICENSE +21 -0
- data/README.md +78 -0
- data/Rakefile +1 -0
- data/duckpond.gemspec +23 -0
- data/lib/duckpond/binoculars.rb +57 -0
- data/lib/duckpond/duck.rb +43 -0
- data/lib/duckpond/sighting.rb +36 -0
- data/lib/duckpond/version.rb +3 -0
- data/lib/duckpond.rb +7 -0
- data/spec/binoculars_spec.rb +52 -0
- data/spec/classes/fooinator.rb +10 -0
- data/spec/duck_spec.rb +53 -0
- data/spec/sighting_spec.rb +38 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/the_problem_spec.rb +120 -0
- data/spec/the_solution_spec.rb +102 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 515a4dbf8c5dfcc7ac533ebc75579053972755fe
|
4
|
+
data.tar.gz: 2b99253f7d6f981e307a01fa324ba20d3bb0dc87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 21b5db87d898f1c3759fed7c1beb9b254d57e32646daddaff7808d072ee42ca206d6f128b96656bd29d6996b1e565863ac0ca5a4aa1c14e43056ad751b3defae
|
7
|
+
data.tar.gz: 8a4f5c3892a69fbc10bb59deee0d20f0bdec7e046fb31ce0076d7dc7b8eb7a8c8bd767b8469aa8aea676d9fd31c6f6193ded7b5e3b3a03f7d6afc903edf08f0e
|
data/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/lib/bundler/man/
|
26
|
+
|
27
|
+
# for a library or gem, you might want to ignore these files since the code is
|
28
|
+
# intended to run in multiple environments; otherwise, check them in:
|
29
|
+
# Gemfile.lock
|
30
|
+
# .ruby-version
|
31
|
+
# .ruby-gemset
|
32
|
+
|
33
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
34
|
+
.rvmrc
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Mikey Hogarth
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Duckpond
|
2
|
+
|
3
|
+
Explicit duck typing for ruby.
|
4
|
+
|
5
|
+
|
6
|
+
## Inspiration
|
7
|
+
|
8
|
+
Duck Typing can make code confusing and unreadable, particularly
|
9
|
+
when multiple developers are working on the same project or when
|
10
|
+
projects are inherited by new developers.
|
11
|
+
|
12
|
+
The spec folder has two files that go through the purpose of this gem
|
13
|
+
in spec form. This would be a good place to start, although there is a crash
|
14
|
+
course below.
|
15
|
+
|
16
|
+
* [the_problem_spec](spec/the_problem_spec.rb) - This outlines the problems with duck typing.
|
17
|
+
|
18
|
+
* [the_solution_spec](spec/the_solution_spec.rb) - This outlines how duckpond gets around these problems.
|
19
|
+
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
gem 'duckpond'
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install duckpond
|
34
|
+
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Usage is demonstrated in 'the_solution_spec', but in a nutshell you can create
|
39
|
+
"duck" classes by inheriting from DuckPond::Duck. This file should be commented
|
40
|
+
extensively as it describes the contract the duck represents.
|
41
|
+
|
42
|
+
class MyDuck < DuckPond::Duck
|
43
|
+
quacks_like :length
|
44
|
+
end
|
45
|
+
|
46
|
+
Once you've declared a duck, you can use "binoculars" to see if objects quack like
|
47
|
+
that duck:
|
48
|
+
|
49
|
+
obj = "Hello World"
|
50
|
+
sighting = DuckPond::Binoculars.identify(obj)
|
51
|
+
sighting.quacks_like? MyDuck
|
52
|
+
=> true
|
53
|
+
|
54
|
+
There are other syntaxes:
|
55
|
+
|
56
|
+
#This syntax gets all the comparison done in one line
|
57
|
+
DuckPond::Binoculars.confirm(obj, MyDuck)
|
58
|
+
=> true
|
59
|
+
|
60
|
+
#This syntax does the same thing, but raises an excaption instead of returning false
|
61
|
+
DuckPond::Binoculars.confirm!(obj, MyDuck)
|
62
|
+
|
63
|
+
|
64
|
+
Ducks can be combined into composite "super ducks" using the looks_like method - ducks which are made up of various other ducks:
|
65
|
+
|
66
|
+
class MySuperDuck < DuckPond::Duck
|
67
|
+
looks_like MyDuck
|
68
|
+
looks_like MyOtherDuck
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
## Contributing
|
73
|
+
|
74
|
+
1. Fork it
|
75
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
76
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
77
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
78
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/duckpond.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'duckpond/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "duckpond"
|
8
|
+
spec.version = Duckpond::VERSION
|
9
|
+
spec.authors = ["Mikey Hogarth"]
|
10
|
+
spec.email = ["mikehogarth20@hotmail.com"]
|
11
|
+
spec.description = %q{Explicit duck-typing for ruby}
|
12
|
+
spec.summary = %q{Explicit duck-typing for ruby}
|
13
|
+
spec.homepage = "https://github.com/mikeyhogarth/duckpond"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 0"
|
23
|
+
end
|
@@ -0,0 +1,57 @@
|
|
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 WrongDuckError < 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 WrongDuckError unless confirm(obj, duck)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# DuckPond::Duck
|
3
|
+
#
|
4
|
+
# Ducks are essentially "contracts" or "interfaces". They are not intended
|
5
|
+
# to ever be instantiated, and are completely configured at the class level.
|
6
|
+
#
|
7
|
+
module DuckPond
|
8
|
+
class Duck
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def quacks
|
12
|
+
@quacks ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# quacks_like
|
17
|
+
#
|
18
|
+
# Use this to specify that this duck quacks with
|
19
|
+
# a certain method.
|
20
|
+
#
|
21
|
+
def quacks_like(method)
|
22
|
+
quacks << method
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# looks_like
|
27
|
+
#
|
28
|
+
# Use this to specify that this duck looks like
|
29
|
+
# another duck.
|
30
|
+
#
|
31
|
+
def looks_like(other_duck)
|
32
|
+
other_duck.quacks.each do |other_ducks_quacking|
|
33
|
+
quacks_like other_ducks_quacking
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def quacks_like?(method)
|
38
|
+
quacks.include?(method)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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.quacks.each do |quack|
|
31
|
+
return false unless @sighted_object.respond_to? quack
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/duckpond.rb
ADDED
@@ -0,0 +1,52 @@
|
|
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 StringDuck < Duck
|
28
|
+
quacks_like :to_s
|
29
|
+
quacks_like :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", StringDuck)).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, StringDuck)).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, StringDuck)}.to raise_error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/spec/duck_spec.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DuckPond
|
4
|
+
|
5
|
+
describe Duck do
|
6
|
+
describe 'attributes' do
|
7
|
+
context '.quackings' do
|
8
|
+
it 'responds to and memoizes the result' do
|
9
|
+
expect(Duck).to respond_to :quacks
|
10
|
+
expect(Duck.quacks).to be_an Array
|
11
|
+
expect(Duck.quacks).to be_empty
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class MyDuck < Duck
|
18
|
+
quacks_like :foo
|
19
|
+
quacks_like :bar
|
20
|
+
end
|
21
|
+
|
22
|
+
describe MyDuck do
|
23
|
+
context '.quackings' do
|
24
|
+
it 'quacks like foo and bar' do
|
25
|
+
quacks = MyDuck.quacks
|
26
|
+
expect(quacks.length).to eq 2
|
27
|
+
expect(quacks).to include :foo
|
28
|
+
expect(quacks).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(MyDuck.quacks_like? :foo).to be true
|
35
|
+
expect(MyDuck.quacks_like? :chunky_bacon).to be false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MySimilarDuck < Duck
|
41
|
+
looks_like MyDuck
|
42
|
+
quacks_like :chunky_bacon
|
43
|
+
end
|
44
|
+
|
45
|
+
describe MySimilarDuck do
|
46
|
+
it 'retains its parents quackings' do
|
47
|
+
expect(MySimilarDuck.quacks_like? :chunky_bacon).to be true
|
48
|
+
expect(MySimilarDuck.quacks_like? :foo).to be true
|
49
|
+
expect(MySimilarDuck.quacks_like? :bar).to be true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module DuckPond
|
4
|
+
describe Sighting do
|
5
|
+
|
6
|
+
describe '#initialize' do
|
7
|
+
it 'is constructed using any ruby object' do
|
8
|
+
obj = Object.new
|
9
|
+
sighting = Sighting.new(obj)
|
10
|
+
expect(sighting.sighted_object).to be obj
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#quacks_like?' do
|
15
|
+
|
16
|
+
class MyChunkyBaconDuck < Duck
|
17
|
+
quacks_like :chunky_bacon
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when the sighted object quacks like the duck' do
|
21
|
+
it 'returns true' do
|
22
|
+
obj = OpenStruct.new(:chunky_bacon => :mmm)
|
23
|
+
sighting = Sighting.new(obj)
|
24
|
+
expect(sighting.quacks_like? MyChunkyBaconDuck).to be true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when the sighted object does not quack like the duck' do
|
29
|
+
it 'returns false' do
|
30
|
+
obj = OpenStruct.new(:vegan_fallafal => :yuck)
|
31
|
+
sighting = Sighting.new(obj)
|
32
|
+
expect(sighting.quacks_like? MyChunkyBaconDuck).to be false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,120 @@
|
|
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
|
+
|
@@ -0,0 +1,102 @@
|
|
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 ObjDuck < DuckPond::Duck
|
13
|
+
quacks_like :to_s
|
14
|
+
quacks_like :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?(ObjDuck)
|
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?(ObjDuck)
|
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, ObjDuck)
|
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, ObjDuck) }.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,ObjDuck)
|
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
|
+
# * Ducks 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
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: duckpond
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mikey Hogarth
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Explicit duck-typing for ruby
|
42
|
+
email:
|
43
|
+
- mikehogarth20@hotmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- Gemfile
|
50
|
+
- Gemfile.lock
|
51
|
+
- LICENSE
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- duckpond.gemspec
|
55
|
+
- lib/duckpond.rb
|
56
|
+
- lib/duckpond/binoculars.rb
|
57
|
+
- lib/duckpond/duck.rb
|
58
|
+
- lib/duckpond/sighting.rb
|
59
|
+
- lib/duckpond/version.rb
|
60
|
+
- spec/binoculars_spec.rb
|
61
|
+
- spec/classes/fooinator.rb
|
62
|
+
- spec/duck_spec.rb
|
63
|
+
- spec/sighting_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- spec/the_problem_spec.rb
|
66
|
+
- spec/the_solution_spec.rb
|
67
|
+
homepage: https://github.com/mikeyhogarth/duckpond
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 2.2.2
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: Explicit duck-typing for ruby
|
91
|
+
test_files:
|
92
|
+
- spec/binoculars_spec.rb
|
93
|
+
- spec/classes/fooinator.rb
|
94
|
+
- spec/duck_spec.rb
|
95
|
+
- spec/sighting_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
- spec/the_problem_spec.rb
|
98
|
+
- spec/the_solution_spec.rb
|