How to instantiate Moose classes from a big hash

2019-05-17 07:49发布

问题:

I have a big hash many levels deep, and I'd like to turn this hash into a set of Moose classes.

The hash looks something like this:

my %hash = (
    company => {
        id => 1,
        name => 'CorpInc',
        departments => [
            {
                id => 1,
                name => 'Sales',
                employees => [
                    {
                        id => 1,
                        name => 'John Smith',
                        age => '30',
                    },
                ],
            },
            {
                id => 2,
                name => 'IT',
                employees => [
                    {
                        id => 2,
                        name => 'Lucy Jones',
                        age => '28',
                    },
                    {
                        id => 3,
                        name => 'Miguel Cerveza',
                        age => '25',
                    },
                ],
            },
        ],
    }
);

And the Moose classes:

package Company;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');
1;

package Company::Department;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');
1;

package Company::Person;
use Moose;

has 'id'         => (is => 'ro', isa => 'Num');
has 'first_name' => (is => 'ro', isa => 'Str');
has 'last_name'  => (is => 'ro', isa => 'Str');
has 'age'        => (is => 'ro', isa => 'Num');
1;

Whats the best way to turn this hash into a Company object?

The options I've considered so far are:

  1. Manually loop the %hash, find the deepest "classes" (e.g Person), create these first, then manually add these to the newly created higher level classes (Department), and so on.
  2. Add some kind of coercion functionality to each class, which lets me do something like Company->new(%hash), and make each class create its own "subclasses" (via coercion)
  3. Convert the %hash into a structure similar to what MooseX::Storage would serialize to, then use MooseX::Storage to instatiate everything for me...

Any other ideas or suggestions?

回答1:

You could have a BUILDARGS handler which converts unblessed references in those slots to objects. Coercions is probably the best, but it takes more doing. (Unless this is all coming from a RDBMS, in which case use DBIx::Class).

#!/usr/bin/env perl

use warnings;
use strict;

package Company;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');

sub BUILDARGS {
  my $self = shift;
  my $args = $self->SUPER::BUILDARGS(@_);
  @{ $args->{departments} } = 
    map { eval{ $_->isa('Company::Department') } ? $_ : Company::Department->new($_) }
    @{ $args->{departments} };

  return $args;
};

package Company::Department;
use Moose;

has 'id'   => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');

sub BUILDARGS {
  my $self = shift;
  my $args = $self->SUPER::BUILDARGS(@_);
  @{ $args->{employees} } = 
    map { eval{ $_->isa('Company::Person') } ? $_ : Company::Person->new($_) }
    @{ $args->{employees} };

  return $args;
};

package Company::Person;
use Moose;

has 'id'         => (is => 'ro', isa => 'Num');
has 'name'       => (is => 'ro', isa => 'Str');
has 'age'        => (is => 'ro', isa => 'Num');

package main;

my %hash = (
    company => {
        id => 1,
        name => 'CorpInc',
        departments => [
            {
                id => 1,
                name => 'Sales',
                employees => [
                    {
                        id => 1,
                        name => 'John Smith',
                        age => '30',
                    },
                ],
            },
            {
                id => 2,
                name => 'IT',
                employees => [
                    {
                        id => 2,
                        name => 'Lucy Jones',
                        age => '28',
                    },
                    {
                        id => 3,
                        name => 'Miguel Cerveza',
                        age => '25',
                    },
                ],
            },
        ],
    }
);

my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;


回答2:

I've used your option 2 several times and it worked fine for me. Last instance was inflating JIRA REST API results into real objects. Note that with coercions you can also lookup an existing instance by id and create only if it does not exist.

Edit: Here is some code to demonstrate those coercions:

package Company::Types;
use Moose::Util::TypeConstraints;

subtype 'Company::Departments', as 'ArrayRef[Company::Department]';
coerce  'Company::Departments', from 'ArrayRef', via {
    require Company::Department;
    [ map { Company::Department->new($_) } @$_ ]
};

subtype 'Company::Persons', as 'ArrayRef[Company::Person]';
coerce  'Company::Persons', from 'ArrayRef', via {
    require Company::Person;
    [ map { Company::Person->new($_) } @$_ ]
};

no Moose::Util::TypeConstraints;

and in those classes:

use Company::Types;

has 'departments' => (is => 'ro', isa => 'Company::Departments', coerce => 1);
has 'employees'   => (is => 'ro', isa => 'Company::Persons', coerce => 1);

then you can pass whole structure into Company constructor and all gets inflated properly.



标签: perl moose