Explicit duck typing for ruby.
Duck Typing can make code confusing and unreadable, particularly when multiple developers are working on the same project or when projects are inherited by new developers.
the_problem - This outlines the problems with duck typing.
the_solution - This outlines how duckpond gets around these problems.
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install duckpond
There is now a screencast demonstrating the duckpond gem!
Usage is demonstrated in 'the_solution', but in a nutshell you create "contract" classes by inheriting from DuckPond::Contract. These classes should be commented extensively.
The "has_method" method is used to specify which methods the contract expects to see. The following contract describes classes which respond to #length and #to_s
class MyContract < DuckPond::Contract has_method :length has_method :to_s end
Once you've declared a contract, you can compare objects to it to see if the contract is fulfilled by the object:
MyContract.fulfilled_by? "Hello" => true MyContract.fulfilled_by? 12 => false
There is also a "bang" version of the #fulfilled_by method, that raises an error instead of returning false. The error message details why it got raised.
MyContract.fulfilled_by! :foo => DuckPond::Contract::ContractInfringementError: One or more clauses from MyContract were not fulfilled by :foo (Symbol) Expected subject to respond to method 'length'
Contracts 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:
class MyCompositeConrtact < DuckPond::Contract include_clauses_from MyContract include_clauses_from MyOtherContract end
Another feature of duckpond is the ability to specify what your expected result from a contract should be. For example, the following contract expects a subject to have a method called "even" and for that method, when called, to return a true value:
class MyEvenContract < DuckPond::Contract has_method :even?, responds_with: true end
If the method needs args, you can specify those too. This next contract will pass the string "Hell" to the #include? method, and expect the value to be true. This contract will therefore be satisfied by strings containing the word "Hell" that are five characters long (such as the word "Hello"):
class MyHellContract < DuckPond::Contract has_method :include?, responds_with: true, given_args: "Hell" has_method :length, responds_with: 5 end
Finally, if you really want to get down and dirty with interrogating method results, you can use a block thusly (the result of calling the method will be yielded to the block):
class MyBlockContract < DuckPond::Contract has_method :length do |method_result| method_result == 5 end end class MyOtherBlockContract < DuckPond::Contract has_method :include?, given_args: "Hell" do |method_result| method_result == true end end
Note: The method will only be called if you use the :reponds_with option. Otherwise, it just tests for the method's existence.
In the real world, a contract might look like this:
class IEmailable < DuckPond::Contract #send: should send the results of :message via email to :to has_method :send #to: Should be an email address to which this will be sent has_method :to #message: The message to send has_method :message end
And then be implemented in a method like this:
class Emailer def send(email) IEmailable.fulfilled_by! email email.send end end
CI tests exist for the following rubies, other versions are not supported but should still work;
- Fork it
- Create your feature branch (
git checkout -b my-new-feature)
- Commit your changes (
git commit -am 'Add some feature')
- Push to the branch (
git push origin my-new-feature)
- Create new Pull Request