このエントリーをはてなブックマークに追加

Symfony Advent Calendar 2020 24日目の記事です!
カルテット開発部では基本的にWebアプリケーションはSymfonyを使って開発していますが、実は「フレームワークはどうでもいい」と考えています。 「え?どういうこと?」という反応が予想されるので、内容と理由について説明します。

「フレームワークはどうでもいい」

私達が作りたいのは我々のモデリングしたドメイン(リスティング広告運用業務のドメイン)を体現したアプリケーションです。フレームワークのイケてる機能を使ったアプリケーションではありません。 モデリングしたドメインを適切に表現してユーザーに提供できるなら、フレームワークもプラットフォームも何でも良いのです。究極的にはエクセルマクロでも良いぐらいです。

現時点では「Webアプリケーションという形式に乗せたい」という要求があるため、WebアプリケーションのプラットフォームであるPHPを利用していて、WebアプリケーションのフレームワークであるSymfonyを選択しています。
Lisketも最初は社内向けのエクセルマクロから始まっていました。Webアプリケーションにすることで、社内の担当者間・チーム間での情報共有や引き継ぎがスムーズになります。SaaSとして社内のみならず社外にも価値を届けることができます。
なお、コロナウィルス感染症対策のためにカルテットの営業・運用部門でもリモートワークが実施された際、必然的に多拠点対応が必要になりましたが、Webアプリケーションになっていたことで担当者のマシンスペックやOSに左右されずに各種効率化ツールを提供し続けることができました。

フレームワーク選定基準

次に、PHPのWebアプリケーションフレームワークが多数ある中で、特にSymfonyを選んだ理由について。 最初に選んだから惰性で使い続けているわけではありません。日本Symfonyユーザー会のメンバーが在籍しているからSymfonyを使っているわけでもありません。
全面刷新するリニューアルのタイミングでもSymfonyを選び続けているのです。

  • 認証やセキュリティ・ルーティング、HTTPのリクエストを受け取ってレスポンスを返すのが面倒じゃない
  • 私達がモデリングしたピュアPHPによるドメインのコードを邪魔しないこと
  • 今のメンバーだけでなく将来に渡って、ピュアPHPによるドメインのコードを邪魔しない思考法を誘導してくれる

の3点を満たすPHPフレームワークがSymfony以外に見当たらないという理由です。

1つ目は、普通のWebアプリケーションフレームワークなら大抵は満たしますね。少なくともこの部分にバグがあったり、頭を使って工夫しないと実現できないようだと採用するのは難しいです。

2つ目は、クラスの命名規則やファイルの配置について、フレームワークからの強制がないかどうか、という点です。Webアプリケーションの構造の中にドメインのコードを埋め込みたいので、フレームワークの規約に合わせるためにドメインのコードの側を変えることを求められるようだとNGです。 Symfonyはautoloadしたものはほぼautowireできるうえに、Resolverに大量の同種interfaceをaddしたいときにもフレームワークのDIコンテナが(邪魔するのではなく)助けてくれます。
※どのように助けてくれるのか?は過去記事をご参照ください。 Symfonyで同種のクラスを大量に使うときDIで楽するテクニック(自作タグのススメ)

3つ目については、先日小さな勉強会でLaravelについて私が発表した こちら がちょうど反面教師となっています。
Laravelの提供する便利なresolve関数があることで、やろうと思えば resolve(Hoge::class) のようなコードをドメインのコード内に書いて、ドメインからフレームワークへの逆方向の依存を発生させることができてしまいます。ベテランばかりの今のメンバー構成だと良いのですが、今後新人さんが入ってきたときには、わざわざ「resolveを使わない」のようなローカルルールを覚えてもらわなければなりません。その点Symfonyでは、DependencyInjectionパターンの利用を原則として強制されるので安心です。
なお、Symfonyでも厳密に言えば ContainerAwareInterface を使ってドメインのコードにコンテナをinjectすることは可能です。が、普通に初心者向けドキュメントを読みながら開発してもたどり着かない方法なので、新人さんをいきなりタスクにアサインしたとしても不意に使われる心配はほぼありません。


このエントリーをはてなブックマークに追加

BEAR.Sunday Advent Calendar 2020 21日目の記事です!(18日目から始まったので正確には4日目?)

Symfonyしか使ってないと思われがちな弊社ですが、主にマイクロサービス系のコマンドラインアプリケーションではDIにRay.Diを活用しています。 Ray.Diのダウンロード数を日々上げ続けているのは弊社かもしれません(主にCIで…汗)。

