++ed by:

2 non-PAUSE users.

Chad Granum

NAME

DCI - Collection of utilities for writing perl code that fits the DCI methodology.

DESCRIPTION

Defines the DCI concepts of Context DCI::Context and Role DCI::Cast.

The DCI concept of a 'role' differs from the concept of roles as defined by Moose. Because of this the term 'Cast' is used by DCI to refer to DCI Roles. This will hopefulyl avoid some confusion.

SYNOPSYS

Here we will implement a complete algorthm including Data classes, a Context class, and Casts. We will implement a banking transfer as that is a common example used to explain DCI.

This example is implemented in the t/bank.t test if you wish to see it in action.

THE DATA CLASSES

Account

This is an implementation of a bank account that follows the DCI principal that Data objects should be fairly dumb.

    package Account;
    use strict;
    use warnings;

    sub new {
        my $class = shift;
        my ($balance) = @_;

        return bless( \$balance, $class );
    }

    sub add_balance {
        my $self = shift;
        my ( $delta ) = @_;
        $$self += $delta;
    }

    sub subtract_balance {
        my $self = shift;
        my ( $delta ) = @_;
        $$self -= $delta;
    }

    sub get_balance {
        my $self = shift;
        return $$self;
    }

Log

This is a package for a log or recipt class, also dumb.

    package Log;
    use strict;
    use warnings;

    sub new {
        my $class = shift;
        return bless( [], $class );
    }

    sub record {
        my $self = shift;
        my ($line) = @_;
        chomp( $line );
        push @$self => $line;
    }

THE CONTEXT (Use-Case)

This is the package for our use case, a transfer between accounts.

    package Transfer;
    use strict;
    use warnings;

    use DCI::Context;

    # Define the roles
    cast from_acct => 'Transfer::FromAccount',
         dest_acct => 'Transfer::DestAccount',
         # Roles with no cast class will be used as-is
         recipt    => undef,
         ammount   => undef;

    # Create a sugar function that is exported when this use-case is imported.
    sugar 'account_transfer';

    sub start_transaction {
        my $self = shift;
        $self->recipt->record( "Transaction started" );
        # ... Stuff to record current state in case of issue
    }

    sub rollback_transaction {
        my $self = shift;
        my ( $error ) = @_;
        $self->recipt->record( "Transaction aborted: $error" );
        # ... Stuff to restore previous state
    }

    sub commit_transaction {
        my $self = shift;
        $self->recipt->record( "Transaction completed" );
        # ... Stuff to finalize state
    }

    sub run {
        my $self = shift;

        $self->start_transaction;

        my $success = eval {
            $self->from_acct->verify_funds();
            $self->from_acct->withdrawl();
            $self->dest_acct->deposit();
            1;
        };

        if ( $success ) {
            $self->commit_transaction;
        }
        else {
            my $error = $@;
            $self->rollback_transaction( $error );
        }
    }

THE ROLES (CAST)

We define 3 roles, from_acct, dest_acct, and recipt.

Transfer::FromAccount

    package Transfer::FromAccount;
    use strict;
    use warnings;

    use DCI::Cast;

    # Require that core class is an Account object.
    restrict_core qw/Account/;

    sub verify_funds {
        my $self = shift;
        my $ammount = $self->CONTEXT->ammount;
        die "Origin account has insufficient funds\n" unless $ammount <= $self->get_balance;
    }

    sub withdrawl {
        my $self = shift;
        my $ammount = $self->CONTEXT->ammount;
        $self->subtract_balance( $ammount );
        $self->CONTEXT->recipt->record( "$ammount removed from origin account" );
    }

Transfer::DestAccount

    package Transfer::DestAccount;
    use strict;
    use warnings;

    use DCI::Cast;

    restrict_core qw/Account/;

    sub deposit {
        my $self = shift;
        my $ammount = $self->CONTEXT->ammount;
        $self->add_balance( $ammount );
        $self->CONTEXT->recipt->record( "$ammount added to destination account" );
    }

