第8章 Mojolicious ベースにリファクタリングをする(その1)

前回までで構造体を変更するにはどうすれば良いかを考えてみました、今回は具体的にどのようなやり方で既存のコードを変更してゆくかみていきましょう。

今回やることは既存のアプリに新しい機能を加えるわけではなく、保守がしやすい形に書き換えていきます、こういう作業を一般的にはリファクタリングと言ったりします。

リファクタリングというのはやってみると思ったより時間がかかることが多いです、既存の挙動をかえないようにコードを書き換えていくのは場合よってはあたらしく機能を追加するより難しい場合もあります。 仕事としてアプリ開発を行なっている場合どうしても時間コストというものを意識しなくてはいけない状況というのはありますので、今回のように構造体を変更するようなリファクタリングを行うのは 時間コストを考えると難しい場合もあります。この辺の成果物を仕上げていくことと、少ない時間で成果物を生み出すことのバランス感覚は経験のなかでしか学ぶのは難しいのかもしれません。

今回は作業内容が多いので第8回目は4回に分けて解説してゆきます。

8.1 モデルロジック

リファクタリングをすすめていくのに役に立つのがテストコードです。リファクタリングを行なった後は必ずテストコードを実行して挙動を確認するようにしましょう。

下記のような手順で切り離してゆきます

  • 移植先のファイルとディレクトリを用意
  • teng のメソッド
  • 一覧取得のメソッド
  • 登録のメソッド
  • 削除のメソッド

8.1.1 移植先のファイルとディレクトリを用意

(モデル用のファイルディレクトリを新設)
% mkdir -p lib/BeginningMojo/Model
(モデル用の基底となるファイルを新設)
% touch lib/BeginningMojo/Model/Base.pm
(モデル用の呼び出しファイルを新設)
% touch lib/BeginningMojo/Model.pm
(モデル用のロジックを新設)
% touch lib/BeginningMojo/Model/Bulletin.pm

各ファイルは下記のようにしてみます

lib/BeginningMojo/Model/Base.pm

package BeginningMojo::Model::Base;
use Mojo::Base -base;

# 全てのモデルで共通して使えるメソッドを定義

1;

lib/BeginningMojo/Model.pm

package BeginningMojo::Model;
use Mojo::Base 'BeginningMojo::Model::Base';
use BeginningMojo::Model::Bulletin;

# 各モデルにアクセスするためのアクセスメソッドを定義
has bulletin => sub { BeginningMojo::Model::Bulletin->new(); };

1;

lib/BeginningMojo/Model/Bulletin.pm

package BeginningMojo::Model::Bulletin;
use Mojo::Base 'BeginningMojo::Model::Base';

# ロジックメソッドを定義

1;

bulletin.pl

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
# ...
use lib "./lib";
use BeginningMojo::Model;

helper model => sub { BeginningMojo::Model->new(); };
# ...

ここまですすんだらテストコードを実行してみましょう

% docker-compose exec web carton exec -- prove

8.1.2 teng のメソッドを移植

bulletin.pl

# ...
helper model => sub { BeginningMojo::Model->new(); };
helper teng  => sub { BeginningMojo::Model->new()->teng };

# ...
get '/list' => sub ($c) {
    # ...
    my $teng = $c->teng;

    # ...
};

# ...
post '/store' => sub ($c) {
    # ...
    my $teng = $c->teng;
    # ...
};

# ...
post '/remove' => sub ($c) {
    # ...
    my $teng = $c->teng;

    # ...
};
# ...

lib/BeginningMojo/Model/Base.pm

package BeginningMojo::Model::Base;
use Mojo::Base -base;
use Teng;
use Teng::Schema::Loader;

# 全てのモデルで共通して使えるメソッドを定義
sub teng {
    my $self    = shift;
    my $dsn_str = 'dbi:SQLite:./db/bulletin.db';
    if ( $ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'testing' ) {
        $dsn_str = 'dbi:SQLite:./db/bulletin.testing.db';
    }
    my $user   = '';
    my $pass   = '';
    my $option = +{
        RaiseError     => 1,
        PrintError     => 0,
        AutoCommit     => 1,
        sqlite_unicode => 1,
    };

    my $dbh  = DBI->connect( $dsn_str, $user, $pass, $option );
    my $teng = Teng::Schema::Loader->load(
        dbh       => $dbh,
        namespace => 'DB::Teng',
    );
    return $teng;
}

