本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは須藤将史さんで、テーマは
<前回
新規のコードで既存のコードを包む
リプレイス先のアプリケーションで既存のコードをどれだけ活かすかは、リプレイスで解決したい課題しだいです。本稿では開発効率の改善のため、クライアントとサーバを疎結合にしてREST APIで開発したいとします。この場合、データベースと接続するコードはひとまずそのまま活かします。
データベースと接続するコードはサーバの内側に位置するコードです。ここを起点にクライアント側へと向かってどこまで既存のコードを活かせるか、解決したい課題と照らし合わせて実装します。
既存のコードでは解決できない課題を見つけたら、そこから新規のコードを書きます。これで新規のコードが既存のコードを包んだ状態になります。id
でテスターを取得するGETメソッドのREST APIだと、次のコードになります。
package API::Tester::Controller;
...
use Monolith::Repository::Tester; # 既存のコード
use API::Tester::Response; # 新規のコード
sub endpoint ($class, $r) {
my $tester = $r->any('/tester')->to(
controller => 'Tester::Controller'
);
$tester->get(
'/id/:id' => [id => qr/[\d]+/a]
)->to('#id');
return;
}
# サンプルコードのエラー処理は割愛
sub id ($c) {
my $id = $c->param('id');
# 既存のコードを呼び出す
my $props = Monolith::Repository::Tester->lookup($id);
# 新規のコードで既存のコードの出力を包む
my $response = API::Tester::Response->new($props);
return $c->render(json => $response->as_json);
}
次に、APIの入出力のテストを書きます。内側に位置する既存のコードを使って、新規のコードが正常に動作するかをテストします。Perl製のWebアプリケーションフレームワークのMojoliciousをREST APIサーバで利用した場合、次のテストコードになります。
use v5.36;
use Test::Mojo;
use Test2::V0;
my $t = Test::Mojo->new('API::App');
my $response = {
id => 1,
name => 'tester_01',
role => 'owner'
};
subtest 'get a tester' => sub {
my $res = $t->get_ok('/tester/id/1');
$res->status_is(200)->json_is($response);
};
subtest 'cannot get tester with non-existent id' => sub
{
my $res = $t->get_ok('/tester/id/2');
$res->status_is(400)->json_is({
error => 'bad request: not found tester',
message => '該当のテスターが見つかりません'
});
};
done_testing;
包んだコードを早期にデプロイまたはリリースする
正常に動作すれば、ひとまずリプレイスのAPIの実装が完了です。このまま開発環境へデプロイし、既存システムからアクセス可能かを検証します。そのあと、問題がなければ本番環境へリリースします。継続的なデリバリが維持できる範囲で、既存のコードを新しいコードで包みましょう。もしこれが困難であれば、リプレイスの範囲を再検討すべきです。
リプレイスの実装 ── モダンなCPANモジュールで開発効率を上げる
最後に、サンプルコードで利用したPerlの開発効率を上げるモダンなCPANモジュールを紹介します。
Mojolicious ── モダンなREST APIサーバ
Mojoliciousは現在も開発が継続されているPerl製のWebアプリケーションフレームワークです[1]。純粋なREST APIサーバとしても運用可能でPerlの標準モジュール以外に依存がないため、PerlでREST APIを始める場合に適しています。
テストもTest2::V0
[2]とTest::Mojo
を合わせて利用でき、テストコードを簡潔に書けます。リプレイスにはテストコードが必須なので、専用のテストモジュールがあるのも利点です。
なお、サンプルコードではDockerを使い、MojoliciousのREST APIサーバをコンテナ化しています。ローカル環境ではcompose.
からmorbo -l http://*:5000 -w lib -w etc etc/
でAPIのコンテナを起動させています。morboは、MojoliciousにビルトインされているHTTPとWebSocket対応の開発用サーバです。-w
オプションは、対象のディレクトリの変更を監視して、変更があれば自動的に再起動する設定です。
本番環境でAPIコンテナを起動させるなら、morboではなく本番専用にビルトインされているhypnotoadを使いhypnotoad -f etc/
で起動させます。詳細は、Mojoliciousの公式ガイド[3]を参照してください。
Type::Tiny
とMoo
── 仕様を型で定義する
Stranglerパターンで既存のコードを新規のコードで包むとき、Type::Tiny
を使い型で仕様を定義するとよいでしょう。さらに、Moo
も併用してオブジェクトにすれば、APIの入出力を型で定義できます。Perlは静的型付き言語のように実行前に型でエラーの検知はできませんが、リプレイスの境目を型で定義すると不具合の原因調査を楽にできます。
たとえば、サンプルコードのlib/
は、既存のコードの出力を受け取り、期待するAPIのレスポンスを作るオブジェクトです。このオブジェクトにはテスターの役割を表すrole
プロパティがあります。role
はRole
型で、lib/
に定義しています[4]。Perlは標準で列挙型がサポートされていないので、Type::Tiny
のenum
を使って列挙型を定義します。以下は、サンプルコードのlib/
です。
package API::Tester::Type;
use v5.36;
use Type::Library -base;
use Type::Utils -all;
enum Role => ['member', 'admin', 'owner'];
1;
この型があると、期待しない値が来た場合のエラーは以下になります。既存のコードから受け取った値と期待する値、さらにエラーが発生した箇所も標準出力でまとめて表示されます。
GET "/tester/id/1" Routing to controller "API::Tester::Controller" and action "id" Value "billing" did not pass type constraint "Enum[admin,member,owner]" (in $args->{"role"}) at /opt/app/lib/API/Tester/Controller.pm line 20 "Enum[admin,member,owner]" requires that the value is equal to "admin", "member", or "owner" 400 Bad Request (0.021319s, 46.907/s)
まとめ
本稿では、現役のPerl Webアプリケーションをリプレイスする方法としてStranglerパターンを使いました。そして、API駆動な開発を可能にする設計と実装を紹介しました。
最初に、リプレイスの心得を書きました。既存のコードを捨てずに活かせるものは活かして、リプレイスを段階的に進める重要性を説明しました。さらに、リプレイスは中断したり再開したりしてもよく、変更範囲を小さくしてリスクを減らしながら早期のリリースを目指すべきだと述べました。
次に、リプレイスを実践する具体的な方法として、Stranglerパターンを参考に既存のコードを新規のコードで包む設計を紹介しました。
最後に、リプレイス後の開発効率を上げるモダンなCPANモジュールを紹介しました。
すべてのコードを捨てゼロから開発したり、別の開発言語に置き換えたりする以外にも、現役のサービスを改善する方法はあります。少しずつより良いサービスを目指して段階的に開発できる進め方も試しましょう。
さて、次回の執筆者は石垣憲一さんで、テーマは「最近Perlに追加された実験的機能」です。お楽しみに。