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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed77b024ca7fdcad68dff17391aeb0de607e7da9
4
- data.tar.gz: e00d0cc54ecc3dea1b83eb7a75bcea3382b784bc
3
+ metadata.gz: 49a48ab6dd27f5acfb06ff204a3679299aabab0d
4
+ data.tar.gz: adeae633ba2f56c289cff8b0f3ae902e2f63e2d2
5
5
  SHA512:
6
- metadata.gz: f79bd621cd72f4e2ca362633e25c4433ea9ba3f1242dd69ae2410061035768bef3f2c8b38cf91fe924394f01ac6c4801e71fba1fb121bc6eff1504ddc972d75b
7
- data.tar.gz: d214ef206b663b6d7b84b7cf05239d4f4105ff68d460934200e53f36e709952c70ba3cb4a1c2bbe2d8ab3f9de4e011665bfb2ed82bf407a27c20a29a531e46d6
6
+ metadata.gz: 21d67910df3b44f7aac195706ab565b3e20b23549d73251f8ba7dba36f31d033c2e293333d3c17b33b83555b23a345b6b73e97be7ba3cf49b9c19338d64b6c3a
7
+ data.tar.gz: 2cbf35d78a704430210fff5e7fe78aea5da7c04aba15d484e420501a8b5e4ec26948f6682e019687fed20fd32ed04964d9ea5d7615dcfd5b091b8f5a32184c36
data/.travis.yml CHANGED
@@ -1,8 +1,9 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - "1.9.2"
5
- - "1.9.3"
6
- - "2.0.0"
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.1
7
+ - jruby-19mode
7
8
 
8
9
  script: bundle exec rspec spec
data/Gemfile CHANGED
@@ -4,6 +4,6 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :test do
7
- gem 'rake'
8
7
  gem 'rspec'
8
+ gem 'coveralls', require: false
9
9
  end
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
- rake (10.3.2)
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
  [![Gem Version](https://badge.fury.io/rb/duckpond.svg)](http://badge.fury.io/rb/duckpond)
2
2
  [![Build Status](https://travis-ci.org/mikeyhogarth/duckpond.svg?branch=master)](https://travis-ci.org/mikeyhogarth/duckpond)
3
+ [![Coverage Status](https://img.shields.io/coveralls/mikeyhogarth/duckpond.svg)](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
- The spec folder has two files that go through the purpose of this gem
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
- * [the_problem_spec](spec/the_problem_spec.rb) - This outlines the problems with duck typing.
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 '[the_solution_spec](spec/the_solution_spec.rb)', but in a nutshell you can create
42
- "contract" classes by inheriting from DuckPond::Contract. This file should be commented
43
- extensively as it describes the contract the duck represents. The "has_method" method
44
- can be used to specify methods as symbols.
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 use "binoculars" to see if objects quack like
52
- that duck:
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
- #This syntax gets all the comparison done in one line
62
- DuckPond::Binoculars.confirm(obj, MyContract)
63
- => true
53
+ MyContract.fulfilled_by? "Hello"
54
+ => true
55
+ MyContract.fulfilled_by? 12
56
+ => false
64
57
 
65
- #This syntax does the same thing, but raises an excaption instead of returning false
66
- DuckPond::Binoculars.confirm!(obj, MyContract)
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
- Ducks can be combined into composite "super contracts" - contracts which are made up of various other contracts. This ties in with the reccomendation of preferring composition over inheritance:
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
- A *serious duck* might look like this:
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
- DuckPond::Binoculars.confirm!(email, IEmailable)
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 = Duckpond::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}
@@ -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
- # The main quack checking method for a duck
39
+ # fulfilled_by?
42
40
  #
43
- def quacks_like?(method)
44
- clauses.include?(method)
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
@@ -1,3 +1,3 @@
1
- module Duckpond
2
- VERSION = "1.0.2"
1
+ module DuckPond
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/duckpond.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'duckpond/version'
2
2
  require 'duckpond/contract'
3
- require 'duckpond/binoculars'
4
- require 'duckpond/sighting'
3
+ require 'duckpond/inspection'
5
4
 
6
- module Duckpond
5
+ module DuckPond
7
6
  end
@@ -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 Sighting do
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
- sighting = Sighting.new(obj)
10
- expect(sighting.sighted_object).to be obj
8
+ analysis = Inspection.new(obj)
9
+ expect(analysis.subject).to be obj
11
10
  end
12
11
  end
13
12
 
14
- describe '#quacks_like?' do
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
- sighting = Sighting.new(obj)
24
- expect(sighting.quacks_like? MyChunkyBaconContract).to be true
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
- sighting = Sighting.new(obj)
32
- expect(sighting.quacks_like? MyChunkyBaconContract).to be false
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
@@ -1,2 +1,5 @@
1
1
  require 'duckpond'
2
2
  require 'classes/fooinator'
3
+ require 'coveralls'
4
+
5
+ Coveralls.wear!
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.2
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-17 00:00:00.000000000 Z
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/sighting.rb
62
+ - lib/duckpond/inspection.rb
61
63
  - lib/duckpond/version.rb
62
- - spec/binoculars_spec.rb
63
64
  - spec/classes/fooinator.rb
64
- - spec/duck_spec.rb
65
- - spec/sighting_spec.rb
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/duck_spec.rb
97
- - spec/sighting_spec.rb
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
@@ -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
-
@@ -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
@@ -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
@@ -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
-
@@ -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