さて、導入から今まで、普通のInjectorしか使っていなかったのですが、初めて コンパイル 機能を使ってみようと思い立ちました。 以前は、実装を変更したときに本番サーバー上でキャッシュクリアする実装ないしオペレーションをしないといけないのは面倒だと思い、実行効率より開発効率優先で敢えてコンパイル機能を使わないという方針でやっていました。 しかし、インフラのECS化が進んできた中で、毎回新規にコンテナが起動してフレッシュなソースコードが使われるようになり、キャッシュクリアの手間を考えなくて良くなったため、ようやく実行効率を考えることができるようになりました。

早速使ってみることにします。Ray.Diの コンパイル 機能のドキュメントはこちらにあります。 https://github.com/ray-di/Ray.Di#performance-boost

実際のソースコード上の変更

今回の対象はECSのFargate上で動くコマンドラインアプリケーションです。 アプリケーションはキューからコマンドの引数を取ってコマンドを実行し続けるランナースクリプトと、そのランナーから起動される実際のコマンドを実行するスクリプトで構成されています。 ランナーのほうはそのコンテナ内で毎回1回だけ起動し、ランナーが死んだらコンテナも死ぬ仕組みなので、コンパイルしても旨味がないと思われます。そこで、「実際のコマンドを実行するスクリプト」のみを対象にInjector部分を変更してみることにしました。

BEFORE

<?php

// 略

$injector = new Injector(new FooModule());
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

AFTER

<?php

// 略

$injector = new ScriptInjector(__DIR__.'/cache/', function(){ return new FooModule(); });
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

ドキュメント通りではないのですが、ScriptInjectorのソースコード読んだ限りちゃんと「まだコンパイルされたものがなかったらコンパイル」という挙動があるようだったので、こうしてみました。

動作確認

ScriptInjectorにした状態で1回目の実行をすると、指定したキャッシュフォルダにキャッシュファイル(コンパイルされたDI設定のファイル)ができます。 DI内で $this->bind() したクラスやinterfaceの数だけファイルができていました。
※ 今回の対象が小さなスクリプトなので数は少ないです。

スクリーンショット_2020-12-21_13_14_58

キャッシュファイルの1つの中身を見てみると、こんな感じでした。

<?php

namespace Ray\Di\Compiler;

$instance = new \FooApp\DependencyInjection\LoggerProvider('/Users/h-hishida/projects/foo-app/src/DependencyInjection/../../var/foo.log');
$is_singleton = false;
return $instance->get();

ScriptInjectorに切り替えても、元のコマンドラインアプリケーションのスクリプト自体は正常に実行完了し、生のInjectorを使ったと時と変わらない成果物も生成できることが確認できました。

なお、私は疑い深いので、「こんなんでホントにコンパイルされたものが使われているのかな?」と不安になったので、キャッシュファイルの1つに echo 'Hello ScriptInjector'; と書き込んだ状態で実行してみました。 正常実行のログとともに Hello ScriptInjector が出力されたので、ちゃんと使われているようです :smile:

計測結果

time をつけてコマンドを実行する簡易計測です。

通常のInjector利用

7.46s user 0.08s system 99% cpu 7.559 total

ScriptInjector利用(1回目…コンパイル処理あり)

7.65s user 0.08s system 99% cpu 7.769 total

ScriptInjector利用(2回目…コンパイルされたものを利用できる)

7.33s user 0.06s system 99% cpu 7.403 total

スクリプトが小さすぎるせいか、あまり有意な差が出たようには見えませんね…。 しかし、通常、1つのコンテナが起動してから死ぬまで、数十〜数百回実行されるスクリプトなので、1秒未満の差でもチリツモで効果が出ることが予想されます。 (ECSで実行する前提なので、1個のコンテナの処理可能件数が増えたり、同じ物量のキューを流したとき、より短い時間で処理が終わることを期待しています)

まとめ

次々に新しいアプリケーションを開発する受託開発と異なり、自社サービス開発では1つのアプリケーションを数年に渡って保守し続けることになるため、意外と地味な作業が多くなります。 同じアプリケーションの機能改善やバグ修正を続ける中でも、新しい技術やライブラリの新しい機能を使ってみることで、技術的好奇心も満たしながら開発を進めることができます。

完全に技術的興味だけによる提案はNGなのですが、アプリケーションを正しい方向に改善する視点をもって提案するのがポイントだと思います!


このエントリーをはてなブックマークに追加

