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.

  1. SRP (Single responsibility principle)

  2. Typed Variables (especially needed in Ruby)

  3. 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.