PUTTING IT ALL TOGETHER

USING THE SUGAR FUNCTION

    # This will import the 'account_transfer()' function.

    use Transfer;
    my $from = Account->new( 1000 );
    my $to = Account->new( 100 );
    my $log = Log->new();

    Transfer->import();

    account_transfer(
        from_acct => $from,
        dest_acct => $to,
        recipt    => $log,
        ammount   => 500,
    );

    is( $from->get_balance, 500, "500 removed from origin account (sugar)" );
    is( $to->get_balance,   600, "500 added to dest account (sugar)" );

    is_deeply(
        $log,
        [
            'Transaction started',
            '500 removed from origin account',
            '500 added to destination account',
            'Transaction completed'
        ],
        "Recipt is accurate (sugar)"
    );

SUCCESSFUL TRANSFER

    my $from = Account->new( 1000 );
    my $to = Account->new( 100 );
    my $log = Log->new();

    my $context = Transfer->new(
        from_acct => $from,
        dest_acct => $to,
        recipt    => $log,
        ammount   => 500,
    );

    $context->run();

    is( $from->get_balance, 500, "500 removed from origin account" );
    is( $to->get_balance,   600, "500 added to dest account" );

    is_deeply(
        $log,
        [
            'Transaction started',
            '500 removed from origin account',
            '500 added to destination account',
            'Transaction completed'
        ],
        "Recipt is accurate"
    );

FAILED TRANSFER

    my $from = Account->new( 100 );
    my $to = Account->new( 100 );
    my $log = Log->new();

    my $context = Transfer->new(
        from_acct => $from,
        dest_acct => $to,
        recipt    => $log,
        ammount   => 500,
    );

    $context->run();

    is( $from->get_balance, 100, "Transaction failed, balance uneffected" );
    is( $to->get_balance,   100, "Transaction failed, balance uneffected" );

    is_deeply(
        $log,
        [
            'Transaction started',
            'Transaction aborted: Origin account has insufficient funds',
        ],
        "Recipt is accurate"
    );

SEE ALSO

DCI::Context
DCI::Cast

DCI Overview

DCI Stands for Data, Context, Interactions. It attempts to solve the problems of OOP. DCI was designed and proposed by the same guy that created the MVC concept that has become hugely successful. The key to DCI is a seperation of concepts:

Data, what the system is.
Context, A use case.
Interactions, How objects behave within a context.

The idea is to create data objects that have no algorithm or business logic at all. These would be very simple "dumb" objects. A good example would be ORM objects that get used in many contexts. The key is to only add methods which make sence without any knowledge of the business logic or use cases in which they will participate.

Once you have your data objects you then move on to a context. A context itself can be thought of as an object. A context implements a use case, which could be an encapsulated bit of business logic, or an algorithm. The use case object would keep track of objects necessary to complete the task. The context keeps track of these items by the concept of what role they will play.

In DCI the concept of a role only superficially resembles roles as they are implemented by Moose. In Moose a role is essentially a mixin with methods that get injected into a class as soon as the role is used, and then they remain present for the life of the class. In DCI roles can also be thought of as mixins, however the methods they contain should only be present in your data object when it is used in context.

To wrap up:

Data objects

Such as objects in an ORM, should not contain business or algorithm logic. Only methods that make sence without any context belong in the data objects.

Context objects

Implement an algorithm by defining roles, assigning data objects to roles, and then kicking off the interactions between the roles.

Roles

Collections of methods used for interactions between objects in a specific use-case (context)

DCI RESOURCES

http://www.artima.com/articles/dci_vision.html
http://en.wikipedia.org/wiki/Data,_Context_and_Interaction
https://sites.google.com/a/gertrudandcope.com/www/thedciarchitecture
http://oredev.org/videos/dci--re-thinking-the-foundations-of-oo

AUTHORS

Chad Granum exodist7@gmail.com

COPYRIGHT

Copyright (C) 2011 Chad Granum

DCI is free software; Standard perl licence.

DCI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.