Code Polaris Advent Calendar 2020 19日目の記事です! Code Polarisは女性エンジニアのコミュニティで、普段は主にSlack上で交流しています。私は残念ながら都合が合わず、参加したことがないのですが、オンラインのもくもく会や勉強会も開催されています。 みんなでオープンソースのアプリケーションを開発してみるプロジェクトなどでわいわいやってます!

Code PolarisのSlackでときどき話題になる「ユニットテスト」周りについて。 なぜユニットテストは「書くべき」なのでしょうか?

まず、すべてのパターンを自動でテストすることでうっかりのバグを防ぐことができます。 自動テストは複雑な仕様変更時に威力を発揮します。内部の条件分岐がどのように変わったか、それによってin/outがどのように変わったかを全パターンもれなくテストするのは、手動では大変すぎるからです。 また、「テストが従前と変わらず通ること」によって、機能を壊していないことを証明しながら内部の実装を整理し直す(リファクタ)こともできます。

個人的にはテストしやすさを追求するとメンテしやすい設計を得られやすい点も重要だと思っています。 テストを助けに使ってメンテしやすい設計をする手法については、2019年のPHPカンファレンス北海道で発表した際のスライドにテクニックを紹介してあります。もし興味がある方は見てみてください。 https://speakerdeck.com/77web/tesutowozhu-kenishi-tuteshe-ji-wogai-shan-siyou-rihuakutaringukotohazime

さて、せっかく書いたテストを有効に活用していくために気をつけたほうが良いことが2点あります。

テストが通る状態を維持する

テストは全部通って当たり前という状態を維持できるようにすることです。

「割れ窓理論」としてTwitterでも時々話題になりますが、1つでも落ちるテストがあると、

  • 自分が新規に実装したコードにテストを書いていない
  • 自分が改修したコードによってテストが落ちるようになった

のようなときに、「自分だけじゃない、今回だけじゃないから別にいいや」という気分になりやすいです。
その結果、テストがないコードの割合、テストが落ちる状態のままメンテナンスされていないコードの割合がどんどん増えていき、せっかく書いたテストがいつの間にか活用されなくなってしまいます。

テストが落ちる=異常事態という意識を常に保ちましょう。 どんなに急いでいるときでも(急いでいるときほど)テストをちゃんと書き、通ることを確認してからマージ・リリースする癖を付けましょう。

テストの速度を維持する

気軽に全テスト実行できるように、テスト実行に関してある程度の速さを維持することも大切です。

想像してみてください。全テスト実行するのに24時間かかるとしたら、実行する気がなくなりますよね? 時間がかかりすぎると、コードを書いてすぐテスト実行する習慣がつかず、やはりテストが活用されなくなってしまいます。 テスト実行の所要時間が長いとCIプロバイダに払うお金も増大します。 テストをすぐ実行する習慣を維持するためにも、お金を節約するためにも、テスト実行時間が長くなりすぎないように注意しましょう。

もしも巨大なアプリケーションを開発していて、テストパターンもテスト対象機能も大量にある場合は、各機能ごとの単位でいつでもテスト実行できる体制を維持するために、パッケージ(レポジトリ)を分けることも検討してください。 私も1年前からチームメンバーと一緒にこのパッケージ分割に取り組んでおり、テスト実行時間に関しては大きな成果を得ています(SymfonyというPHPのフレームワークを使う場合に特化したテクニックの説明になっているため、他の言語・フレームワークの利用者の方にはわかりにくいかもしれません。ごめんなさい) https://speakerdeck.com/77web/an-yorisheng-maresihun-dun-falsemo-wang-wofeng-yin-surutameni-kurosurihuarensuniyorukaosunasymfonypuroziekutowokoreyi-shang-zuo-ranaihuo-dong-shi-jian-bao-gao

〜ここからちょっとPR〜

カルテット開発部は現在、バックエンドリードエンジニア(私)、フロントエンドリードエンジニアとも女性エンジニアが就いています。 現在育休中で来年復帰予定の女性エンジニアもいます :relaxed: 皆、バリバリ生き生きとコードを書いていますし、各種ミーティングでも自分の意見が言える職場です(むしろ女性の方が強い説があります…笑)。 残業がほとんどなく(ホントにないです)家庭や育児との両立もしやすい環境です。 私もパートタイムで入社して、子供の成長とともに正社員になりました :smile:

もしカルテット開発部に興味を持ってくれた女性エンジニアがいたら、Code Polaris SlackまたはTwitterにて私( @77web )にお気軽に問い合わせください。