ccp
CCP is a Ruby library for Composite Command Programming that helps you to split spaghetti codes into pieces.
Websites
What is a Composite Command Programming?
There are three principles.
-
SRP (Single responsibility principle)
-
Typed Variables (especially needed in Ruby)
-
Explicit Data Dependencies
As you know, Ruby is a handy and powerful programming language. We can use variables without definitions and re-assign them even if type mismatch. Although it is very comfortable while you are writing, it would be painful for others.
CCP is a framework composed with above principles and a few debugging utils that gives you(as writer) a little restrictions and gives you(as reader) a readability.
Example
usual ruby code
class SomeOperation
def execute
# fetching data
# calculating it
# print results
end
end
Later, it would be a more complex and longer code like this.
class SomeOperation
def execute
@data = fetch_data
...
@result = calculate(@data)
...
print_results(@result)
end
def fetching_data
# accessing instance variables, and long code here
...
Let’s imagine a situation that you need to replace above “fetch” code after several years. It is too hard to see data dependencies especially about instance variables.
with CCP
class SomeOperation
include Ccp::Commands::Composite
command FetchData
command Calculate
command PrintResult
end
class FetchData # 1. SRP
include Ccp::Commands::Core
# {before,after} methods can be used like Design By Contract
def after
data.check(:fetched, {Symbol => [Float]}) # 2. Typed Variables
end
def execute
# fetching data...
data[:fetched] = ... # 3. Data Dependencies
end
end
class Calculate
...
All sub commands like FetchData,Calculate,PrintResult are executed in each scopes, and can share variables only via data object.
So you can easily refactor or replace FetchData unit because there are no implicit instance variable dependencies and futhermore all depencenies would be explicitly declared in “before”,“after” method.
execute
Just call a “execute” instance method.
cmd = SomeOperation.new
cmd.execute
invokers
Invokers::Base is a top level composite command. It acts same as composite except some special options. Those are profile, comment, logger.
class SomeOperation < Ccp::Invokers::Base
command FetchData # same as composite
command Calculate # same as composite
...
profile true # default false
comment false # default true
...
This profile option prints benchmarks of commands.
ruby -r some_operation -e 'SomeOperation.execute'
[43.3%] 2.5834830 FetchData#execute
[35.9%] 2.0710440 Calculate#execute
...
Fixtures
Let’s imagine a following command that just read :a and write :x.
class TSFC # TestSaveFixtureCmd
include Ccp::Commands::Core
def execute
data[:a] # read
data[:x] = 10 # write
end
end
This may be a part of sequncial commands. When we want to test only this command, usually we should prepare some fixture data for the ‘data’ object.
Generating fixtures
Ccp can automatically generate fixture data for each commands. Pass :save_fixture option to ‘execute’ class method to enable it.
* save_fixture : enable write fixture mode if true
* save_fixture_dir : set root dir of fixture (default: tmp/fixtures)
In above example, we can kick it with data like this.
TSFC.execute(:a => 1)
And if you want to geneate fixures for this command, just add :save_fixture.
TSFC.execute(:a => 1, :save_fixture => true)
This will create following files.
% tree tmp/fixtures
tmp/fixtures
└── tsfc
├── in.yaml
└── out.yaml
1 directory, 2 files
% cat tmp/fixtures/tsfc/*
---
:a: 1
---
:x: 10
Writing tests
Use them as stubs and expected data as you like.
describe TSFC do
it "should work" do
data = YAML.load(Pathname("tmp/fixtures/tsfc/in.yaml").read{})
expected = YAML.load(Pathname("tmp/fixtures/tsfc/out.yaml").read{})
cmd = TSFC.execute(data)
expected.each_pair do |key, val|
cmd.data[key].should == val
end
end
end
This code is highly versatile.