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
...
Skip commands
“skip” prefixed data key names are reserved for command skipping feature. When data given, the XXX command won’t be invoked.
class Program
include Ccp::Commands::Composite
command Cmd1
command Cmd2
end
# normal case
Program.execute # => Both Cmd1 and Cmd2 will be called
# with skip
Program.execute(:skip_cmd1 => true) # => Both Cmd2 will be called
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.
* fixture_save : set true if you want this feature
* fixture_dir : set root dir of fixture (default: tmp/fixtures)
* fixture_kvs : file structure: :file|:dir (default: :file)
* fixture_ext : file format: :json|:yaml (default: :json)
* fixture_keys : save data only keys written in here
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, :fixture_save => true)
This will create following files.
% tree tmp/fixtures
tmp/fixtures
+- tsfc
+- stub.json
+- mock.json
1 directory, 2 files
% cat tmp/fixtures/tsfc/stub.json
{"a":1}
% cat tmp/fixtures/tsfc/mock.json
{"x":10}
Where, reading means stub and writing means mock.
Writing tests
Use them as stubs and expected data as you like.
describe TSFC do
it "should work" do
data = JSON.load(Pathname("tmp/fixtures/tsfc/stub.json").read{})
expected = JSON.load(Pathname("tmp/fixtures/tsfc/mock.json").read{})
cmd = TSFC.execute(data)
expected.each_pair do |key, val|
cmd.data[key].should == val
end
end
end
This code is highly versatile.
Filter commands
* fixture_save : specify command names (negations can be used as "!")
For example, imagine composite commands like this.
Program
+- Cmd1
+- Cmd2
+- Cmd3
In this case,
Program.execute(:fixture_save => true)
generates following files.
* tmp/fixtures/cmd1/*.json
* tmp/fixtures/cmd2/*.json
* tmp/fixtures/cmd3/*.json
* tmp/fixtures/program/*.json
If you want fixture data only for cmd2, run this.
Program.execute(:fixture_save => ["Cmd2"])
“!” is used for special syntax which means ‘negations’.
Program.execute(:fixture_save => ["!Program"])
will generates fixtures for cmd1,cmd2,cmd3 (in short “not program”).
Filter data
* fixture_keys : specify data key names (negations can be used as "!")
For example, imagine a command like this.
class Cmd
def execute
data[:x] = 10
data[:logger].debug "x is set"
end
end
This code will generate fixtures about ‘x’ and ‘logger’ cause it read those vars. In many case, we don’t need ‘logger’ object for test data, and furthermore, the logger object would be failed in Json serialization.
In this case, we want to filter save data. Try fixtures_keys!
Cmd.execute(:fixture_save => true, :fixture_keys => ['x'])
This will generate “stub.json” that contains only ‘x’. And, “!” can be used for negations as same as “Filter commands”.
Cmd.execute(:fixture_save => true, :fixture_keys => ['!logger'])
Static options
Static options(hard code) are also available.
class Cmd
include Ccp::Commands::Core
stub "tmp/stub.json"
mock "tmp/mock.json"
# keys ["x"]
save true
def execute
data[:a]
data[:x] = 10
end
end
Cmd.execute(:a=>1)
This generates “tmp/stub.json”, “tmp/mock.json”.
Test
Once you got fixtures, call “test” method to test it.
Cmd.test
# or
# Cmd.execute(:fixture_test=>true)
This automatically searchs fixtures when no fixture files are explicitly given. This mechanism is as same as ‘fixture_save’.