One of my projects switched persistence mechanisms from KiokuDB to DBIx::Class recently. KiokuDB had the benefit of simplicity—it was very easy to develop the initial project without worrying about managing table schemas and upgrades, but as the project matured, the benefits of DBIC became more apparent.
I like how KiokuDB persists plain old Moose objects. If you understand how your object graph works, there's little effective difference between a Moose object you create yourself or one you retrieve from your object store. Less so DBIC objects, in my experience. This isn't a flaw. It's merely a difference.
The difference became apparent when I started to port a fundamental test file as part of this migration. This test file exercises the document parser. As part of the application, the document parser uses a variant of the Readability algorithm to extract the meaningful portions of HTML documents. The test file itself uses a data-driven approach, where a t/files/documents/ directory holds multiple YAML files containing real data we've encountered along with the expected (correct) results.
#! perl
use Modern::Perl;
use Test::More;
use YAML::XS;
use lib 't/lib';
__PACKAGE__->main( 'App::DocParser', @ARGV );
sub create_docparser
{
return App::DocParser->new(
url => 'http://www.example.com/some_url',
url_base => 'http://www.example.com/',
@_,
);
}
sub main
{
my ($test, $module, @files) = @_;
use_ok( $module ) or exit;
if (@files)
{
$test->run_file_content_tests( $module, @files );
}
else
{
$test->test_find_content_files( $module );
}
done_testing();
}
sub test_find_content_files
{
run_file_content_tests( @_, glob 't/files/documents/*.yaml' );
}
sub run_file_content_tests
{
my ($test, $module, @files) = @_;
for my $file (@files)
{
my $example = YAML::XS::LoadFile( $file );
my $docparser = create_docparser( %$example );
like $docparser->content, $example->{content_regex},
"DocParser should find matching content for $example->{desc}";
is 0 + @{ $docparser->links }, $example->{link_count},
'... with the right number of links';
}
}
The previous version of this code created App::Document
objects—instances of the same class persisted into the object graph with
KiokuDB—and called functions in the DocParser
namespace to
find various pieces of context. A straightforward port of this test file to
DBIC meant I'd have had to figure out how to instantiate dummy DBIC-alike
objects representing documents to pass to the DocParser
functions.
I thought about that for a few minutes. I pondered reaching for Test::MockObject::Extends. Then I didn't.
What's important in this test isn't that App::DocParser
conforms to a specific interface governed by the persistence layer. That's
irrelevant. Its only relevance is how well it identifies and extracts
relevant information from given data.
I turned that namespace full of utility functions into a class. I gave that
class the specific attributes on which it needs to operate, effectively giving
instances of the class the responsibility to manage the data on which they
operate. You can see the result: a simple constructor call creates a
DocParser
object without worrying about setting up test data in an
example database or building a mock object framework which behaves sufficiently
like a DBIC row object.
I really like the object responsibility pattern which Moose seems to encourage, where you create an object by passing all relevant data to its constructor and then operate on that data as needed (perhaps governed by laziness in attribute accessors). You get the benefit of being able to assume that a constructed object is in a safe and sane state as well as a decoupling of data dependencies.
Once I realized that this approach would lead to better code, it took twenty minutes to Moosify the document parser and one minute to change a couple of lines of code to port the test to the new framework. I count that as a sign of effective design.