1;

テストコードを実行

% docker-compose exec web carton exec -- prove

8.1.3 一覧取得のメソッドを移植

bulletin.pl

# 一覧取得のロジックを移植した
get '/list' => sub ($c) {
    my $to_template = $c->model->bulletin->to_template_list;
    $c->stash($to_template);
    $c->render( template => 'list' );
};

lib/Bulletin/Model/Bulletin.pm

package BeginningMojo::Model::Bulletin;
use Mojo::Base 'BeginningMojo::Model::Base';

# ロジックメソッドを定義
sub to_template_list {
    my $self        = shift;
    my $to_template = +{
        bulletin_list => [],
        total         => 0,
    };
    my $teng          = $self->teng;
    my @bulletin_rows = $teng->search( 'bulletin', +{ deleted => 0 } );
    my $bulletin_list = [];
    for my $row (@bulletin_rows) {
        push @{$bulletin_list}, $row->get_columns;
    }
    $to_template->{bulletin_list} = $bulletin_list;
    my $total = scalar @{$bulletin_list};
    $to_template->{total} = $total;
    return $to_template;
}

1;

テストコードを実行

% docker-compose exec web carton exec -- prove

8.1.4 登録のメソッドを移植

bulletin.pl

# 登録のロジックはパラメータをわたさないといけない
post '/store' => sub ($c) {
    my $params = $c->req->params->to_hash;

    # 値が空の場合は登録しない
    my $comment = $params->{comment};
    my $trimmed = '';
    if ($comment) {
        $trimmed = trim($comment);
    }
    if ( !$trimmed ) {
        $c->flash( msg => "正しく値を入力してください" );
        $c->redirect_to('/create');
        return;
    }

    my $model = $c->model->bulletin->req_params($params);
    $model->store;
    $c->redirect_to('/list');
};

lib/Bulletin/Model/Base.pm

package BeginningMojo::Model::Base;
use Mojo::Base -base;
use Teng;
use Teng::Schema::Loader;

# mojo のhas アクセスメソッド提供、呼び出し元が返却される
has [qw{req_params}];

# ...

lib/BeginningMojo/Model/Bulletin.pm

package BeginningMojo::Model::Bulletin;
use Mojo::Base 'BeginningMojo::Model::Base';
use Time::Piece;

# ...
sub store {
    my $self   = shift;
    my $params = $self->req_params;

    my $teng = $self->teng;
    my $t    = localtime;
    my $date = $t->date;
    my $time = $t->time;
    $params = +{
        %{$params},
        deleted     => 0,
        created_ts  => "$date $time",
        modified_ts => "$date $time",
    };
    $teng->fast_insert( 'bulletin', $params );
    return;
}
# ...

テストコードを実行

% docker-compose exec web carton exec -- prove

8.1.5 削除のメソッドを移植

bulletin.pl

# ...
post '/remove' => sub ($c) {
    my $params = $c->req->params->to_hash;
    my $model  = $c->model->bulletin->req_params($params);
    my $msg    = '';
    if ( my $remove = $model->remove ) {
        $msg = $remove->{msg};
    }
    $c->flash( msg => $msg );
    $c->redirect_to('/list');
};
# ...

lib/BeginningMojo/Model/Bulletin.pm

# ...
sub remove {
    my $self   = shift;
    my $remove = +{
        remove_id => undef,
        msg       => '',
    };

    my $params = $self->req_params;
    my $teng   = $self->teng;
    my $t      = localtime;
    my $date   = $t->date;
    my $time   = $t->time;
    my $row    = $teng->single( 'bulletin', $params );
    return if !$row;

    $row->update(
        +{  deleted     => 1,
            modified_ts => "$date $time",
        }
    );
    $remove->{remove_id} = $row->id;
    $remove->{msg}       = "削除しました";
    return $remove;
}
# ...

テストコードを実行

% docker-compose exec web carton exec -- prove

8.1.6 まとめ

8.1.7 次回

今回はモデルロジックのところに焦点をあててリファクタリングをすすめてみました。

次回はテンプレート部分のリファクタリングをすすめてゆきます。