TECH PLAY

株式会社ラクス

株式会社ラクス の技術ブログ

927

弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2023年2月のイベントでは「 Laravel10の新機能 」について語り合いました。 弊社のメンバーが事前にまとめてきたLaravel10の新機能の情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com 特集:Laravel10の新機能 Laravelのリリースサイクルについて PHP8.0系の非対応 Laravel Pennant フィーチャーフラグとは?? 導入手順 機能利用のON/OFFの定義 サービスプロバイダで定義する方法 クラスでの定義 機能フラグの利用方法 クラスに定義している場合 Blade内での利用方法 Native type declarations in Laravel 10 skeleton Invokable Validation rules are the default Processes 関連機能 プロセスの実行方法 便利なメソッド群 並行プロセスの管理 テストのプロファイル オプション Pest Scaffolding パスワード生成ヘルパー Str::password() 設定ファイルパスのカスタマイズ doctrine/dbal is not needed anymore to modify columns in migrations Laravel 10 requires at least Composer 2.2 非推奨となる変更点 参考資料 まとめ 特集:Laravel10の新機能 PHP TechCafeでは過去に何度かLaravelを取り上げています。 下記が過去のLaravel回のまとめになります。資料には過去のバージョンのものも記載していますので、参考にしていただければと思います。 PHPerによるPHPerのための「Laravel8を中心に語り合う」TechCafe PHPerのための「Laravel/PHP8/Dockerで開発環境作りを語り合う」TechCafe PHPerのための「2020年のPHP/Laravel振り返り+2021年」を語るTechCafe PHPerのための「Laravel 入門を語り合う」PHP TechCafe PHPerのための「2021年のPHP/Laravel振り返り+2022年」を語るTechCafe PHPerのための「Laravel 9 について語る」PHPTechCafe Laravelのリリースサイクルについて LaravelはLaravel8以降1年に1回のメジャーバージョンアップになっています。 PHP8.0系の非対応 Laravel10はPHP8.1以上が必要です。 PHP8.0系は2023年の11月末くらいでセキュリティサポートが切れるので妥当かなという意見がありました。 Laravel Pennant 下記のような特徴を持つLaravel Pennantというパッケージが追加されました。 新しいファーストパーティパッケージ フィーチャーフラグを追加できる Composer 経由でインストールが可能 フィーチャーフラグとは?? フィーチャーフラグとはコードを書き換えることなく、システムの振る舞いを変更できるようにする開発手法です。 フラグによって機能のON/OFFが可能となります。 新しい機能を段階的にロールアウトしたり、 A/B テストを行ったりできます。 laravel.com 「最近、結構(フィーチャーフラグの話を)聞くようになった」 「 PDCAサイクル を早める手法として紹介されたり、流行っている印象です。」 などの意見が上がりました。 導入手順 下記の手順でComposer で簡単で導入することができます。 $ composer require laravel/pennant $ php artisan vendor:publish --provider= " Laravel \P ennant \P ennantServiceProvider " $ php artisan migrate 機能利用のON/OFFの定義 下記が機能利用のON/OFFの定義方法になります。 サービスプロバイダで定義する方法 <?php namespace App\Providers; use App\Models\User; use Illuminate\Support\Lottery; use Illuminate\Support\ServiceProvider; use Laravel\Pennant\Feature; // ★ class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot () : void { // 新しいAPIについて Feature :: define ( 'new-api' , fn ( User $ user ) => match ( true ) { // 内部メンバーは利用可能 $ user -> isInternalTeamMember () => true , // トラフィックの多いユーザは利用不可 $ user -> isHighTrafficCustomer () => false , // それ以外の場合、1/100の確率で利用可能 default => Lottery :: odds ( 1 / 100 ) , }) ; } } クラスでの定義 クラスでの定義には下記2つの特徴があります。 機能フラグをクラスで定義することも可能 artisanコマンドから雛形を作成できる $ php artisan pennant:feature NewApi <?php namespace App\Features; use Illuminate\Support\Lottery; class NewApi { /** * Resolve the feature's initial value. */ public function resolve ( User $ user ) : mixed { // 新しいAPIについて return match ( true ) { // 内部メンバーは利用可能 $ user -> isInternalTeamMember () => true , // トラフィックの多いユーザは利用不可 $ user -> isHighTrafficCustomer () => false , // それ以外の場合、1/100の確率で利用可能 default => Lottery :: odds ( 1 / 100 ) , } ; } } 機能フラグの利用方法 Feature::active('KEY名') で ON/OFF を取得できます。 <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; use Laravel\Pennant\Feature; class PodcastController { /** * Display a listing of the resource. */ public function index ( Request $ request ) : Response { return Feature :: active ( 'new-api' ) // 定義した条件通りにtrue/falseが返される ? $ this -> resolveNewApiResponse ( $ request ) : $ this -> resolveLegacyApiResponse ( $ request ) ; } // ... } クラスに定義している場合 フィーチャーフラグを管理しているクラス名を指定する必要があります。 <?php namespace App\Http\Controllers; use App\Features\NewApi; use Illuminate\Http\Request; use Illuminate\Http\Response; use Laravel\Pennant\Feature; class PodcastController { /** * Display a listing of the resource. */ public function index ( Request $ request ) : Response { return Feature :: active ( NewApi :: class )   // ★ ? $ this -> resolveNewApiResponse ( $ request ) : $ this -> resolveLegacyApiResponse ( $ request ) ; } // ... } Blade内での利用方法 @feature ディレクティブを使ってBladeで使用することも可能です。 @feature('new-api') <!-- 'site-redesign' is active --> @else <!-- 'site-redesign' is inactive --> @endfeature 下記のような話が上がり、今回のバージョンアップの注目の機能として紹介されました。 フィーチャーフラグは実はよく使われる機能!? フィーチャーフラグ、フィーチャートグルという言い方だとあまり使ったことないという感じます よくある課金ユーザだけ機能をONにするような機能で使えると思います フィーチャーフラグを定義しておけば可読性が上がるのではないかと考えられます オレオレ実装した時にハマりそうな罠を一通り回避してくれる フィーチャートグルは名前で損していると感じます 仕様を見ると下記のようなことが可能です フィーチャートグルの管理下の機能を一括で有効にしたりできます 処理の中で、あるユーザに対して「ルートAを通ったら有効、ルートBを通ったら無効」になる場合にキャッシュを取って整合性を取ってくれます オレオレ実装でハマりそうなこと 同じ条件のはずのif文同士なのに条件が微妙に違うといったことがあります。 今後、ブログ等で紹介されることを期待しています 複数チームでの開発でも使えます 並行開発するときにも使えます OFFになっていたらマージが簡単になります 複数人で開発していると、ブランチを切ってからブランチがどんどんかけ離れていってしまうことがあります 実装した機能がif文で囲われていて、そこがfalseになっているからマージしてもOKという話だと思います コンフリクトに時間をかけることも少なくできることが期待できます デメリット どこかでifを削除する必要があります カンファレンス等でそのifを自動削除するツールを作った発表を見ましたが、自動テストが充実していないと事故になりかねないと思われます ソーシャルゲーム の場合はよく使われている機能ではないか ソーシャルゲーム で新しい機能をリリースして何か不具合があった場合、フィーチャーフラグを利用しているとすぐに機能を閉じることができます ソーシャルゲーム はアプリという形で配信してしまっているので切り戻しが効かない世界です こういったノウハウを色々と持たれているかなと感じました Native type declarations in Laravel 10 skeleton 以前のバージョンでは新しくひな型を作ると、引数と戻り値に型宣言が追加されていました PHPDocで補足していた型ヒントが消えて、型宣言に置き換えられました 下位互換性があるので、型宣言が追加されたからといって既存プロジェクトが動かなくなることはないと記述されています。 「型のない言語はもう許されない(流行らない)のか……?」という意見もありました。 Native type declarations in Laravel 10 skeleton Invokable Validation rules are the default GitHub の pull リクエスト Laravel9で導入された Invokable 検証ルール をデフォルトにする変更です。 この変更によって得られるメリットは2点です。 コードが簡潔になる コストがシンプルになった分、学習コストが減る Laravelは php artisan make:rule [ルールのファイル名] で独自のバリデーションルールを設定できます。 makeコマンドに引数が無い場合はpassesメソッドとmessageメソッドが実装されたコードが生成されます。 下記コードは Github のものです。 <?php class Quantity implements Rule { protected $ messages = [] ; public function passes ( $ attribute , $ value ) { if ( ! is_array ( $ value )) { $ this -> messages [] = trans ( 'validation.quantity.must_be_an_object' ) ; return false ; } if ( ! array_key_exists ( 'magnitude' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_magnitude' ) ; } if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } return $ this -> messages === [] ; } public function message () { return $ this -> messages; } } passesでバリデーションを実施してfalseであればmessageをreturnします。 上記のコードでいうと、配列のkeyに magnitude が存在しない時、 validation.quantity.missing_magnitude メッセージが返されます。 makeコマンドの引数にinvokableを指定すると invoke メソッドのみが実装されたシンプルなコードが生成されます。(invokable rule) php artisan make:rule [ルールのファイル名] --invokable 下記コードは Github のものです。 <?php class InvokableQuantity implements InvokableRule { public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! is_array ( $ value )) { return $ fail ( 'validation.quantity.must_be_an_object' ) -> translate () ; } if ( ! array_key_exists ( 'magnitude' , $ value )) { $ fail ( 'validation.quantity.missing_magnitude' ) -> translate () ; } if ( ! array_key_exists ( 'units' , $ value )) { $ fail ( 'validation.quantity.missing_units' ) -> translate () ; } } } invoke メソッドの引数 $fail は失敗時に実行されるコールバック関数です。 Laravel10から make: ruleコマンドに引数invokableを渡さなくてもinvokable ruleが適用されるようになりました。 なお呼び出し方はどちらも同じなので 後方互換 性が無くなることはありません。 移行する場合もシンプルで、手順は以下になります。 Quantityクラスpasses内の下記のコードをInvokableQuantityクラスの__ invoke 関数内にコピペします。 <?php public function passes ( $ attribute , $ value ) { 中略 ... if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } } ↓ <?php public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } } messagesに値を入れている部分を$failを使用するように修正します。 <?php public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! array_key_exists ( 'units' , $ value )) { $ fail ( 'validation.quantity.missing_units' ) -> translate () ; } } Invokable Validation rules are the default 下記のような話があがりました。 invoke メソッドとは __ invoke ()を入れておいたら インスタンス 化されたものを関数みたいに呼び方すると呼ばれるものです 呼び出しは簡単になるが、処理が追いづらくなるという意見もありました。 見た目がスッキリしました 元々はオプションだった機能をデフォルトにするので、気をつけたほうがいい 旧方式のバリデートルールを使っている人が新方式に置き換える場合の修正内容 passesの中身を Invoke の中身に移植 messageのところを$failに渡す 実装時の手間が省くことができそうです。 実案件のコードだとrulesの配列に100行、messageの配列に100行書かないといけなかったです それが一気にスッキリかけるようになりました Laravel10の雛形ファイルが Invoke からvalidateに変更されています Processes 関連機能 別のプロセス呼び出しが ファサード 経由で実行可能になりました。 並行プロセスの実行と管理が容易になります。 プロセスの実行方法 Process ファサード の run メソッドで実行可能です。 プロセスは同期、非同期を選択できます。 同期実行 (処理が終わるまで待つ) <?php use Illuminate\Support\Facades\Process; $ result = Process :: run ( 'ls -la' ) ; return $ result -> output () ; 非同期実行 <?php use Illuminate\Support\Facades\Process; // 非同期実行(タイムアウトも設定) $ process = Process :: timeout ( 120 ) -> start ( 'bash import.sh' ) ; // 実行中かどうかを判定することもできる while ( $ process -> running ()) { // ... } $ result = $ process -> wait () ; // 処理が終わるまで待つことも可能 便利なメソッド群 run で実行したプロセスの結果を検査するためのメソッドです。 <?php $ result = Process :: run ( 'ls -la' ) ; $ result -> successful () ; $ result -> failed () ; $ result -> exitCode () ; $ result -> output () ; $ result -> errorOutput () ; 並行プロセスの管理 複数のプロセスをプールさせることも簡単になります。 <?php use Illuminate\Process\Pool; use Illuminate\Support\Facades\Process; $ pool = Process :: pool ( function ( Pool $ pool ) { $ pool -> path ( __DIR__ ) -> command ( 'bash import-1.sh' ) ; $ pool -> path ( __DIR__ ) -> command ( 'bash import-2.sh' ) ; $ pool -> path ( __DIR__ ) -> command ( 'bash import-3.sh' ) ; }) -> start ( function ( string $ type , string $ output , int $ key ) { // ... }) ; while ( $ pool -> running () -> isNotEmpty ()) { // ... } $ results = $ pool -> wait () ; Processes 関連機能 テストのプロファイル オプション artisan の test  コマンドに --profile オプションが追加されました。 実行が遅いテストを一覧表示できる。(最大10個) テストのプロファイル オプション Pest Scaffolding   Pest テスト フレームワーク です。 Laravelのアプリケーションを新規作成する時に Pest を利用できるようになりました。 $ laravel new example-application --pest Pest Scaffolding パスワード生成ヘルパー Str::password() <?php Str :: password () 特定の長さのランダムパスワードを生成できます。 パスワードは、文字、数字、記号、スペースの組み合わせで構成されます。 デフォルトでは、パスワードの長さは 32 文字です。 パスワード生成ヘルパー Str::password() 設定ファイルパスのカスタマイズ GitHub のプルリク <?php $ app -> configPath ( __DIR__ . '/../some/path' ) ; 設定ファイルへのパスを設定することができるようになりました。 設定ファイルパスのカスタマイズ 下記のような意見があげられました。 Laravelプロジェクト自体の話も飛び出しました。 Gitのプルリクで「なんでこれが必要なんだ」という議論がなされていました 下記の理由が記述されていました 「カスタマイズされたLaravel構造のプロジェクトに取り組んでいて、現在の構成パスはベースパスに残っているが、別 ディレクト リに移動したいと考えています」 「息の長いプロジェクトだとこういうことも考えないといけないんですね」と感想がありました Laravelのプルリクでは「なんで?」が多いです (Laravelのプロジェクトに対する)プルリクが4万件 「ユーザのメリットを教えてくれるかい?」など少し怖さを感じることもあります すごい数のプルリクが来ているから本当に必要なことを判別するため? 作者本人から即レスされたり、書いたプルリクはちゃんと読まれている印象 Symfony に支えられているのがLaravel doctrine/dbal is not needed anymore to modify columns in migrations Laravel9では、 マイグレーション にてテーブル列名を変更する場合は doctrine/dbal をインストールする必要がありました。 Laravel 10 からは doctrine/dbal がインストールは不要になります。 <?php return new class extends Migration { public function up () { Schema :: table ( 'foo' , function ( Blueprint $ table ) { $ table -> unsignedBigInteger ( 'bar' ) -> change () ; }) ; } … } 既に doctrine/dbal がインストールされている場合は、サービスプロバイダに以下の記述が必要です。 <?php use Illuminate\Support\Facades\Schema; … class AppServiceProvider extends ServiceProvider { public function boot () { Schema :: useNativeSchemaOperationsIfPossible () ; } } Laravel 10 requires at least Composer 2.2 Laravel10.xからは Composer 2.2 以上が必要となります。 非推奨となる変更点 以下が非推奨となる変更点です。 Remove various deprecations Pull Request #41136 getBaseQuery の削除 Illuminate\Database\Eloquent\Relations\Relation クラスの getBaseQuery メソッドの名前が toBase に変更されました。 MaintenanceModeException の削除 MaintenanceModeException はアプリケーションがメンテナンスモードの時に ステータスコード 503で投げられる例外です。 MocksApplicationServices https://github.com/laravel/framework/issues/41027 日本語記事 Remove deprecated dates property in Pull Request #42587 Eloquent モデルの非推奨の $dates プロパティが削除されました。 $casts プロパティを使用する必要があります。 Remove handleDeprecation method in Pull Request #42590 非推奨のログを出力するメソッドです。 handleDeprecation メソッドが削除されました。 代わりに handleDeprecationError を使います。 https://laravel.com/api/9.x/Illuminate/Foundation/Bootstrap/HandleExceptions.html#method_handleDeprecation assertTimesSent メソッドが削除された。 #42592 通知が送信された回数の合計をassertするテスト用のメソッドです。 assertSentTimes メソッドを代わりに使用します。 ScheduleListCommand.php の $defaultName プロパティが削除された。 コミットコメント スケジュールされているタスクのリストを表示するコマンドです。 $defaultName プロパティは遅延ロード中にコマンドを識別するために使用されていた模様です。( 修正コミット ) 使用する側には影響なしと思われます。 Route::home メソッドが削除された。 #42614 home として登録されているルートに遷移するメソッドのようです。 dispatchNow() が削除された。 #42591 ジョブをすぐに実行するメソッドです。 ジョブをすぐに実行したい場合は代わりに dispatchSync() を使用する必要があります。 参考資料 Laravel 公式 Laravel News まとめ 今回はLaravel10の新機能について、イベント参加者の生の声を交えてまとめてみましたがいかがでしたでしょうか? イベントでは追加される新機能の内容だけでなく、実用的な知見やノウハウなども紹介されていて、有意義なTech Cafeであったと思います。 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。
アバター
はじめに こんにちは!技術広報課の rks_daigo と申します。 今回は、久しぶりに弊社が主催したLT会イベント「"ChatGPT" をもっと使いたい!~活用事例Tips LT会~」について、まとめさせていただきました! はじめに イベント概要 発表タイトル一覧 発表の紹介 No1. OpenAI APIとDiscordを連携したQOL改善 No2. Power AppsとChatGPTの連携で出来たこと No3. 今更ながらLangChain使ってみた No4. ChatGPTのAPI No5. 良き開発パートナーChatGPT No6. ChatGPTを活用した便利ツールの紹介 おわりに イベント概要 イベント内容 "ChatGPT" をもっと使いたい!~活用事例Tips LT会~ 開催日: 2023/05/31(水) 18:30-20:30 イベントページ rakus.connpass.com 弊社主催イベントでは、LT(ライトニング トーク )形式を採用しております。 LTとは? Lightning Talks(ライトニング トーク )の略 "Lightning"は英語で"稲妻"という意味 つまり 「短いプレゼンテーション」 発表タイトル一覧 今回、LT会に参加された方々の発表タイトルは以下の通りです。 No. 登壇者 タイトル 1 しょーれー さん ※当社 OpenAI API とDiscordを連携した QOL 改善 2 nobuhiro_okamoto_73 さん Power AppsとChatGPTの連携で出来たこと 3 hachimada さん ※当社 今更ながらLangChain使ってみた 4 M-Tokky さん ChatGPTの API 5 KentaroWada さん 良き開発パートナーChatGPT 6 Hank Ehly さん ChatGPTを活用した便利ツールの紹介 発表の紹介 LT会では、弊社エンジニア2名、ゲスト4名の皆様にご登壇いただきました。 本記事では、発表内容を簡単にご紹介したいと思います。 No1. OpenAI API とDiscordを連携した QOL 改善 GitHub から最新のリリース情報を取得 → OpenAI API で翻訳・要約 → Doscordに通知というアプリを作ったお話。 コード作成もほぼChatGPTにお任せ。楽するために頑張るのっていいですよね。 最初のLT登壇という大役でしたが、さらっとこなしていました! No2. Power AppsとChatGPTの連携で出来たこと マイクロソフト のPowerAppsとPowerAutomateで簡単にアプリ作れちゃうよ、そこにChatGPT加えたらさらに便利になるよ、というお話でした。 No3. 今更ながらLangChain使ってみた LangChainを使って独自文書の内容をChatGPTに学習させるというお話。 API 単体だと自前で色々実装しないといけないのがLangChainを使うとかなり楽できそう。まだまだ可能性を秘めてそうなのでまたお話聞きたいです。 No4. ChatGPTの API ChatGPTをもっと使いたい.pptx from TokioMiyaoka www.slideshare.net オープンデータ管理ツールの「dim」を 自然言語 で使えるようにした話と、個人開発で ドラえもん の ひみつ道具 を考えてくれるツールを作ったという2本立て。 No5. 良き開発パートナーChatGPT 開発合宿でChatGPTに本格的に触れ、ハマってしまったというお話。いくつかのアプリを披露していただけました。開発パートナーとしてどんどん活用していこうぜ!という熱が伝わってきました。(ただし、GPT-4がいいよと) No6. ChatGPTを活用した便利ツールの紹介 ラク スのLT登壇では常連のハンクさんです。久しぶりの ラク スイベント待ってましたよ~と言っていただけてとても嬉しかったです。日々増大しているChatGPT関連のブラウザ 拡張機能 の中から特におススメのものを紹介していただけました。これで業務爆速です。 おわりに 本記事では、ChatGPTの活用事例LT会の様子を紹介させていただきました。 発表資料を見たい!という方は、以下イベントページから、ご確認ください。 rakus.connpass.com ChatGPT LT会ですが、かなり盛り上がったため第2弾も開催する予定です。 もし、本記事をお読みいただき、興味関心が湧きましたら是非次回イベントに参加/登壇申し込みいただけますと幸いです。 ラク スでは、イベントを定期的に開催しております。 我々の取り組みが、皆さまにとって新しい気づきや成長につながる機会となっていますと嬉しい限りです。 今後ともよろしくお願いいたします。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非一度ご覧ください。 現役エンジニア・マネージャーのインタビュー記事がおススメ。 career-recruit.rakus.co.jp カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお気軽にお申込みください。 forms.gle イベント情報 直近のイベント情報です。 会社の雰囲気を知りたい方は、弊社主催イベントにご参加ください! rakus.connpass.com rakus.connpass.com
アバター
はじめに PHPStanとは 前提 導入戦略 運用フロー 運用に至るまでのステップ 解析対象の除外設定 カスタムオートローダーの設定 baselineの作成 CIでの解析実行設定 途中で直面した課題 PHPStan実行時のエラー プロジェクト固有のエラー パフォーマンスについて 余談:リソース消費とスペックについて 使ってみて期待できそうなこと はじめに こんにちは。楽楽販売の開発チームに所属しているyanahmです。 最近、当チームではPHPStanの導入を段階的に始めています。 この記事ではレガシーコードへの途中からの導入の際に工夫した点についてお伝えします。 したがって、スタンダードなPHPStan導入方法とは少し異なっている部分もあります。 同じような状況の方の一助になれば幸いです。 PHPStanとは 近年人気の PHP の静的解析ツールです。 PHPStanの詳細については世の中にたくさん情報があるのでここでは割愛しますが、かいつまんで言うと下記のような特徴があります。 解析レベル(0~9) を設定でき、段階的に厳しく設定することもできる 解析時に無視するエラーの定義ファイル(baseline) を作成できる 一部動的に解析を行うため、 PHP をコードとして解釈した上で問題となる所も指摘する カスタムで解析ルール を追加できる 解析対象の設定や解析レベルなど各種設定は phpstan.neon というファイルに定義できます。 前提 まず、解析対象となるアプリの現状です。 15年選手のレガシーアプリ 解析対象は約4500ファイル 名前空間 なし 諸事情により自作オートローダーあり 歴史あるアプリのため、現在のスタンダードとは離れている部分もあります。 導入戦略 静的解析を途中から導入する方針については、おおまかに下記2パターンが考えられます。 解析レベルを1番緩いものから始めて、エラーをなくしたら段階的に厳しいものに引き上げる 既存コードのエラーは無視して、新規開発の範囲だけは厳しいルールで品質を担保する 本来なら1. が望ましいですが、歴史のあるアプリの場合、既存エラーが膨大な数になってしまう/修正による影響範囲が大きいといった問題が出てくると思います。 したがって当アプリでは、まずは新規機能開発の範囲だけでも品質を担保することを目標に 2. を採用しています。 逆に全く新規開発のプロダクトや日の浅いアプリなどの場合は1. の方が全体品質を担保できるので望ましいこともあるかと思います。 運用フロー まだ現在進行形ですが、現在下記のような形でフローで回しています。 新規開発着手前に、ベースとなるブランチでの既存エラーを無視するため、baselineを作成 baseline作成済のベースブランチから開発用ブランチを作成 コードレビューが可能な段階でマージリク エス トを作成 マージリク エス トの作成をトリガーにCI上でPHPStanが実行され、指摘が出る 担当者が指摘箇所を修正&レビュワーがチェック 問題なければメインブランチへマージ ゆくゆくは各担当者の手元で事前に実行できればと考えています。 運用に至るまでのステップ 現在の運用に至るまでの工程について紹介していきます。 解析対象の除外設定 場合によっては現状では修正が難しい/いったん解析対象から外しても問題ないファイルがあるという場合もあるかと思います。 その場合は、設定ファイルの excludePaths: で除外設定を行うことができます。 parameters: paths: # 解析対象 - ../app - ../config … excludePaths: # 設定系は除外 - ../app/config* # 廃止ソースは除外 - ../app/controllers/AbondonedController.php … カスタムオートローダーの設定 composerなど便利なツールのない時代から継続しているアプリでは、クラスマップを自作していたり、 名前空間 の設定がなかったり、複数個所で定数ファイルをrequireしていることもあるのではないでしょうか。 PHPStanには bootstrap という設定があり、PHPStanが実行される前に PHP ランタイムで何かを初期化する必要がある場合 (独自のオートローダなど)、 独自のブートストラップファイルを提供できます。 parameters: bootstrapFiles: - custom-autoloader.php 当アプリでも実行に必要な定数ファイルやカスタムオートローダーがあり、これを設定しないとそもそもクラスパスを解決できませんでした。 baselineの作成 上記の設定を基に、開発着手前にベースとなるブランチで作成します。 当社ではGitlabを使用しているため、パイプラインの手動実行で実行できるようにしています。 ※後述しますが、解析対象が多くPHPStanの実行にメモリを消費するため --memory-limit=2G を設定していないと途中で失敗しました。 php vendor/bin/phpstan --no-progress --memory-limit=2G --generate-baseline=phpstan-baseline.neon CIでの解析実行設定 前述の通り、マージリク エス トをトリガーにCI上でPHPStanが実行されるようにCIに設定を行います。 弊社ではGitLabを利用しているため、GitLab CIを使用しています。 途中で直面した課題 PHPStan実行時のエラー baselineを作成するために解析対象全体に対して実行してみると、当初は実行途中で失敗していました。 解析対象が多いレガシーコードのため、最初からスムーズにはいかないことが多いです。 $ php vendor/bin/phpstan --generate-baseline … [ERROR] An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what's going on. このような場合は、 --debug オプションをつけて実行すると、下記のようにエラーとなるソースのところでストップします。 $ php vendor/bin/phpstan --debug /PATH_TO_APP/app/controllers/SampleController.php ... /PATH_TO_APP/app/controllers/BadController.php # エラー原因となるソース 次に、 -v オプションをつけて実行するとエラースタックを出力してくれるので、原因が特定しやすくなります。 php vendor/bin/phpstan analyse /PATH_TO_APP/app/controllers/BadController.php -v ちなみに、 -vvv オプションをつけて実行すると、各ファイル解析時点で消費された合計メモリや解析にかかった秒数も表示されるようになるので、リソースの問題で問題が起きた時の デバッグ に役立ちます。 /PATH_TO_APP/app/controllers/Sample1Controller.php --- consumed 36 MB, total 82 MB, took 8.35 s ... /PATH_TO_APP/app/controllers/Sample2Controller.php --- consumed 0 B, total 1.25 GB, took 0.74 s /PATH_TO_APP/app/controllers/Sample3Controller.php --- consumed 0 B, total 1.29 GB, took 0.16 s プロジェクト固有のエラー 実際に解析を回し始めると、baselineで既存エラーは無視したものの、現状では修正が難しいエラーだが毎回指摘が出てしまいノイズになるというケースがあると思います。 その場合は、無視したいエラーを 正規表現 で定義しておくことができます。 parameters: ignoreErrors: - message: '#^Access to an undefined property App\\Foo\:\:\$bar\.$#' paths: - /{APP_PATH}/app/foo/* ... これらをbaselineとは別ファイルに定義して読み込ませることも可能です。 includes: - project-ignore.neon - phpstan-baseline.neon 当チームでは仮運用開始後、そういったものがないかを 継続的にマージリク エス トをモニタリングし、メンバーからも記入しもらい定期的に棚卸できるようにしています。 本運用に乗せるためにはこの作業が一番重要な作業だと考えています。 パフォーマンスについて 前述の通り当アプリは解析対象数が多く、全ファイル解析するとそれなりのマシンパワーを消費します。 現状検証中でCI用に割り当てたマシンがそれほどスペックが高いものではないため、暫定処置として解析対象を差分ファイルのみに絞り込むようにしました。 git diff コマンドを用いて差分ファイルを抽出しています。 --diff-filter オプションでA: 追加 / C: コピー / M: 変更 / R: リネームされたファイルを対象にしています。 # 差分のあるファイルだけ抽出 - cd $CI_PROJECT_DIR - git diff --name-only --diff-filter=ACMR origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...${CI_COMMIT_SHA} -- 'app/*.php' | sed -e "s|^|${APP_HOME}\/|g" | sed "s|\n| |g" > target.txt - > if [ ! -s target.txt ]; then echo "No target exists." exit 0 fi # 解析実行 - php vendor/bin/phpstan analyse --no-progress --memory-limit=2G $(cat $CI_PROJECT_DIR/target.txt) | sed -e s@$APP_HOME/@@g ※注意※ 実は git diff を使用するやり方は 公式では推奨されておらず、毎回全体を対象に解析することが望ましいです。 1回解析実行されると結果はキャッシュされるため、2回目以降の解析速度は上がるのですが、ファイル数が多いと1回目にかなり時間がかかってしまうため、暫定処置として行っています。 将来的にはCIマシンスペックの調整を行う想定です。 余談:リソース消費とスペックについて 解析対象数は約4500で実行にかかる時間は 1コア・メモリ2GB環境:16分ほど 8コア・メモリ15GB環境:2~3分ほど メモリ消費はいずれも2~2.6GBほど でした。コア数に依存していますね。 これはPHPStanが Parallel processing に対応しているためです。 設定はデフォルトで有効になっています。 実際8コア環境で実行してみると、下記のようにコア数分workerが起動されています。 $ top PID PPID USER P S %CPU %MEM TIME SWAP DATA COMMAND 7755 7736 root 7 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7757 7736 root 5 R 100.0 4.9 0:17 0 791832 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7760 7736 root 1 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7753 7736 root 4 R 99.7 5.0 0:17 0 797844 /usr//bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7756 7736 root 2 R 99.7 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7759 7736 root 6 R 99.7 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7758 7736 root 0 R 99.3 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7754 7736 root 3 R 99.0 4.9 0:17 0 783508 /usr/bin/php -c /usr/lib/php.ini 現時点では差分のみ解析対象としているためそこまで問題になっていませんが、解析対象が多いとそれだけ要求スペックや実行時間がかかってくるため、状況に応じてCI実行環境のスペックは検討したいところです。 使ってみて期待できそうなこと 日々のコードレビューについては 機械的 にチェックされるため、人の目で見るより取りこぼしが少なく、コード品質向上が期待できる 上記効果によりレビュワーの負担が軽減され、より業務ロジックに集中したレビューに専念できる また、副次的な効果として 大規模レガシーアプリの リファクタリング はどこから手をつけるかの判断が難しいが、PHPStanの解析結果を参考に徐々に改善していく一定の指標になる といった期待が持てました。 また新たに工夫や効果が出た際にはお知らせしていきます。
アバター
はじめに バグの説明 前提:楽楽販売について バグの発見経緯 バグの原因調査 バグ報告の手順 バグ報告用のテンプレート に従う 英語で書く バグの解決 おわりに はじめに 楽楽販売の開発チームに所属している kasuke18 です。担当領域はアプリケーションの運用周りです。 最近、アプリケーション開発・運用中に OSS のバグを発見し GitHub の Issue を登録しました。 この記事では、そのバグの内容や OSS への Issue の報告方法についてお伝えします。 今回バグを発見した OSS は Guzzle という、 PHP ではメジャーなHTTPクライアントライブラリです。 バグの説明 バグの内容は Guzzle の Issue に登録していますので、経緯にご興味がなければそちらをご参照ください。 前提:楽楽販売について 楽楽販売にはファイルアップロード機能があり、アップロードされたファイルは Cloudian HyperStore というオブジェクトストレージで管理しています。 Cloudian HyperStore は Amazon S3 とインタフェースの互換性がありますので、ファイルアップロードなどの操作は AWS SDK を利用することができます。 今回の主題である Guzzle は私達のアプリケーションが直接利用しているのではなく、 AWS SDK for PHP の中でHTTPクライアントとして使用されています。 参考:このあたりに触れた過去記事がありますので、ご興味があればご参照ください。 tech-blog.rakus.co.jp バグの発見経緯 開発・運用中に特定のファイルをアップロードしようとすると、エラーが発生し、アップロードができない問題に遭遇しました。エラーは PHP の処理でアップロードを試みた場合にのみ発生し、 CLI の aws コマンドでは同じファイルをアップロードできました。 特定のファイルは、中身が 0 という文字だけのファイルでした(md5sum値は cfcd208495d565ef66e7dff9f98764da )。 ※ PHP の開発者であれば、この時点で何となく原因を推測できるかもしれません。 また、エラーログには以下のような内容が出力されていました。 Error executing "PutObject" on "${アップロードURL}"; AWS HTTP error: Error creating resource: [message] fopen(${アップロードURL}): Failed to open stream: HTTP request failed! [file] /path/to/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php [line] 324 バグの原因調査 エラーメッセージを見ると、 PHP の関数 fopen の処理でエラーが発生したと書かれています。 AWS SDK for PHP では、ファイルアップロードを WebAPI(HTTP PUTリク エス ト)で行っています。 最初はファイル アップロード なのに、なぜ fopen 関数を使用してファイルを開いているのか疑問に思いました。しかし、調査を進めるうちに、実際に fopen 関数を使用して WebAPI を実行できることがわかりました。 したがって、 fopen 関数の使用自体に問題はなく、使用方法に何らかの問題があることが分かりました。 以下は、 fopen 関数を使用してPUTリク エス トを送信するサンプルですが、重要なポイントは2つあります。 stream_context_create 関数でリク エス トの内容を設定すること その設定内容を fopen 関数に渡すことで、WebAPIを実行できること ▶ fopen 関数で PUTリクエスト を送信するサンプル <?php // 送信するデータ $ data = "This is the data to be sent" ; // リクエストのURL $ url = "http://example.com/api/endpoint" ; // リクエスト内容を設定 $ options = array ( 'http' => array ( 'method' => 'PUT' , // リクエストヘッダ 'header' => "Content-type: text/plain \r\n " . "Content-length: " . strlen ( $ data ) . " \r\n " , // リクエストボディ 'content' => $ data ) ) ; // リクエスト送信 $ context = stream_context_create ( $ options ) ; $ result = fopen ( $ url , 'r' , false , $ context ) ; したがって stream_context_create 関数を呼び出している箇所のコードで何か問題が起こっていないかを確認するため、Guzzle の ソースコード を調べました。その結果、リク エス トボディを設定する部分で怪しい処理を見つけました。具体的には、リク エス トボディに設定したい内容を文字列にキャストし、 empty 関数で条件分岐していました。 デバッグ を行いながら処理を追っていくと、ファイルの中身が 0 だけの場合、 Content-Length は設定されるがリク エス トボディが設定されない、ということが確認できました。 通常、サーバーはこのようなリク エス トを受け入れることは考えられないため、これが fopen 関数でエラーが発生した原因であると判断しました。 バグ報告の手順 ここでは、バグを OSS の開発者に報告するために実施した手順について説明します。今回の場合、Guzzle のバグ報告は GitHub の Issue を通じて受け付けられているため、その内容と書き方について話をします。 Issue を作成する際に注意した点は、以下の2つです。 OSS が提供しているバグ報告用のテンプレートに従うこと 英語で頑張って書くこと バグ報告用のテンプレート に従う 多くの主要な OSS では、このようなバグ報告に使用するためのテンプレートが用意されています。これに従って記述することで、フォーマットに悩む必要がなくなりますし、テンプレートを使用していない場合は情報が不足しているとして拒否されることもあります。Guzzle のバグ報告テンプレートでは、以下の項目を可能な限り埋めるよう求められています。 使用しているバージョン(Guzzle, PHP , cURL ) 概要 再現するためのコード 解決策と考えられる方法 追加情報 その中で、今回は「再現するためのコード」に苦労しました。 まず最初の課題は、私たちのアプリケーションが直接 Guzzle を使用していないため、ほぼゼロからコードを作成する必要があったことです。Guzzle の使用方法に慣れていなかったため、再現コードの正確さに不安がありましたが、「バグだと思われるコードを通過する」ことを目的として作成し、その旨を Issue に記載するという対応をしました。 もう一つの課題は、Guzzle が HTTP クライアントライブラリであるため、動作確認にはモックサーバーや関連環境が必要になることです。再現コードの検証だけならば、ローカルにモックサーバーを立ち上げることで十分ですが、厳密に報告するならその手順を記載する必要があります。 とはいってもその手順を Guzzle の開発者に提供してもあまり意味がないため、「Guzzle の開発者なら手順が確立されているだろう」と仮定し、特に記述しませんでした。 英語で書く 一般的に、 OSS 開発者に日本語の理解を要求することはできないため、英語で記述する必要があります。 しかし、私自身は英語が得意とは言えませんので、翻訳ツールに頼りました。 手順としては「①日本語で記述する」→「②DeepLなどを使用して英語に翻訳する」だけではなく、「③再度日本語に翻訳し直す」ことで、日本語で書いた際の意図が抜け落ちていないかを確認しています。 この手順は、社内のエンジニアがオフショア先のチームとコミュニケーションを取る際に行っている方法を聞いたことがあり、それを取り入れてみました。 こうすることで成果物を日本語にすることができました。つまり得意ではない英語ではなく、日本語でレビューできるということになり、この点が大きなメリットでした(レビューといっても、誰かに見てもらうわけではなく、セルフチェック程度ですが...)。 バグの解決 原因コードや修正方法を提供したおかげか、追加情報を求められることもなく、すぐに修正されました。 7.5.2 でリリースされています。 おわりに OSS へのバグ報告は初めての経験でしたが、以下の点に従うことでスムーズに進めることができました。 提供されたIssueテンプレートに従う 提供する情報に過不足がないかを丁寧に確認する 今回のバグは限定的なケースでのみ発生する軽微なものでしたが、それでも報告することで、一人の利用者として OSS に貢献できたのではないかと思います。
アバター
こんにちは、技術広報の yayawowo です。 突然ですが、株式会社 ラク スと聞いて何を思い浮かべますでしょうか? 弊社 ラク スでは、様々なプロダクトを展開していますが正直認知度は低いと思っております。 そこで今回、弊社についてもっともっと知っていただくため・・・ ラク スが展開している全10プロダクト 全10プロダクトの技術スタック インフラ/SRE/デザイナーの技術スタック について、ご紹介させていただきます! SaaS 開発に携わる方、弊社に少しでも興味を持っている方の一助となれば幸いです! ラクスが展開している全10プロダクトとは? バックオフィス向け フロントオフィス向け 10プロダクトの技術スタック 楽楽販売 楽楽精算 楽楽明細 楽楽電子保存 楽楽勤怠 MailDealer 配配メール Curumeru チャットディーラーAI blastmail & blastengine フロントエンドの技術スタック インフラ・SREの技術スタック インフラ SRE デザイナーの利用ツール UIデザイナー エンジニア/デザイナーの募集職種 過去イベント動画をYoutubeで公開中 終わりに ラク スが展開している全10プロダクトとは? まずは、弊社が展開しているプロダクトを一覧で見てましょう。 いくつのプロダクトをご存知でしょうか? バックオフィス向け 名称 主な利用部門 提供機能 リリース年 楽楽販売 複数のスタッフでデータや 情報共有が必要な さまざまな部門 ・販売管理 ・請求管理 ・稟議申請管理  2008年 楽楽精算 交通費精算や経費精算の申請や 支払手続を行う 営業や 経理 部門 ・交通費精算 ・経費、出張精算  2009年 楽楽明細 請求書、支払明細といった 帳票を扱う 営業や 経理 部門 ・帳票:請求書、納品書、支払明細 ・発送方法:WEB、メール添付、郵送、FAX  2013年 楽楽勤怠 打刻や休暇申請を行う 全従業員 と 勤怠の締めを行う 総務人事部門 ・打刻機能 ・打刻修正、休暇、残業などの申請 ・休暇管理 2020年 楽楽電子保存 請求書、支払明細といった 帳票を扱う 営業や 経理 部門 ・帳票の電子保存・一元管理 2022年 フロントオフィス向け 名称 主な利用部門 提供機能 リリース年 MailDealer 複数名のスタッフでメール対応を している カスタマーサポート部門 ・問合せメールの返信状況管理 ・顧客との対応履歴管理 2001年 配配メール 見込客や顧客にメルマガを 配信している 営業や マーケティング 部門 ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング 2007年 Curumeru 同じ ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング ・メールの承認フロー 2011年 チャットディーラー ECサイト やコールセンターなどの カスタマーサポート部門 経理 、総務、人事、情報システム などの 管理部門 ・チャットボット ・顧客動向の効果測定 ・訪問者情報の取得 2017年 blastmail & blastengine 見込客や顧客にメルマガを 配信している 営業や マーケティング 部門 ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング - 3つ以上知っていてる方は、 ラク スマニアと言っても過言ではないですね! 初めて聞いた!という方がおりましたら、是非サイトにいき、ご確認いただけますと幸いです。 10プロダクトの技術スタック では、我々 ラク ス開発本部についてご紹介していきたいと思います。 ラク ス開発本部のミッションは 「日本を代表する SaaS 開発エンジニア集団へ」 を掲げております。 また、弊社はおよそ2年おきに最新プロダクトをリリースしております プロダクトの技術選定は、「ベスト・オブ・ブリード(Best of Breed)」という考えのもとリリース時により良い最適な技術を採用しております。 そのような背景も踏まえ、 ラク ス開発本部にて扱っている10プロダクトの 技術スタック をこれからご紹介していきたいと思います! 楽楽販売 図1:楽楽販売の技術スタック 楽楽販売 は、販売管理・案件管理をはじめとした、あらゆる社内業務をシステム化することができるWebデータベースシステムです。 Excel での業務管理を卒業して、販売管理などの業務を ラク にします。 リリースは2008年10月であり、14年以上続いているプロダクトになります! また、楽楽販売の開発拠点は、 関西 となります。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、 Apache 、Redis FW・ライブラリ Zend Framework 、 jQuery 開発ツール PhpStorm、GitLab、 GitHub Copilot CI・テスト Selenium /Selenide、 PHPUnit 、 JMeter 、SonarQube、Jenkins ※2023/5/26時点での情報です。 楽楽販売は、 CRM (BtoB向け)のシステムであり API 連携が多いのが特徴です。 また、お客様がノンコード(UI上)で処理を作成できるといったプロダクト特性があります。 楽楽精算 図2:楽楽精算の技術スタック 楽楽精算 は、経費・交通費・出張費・旅費・交際費など、お金にかかわる全ての処理を一元管理できる クラウド 型の交通費・経費精算システムです。 リリースは、2009年7月で弊社の中でも最も勢いのあるプロダクトであるため、開発規模も最大になります。 開発拠点は、 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、Swift、Kotlin MW PostgreSQL 、 Postfix 、 Apache FW・ライブラリ Spring Boot、 jQuery 開発ツール eclipse 、GitLab、 IntelliJ IDEA、 Xcode 、 Android Studio 、Flyway、 Redmine 、 GitHub Copilot CI・テスト Selenide、Gradle、 JMeter 、 jUnit 、Jenkins、Bitrise ※2023/5/26時点での情報です。 技術の選定の大きな方針としては、保守性の観点で、なるべく普及しているものを選定するようにしています。 また、主要な開発言語は Java 、上記に記載したツールを利用し、開発を行っております。 楽楽明細 図3:楽楽明細の技術スタック 楽楽明細 は、請求書・納品書・支払明細・領収書などをWEB・メール・郵送で自動発行し、 印刷・封入・発送などの帳票発行の手間をゼロにする クラウド サービスです。 リリースは2013年9月、開発拠点は 東京・ ベトナム です。 市場の成熟とともに急激に伸びているプロダクトになります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、TypeScript MW PostgreSQL 、 Apache 、 Postfix 、Docker FW・ライブラリ React、Redux、SpringBoot、JasperReports、 Lombok 、Jooq、 jQuery 、GraphQL 開発ツール IntelliJ IDEA、 Redmine 、GitLab、gulp.js、webpack、Storybook、 GitHub Copilot CI・テスト Gradle、 JMeter 、 jUnit 、TestCafe、SonarQube、Jenkins、Cypress、Spock ※2023/5/26時点での情報です。 サーバーサイドを Java で、フロントエンドをTypeScriptで構築しています。 サービスローンチから8年を数え、比較的古い アーキテクチャ となっているコア部分と、新しいFWなどを用いた部分とがハイブリッドになっています。 主要な新規機能開発部はサーバーサイドとフロンドを分離されており、SpringBootやReactを用いて開発をしています。 DDDを用いた設計手法の導入、 モノリス から バッチ処理 部分を分離して アーキテクチャ を更新する、オフショア開発をスタートするなど、 サービスの成長に合わせて開発手法や組織を変化させていっています。 ◆ 技術・デザイン情報ページ フロント刷新から設計手法アップデートまでバランス感覚を活かして幅広く推進 | ストーリー | 株式会社ラクス キャリア採用 楽楽電子保存 図4:楽楽電子保存の技術スタック 楽楽明細 は、楽楽明細と連携し、電子発行された請求書・納品書・支払明細・領収書などを 保存・一元管理できるサービスです。 電子帳簿保存法 対応により、ニーズが高まっているプロダクトです! リリースは2022年、開発拠点は 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、TypeScript MW PostgreSQL 、 Apache 、 Tomcat 、 Postfix 、Docker FW・ライブラリ React、Redux、MUI、styled-components、Spring Boot、Jooq 開発ツール IntelliJ IDEA、GitLab、Vite、Storybook、 GitHub Copilot CI・テスト Gradle、 JUnit 、SonarQube、Jenkins、Jest、Cypress ※2023/5/26時点での情報です。 楽楽勤怠 図5:楽楽勤怠の技術スタック 楽楽勤怠 は、主に中小企業3,000社超のバックオフィスの効率化を実現した クラウド 型経費精算システム「楽楽精算」の開発・提供で培った様々なノウハウを活用し、勤怠管理業務の効率化を実現する クラウド サービスです。 リリースは2020年10月となります! 主な開発拠点は、 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、 Python 、TypeScript、Sass MW PostgreSQL 、 Apache 、RabbitMQ FW・ライブラリ Spring Boot、Vue.js、Resilience4j 開発ツール IntelliJ IDEA、GitLab、Docker/ k8s 、 Visual Studio Code 、 GitHub Copilot、 Figma CI・テスト Gradle、Swagger、 JUnit 、 Checkstyle 、SpotBugs、PMD、GitLab CI、 JMeter 、Cypress、Jest、Vue Testing Library、Storybook、Mock Service Worker、StepCI ※2023/5/26時点での情報です。 ラク ス社内では目新しい技術を多く利用しており、DDDを用いた設計思想を取り入れ、フロントエンドとバックエンドを切り離した開発を行っています。 開発側にPdM相当の人がいるので、開発主導でサービスをブラッシュアップしていけます。 MailDealer 図6:Maildealerの技術スタック MailDealer は、顧客からの問合せメールを共有・一元管理し、メール対応業務を効率化するツールです。 2001年4月にリリースされ、最も古いプロダクトとなります。 主要な開発拠点は 大阪・ ベトナム になりますが、フロントエンドは 東京 と拠点を分けた体制です! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP 、Node.js MW PostgreSQL 、 Apache 、 Postfix FW・ライブラリ Laravel、 jQuery 、CKEditor、Socket.IO 開発ツール PhpStorm、 Redmine 、GitLab、Trello、 GitHub Copilot CI・テスト Selenium /Selenide、 PHPUnit 、Jenkins、Ansible ※2023/5/26時点での情報です。 主要な開発言語は PHP 。 リアルタイム通信部分にはNode.js(+Socket.IO)も利用しています。 20年以上の歴史を持つサービスながら、オフショア開発、 リファクタリング や新技術のハイブリッド導入など新たな領域にもチャレンジし続けているサービスです! 配配メール 図7:配配メールの技術スタック 配配メール は、中小企業の集客・販促活動に携わる方のメール配信業務を支援するメール マーケティング の実践に最適なメール配信サービスです。 リリース日は、2007年5月になります。 なお、MailDealerと同様、開発拠点は 大阪・ ベトナム ですがフロントエンドは 東京 になります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、Nginx、 Apache 、Redis FW・ライブラリ Slim、 jQuery 、Vue.js 開発ツール PhpStorm、 REDMINE 、GitLab、Docker、 GitHub Copilot CI・テスト Puppeteer、Jenkins、 JMeter 、 PHPUnit 、PHPStan、 PHP _CodeSniffer、PHPDoc、Ansible ※2023/5/26時点での情報です。 開発言語は PHP 、 フレームワーク は国産 OSS を自社拡張したものを採用しています。 大量メール配信による マーケティング ツールのサービス基盤を自社でメンテナンスしながら、初期リリースから15年以上サービスを継続させています。 また、近年需要が高まりつつあるデジタル マーケティング ツールとして、最新のWeb技術も取り入れながら機能強化を続けています。 変化の激しい マーケティング のビジネス領域に適応するため、 アジャイル 開発や プロダクトマネジメント 手法を取り入れることにもチャレンジしています! Curumeru 図8:Curumeruの技術スタック Curumeru は、低コストで導入できる大量メール配信サービスです。 2011年6月にリリースされており、開発拠点はこちらも 大阪・ ベトナム ! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、 Apache FW・ライブラリ jQuery 開発ツール PhpStorm、 REDMINE 、GitLab、 GitHub Copilot CI・テスト Jenkins、Ansible ※2023/5/26時点での情報です。 開発言語は PHP 、 フレームワーク は配配メールのものをベースに構築されています。 配配メールをベースにしつつ、 API 連携やメールリレーなどの大量メール配信機能をより強化した仕組みで構築されています。 初期リリースから約10年間、お客様のシステムのメール配信を支えています。 現在は ベトナム のオフショアチームが中心となって開発しています。 チャットディーラーAI 図9: チャットディーラーの技術スタック チャットディーラーAI は、チャットによる自動回答などを通じて問合せ対応を低コスト化/効率化するツールです。 リリースは2017年6月になります! 開発拠点は 大阪・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP 、Node.js、 Python MW PostgreSQL 、 Apache 、Nginx、Redis、 MeCab 、Docker FW・ライブラリ Laravel、 jQuery 、Vue.js、Bootstrap、CKEditor、Socket.IO、Express、Handlebars、npm、Composer、 ImageMagick 開発ツール PhpStorm、 Redmine 、GitLab、Jupyter Notebook、 VS Code 、Webpack、 GitHub Copilot CI・テスト Pupperteer、 PHPUnit 、Mocha、Jenkins、 JMeter 、Ansible ※2023/5/26時点での情報です。 主要な開発言語は PHP 、リアルタイム通信部分はNode.js(+Socket.IO)、 自然言語処理 を行う部分では Python を採用しています。 また、 フレームワーク はLaravel, Vue.jsを使っているなど、多様な技術に触れて学ぶ機会が有ります。 2017年リリース以降、毎月リリースを継続しており、スピーディ―な開発も魅力です。 blastmail & blastengine 図10:blastmail & blastengineの技術スタック blastmail と blastengine は、独自開発した配信エンジンと大規模ネットワークにより、280万通/時の超高速配信を実現した クラウド 型メール配信サービスです。 開発拠点は 東京 で、グループ会社である ラク スライト クラウド が扱っているプロダクトになります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 TypeScript、 JavaScript 、 PHP 、 Java 、Go MW Apache 、nginx、jetty、 PostgreSQL 、MongoDB、RabbitMQ、 Memcached 、 Postfix FW・ライブラリ React、Laravel、 CakePHP 、SpringBoot 開発ツール Docker、GitLab、 Redmine 、StoryBook、OpenAPI、GitBook、Slack、 AWS 、 GitHub Copilot CI・テスト GitLab-CI、Jenkins、SonarQube、Jest、ReactTestingLibrary、cypress、 PHPUnit 、 jUnit ※2023/5/26時点での情報です。 マルチな言語環境、マイクロサービスの最適化を目指して日々邁進中です。 直近ではフロントエンドとバックエンドの分離を進めており、より ユーザビリティ や可搬性の高いシステムとサービス品質の向上を目指しています。 フロントエンドの技術スタック ラク スには各プロダクト開発チームの他に、フロントエンド開発を専門とする「フロントエンド開発課」があります。 楽楽シリーズ・ ラク スシリーズの各 SaaS 製品における新機能追加、パフォーマンス向上、技術的改善や刷新などを行っています。 様々なサービスにおいて活躍できる横断組織として、多様な業務に挑戦しています! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 HTML、 CSS 、 JavaScript 、TypeScript MW Docker FW・ライブラリ React、Redux、Recoil、MUI、RHF、zod、Tanstack Query、axios、Laravel、Vue.js、Vuetify、 jQuery 、Sass、Vite、webpack、ESLint、Prettier、Storybook、msw 開発ツール GitLab、 GitHub Copilot、 Redmine 、PhpStorm、 VSCode 、 Figma CI・テスト GitLab CI、Jenkins、Cypress、Playwright、Jest、Vitest ※2023/5/26時点での情報です。 インフラ・SREの技術スタック 前述した10プロダクトを支えているのが、インフラ部門になります。 今回はインフラの技術スタックだけでなく、社内バックオフィス業務の自動化を推進して頂くSREの技術スタックについてもご紹介したいと思います! なお、インフラ部門の開発拠点は 大阪、東京 、SRE部門は 東京 です。 インフラ まずは、インフラの技術スタックをご紹介します。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Python 、 PHP プラットフォーム On-Premise、 AWS (EC2、ECS、EKS、RDS、S3、Lambda など) ネットワーク Cisco 、 Dell 、Fortinet、F5Networks OS CentOS 、 Amazon Linux 仮想化基盤 VMware 、Nutanix MW MySQL 、 PostgreSQL 、 Apache 、 Tomcat 、Nginx など IaC Ansible、Terraform その他ツール Git、Jenkins、 Selenium 、Rundeck、Serverspec、 GitHub Copilot 運用・監視 Zabbix、Grafana、Prometheus、ElasticStack ※2023/5/26時点での情報です。 9割のサービスリソースをオンプレミスで構築しております。 オンプレミス環境でも自動化など、なるべくソフトウェア視点のアプローチが出来るようにHCIで基盤構築し運用効率化をしています。 今後のアップデートとしては、 クラウド で先行構築した クラウド ネイティブなコンテナ環境やCI/CD環境などをオンプレミス環境にフィードバックし、自動化、自立化を推進しつつもコスト優位性を出せるシステムを構築していきます。 SRE 続いて、SREの技術スタックをご紹介します。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Go、 Python プラットフォーム On-Premise、 AWS 仮想化基盤 Kubernetes MW PostgreSQL FW・ライブラリ Gin、Flask 開発ツール GitHub 、Docker CI/CD GitHub Actions、Kustomize、Helm、ArgoCD、 GitHub Copilot 運用・監視 Datadog ※2023/5/26時点での情報です。 主にGoを利用しており、一部 Python も併用しています。今後はGoに言語は統一していく予定です。 新しい技術スタック調査などを進めながらノウハウを各サービスへ広めることで、開発部門全体の アーキテクチャ 刷新へ寄与していきます。 デザイナーの利用ツール ラク スはエンジニアだけでなく、デザイナーも10つのプロダクトを支えております。 職種としては、UIデザイナーとなりますのでこちらの利用ツールも紹介していきたいと思います! デザイナーの所属拠点は、 東京 になります。 UIデザイナー ラク スのプロダクトのUIをデザインします!利用ツールは以下の通りです。 ◆ 技術スタック一覧 カテゴリ 内容 デザインツール Figma コミュニケーションツール Mattermost、Zoom、 Google Meet ※2023/5/26時点での情報です。 ラク スのデザイナーはバックオフィスをはじめとする、業務システムの管理画面をデザインします。 業務 ドメイン の知識を学びながら、顧客の課題を理解し、業務をデザインの力で解決します。 業務 ドメイン の知識習得や、デザインの勉強会・輪読会などをおこなっています。 エンジニア/デザイナーの募集職種 前述した通り、 ラク スでは全10のプロダクトを扱っており、 「日本を代表する SaaS 開発エンジニア集団へ」 を目指し日々精進しております。 そんな弊社ですが、まだまだ人が足りておりません。 そこで、下記に 各開発拠点ごとの募集職種を関連するプロダクトとともに整理しました! 皆様のご応募、お待ちしております!! 【開発拠点:東京】 カテゴリ 募集職種 関連プロダクト マネジメント エンジニアリングマネージャー 楽楽精算・楽楽明細・楽楽勤怠 エンジニアリングマネージャー/オフショア 〃 開発 サーバサイドエンジニア/Java 楽楽精算・楽楽明細・楽楽勤怠 サーバサイドエンジニア/PHP blastmail・blastengine プロダクトマネージャー 楽楽精算・楽楽明細・楽楽勤怠 プロジェクトマネージャー 楽楽精算・楽楽明細・楽楽勤怠 プロジェクトマネージャー/PHP blastmail・blastengine プロジェクトマネージャー/フロントエンド 楽楽明細・楽楽勤怠・メールディーラー リードエンジニア/フロントエンド 楽楽精算 フロントエンドエンジニア 楽楽明細・楽楽勤怠・メールディーラー iOSエンジニア 楽楽精算 Androidエンジニア 楽楽精算 QAマネージャー 楽楽精算・楽楽明細・楽楽勤怠 QAエンジニア 楽楽精算・楽楽明細・楽楽勤怠 SETエンジニア 楽楽勤怠 PMO/品質管理 プロダクト横断 Webエンジニア プロダクト横断 デザイン デザインマネージャー/プロダクト プロダクト横断 UIデザイナー/アシスタントマネージャー 楽楽精算 UIデザイナー 楽楽明細・楽楽販売・メールディーラー・チャットディーラーAI・配配メール UIデザイナー/ポテンシャル採用 楽楽明細・楽楽販売・メールディーラー・チャットディーラーAI・配配メール ※2023/5/26時点での情報です。 【開発拠点:大阪】 カテゴリ 募集職種 関連プロダクト マネジメント エンジニアリングマネージャー 楽楽販売・メールディーラー・チャットディーラーAI・配配メール エンジニアリングマネージャー/インフラ 〃 開発 リードエンジニア/PHP 〃 サーバサイドエンジニア/PHP 〃 プロジェクトマネージャー 楽楽販売・チャットディーラーAI プロダクトマネージャー 楽楽販売 ブリッジSE MailDealer インフラ インフラエンジニア メールディーラー・チャットディーラーAI・配配メール・楽楽販売・Curumeru ※2023/5/26時点での情報です。 過去イベント動画を Youtube で公開中 弊社 ラク スでは、毎週技術イベントを開催しております。 現在 Youtube の「 ラク スチャンネル」にて、過去イベントの アーカイブ 動画を公開中です! 各プロダクトの最前線で活躍しているエンジニアたちが、社内の取り組みを発信していますので是非お時間ありましたらご確認ください。 youtube.com なお、最新の アーカイブ 動画は ラク スDevelopers会員(技術イベント時に登録できるメール会員)限定に公開しております。 もし最新 アーカイブ 動画を見てみたい!という方、まずは技術イベントにご参加いただけますと幸いです! ラク スconnpassページ ラクス - connpass 終わりに ラク スの技術スタックにご興味いただけたでしょうか? 弊社の中では大型開発の楽楽精算を初め、様々なプロダクトが存在しております。 本ブログにより、1つでも多くプロダクトの名前を覚えていただけたら幸いです。 また弊社では、前述した通り募集職種が多くあります。 もし、どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っておりますのでお気軽に以下フォームよりお申込みください。 カジュアル面談お申込みフォーム カジュアル面談について | 株式会社ラクス キャリア採用 長くなりましたら、今後とも ラク スエンジニアブログをよろしくお願いします。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com
アバター
ビジネスアプリケーションとビジネスルール 用語について パターン適用前 Specification パターン Hard Coded Specification Parameterized Specification Composite Specification 条件の再利用性が向上する テスト性が向上する ルールと条件を統一したインターフェースで扱える 動的にルールを構成できる まとめ 関連するデザインパターン Strategy パターン Composite パターン Interpreter パターン 参考文献 補足 ビジネスアプリケーションとビジネスルール 楽楽精算開発部の id:smdr3s です。主に Java を使ったサーバーサイドを担当しています。 弊社のサービスである楽楽精算は、その名のとおり経費精算のサービスです。主に企業にお勤めの方が、業務での移動時ににかかった交通費や業務に必要な物品を購入した際の代金などを経費として会社に申請する際にご利用いただいております。 楽楽精算にはさまざまな機能がありますが、その一つに「申請ルール」という機能があります。 これは、経費申請時にあらかじめ設定しておいたルールで申請内容を検証し、ルール違反があればメッセージを表示して警告したり、申請を拒否したりする機能です。 例えば、交通費精算の際に「利用 交通機関 がタクシーで、料金が3,000円以上、かつ理由欄に記述がない、場合は申請できないようにする」ようなことが可能です。 申請ルールの設定 申請時にルール違反があった場合 このように、ビジネスアプリケーションには複数の条件を組み合わせたビジネスルールによる検証を行うことがよくあります。 また、ビジネスルールに従って蓄積されたデータの中から条件に合致するものを抽出したりすることもあります。 そのような検証や抽出に使用するルールや条件を実装するときに役立つのが Specification パターン です。 用語について この記事では、パターン名にも使われている Specification という用語を以下の二通りに訳して使用しています。 ルール :最終的に満たされるべき基準の総称。「タクシー利用ルール」など。 条件 :ルールに含まれ、構成する個々の基準の名称。「金額上限条件」「理由記述条件」など。 Specification をそのまま訳すと「仕様」であり、「仕様」にも上記のような「満たすべき基準」の意味はあるかと思いますが、個人的にあまりしっくりこなかったため別の言葉を使用しています。また、全体と個々の中身を区別したい場面が多かったためそれぞれに別の言葉を割り当てました。(後述しますが、それらを同一視できるのがこのパターンのメリットの一つでもあるのですが、便宜的に使い分けました。) パターン適用前 上の「申請ルール」の説明に使用したビジネスルールを基に「タクシー利用申請アプリケーション」の実装を検討します。 このアプリケーションで検証するビジネスルールは以下のとおりとします。 タクシー利用ルール 料金が3,000円以上の場合は理由欄に理由を記述すること 申請はこのようなレコードです。 public record TaxiApplication( int fee, // 料金 String description // 理由 ) { } 申請時にルールに沿っているかを検証します。 手続き的に書く場合は検証処理は Logic クラスあたりに書かれそうです。 public class TaxiApplicationLogic { public void apply(TaxiApplication application, User applicant) { // いろいろ // タクシー利用ルールのチェック // 3,000円以上かつ理由欄が空の場合は検証エラー if (application.fee() >= 3000 && application.description().length() <= 0 ) { throw new ApplicationRuleValidationException(); } // いろいろ } } ただ、このように書いてしまうと下記のような問題があります。 ルール検証のテストがしづらい ルールが変わるごとに Logic クラスに修正が入る 同じルールを使用している箇所があった場合、ロジックが重複する 全体のコードからルールを司るコードの特定が困難 この状態は、ソフトウェア品質特性で言う、保守性の副特性である試験性、安定性、解析性に影響が出ているか、出る可能性が比較的高い状態かと思います。 これは重要なビジネスルールであるはずのタクシー利用ルールのコードが手続きの中に隠され、独立していないことが最も大きな原因と考えられます。 Specification パターン これを Specification パターンを使用して改善していきたいと思います。 Specification パターンにはいくつかの方式があるのですが、基本的にはルールや条件の検証を実行する Specification インターフェースが基になります。 public interface Specification<T> { boolean isSatisfiedBy(T candidate); } Specification インターフェースには、通常 isSatisfiedBy という名前のメソッドを定義します。このメソッドは検証対象のオブジェクトをパラメータとして受け取り、実装ではそのオブジェクトが定義されたルールまたは条件を満たしているかどうかを検証し結果をブール値で返します。 Hard Coded Specification 最も単純な実装は Hard Coded Specification です。 ルールの検証ロジックをそのまま Specification インターフェースの実装クラスに実装します。 // タクシー利用ルールクラス public class TaxiUsageSpecification implements Specification<TaxiApplication> { @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < 3000 || candidate.description().length() > 0 ; } } (先ほどの Logic クラス内の実装ではルールに沿わないことを判定していましたが、今回はルールに沿うことを判定するため、真偽が逆になっています。) これでルールをクラスにすることができました。 このクラスを利用するよう上記の Logic クラスのチェック部分を変更します。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { return new TaxiUsageSpecification(); } } public class TaxiApplicationLogic { public void apply(TaxiApplication application, User applicant) { // いろいろ // タクシー利用ルールオブジェクトの取得 Specification<TaxiApplication> taxiUsageSpecification = SpecificationFactory.getTaxiUsageSpecification(); // 申請がルールに沿っていなければエラー if (!taxiUsageSpecification.isSatisfiedBy(application)) { throw new ApplicationRuleValidationException(); } // いろいろ } } ルールのコードをクラスに切り出すことでコードの特定が容易になり、同じルールであればこのロジックを使い回せるようになりました。 このように Hard Coded Specification は実装が簡単で、ルールがコードに直接表現されているため、ルールが単純な場合には読みやすいかと思います。 しかし、ルールをコードにベタ書きしているのはパターン適用前と変わらず、少しでもルールに変更があった場合にはコードの修正が必要となります。 ルールの条件が固定されており変更の可能性も低い場合にのみ使用するのが良いかと思います。 Parameterized Specification ルールの大まかな条件は決まっているが、細かい差異や変更がある場合に使用できるのが Parameterized Specification です。 ルールの実装クラスのオブジェクトを作成するときにパラメータを渡せるようにすることで、差異のあるルールを作成することができます。 今回の例では、タクシー料金が3,000円以上の場合に理由の記述が必要なルールとなっていますが、将来的にこの金額を変更したい、という話が出てきそうですのでこれをパラメータで渡せるようにしてみます。 // タクシー利用ルールクラス public class TaxiUsageSpecification implements Specification<TaxiApplication> { private final int maxFreeFee; // 理由記述が不要な料金上限 TaxiUsageSpecification( int maxFreeFee) { this .maxFreeFee = maxFreeFee; } @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < maxFreeFee || candidate.description().length() > 0 ; } } // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getManagerTaxiUsageSpecification() { // 理由記述が必要な基準料金等の条件はDB等から取得できるものとします int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold; return getTaxiUsageSpecification(freeThreshold); } } タクシー利用ルールが Specification<TaxiApplication> のオブジェクトであることに変更はありませんので Logic クラスに修正は必要ありません。 このように Parameterized Specification ではパラメータを使用することでルールの条件を調整することができます。 今回はパラメータを一つだけしか渡していませんが、ルールに含まれる複数の条件にパラメータを渡したり、一つの条件に複数の複数のパラメータを渡したりすることも可能です。 また、オブジェクトの生成ごとに異なるパラメータを渡すことができますので、例えば従業員と管理職で制限金額が異なる場合などに、ロジックは共通のまま異なるルールオブジェクトを作成することができます。 一方、ルールに直接条件が書かれているのは変わりませんので、条件の追加や削除などの要件に対応するにはコードの修正が必要になります。 Composite Specification 最も柔軟で動的なルールを作成できるのが Composite Specification です。 いままでの方式では ルールごと に Specification インターフェースを実装したクラスを作成していましたが、 Composite Specification では 条件ごと に独立したクラスを作成し、Composite の名のとおりそれらの条件クラスを組み合わせて最終的なルールを構成できるようにします。 今回のケースでは「料金上限」と「理由記述」の2つの条件がありますので、それらの条件ごとに Specification インターフェースを実装したクラスを作成します。 // 料金上限条件クラス public class MaxFeeSpecification implements Specification<TaxiApplication> { private final int maxFee; MaxFeeSpecification( int maxFee) { this .maxFee = maxFee; } @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < maxFee; } } // 理由記述条件クラス public class ReasonSpecification implements Specification<TaxiApplication> { @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.description().length() > 0 ; } } 条件の組み合わせを行うクラスを実装します。これも Specification インターフェースを実装します。 Specification インターフェースの結果はブール値ですので、AND, OR, NOTそれぞれの論理演算を行う条件クラスがあればすべての組み合わせに対応できます。 // AND条件 public class AndSpecification<T> implements Specification<T> { private final Specification<T> left; private final Specification<T> right; public AndSpecification(Specification<T> left, Specification<T> right) { this .left = left; this .right = right; } @Override public boolean isSatisfiedBy(T candidate) { return left.isSatisfiedBy(candidate) && right.isSatisfiedBy(candidate); } } // OR条件 public class OrSpecification<T> implements Specification<T> { private final Specification<T> left; private final Specification<T> right; public OrSpecification(Specification<T> left, Specification<T> right) { this .left = left; this .right = right; } @Override public boolean isSatisfiedBy(T candidate) { return left.isSatisfiedBy(candidate) || right.isSatisfiedBy(candidate); } } // NOT条件 public class NotSpecification<T> implements Specification<T> { private final Specification<T> specification; public NotSpecification(Specification<T> specification) { this .specification = specification; } @Override public boolean isSatisfiedBy(T candidate) { return !specification.isSatisfiedBy(candidate); } } そして、これらの条件を組み合わせればルールを表すオブジェクトを作成できます。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold; // 条件を組み合わせてルールを作成します return new OrSpecification( // 以下のいずれかの条件を満たすこと new MaxFeeSpecification(freeThreshold), // 料金が指定金額以下 new ReasonSpecification()); // 理由が記述されている } } 今回のようにルール単純なケースでは実装が複雑になっただけのように見えるかもしれませんが Composite Specification には以下のようなメリットがあります。 条件の再利用性が向上する 条件が個々のクラスに独立したことにより、条件だけを再利用することが可能になります。 例えば、タクシー利用ルールに「料金は○万円未満」の条件が追加されたとしても、(従来は理由の記述が必要な料金の条件判断に利用していた) MaxFeeSpecification クラスを使用し、以下のように実装することができます。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold(); int limitFee = CompanyRuleRepository.getTaxiRule().limitFee(); return new AndSpecification( // 上限は必須条件のためAND new MaxFeeSpecification(limitFee), // 上限を超えていないこと new OrSpecification( // その他の条件は従来どおり new MaxFeeSpecification(freeThreshold), new ReasonSpecification())); } } テスト性が向上する その他の方式ではルール全体で一つのクラスであったため、テストを行う場合はルール全体を対象とするしかなく、それに含まれる条件ごとにテストを行うことはできませんでしたが、 Composite Specification では条件ごとにクラスを作成するため、それぞれの条件の独立したテストを行うことが可能になります。 ルールと条件を統一したインターフェースで扱える 最終的なルールである SpecificationFactory+getTaxiSpecification() の戻り値オブジェクトも、それを構成する条件である MaxFeeSpecification , ReasonSpecification や AND, OR, NOT の論理条件も、すべて Specification インターフェースを実装したものであるため、相互に組み合わせや代替が可能です。 (ルールと条件を区別していたのは私の都合でしたので当然ではありますが…) 例えばあるロジックでは (A && B) のルールが利用されており、他のロジックでは ((A && B) || C) のルールが利用されていた場合、 (A && B) で X というルールクラスを作成すれば、前者のロジックで X を利用できるのはもちろん、後者のロジックでも X を条件として (X || C) として利用することができます。 この場合、テストも X に対して行えるため、後者のルールのテストも単 純化 することができます。 動的にルールを構成できる 条件がオブジェクトとして独立しているため、これらを動的に生成し、組み合わせを行って自由にルールを作成することができます。 もちろん利用される個々の条件はあらかじめ実装しておく必要がありますし、それらを組み合わせてルールを構成するロジックの実装の難易度は高いかと思いますが、要求の変化があるたびにコードを変更する必要がなくなるため、さまざまなユーザの要求に迅速に対応しやすくなります。 楽楽精算の「申請ルール」機能のように、ユーザが自由にルールを設定できる要件にも対応できます。(なお、楽楽精算の実際の実装とは異なる可能性があります。) まとめ Specification パターン は、基本的にインターフェース1つ、メソッド1つで構成される単純な構成でありながら非常に強力な効果が得られるパターンです。 そして、活用がしやすく実践的でありながら、 関数プログラミング や論理プログラミングといった パラダイム にも触れることができ、興味深く勉強できる利点があります。(個人の感想です。) 参考文献には、拡張として2つのルールや条件の包摂を判断する手法 [1] や、条件判断の実装を外部に依存する(させる)ことで SQL 等の実装を使用して抽出時のパフォーマンスを上げる方法 [2] も述べられておりますので、ぜひ参考にしてみてください。 関連する デザインパターン Strategy パターン Specification パターンはルールをインターフェースに実装し、オブジェクトによってふるまいを変化させることができますので、本質的には Strategy パターン [1] であると言えます。 Composite パターン Composite Specification では、ルールと条件を同一のインターフェースで扱うことでそれらを 再帰 的に構成することを実現しています。これはその名のとおり Composite パターンを適用したものです。 Interpreter パターン Composite Specification で動的にルールを構成する場合、 Interpreter パターンで条件オブジェクトを組み合わせて最終的なルールオブジェクトを構成することが可能です。 参考文献 Eric Evans and Martin Fowler. "Specifications". https://www.martinfowler.com/apsupp/spec.pdf Elic Evans. エリック・ エヴァ ンスの ドメイン 駆動設計. 翔泳社 , 2011, 576p. Martin Fowler. エンタープライズ アプリケーション アーキテクチャパターン . 翔泳社 , 2005, 576p. 補足 上の Composite Specification の実装では AND, OR, NOT の条件を独立したクラスとして実装しましたが、 Java 8 以降の interface の default 実装を使用することも可能です。 public interface Specification<T> { boolean isSatisfiedBy(T candidate); default Specification<T> and(Specification<T> other) { return candidate -> isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate); } default Specification<T> or(Specification<T> other) { return candidate -> isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate); } default Specification<T> not() { return candidate -> !isSatisfiedBy(candidate); } } この場合のルールの組み立ては以下のように行います。 public class TaxiRule { public static Specification<TaxiApplication> getRule() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold(); int limitFee = CompanyRuleRepository.getTaxiRule().limitFee(); return new MaxFeeSpecification(limitFee) .and( new MaxFeeSpecification(freeThreshold) .or( new ReasonSpecification())); } } また、上の Specification<T> と同様の実装が Java 8 以降で関数型インターフェース Predicate<T> として標準実装されているため、こちらを使用することも可能です。
アバター
こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、僕が開発に携わっている製品のE2Eテストに取り入れたページオブジェクトモデル(POM)という実装パターンの概要と取り入れたキッカケ、POMへ リファクタリング する簡単な例をご紹介させていただきます。 僕と同じようにE2Eテストに関わっている方、E2Eテストに興味を持っている方などに読んでいただけると幸いです。 目次は下記のようになっています。 POMとは なぜPOMを使い始めたのか POMへのリファクタリング ログイン画面 テスト内容 POM導入前のテストコード ページオブジェクト作成 POM導入後のテストコード 終わりに POMとは Webアプリケーションのテスト自動化において、テストコードとWebページを分離して管理する手法です。 POMを使わない従来のテストコードはWebページと分離しないため、どうしてもDOMの構造を意識したものになってしまい、コードは長くなり読みにくくなります。大規模になると保守性なども問題になってきます。 下図はPOMを使わないテストです。同じ要素を複数回取得する必要があり、テストコードの重複もあります。 POMはこれらの問題点を解消します。具体的にはWebページごとのクラス(ページオブジェクト)を作成し、各クラス内でそのページの要素に対する操作をメソッドとして定義します。テストコード側はページオブジェクトを呼び出し、メソッドを使うだけで要素を操作できます。 下図はPOMを使ったテストです。テストコードとWebページは分離されています。テストコードはDOMを意識せずにメソッドと アサーション だけを使えば簡単にテストが書けます。 まとめるとPOMの利点は以下が考えられます。 可読性の向上 テストコードではDOMを意識しなくて済むため、簡潔なコードとなり読みやすくなると考えられます。 保守性の向上 DOMに変更があっても、ページオブジェクトを変更するだけでテストコードの変更は不要になるため、保守性は向上します。 再利用の向上 ページオブジェクトを作成しておけば、そのページの様々なテストケースにおいてコードの重複を防ぐことができます。 チーム開発の効率化 テストコードをチームメンバーで共有しやすくなります。また、テストコードを迅速に作成することができます。 なぜPOMを使い始めたのか 上記で利点をいくつか挙げましたが、その中でもPOMを使い始めた一番の理由は「保守性の向上」です。 POMを使う前は愚直に多くのページに対して テストコードを書いていました。 テストコードを書き続けるうちに1つ1つのページに対してのテストケースが多くなっていき、1つのケースに対して関係してくるページも多くなっていました。 そして、ある日、DOMを変更した瞬間、今まで問題なく通っていたE2Eテストは落ちてしまいました。1箇所の変更だけでもそのDOMが関わるテストは下図のように全て落ちてしまいます。 そこからは何か仕様変更が起こるたびに下記のようなループが起きます。 「仕様変更発生」→「DOMの変更」→「大量のテストが落ちる」→「大量のテストの修正」→「仕様変更発生」→ ...以下略 DOMの変更点が多い日には超大量のテスト修正という虚無の時間が訪れます。 このループが続いているとテストの失敗を放置してしまうようになってしまいます。 「せっかくE2Eテストを書いたのに...」「でも、毎回メンテナンスするなんて...」「もっと修正点が少なくなればいいのに...」とメンタル的にもしんどくなってきました。 同じような経験をした人や今現在している人もいるのではないでしょうか。 テストの失敗が続いてしまうと、信頼性も下がってきます。テストの意味もなくなってきます。もっと保守性の高いコードにするべきです。 そこでPlaywrightのドキュメントを読んでいたときに見つけたのが「ページオブジェクトモデル」です。 今までのWebページとテストコードの間にページオブジェクトを挟むことでページをオブジェクトとして扱える! DOMの変更が起きても、修正するのは該当のページオブジェクトでテストコードは修正しなくて済む! 操作をメソッド化すればテストコードが簡潔になり、テストコードに慣れていない人でも簡単に読める! ページオブジェクトを作る手間はかかるけど、あの虚無の日々を考えたら全然大したことない! 保守性の高いテストが書ける! と思ったため、導入するに至りました。 今では仕様変更が発生しても、下図のようにページオブジェクトのみを修正すればすぐに全てのテストが動くようになり、大幅に修正の時間も減り、保守性を高めることができました。 POMへの リファクタリング POMの概要とどのような経緯で使うことに至ったのかの説明をしてきました。 最後は実際に簡単なログイン画面をもとに普通のテストコードからどのようにPOMへ リファクタリング していくかを説明させていただきます。 ログイン画面 ログイン画面は以下のようにシンプルに「名前」と「パスワード」の入力欄があります。 テスト内容 ログイン画面へ遷移する 名前入力欄に「user」を入力 パスワード入力欄に「password」を入力 ログインボタンをクリック 一覧画面へ遷移できているかテスト POM導入前のテストコード 名前入力欄、パスワード入力欄の要素へ記入し、ログインボタン要素をクリックしています。 ログイン後、トップ画面へ遷移できているかはURLを見ることでチェックします。 ※ 下記のコードではパスワードをベタ書きしていますが、Gitなどに上げる場合にはenvファイルを経由するなどして直接は書き込まないようにしてください。 // login.spec.ts import { test , expect } from '@playwright/test' ; test ( "ログインできているか" , async ( { page } ) => { await page. goto( "/login" ); await page.getByLabel ( '名前' ) .fill ( "user" ) await page.getByLabel ( 'パスワード' ) .fill ( "password" ) await page.getByRole ( 'button' , { name: 'ログイン' } ) .click (); await expect ( page ) .toHaveURL ( "/" ) } ) ページオブジェクト作成 ページオブジェクトを作成するために適当な ディレクト リでpageObjectフォルダを作成します。 おすすめとしてはプロジェクト直下にE2Eフォルダを作成し、その中で E2E/tests/*.spec.ts や E2E/pageObject/*.ts などを管理するのが良いと思います。 では、ページオブジェクトを作成していきます。 基本は下記のような「要素の定義」「コンスト ラク タの定義」「操作の関数定義」の形で作成します。 goto関数やlogin関数はこのテストでは必須です。 waitForPageContentsのような読み込みを待つ関数は要素の読み込みを待たないことによるテストの失敗が多い場合に定義すると良いと思います。 // loginPage.ts import { expect , Locator , Page } from '@playwright/test' ; export class LoginPage { // 要素の定義 readonly page : Page readonly name : Locator readonly password : Locator readonly loginButton : Locator // コンストラクタの定義 constructor( page : Page ) { this .page = page this .name = page.getByLabel ( "名前" ) this .password = page.getByLabel ( "パスワード" ) this .loginButton = page.getByRole ( 'button' , { name: 'ログイン' } ) }   // 関数の定義 async goto() { await this .page. goto( "/login" ) } async waitForPageContents () { await this .name.waitFor () await this .password.waitFor () await this .loginButton.waitFor () } async login ( name: string , password: string ) { await this .name.fill ( name ) await this .password.fill ( password ) await this .loginButton.click () } } POM導入後のテストコード ログインページのページオブジェクトが完成したので、実際にテストコードを リファクタリング していきましょう。 やることは簡単で、テストコードの先頭で インスタンス を生成して、あとは操作を記述していくだけです。 今回の場合は下記のようなコードになります。 import { test , expect } from '@playwright/test' ; import { LoginPage } from '../pageObject/loginPage' ; test ( "ログインできているか" , async ( { page } ) => { const loginPage = new LoginPage ( page ); await loginPage. goto(); await loginPage.login ( "user" , "password" ); await expect ( page ) .toHaveURL ( "/" ); } ) ページオブジェクトを用いてログインのテストを リファクタリング ができました。 テストコードはDOMを意識したものではなくなっているので仕様変更が起きてもテストコードを変更する必要がありません。 終わりに 今回はE2Eテストにページオブジェクトモデルを導入した話をさせていただきました。 どうでしょうか。ページオブジェクトモデルの導入によりテストコードは多少見やすくなりましたが、色々実装することを考えると微妙だなと感じた人もいるのではないでしょうか。 僕も最初はそのように感じました。保守性を高めるメリットに対して、ページオブジェクト作成とテスト改修のコストがかかるというデメリットがあるため、微妙だと感じやすいです。 しかし、テストケースが増えたり、テストする画面が増えたりなど、大規模になればなる程、ページオブジェクトモデルは真価を発揮するものです。 なので、自分が開発している製品の規模感に合わせて導入するかを検討するのが良いと思います。 ここまで読んでいただきありがとうございます。この記事を機に、少しでもページオブジェクトモデルに興味を持っていただけたら幸いです。
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年7月のイベントでは「PHPDoc」について語り合いました。 弊社のメンバーが事前にまとめてきたPHPDocの情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com そもそもPHPDocとは何か 活用するポイント 課題 代表的な書き方 基本的なタグ 型の記述 PHPDocに関する質問 最低限書いておきたいコメント 必須 微妙 その他 phpDocumentor使ってますか? PHPDocの記載がすべて出力されるか phpDocumentorの使いどころは? PHP TechCafeメンバーの中で出た意見 phpDocumentorで出力されるもの WEB APIに関しての使いどころ PHPDocの記述だけで WEB APIの動作確認をする方法はないか APIドキュメントを書いておいて、フロントエンドとバックエンドを疎結合にする開発 戻り値がvoidの場合、”@return void”は書いたほうが良いか? @throws\Throwableは書くべき? 各IDEでサポートしているPHPDocの範囲が分からない レガシーシステムとPHPDocの向き合い方 他の言語のDocコメントとの違いは? まとめ 編集後記 そもそもPHPDocとは何か PHPDocについては、2021年5月に一度 【PHPDocについて語り合う】と題して、PHPTechCafeが開催されており、 その時のページを基におさらいをしました。 rakus.connpass.com PHPDocとは、関数、定数、クラス、メソッド、プロパティなどにブロックの説明として残すコメントのことです。 基本的にはただのコメントなのでプログラミングに影響はありませんが、一部のツールではPHPDocの内容によって処理を行うものもあります。 ただPHP8.0で アトリビュート が追加されたので、PHPDocとしての記述も アトリビュート 方式の記述へどんどん変わっていくのではないかというのが前回の見解でした。 活用するポイント 編集者の理解が捗る IDE で補完が効きやすくなる 静的解析がより正確に実行できる 実行時に型チェックされる 等が挙げられます 課題 明確な記述ルールは決まっていないというのが結構ネックです。 色々な書き方がありますが、 " PHP ドキュメントジェネレータであるphpDocumentorの記述が デファクトスタンダード なのか" とか、"PhpStormにあるようなものを採用すればいいのではないか" とか、色々な意見があります。 そのような点を標準化するべく、 PSR-5 や PSR-19 で議論が行われています。 今のところまだドラフト中ではありますが、PSR-5ではPHPDocのことが書いてあります。 コメントとして 「静的解析ツールでそれぞれ独自にやりたい方法でやっている感じがします。」 「PHPDocはPhpStormで生成されるものを書いているだけです」 というようなものをいただきました。 IDE で使えるものを使うという方もおられる印象です。 代表的な書き方 基本的なタグで、よく使うものをピックアップしています。 基本的なタグ @ から始まる文字列。何についてのドキュメントかを示す。 @param:関数またはメソッドの引数について記述 @return:返り値について記述 @throws:例外について記述 @var:変数について記述 @todo:開発でやるべきことがあることを示す 型の記述 前述の タグ の後に記述することで、対象の型を記述する 例 @return int 整数を返す リテラル 型 int, bool, string etc... 配列型 ただの配列(中身不明): array 数値キーの配列: int 複数の型があり得る配列: (int|string) 文字列キーの配列: array<string, int> キーごとに値の型が異なる配列: array{id: int, name: string} その他の書き方 false型 booleanではなくfalse型 property __get() , __set() を使った動的なプロパティを記述することが可能 ローカル変数の型指定 @var で指定 このようなことを記入しておくと、 IDE で補完が効いたり静的解析でチェックされるというメリットがあります。 基本的なものは上記のとおりですが、型の記述ではBoolean型ではなくfalse型があったり、 __get() , __set() を使った動的なプロパティを作れるpropertyなどがあります。 このfalse型などの変わったものについては まとめページ に書いてあるので、目を通していただければと思います。 この中でいただいたコメントでは、 「個人開発ではめっちゃ@todo使います。これを書いているとPhpStormとかでコミットする時に「まだTODO残ってるよ」って教えてくれますね。」 「@varは変数とかよりはpropertyのためのものだと思ってもらった方がいい。PHPStan対応で必要な場合を除いてあまり書かずに済ませたいですね。」 さらに興味深いところでは 「 アノテーション に進捗状況を記入して、 ガントチャート を自動生成する」 というものをいただきました。 これについては、 「その発想はなかった!」 「嫌だそれ。 アノテーション に プログレ スとか書いて、このクラスどこまで出来上がったみたいな、そこから自動的に ガントチャート に進捗率が反映されるって。中々ですね。」 「「@progress 70%」とか世知辛いですね~、どうせこれ人が書いているから99%とかで止まっているんでしょうね。」 というような、いかにも業界な感想で話が盛り上がりました。 PHPDocに関する質問 今回は ”そもそもPHPDocってどういう時に使うか” という点を念頭に議論が行われました。 最低限書いておきたいコメント 必須 @var @param @return @throws(明示的にthrowしているなら) @var,@param,@returnに関しては、ソース上型を書けば十分という意見もあります。@throwsは何かあった時にツールが補完してくれるということも考えられます。 「@returnや@paramはArrayの時だけかな」というコメントもいただきましたが、 これについては、(Arrayの)中身ということで考えれば、記載があった方が良いと思われます。 ただ、変なArrayになるのであれば、クラスで返す事などを考えて、Arrayを受け取らなければならないシーンを無くしていきたいと思います。 他にも 「型宣言と同じものをPHPDocに書き写さなくて良い」 「記載不要と思っていてもPhpStormがコメントを入れるよう求めてくる」 というコメントをいただきました。 こちらについては、型宣言しているのなら二重メンテになるので記載不要と思われますが、PhpStormは入れるよう警告してきますので「そこまで補完しなくてもいいのでは?」という意見もあがりました。 微妙 @use/@used-by @package @use/@used-byは、可変の関数を使ったときに、”ここで使っています” と、示すために使うものです。 ですが、@use/@used-by で説明しなければいけないようなロジック自体をできるだけ避ける方が望ましいと思われます。 @package は、 名前空間 の対応物または補足として使用できます。 要素を異なる階層でグループ化できる論理的な細分化を提供できます。 その他 「参考となる物が存在する場合は @link を使う場合があります」 というコメントをいただきました。 コードのコメントとしては、”意味があるものは書くべき” と思いますので、@linkの記述はあると便利です。 また、ここまでの話から、 「 PHP に ジェネリクス 導入してほしいしてほしい」 というコメントがありました。 そうなるとPHPDocのコメントを書かなくてよくなることも考えられそうですが、 半面、 「「言語組み込み ジェネリクス はいらない」という考え方もあり、実行時それまでやってしまうと重くなり、そこはPHPDocで収めておくのがいいんじゃないか 」 「パフォーマンスに影響するなら考えものですね。PHPDocで良い気がする」 というコメントもありました。 phpDocumentor使ってますか? phpDocumentor は PHP プロジェクトのドキュメントを自動で作成してくれるツール 基本的なPHPDocコメントだけでも作成してくれうのでかなり有効 CIに組み込めばドキュメントのメンテナンスコストも削減できると考えられる と紹介されました。 phpDocumentorを使うことによってプロジェクトのドキュメントを自動で作成してくれますので、 基本的にはPHPDocコメントが書かれていれば、人によっては見やすいドキュメントを生成します。 Documentation のサイトでは、出力されるドキュメントのイメージを確認できます。 右側にあるSearch検索ボックスにクラス名などを入力するとクラス一覧が表示されます。 それをクリックすると説明が表示されます。 この ページ のTable of Contentsに P と書いてあり鍵マークが外れているものがpublic、鍵が付いているものがprivate、 M がmethod、interfaceは I となっています。 PHPDocの記載がすべて出力されるか 主催者側で検証したところ、PHPDocで型を書いているところは全部出力してくれたようです。 プロダクトではまだ使っていませんが、試しに実行してみたところしっかり出力してくれたようです。 ドキュメントのメンテナンスコストが減り、CIに組み込めば開発コストも下げられる期待があります。 phpDocumentorの使いどころは? PhpStormを使っている場合、調べたいものがあればShift+Ctrlで調べられたりします。 詰まるところ ”みんながコード読める環境ならphpDocumentorによるドキュメントは無くても良い” ということなのでしょうか。 使いどころを考えてみます。 PHP TechCafeメンバーの中で出た意見 「受託開発で一部機能だけ作ってくれという案件で納品する時にあると良いのかな」 「 API のドキュメントであれば、内部実装よりは「何を受けて何を返すのか」が最低限分かればいいので、簡単にチェックできるものがあれば良いのかな」 という意見があったことが紹介されました。 参加者の方からは、 「ライブラリにはあってほしい」 というコメントをいただきました。 ライブラリ内部までは不要ですが、どう使ったら良いかはわかると嬉しいと思います。 「ライブラリも基本的にソースを読んじゃうから、別にドキュメントいらない説」 「英語を読むより ソースコード のほうが読みやすくないですか?」 そんなコメントもいただきました。 これについては PHP TechCafeのメンバーの中では、”PhpStormで検索すればだいたい賄えるのでは”とも話が挙がっていました。 phpDocumentorで出力されるもの PHPDocで型定義をしていない変数については、phpDocumentorは出力してくれるのでしょうか? この疑問については、以下の結果が紹介されました。 PHPDocに書いていなくてもコードで定義されている型が反映される 引数なども反映される PHPDocを書いていれば、関数の説明などのコメントも反映される WEB API に関しての使いどころ WEB API の仕様をコメントの中に記述していくことが考えられますが、ここからWEB API に関して話が盛り上がります。 Swagger などはその場で実行できるので便利ですが、 API のドキュメントとなると意識的に最新化し続けないといけないという意見が挙がりました。 WEB API がドキュメント化されていても、 PHP に慣れている人からすると ”ソース読めばよいのでは” という考えも出てきます。 とはいえ、ざっくりと概要を知りたい人にはドキュメントがあったほうが助かるかもしれません。 PHPDocの記述だけで WEB API の動作確認をする方法はないか PHPDocの記述の中に、 API の仕様を記載しておくだけで API の動作を試すようなことはできないのでしょうか。 実際にそのようなものが OSS に存在しているのでしょうか。 参加者の方が、見つけてくださいました。 swagger-php NelmioApiDocBundle API ドキュメントを書いておいて、フロントエンドとバックエンドを 疎結合 にする開発 先にOpenAPIを使って API 仕様を決めておけば、バックエンドとフロントエンドを分けて開発する事ができそうですが、 PHPDocの記載にによって API 仕様を定義するのであれば、先にバックエンド側のコードの方に API 仕様を書くことになるため、バックエンドとフロントエンドを同時に分けて開発することは難しいと思われます。 他にも以下のような意見が寄せられました。 Swaggerを使う場合、別環境を準備しなければいけない為、個人開発でミニマムに済ませたいときはPHPDocに書いたほうが楽なのではないか。 PHPDocはコメントとしての力が強いので、自由に書けるというのがメリット 戻り値がvoidの場合、”@return void”は書いたほうが良いか? 「書かないとPHPStanで怒られる。」 というコメントをいただきました。 「 わざわざそこまで書くのか?」 「言語の方で補えるものは書かなくても良くなってきているかもしれないです。」 という話もありましたが、 void を書くのが圧倒的多数のようでした。 @throws\Throwableは書くべき? これは「書きたい」という意見が多かったようです。 言語仕様のほうで引数の型や戻り値の型は表現できますが、@throws\Throwableの方はしっかり書いといたほうが良さそうです。 「PhpStormでWarningが出るときと出ない時がある。 」 「CatchしているときはThrowsを書かないと警告」 「逆に投げているのにCatchしていないとそれも警告」 というコメントもいただきました。 各 IDE でサポートしているPHPDocの範囲が分からない PhpStormの提言として、PHPDocにどこまで対応しているかというリストはないようです。 公式の記載 である程度情報がまとまっているようですが、特にどこまで対応しているという記載はありません。 PhpStormが公開しない理由があるのでしょうか。 「勢いよく増えていくから、書いているうちに古くなるから。」 「メンテナンスのコストがかかるんでしょうか。 」 「結局PSRの議論がクローズしないのも、こういうことがあるからなのでしょうか。 」 という意見がありました(実態は不明のようです)。 レガシーシステム とPHPDocの向き合い方 レガシーなシステムをメンテナンスする際のPHPDocの扱い方についての議論が行われました。 PHPDocはあくまでもコメントですので、型エラーになるわけでもありません。そのため、レガシーなシステムにもPHPDocを書いていくほうが良いのではないかという考えが紹介されました。 どうしてもレガシーなところでコードを直 接触 るのをためらうときには、PHPDocコメントを書いて解析に委ねるのも戦略の一つです。 レガシーな記述方法で言えば、その一つに、array shapes 1 がありますが、素直にコメントを書いていくとすれば大変です。 リファクタリング によりクラスに置き換えることできることを考えると、PHPDocの記述で補完するよりは リファクタリング することを優先したほうが良いように思います。 他の言語のDocコメントとの違いは? 参加者から以下のような意見がありました。 JavaDoc 「あまり詳しく無いんですがあまり規約とか無いんですかね。」 Python 「派閥的なものがあるみたいです。」 PHPDocも、前述の通り、 ”これは必ず書かないといけない” という決まったルールはまだありませんが、それは他の言語でも同様のようです。 とは言え、 ”これくらいは記載したいよね” という緩いルールのような共通認識はあるようです。 まとめ 「考えていることがみんな似ていてホッとする」 「PHPDocを深く考えたことがなかったので勉強になりました」 というコメントをいただきました。 明確なルールがない部分もありますが、参加者同士で疑問を共有して考えを知れたので良い機会となりました。 主催者も含めて、この機会に勉強することができたようです。 編集後記 以上、PHPerのための「PHPDoc相談会」として、PHPDocについて深く掘り下げていきました。 PHP で書くコメントは開発者が普段から当然のように利用しているものですので、主催者/参加者ともに非常に盛り上がりを見せていました。 今後も PHP に関することや、新たなニュースに着目していきたいと思います! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 PHPDocの配列表記方法の一つ。キーごとに値の型が異なる配列等を表現する。 ↩
アバター
.entry-inner img{ border: 1px solid #000; } こんにちは!技術広報課の rks_daigo と申します。 コロナ禍では多くの企業でビアバッシュ等のオフラインイベントが制限されていたと思いますが、 弊社も感染拡大を防ぐためにオフラインでの開催を自粛しておりました。 そのような中、2023年に入り新規感染者が落ち着いてきたタイミングで、感染にも配慮しながらオフラインイベントを再開しておりました。 本記事では4月に開催しました『ChatGPやってみたビアバッシュ@東京』が大変盛り上がりましたので、 そちらの様子をご紹介したいと思います! ※大阪開発拠点でも毎月ビアバッシュを開催しておりますので、また別のレポートでご紹介できればと思います。 今回のビールのお供 3月に開催された デザインビアバッシュ に触発され、おしゃれなオードブルを用意してみました。 ビールのお供 みんな喜んでくれたかな?と事後アンケートで次回のリク エス トを聞いてみたところ ピザ ×4 ピザ・お寿司 ピザや寿司があると嬉しいです 前回のように寿司&ピザみたいなものだと嬉しいです 寿司、ピザが嫌いな人はいないと思います! 銀のさら どんだけピザと寿司好きやねん。 次回はピザと寿司にしたいと思います。。 LTテーマ:「ChatGPTやってみた」 旬過ぎるテーマですね。テーマのパワーもあり、多くの方に参加していただきました。 司会は新卒で2年目のだーやまさんです。上手にまわしていただきました。感謝。 会場の様子 LT発表内容 ChatGPTを見習ってChatBotを作ろう~一家に一台ChatBot~ Google Colab上でChatBotを実装した話。Dolly 2.0使ってたのがよかったです。 おまけで Stable Diffusion も Colab に実装してましたが、ヤツは時間が溶ける。 発表者:ぐっちさん(フロントエンド開発課) LLM活用事例を考える_AutoGPT,LlamaIndex 話題のAutoGPTの実装デモ見れたのがよかった! LlamaIndexでチャットボットがさくっと作れてしまう時代ですね。 発表者:だーやまさん(楽楽勤怠 開発1課) LLMのど素人がOpenAIのEmbeddingAPIを使ってChatBotを作ってみた 文字列からベクトルを取得してコサイン類似度で突合する話です。(なんそれ) スライドも自己紹介も自動生成するというAIづくしのLTでした。 発表者:えーいちさん(楽楽勤怠 開発1課) Steamレビューを解析して要約してくれるアプリ Gamer GPT を作ってみた ChatGPTのレスポンス待ちをゲームのローディング画面っぽく実装。よいア イデア です。 アプリのUIが素敵でした。Steamって API あるんですね。 発表者:とっしーさん(フロントエンド開発課) ChatGPT加熱しすぎ問題 ChatGPTの急激な盛り上がりに一石を投じるLT。ここでは何も書けないw 技術推進課では、AI関連もどんどん追っかけていくみたいです。イイネ! 発表者:いさむさん(技術推進課) ChatGPTを使っておかやま家の生産性をXX%改善した話 おかやま家のAI事情にほっこり。うちのおかんもこんな感じです。 シリーズ化を望みます。 発表者:おかやまさん(楽楽勤怠 開発2課) OpenAPIを利用した Kubernetes 診断ツールを触ってみよう(飛び込み) ビール飲みながらさくっと実装しビール片手に登壇ニキ。 さくっと実装しちゃうところが、かっこよかったですね。 発表者:MCさん(SRE課) 終わりに 今回は、弊社のビアバッシュをご紹介させていただきました! なんか楽しそうなことしてるなと思っていただけましたら幸いです。 ラク スの開発本部では、アウトプットすることを推奨しているため ビアバッシュの予算も毎月分、確保できており(感謝) エンジニアにとって「いつでも」「気軽に」LT登壇できる環境があるのは うれしいことなんじゃないかと、しみじみ感じております。 そんな ラク スでは、一緒にビアバッシュを盛り上げてくれる仲間を募集しております! 少しでも興味を持っていただけましたら、まずはカジュアルにお声掛けください。
アバター
はじめに こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、E2Eテスト フレームワーク として用いられるPlaywrightのインストールといくつか基本的なテストコード、最後に 拡張機能 についてもご紹介させていただきます。これからPlaywrightでテストを書きたい人、E2Eテストに少しでも興味を持っていただける方に読んでいただけると幸いです。 目次は次の通りになっています。 はじめに Playwrightとは インストール 簡単なテストを書いてみよう Playwrightの設定を編集しよう 別タブを開くテストを書こう 別タブをコードから開くケース 別タブをリンクから開くケース リクエスト・レスポンスをテストしよう モックを使ってテストしよう Playwright拡張機能を使おう テストの実行 実行ブラウザの変更 ヘッドレスモードの選択 要素の取得 テストコードの生成 おわりに Playwrightとは Webブラウザ の自動化テストを実行するためのライブラリであり、Node.jsアプリケーションで使用することができます。 スクレイピング やテスト自動化、UIテスト、パフォーマンステスト、 スクリーンショット 、PDFの生成などさまざまな用途で使用することができます。特徴は下記のようになっています。 Chrome ・ Firefox ・ Safari ・Edgeなど複数のブラウザをサポートしている 簡潔な API を提供しており、直感的にWebページを操作できる 非同期で動作することによる、テストの高速実行。ブラウザのクラッシュやフリーズに対する強い耐性を持っている Puppeteerと類似してることによりPuppeteerユーザーは取り入れやすい インストール Playwrightを使用するにはPlaywright Testのインストールとブラウザのインストールが必要となります。 下記のコマンドでPlaywright Testとブラウザを同時にインストールできます。 npm init playwright@latest インストールする際に次の項目について聞かれます。 TypeScriptか JavaScript のどちらを使用しますか (デフォルトはTypeScript) -> デフォルトを選択 テストフォルダーの名前 (デフォルトは tests または e2e) -> デフォルトを選択 GitHub Actions ワークフローを追加して、CI でテストをする -> falseを選択 Playwright ブラウザー をインストールします (デフォルトは true) -> デフォルトを選択 準備はこれだけです。あとはテストコードを作成するだけで簡単にテスト自動化を実現できます。 簡単なテストを書いてみよう 早速ですが、簡単なテストを書いてみましょう。テスト内容は次のようにします。 Chrome ブラウザで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック ページが正しく表示されているかテスト テストのコードはデフォルトで作成されている tests/example.spec.ts に書いていきます。 例として書かれているテストを真似して、testの大枠を書いてみましょう。内容は一旦、コメントで書いておきます。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く // 検索欄に「株式会社ラクス」を入力 // Enterを押す // 検索結果から公式サイトをクリック // テスト:公式サイトが表示されているか } ) test関数は第一引数にテストの名前を、第二引数にテストの関数を書いていきます。テストの関数に引数として入っているpageとはブラウザで開かれたWebページを表すオブジェクトです。 テストを記述していきます。「 Google 検索エンジン を開く」の下に書きましょう。何かのページへ移動したい場合はgoto関数を使います。引数にはURLを入れます。 await page. goto( 'https://www.google.com/' ); これで Google 検索のページへ遷移するはずです。 次は、検索欄に「株式会社 ラク ス」と入力するコードを書いていきます。このコードには検索欄の要素を取得が必要です。ですが、要素を取得するのは意外と面倒です。 このような場合はPlaywrightのコードジェネレータを使いましょう。使い方は簡単です。下記のコマンドを打ちます。 npx playwright codegen https://www.google.com Playwright Inspectorと Chromium で Google 検索が開かれると思います。 Google 検索の検索欄の要素が欲しいので、そこにホバーします。すると、下記の画像のように要素が表示されます。 この要素をクリックして、検索欄に「株式会社 ラク ス」と入力し、enterキーを押します。すると、Inspectorの方に次のようなコードが生成されていると思います。 getByRoleとは名前の通り、要素を役割別に取得できるものです。今回はcomboboxというRoleを指定していますが、buttonやinputなどもあります。 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); 最後に公式サイトを検索結果からクリックするだけです。Inspectorから生成されたコードが次のようになっていれば問題ありません。 await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 生成されたコードを example.spec.ts にコピペします。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「株式会社ラクス」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // Enterを押す await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか } ) 最後に公式サイトが表示されているかをテストします。言い換えると、現在のページのURLが ラク スの公式サイトURLと一致するかのテストです。 次のコードを「テスト:公式サイトが表示されているか」のコメント下に追記します。 expectは アサーション ライブラリの一つで期待される結果を検証するときに用いるものです。pageが https://www.rakus.co.jp/ を持っているかの検証を示しています。 await expect ( page ) .toHaveURL ( "https://www.rakus.co.jp/" ); テストを書き終えたので実行をします。実行は下記のコマンドです。"9 passed"と表示されていればテスト通過できています。 npx playwright test レポートは npx playwright show-report を実行し、該当のページを表示すると見ることができます。 テストを実行することができました。 ちなみに、テストコードを書くためには対象となる要素を取得することが基本となり、PlaywrightではLocatorというWebページの要素を検索するためのオブジェクトを使います。 CSS セレクタ 、テキスト、要素の属性など、様々な方法で要素を検索できます。 とても便利ですが、Locatorはその便利さがゆえに使いすぎてコードがわかりにくくなったり、様々な検索が可能なために複雑なHTML構造を持つ要素を見つけるのが困難な場合があります。 なので、できる限りgetByRoleなどの明示的な組み込みロケーターを使います。組み込みロケーターはLocatorと比べ 、以下のようなメリットがあります。 シンプルでわかりやすいコードが書ける ボタンを特定するためにpage.locator()を使用するよりも、page.getByRole('button')のようにgetByRoleメソッドを使用した方がわかりやすいコードになります。 アクセシビリティ の改善 getByRole()などの組み込みのロケーターは、 Webアクセシビリティ の向上を目的としています。これらのロケーターを使用することで、 視覚障害 者やその他の障害を持つユーザーがWebページを使用するのに役立ちます。 ロケーターの選択が自動化される getByRole()などの組み込みのロケーターを使用することで、Playwrightが適切なロケーターを自動的に選択してくれます。getByRole('button')が実行されると、Playwrightは自動的にrole="button"属性を持つ要素を検索します。 Playwrightの設定を編集しよう テスト実行後のレポートを見てもらうと Chromium 、 Firefox 、 Webkit の3ブラウザで各テストが実行されていることが確認できます。もちろん複数ブラウザでテストすることも大事ですが毎回は時間がかかるのでブラウザを変更したい場合もあるでしょう。また、人によっては実行中のブラウザが表示して欲しい人もいると思います。 そのようなテストに関する設定は playwright.config.ts からできます。デフォルトでは下記のような設定になっています。 export default defineConfig ( { testDir: './tests' , /* Run tests in files in parallel */ fullyParallel: true , /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !! process .env.CI , /* Retry on CI only */ retries: process .env.CI ? 2 : 0 , /* Opt out of parallel tests on CI. */ workers: process .env.CI ? 1 : undefined , /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html' , /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' , } , /* Configure projects for major browsers */ projects: [ { name: 'chromium' , use: { ...devices [ 'Desktop Chrome' ] } , } , { name: 'firefox' , use: { ...devices [ 'Desktop Firefox' ] } , } , { name: 'webkit' , use: { ...devices [ 'Desktop Safari' ] } , } , ] } ); testDirはテストファイルを格納しておくフォルダのことで、ここに設定された ディレクト リ以外でテストファイルを作成してもPlaywrightは認識してくれないので注意です。 他はデフォルトでコメントをつけてくれているので省略します。 重要なのはprojectsで、デフォルトでは chromium 、 firefox 、 webkit が設定されています。 chromium だけで実行したい場合は、 firefox と webkit を コメントアウト します。これだけで実行ブラウザを変更することができます。 実行中にブラウザを表示するのも簡単です。reporterとprojectsの間にあるuseの中に下記を書き加えるだけです。 headless : false headlessオプションを書かない場合はデフォルトでtrueになっているので、falseを書き加えることで実行中にブラウザ表示させることができます。 保存して、もう一度テストを実行してみましょう。 すると、 chromium が3つ立ち上がり、テストがブラウザで実行されているのが確認できると思います。3つ立ち上がっているのはfullyParallelオプションがtrueになって、並列処理がされているためです。見にくい場合はfalseにすることで一つずつ実行されます。 オプションは他にも様々ありますので、詳しくは TestConfig | Playwright を見てください。 別タブを開くテストを書こう 別タブ操作機能の有無はE2Eテスト自動化ライブラリの選定基準の一つになっていることが多いと思います。Playwrightは別タブでの操作が可能となっています。 別タブを開くケースとしては「コードから自分で開くケース」と「ボタンを押すなどのトリガーにより開かれるケース」があると思われるのでその2パターンを紹介します。 別タブをコードから開くケース テストは内容は次のようにします。 ページを開く 別タブを開く 別タブで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック 別タブのページが正しく表示されているかテスト 最初に書いたテストを別タブで行うだけです。テストのコードは example.spec.ts に書いていきます。 例として書かれていた二つは消してしまいましょう。 ページを生成するのは難しくありません。下記のようにBrowser型を使うだけです。 const page = await browser.newPage () これだけなので、テストも下記のように簡単に書けます。 test ( '別タブでラクスのサイトが表示されているか' , async ( { browser } ) => { // 最初のタブを開く const page = await browser.newPage () // 別タブを開く const newPage = await browser.newPage (); // 別タブでGoogle検索エンジンを開く await newPage. goto( "https://www.google.com" ); // 検索欄に「株式会社ラクス」を入力 await newPage.getByRole ( 'combobox' , { name: '検索' } ) .click (); await newPage.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // 検索ボタンを押す await newPage.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await newPage.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか await expect ( newPage ) .toHaveURL ( "https://www.rakus.co.jp/" ); } ) 実行してみると、別のタブが開くことが確認できると思います。 別タブをリンクから開くケース ラク スの公式サイトから別タブを開くリンクを押すテストにしました。内容は以下のようにします。 ラク スの公式サイトを開く ヘッダーメニューの「事業内容」をホバーする 表示される様々なリンクから「楽楽精算」をクリックする(自動で別タブで楽楽精算のページが開かれる) 別タブのURLが楽楽精算のページと一致するかをテスト example.spec.ts に書いていきましょう。 公式サイトを開くのはgoto関数で簡単にできます。 続いて、ヘッダーメニューとはサイト上部の「 ラク スの思い」「会社情報」などが並んでいる部分です。 事業内容のリンク要素を取得したいです。コードジェネレータを使えば簡単に取得できますが、下記画像のようにだいぶ長いコードになってしまいます。 コードジェネレータに頼りすぎるとわかりにくいコードになることもあるのであくまでサポートくらいにするといいと思います。 コードジェネレータでうまく要素が取り出せない時には、HTMLを確認してみましょう。IDやClassが割り振られていればかなり要素を絞れると思います。 ヘッダの各要素には p-header__menu__item というClassが割り振られていました。ClassやIDで取得する場合にはlocatorを使います。 あとは「事業内容」というテキストを取ればいいので次のようなコードで要素を取得できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) かなりシンプルに取得できます。他にも下記のような取得方法もあります。この場合は「事業内容」というリンクがいくつかあり、その最初の要素が欲しいのでfirst()をつけることで取得できます。 await page.getByRole ( "link" , { name: "事業内容" } ) .first () 要素が取得できたのでホバーをするだけです。ホバーは要素にhover関数をつけるだけなので下記のように簡単に実装できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover () 別タブでページを開く際にはイベントが発生する前にイベントを待つ関数を定義しないといけません。これは下記のように書けば実装できます。 const newPagePromise = page.waitForEvent ( 'popup' ); イベントを発生させましょう。「楽楽精算」というリンクをクリックするコードです。ここは下記のようにgetByRoleでリンク要素かつ楽楽精算というテキストで要素を絞ります。exactとは完全一致を示します。exactをつけないと部分一致になってしまいます。 await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click () 別タブで「楽楽精算」のページが開くはずなので、イベントを解決しましょう。先ほどのnewPagePromiseはPromise 型になっているのでawaitをつけることでPageが取り出せます。 const rakurakuseisanPage = await newPagePromise rakurakuseisanPageは「楽楽精算」のページを表しているはずです。 最後、rakurakuseisanPageが「楽楽精算」のページURLと一致するかのテストを書きます。ここは他のテストと同じように書けます。 await expect ( rakurakuseisanPage ) .toHaveURL ( /^https:\/\/www\.rakurakuseisan\.jp\/.*$/ ); これでテストが書けました。念の為、テストコードを全て載せておきます。 test ( '別タブで楽楽精算のページが開かれているか' , async ( { page } ) => { // 最初のタブでラクスの公式サイトを開く await page. goto( 'https://www.rakus.co.jp/' ); // ヘッダーのリンクをホバーする await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover (); // 新しいページを開くイベントを待つ const newPagePromise = page.waitForEvent ( 'popup' ); // 楽楽精算のリンクをクリックする(新しいページを開くイベント) await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click (); // イベントを解決する const rakurakuseisanPage = await newPagePromise ; // テスト:新しいページのURLが楽楽精算のページと一致するか await expect ( rakurakuseisanPage ) .toHaveURL ( /^https: \ / \ /www \ .rakurakuseisan \ .jp \ /.*$/ [ f:id:koki_matsura:20230505222421p:plain ] ); } ) テストを実行してみましょう。自動で開く別タブにも対応できることが確認できます。 リク エス ト・レスポンスをテストしよう 正常にリク エス トが飛んでいるか、レスポンスが想定通り返ってきているかを確かめたい時もあると思います。 そのようなテストもできるので簡単な例とともに紹介させていただきます。テスト内容は次のようになっています。 Google 検索エンジン のトップページを開く 検索項目に「Playwright」と入力し、Enterをクリック リク エス トとレスポンスを取得 リク エス トのparamに「Playwright」が含まれているか レスポンスのステータスが200で返ってきているか リク エス トとレスポンスを取得するのは新しいページを取得するのと同じでwaitoFor系の関数を使います。 具体的には次のようなコードで書くことができます。 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // リクエストを発生させるイベント page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) waitForRequestとwaitForResponse内の条件に当てはまった場合だけrequestとresponseが返ってきます。 なので、この時点でparamに「Playwright」が含まれていないとテストは失敗します。 リク エス トとレスポンスが取得できれば、あとは アサーション するだけです。 テストするのはリク エス トのparamに「Playwright」が含まれているかどうかとレスポンスのステータスが200で返ってきているかどうかです。 取得したリク エス トとレスポンスを使えば簡単に次のように実装できます。 // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); リク エス トに関しては先ほどのwaitForRequestでparamをチェックしているのでもう一度確認する必要はないのですが一応、書いておきました。 残りの部分は他と同じなのでテストコードは次のようになります。 test ( '「Playwright」と検索されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「Playwright」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( 'Playwright' ); // リクエスト・レスポンスを取得 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // 検索を実行 page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); } ) テストを実行してみましょう。正常に検索されれば、テストは通過するはずです。 モックを使ってテストしよう モックとは、リク エス トやレスポンスをテストする際に、実際のサーバーに接続せずに、事前に準備した偽のレスポンスを返すようにする機能です。 これを使用することで以下のようなメリットがあります。 テストの信頼性向上 外部 API はテスト実行中に変更されることがあります。これにより、実行結果が変化する可能性があります。モックを使用すると、毎回同じレスポンスを返すことができ、テストの信頼性を向上させることができます。 テストの速度向上 外部 API を呼び出すと、実行に時間がかかる場合があり、 タイムアウト になる可能性もあります。モックを使用すると、 API を待つ必要がないのでテストの速度を向上させることができます。 外部との依存排除 モックを使うと外部 API への依存関係を排除することができ、テスト実行環境の制約を減らすことができます。 エラーの発見と修正の易化 例えば、外部 API がレスポンスを返さない場合、モックを使用すればその API の問題を発見することが容易にできます。 モックはE2Eテストとは切っても切り離せない機能です。ぜひ、積極的に使っていきましょう。 早速ですが、テストの内容は次のようにします。 ラク スのページを開く ラク スのページ以下( https://www.rakus.co.jp/** )をモックする 「 ラク スの思い」(/about)の場合だけレスポンスを404にする 「 ラク スの思い」をクリックする 404ページが表示されているかをテスト Playwrightでモックを実装するにはroute関数を使用します。第一引数にはモックを反映するURLを、第二引数にはハンドラーを設定します。 このテストの場合は ラク スのページ以下を全てモックするので、下記のようにします。 await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); rakusRouteHandlerではURLが /about を含む時だけレスポンスを404にするという設定にしたいので、次のように実装しました。 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; requestのURLにaboutが含まれているときだけ404を返し、それ以外はroute.continue()で通常のレスポンスを返すようにしています。 これだけで簡単にモックすることができます。 テストの全コードは以下のようになります。 test ( "ラクスのページをモックする" , async ( { page } ) => { // モックするためのルートハンドラを作成 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; // ラクスのトップページを開く await page. goto( 'https://www.rakus.co.jp/' ); // トップページ以下のリクエストをモックする await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); // 「ラクスの思い」をクリック await page.locator ( '.p-header__menu__item' ) .getByText ( "ラクスの思い" ) .first () .click (); // テスト:404ページが表示されていること await expect ( page.locator ( 'h1' )) .toHaveText ( "この www.rakus.co.jp ページが見つかりません" ) } ) アサーション の部分はページのh1に「この www.rakus.co.jp ページが見つかりません」が表示されているかどうかを見ています。 テストを実行してみると、以下のように404ページが表示されていると思います。 また、 /about 以外は通常のレスポンスが返されるかも確認してみましょう。 「 ラク スの思い」をクリックする部分のコードを以下のように変えます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .click () テストを実行してみると、正常に事業内容のページが表示されていると思います。 ハンドラで実装した通りにできています。 今回は簡単に404を返す実装にしましたが、route.fulfill内を変えるだけで柔軟に様々なレスポンスを実装することができます。 Playwright 拡張機能 を使おう VSCode 使っている人専用にはなりますが、Playwrightの 拡張機能 がとても便利なのでご紹介します。 まずは VSCode の 拡張機能 から「Playwright」と検索をし、おそらく一番上に表示される「Playwright Test for VSCode 」をインストールしましょう。 インストールが完了するとエディターのサイドバーに三角ビーカーのアイコンが表示されていると思います。 テストの実行 サイドバーの三角ビーカーをクリックすると、 tests/ 以下のテストが表示されていると思います。 再生ボタンを押すことで実行することが可能です。テストファイルの方にも同様に各テストの先頭行に再生ボタンが表示されます。 成功したテストは緑のチェックマーク、失敗したテストは赤の バツ マークがつきます。 実行ブラウザの変更 拡張機能 を入れる前は playwright.config.ts のprojectsを書き加えたり、 コメントアウト したりして実行するブラウザを変えていました。 拡張機能 の場合は三角ビーカーをクリック、下画像の「Select Configuration」をクリック、「既定のプロファイルの選択」をクリックする。 下画像のようなセレクトボックスが表示されるのでここで実行したいプロジェクトを選択して、OKを押せば簡単に実行ブラウザを変えることが可能です。 ヘッドレスモードの選択 拡張機能 を入れる前はheadressをfalseにすることで実行中の画面を表示していましたが、テストの中にはわざわざ画面で見る必要のないものもあると思います。そのようなテストを実行するたびにheadressモードにしたりするのはとても面倒です。 拡張機能 の左下、「Show browser」という チェックボックス があります。 ここにチェックを入れるとheadressをfalseにしている状態と同じになります。チェックを外すとheadressモードになります。 playwright.config.ts に書いたheadressの設定は消さないとチェックしてもしなくても画面が表示されてしまうので注意です。 要素の取得 「Show browser」の下にある「Pick locator」をクリックするとブラウザが立ち上がります。そのブラウザでテストしたい画面に移り、欲しい要素をクリックすると、下画像のように VSCode 上部に要素のコードを記してくれます。 簡単に要素を取得したい分にはこれだけで十分です。 テストコードの生成 「Pick locator」の下にある「Record new」をクリックするとブラウザが立ち上がります。 ブラウザが立ち上がるとともに、 VSCode 上で新たなテストファイルも作成され、テストの雛形が書かれていると思います。 立ち上がったブラウザ上で操作をすると、全てこのファイル内にコードが生成されていきます。コードジェネレータと同じです。コマンドでジェネレータを立ち上げなくて済むのでかなり楽です。 テストの大枠をジェネレータに生成してもらい、細かい部分を自分で修正したりすることでかなり効率化できると思います。 既存のテストファイルにコードを生成して欲しい方は「Record at cursor」をクリックしてください。 コードを生成して欲しいファイルの適当な行を選択しておきましょう。 立ち上がったブラウザで操作すると、選択した行にコードが書き加えられていきます。 例えば、「簡単なテストを書こう」のテストと同じ操作をすると下記のようなコードが example.spec.ts に生成されました。 await page. goto( 'https://www.google.com/' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 検索欄への文字入力などが認識されていませんが、そこは自分で付け足せば、簡単にテストが作れます。 とても便利なのでどんどん使ってください。 おわりに 今回はPlaywrightを初めて書く人やE2Eに興味を持っている方に向けて、簡単なテストの例と 拡張機能 についてご紹介させていただきました。 Playwrightは直感的にテストを書くことができるので初めての人でもすぐに飛びつくことができ、コードジェネレータと組み合わせることで高速にテストコードを生み出すことも可能です。 まだまだ紹介していない機能がありますので、 公式サイト を見ていただけばなと思います。 とても長い記事になりましたが、ここまで読んでくださった方、ありがとうございます。 これを機にPlaywrightを使っていただければ、とても嬉しいです。
アバター
こんにちは、新卒2年目になりました菊池(akikuchi_rks)です。 新卒1年目では開発エンジニアとして様々な経験をさせていただきましたが、その1つとしてLaravel8→9へのバージョンアップ作業を行いました。 今回はこのLaravel9へのバージョンアップにおいて自分が躓いた経験から、注意が必要だと感じた点を紹介させていただきます。 はじめに 注意すべきこと 依存パッケージの確認 Trusted Proxiesファイルの修正 app/Http/Middleware/TrustProxies.php の修正 $headersの修正 Storageファサードを利用したファイル操作の仕様変更 ファイル書き込み時の仕様 略語を使用したメソッド名の変更 Unvalidated Array Keysの仕様変更 終わりに はじめに Laravel9が2022年2月8日、正式リリースされました。 標準 メーラー の変更、Flysystem3.xへのバージョンアップなど様々な変更点がありますが、これらの変更点に関する説明は他の記事で既にまとめられているため本記事では割愛させていただきます。 弊社でも毎月開催している勉強会「 PHP TechCafe」において『PHPerのための「Laravel9について語る」』というテーマで議論した回があり、 その内容のレポート記事で、Laravel9の変更点について簡潔にまとめられているので、良ければ参考にしていただければと思います。 tech-blog.rakus.co.jp Laravelのバージョンアップを行う際は、基本的に Laravel公式のアップグレードガイド でアップグレード手順を確認しながら作業を行うことになると思います。 手動で変更する必要がある修正内容については、ほとんどこちらの公式ドキュメントで説明されているのでこちらを参考に修正を行えば問題ありませんが、私が実際にバージョンアップ作業を行う中で 公式ドキュメントに書かれておらず修正が漏れていた 公式ドキュメントの説明を正確に理解出来ていなかった など、いくつか躓いた点がありました。 本記事ではこのような躓いた経験をもとに、注意すべきだと感じた点を紹介させていただきます。 注意すべきこと 依存パッケージの確認 バージョンアップ作業に入る前の話になりますが、アプリケーションの依存パッケージを事前に確認する必要があります。 特に、Laravel9ではSymfony6.x系に依存していますがSymfony6.x系をまだサポートしていないcomposerパッケージが意外とあります。 このような場合は、 別のパッケージを使用する パッケージを自力でカスタムする などの対応が必要になります。 Trusted Proxiesファイルの修正 Laravel9へのバージョンアップ対応において、Composerの依存パッケージの変更以外で手作業での修正が必須となるのはこちらの項目くらいだと思います。 そのため、Laravel9へバージョンアップ後にアプリケーションが動かなくなったという場合はまずはこちらの修正が正しく行えているかを確認すると良いと思います。 app/Http/Middleware/TrustProxies. php の修正 変更前 use Fideveloper\Proxy\TrustProxies as Middleware 変更後 use Illuminate\Http\Middleware\TrustProxies as Middleware $headersの修正 変更前 protected $headers = Request::HEADER_X_FORWARDED_ALL; 変更後 protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_POST | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB ; 因みに TrustProxies.php 以外のファイルでも HEADER_X_FORWARDED_ALL の定数が使われている場合は修正が必要です。 私の作業を行ったプロジェクトでも他のファイルで HEADER_X_FORWARDED_ALL が使われていましたが、そのことに気づかず修正が漏れていたことが原因でサービスが動きませんでした。 Storage ファサード を利用したファイル操作の仕様変更 Laravel9ではFlysystem 1.x から 3.xへの移行を行っています。 それに伴い、Storage ファサード を利用したファイル操作の仕様が一部変わっています。 ※参考:Flysystem公式ドキュメント flysystem.thephpleague.com ファイル書き込み時の仕様 特に注意すべきなのは、 write() 、 writeStream() でのファイルの書き込み時の仕様変更です。 Flysystemの公式ドキュメントにも書かれているように、以下の表のように書き込み関連のメソッドの仕様が変更されました。 Flysystemの書き込みメソッドの仕様変更表 Flysystem 1.xでは書き込み用のメソッドと更新用のメソッドが別で用意されており、 write() が上書き処理をすることはありませんでした。 また、書き込みと更新を自動で使い分けてくれるメソッドとして put() が用意されていました。 Flysystem 3.xでは以前の put() の機能を write() が担うようになり、 wirte() と update() は削除されました。 この仕様変更に伴い、Laravel9のStorage ファサード でput()やwrite()を使用した際は、どちらもFlysystemの write() が呼び出され、自動的に既存ファイルを上書きするようになりました。 もし、既存のファイルを上書きしたくない場合は、手動でファイルの存在確認をする必要があります。 この対応を怠ると、意図せず既存ファイルを上書きしてしまう、という事象が発生してしまうため注意が必要です。 略語を使用したメソッド名の変更 こちらはLaravel公式のアップグレー ドガ イドに載っていない内容ですが、該当のメソッドを使用している場合は対応が必要です。 Flysystem 1.x から 3.xへの移行の影響で省略された単語を含むメソッド名が省略なしの単語を使ったメソッド名に変更されました。 Laravelでは Illuminate\Filesystem\FilesystemAdapter.php に定義されていないメソッドをStorage ファサード で呼び出した際、Flysystemのメソッドを直接呼び出す仕組みがありますが、 この仕組みを利用して、今回変更があったメソッドを直接呼び出している場合は、変更後のメソッド名に修正する必要があります。 以下が変更されたメソッド名の例です。 変更前 $filesystem- > createDir($path); $filesystem- > deleteDir($path); 変更後 $filesystem->createDirectory($path); $filesystem->deleteDirectory($path); Unvalidated Array Keysの仕様変更 配列に対してバリデーションを行った際の挙動が変わりました。 具体的にはLaravelのバリデータが返すバリデーション済みデータにバリデーションしていない配列キーを含むかどうかが変わっています。 このあたりの仕様変更については、ドキュメントを読んだだけではいまいちピンと来なかったため、具体例を用いて説明しようと思います。 例えば 'staff' => ['name' => '田中太郎', 'age' => 26] のような配列のバリデーションをする場合を考えてみます。 <?php // ①バリデーションしていない配列キーを含む設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} // ②バリデーションしていない配列キーを除外する設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(1) { // ["name"]=> // string(12) "田中太郎" // } //} 上記の例のように配列の特定のキーに対してバリデーションを行わなかった場合に、 validate() で返されるデータがそのキーを含むかどうかに違いがあります。 以前のリリースのLaravelではデフォルトで①、アプリケーションのサービスプロバイダの boot() 内で excludeUnvalidatedArrayKeys() を呼び出している場合は②の挙動となる仕様でした。 一方で、Laravel9ではデフォルトで②、アプリケーションのサービスプロバイダの boot() 内で includeUnvalidatedArrayKeys() を呼び出している場合は①の挙動となる仕様に変更されています。 そのため、推奨はされていませんが、以前のリリースの動作を保つためには includeUnvalidatedArrayKeys() を呼び出す必要があります。 因みに、配列の要素に対してバリデーションを全く設定していない場合は②のようなバリデーションしていない配列キーを除外する設定にしていても、 validate() で返されるデータに全てのキーが含まれる挙動になっているようなので注意が必要です。 <?php $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} 終わりに 今回は、Laravel8からLaravel9へバージョンアップ作業を行う際の注意点について紹介させていただきました。 Laravel9は残念ながらLTSではなくなったので、Laravel10へのバージョンアップ作業も早々に取り組む必要がありそうですね、、 Laravel10へのバージョンアップを行う際は、今回の経験を生かしてよりスムーズに作業を終えられるよう頑張りたいです。 以上です。 本記事が少しでもLaravelバージョンアップ時の手助けになれば嬉しいです。
アバター
こんにちは。配配メール開発課のmoryosukeです。 最新のLaravelではデフォルトのフロントエンドビルドツールがLaravel MixからLaravel Viteへと移行しました。 そこでTailwind CSS をビルドする手順を追いながらLaravel Viteに慣れていこうと思います。 Laravel Viteとは 事前準備 プロジェクト作成 Tailwind CSSを導入する 補足 最後に Laravel Viteとは Laravel ViteはLaravel用の高速なフロントエンド・ビルドツールであり、 JavaScript のパッケージマネージャである Vite を使用しています。Vite は、フロントエンドのアプリケーションを高速に開発するためのツールであり、Webpack よりも高速で、リアルタイムのホットリロードや開発時の最適化機能を提供しています。 Laravelのバージョンが9.18以前はLaravel Mixがデフォルトで使用されていました。Laravel ViteとLaravel Mixの性能比較は以下の資料が参考になります。 speakerdeck.com readouble.com 事前準備 プロジェクト作成 今回はLaravel Sailを使ってプロジェクトを作成していきます。 Docker Desktopをインストール後、プロジェクトを作成する ディレクト リ下で以下のコマンドを実行します。 curl -s https://laravel.build/vite-sample | bash プロジェクト作成が完了するとvite.config.jsという設定ファイルが追加されていることが確認できます。 ここで以下のような設定が可能です。 root:プロジェクトのルート ディレクト リを指定する。 base:公開されるアセットのベースパスを指定する。 build:ビルドに関する設定を指定する。target、minify、outDirなどのプロパティがあります。 plugins:Vite プラグイン を追加するための設定です。例えば、PWA プラグイン やSass プラグイン などがあります。 server:開発サーバーの設定を指定する。port、host、proxy、 https などのプロパティがあります。 resolve:importやrequireで解決するファイルの拡張子などの設定を指定する。 ここでは、開発サーバの設定だけを追加で行います。 vite.config.js import { defineConfig } from 'vite' ; import laravel from 'laravel-vite-plugin' ; export default defineConfig( { ///ここから server: { hmr: { host: 'localhost' , } , } , ///ここまで追加 plugins: [ laravel( { input: [ 'resources/css/app.css' , 'resources/js/app.js' , ] , refresh: true , } ), ] , } ); Tailwind CSS を導入する 以下のコマンドでTailwind CSS をインストールします。 ./vendor/bin/sail up -d sail npm install -D tailwindcss postcss autoprefixer sail npx tailwindcss init -p すると、tailwind.config.jsという設定ファイルが作成されます。 ここで以下のような設定が可能です。 theme:主に色、フォントサイズ、余白などの設定を変更するために使用されます。例えば、ヘッダーの高さやフォントファミリー、リストアイテムのインデント、ボックスシャドウのカラーやサイズなどを指定できます。 variants:各クラスに適用される状態を指定するために使用されます。例えば、hoverやfocus、activeなどの状態に応じたスタイルを定義することができます。 plugins:Tailwind CSS に新しい機能や変数を追加するために使用されます。例えば、フォントサイズを変数にまとめたり、新しい テキストエフェクト を追加することができます。 ここでは、デフォルトのままとします。 tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./resources/**/*.blade.php" , "./resources/**/*.js" , "./resources/**/*.vue" , ] , theme: { extend: {} , } , plugins: [] , } 次にresources/ css /app. css に以下のコードを追加し、Tailwindのプリセットを使用できるようにします。 resources/ css /app. css @tailwind base ; @tailwind components; @tailwind utilities; これらのプリセットを使用することで、プロジェクトにTailwind CSS を適用するために必要な基本スタイル、 コンポーネント 、およびユーティリティクラスがインポートされます。 これでビルド時にTailwind CSS が読み込まれるようになりました。 bladeから CSS を呼び出すと完成です。 <!doctype html> < html > < head > < meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > @vite('resources/css/app.css') </ head > < body class = "bg-gray-400" > < h1 class = "text-3xl bg-red-500 font-bold underline" > Hello world! </ h1 > </ body > </ html > 以下のコマンドを入力すると開発サーバに CSS が配置されます。 sail npm run dev アクセスすると以下のような画面が表示されます。 補足 上記で実行した sail npm run dev は、開発サーバーを起動するためのコマンドであり、プロジェクトのファイルを監視し、変更があった場合に自動的に再ビルドして更新されたコードを提供します。このコマンドは、開発中に使用され、 ソースコード の変更を即座に確認することができます。 本番環境用にビルドする際は、 sail npm run build を使用します。これにより、public ディレクト リにファイルが出力され、本番環境にデプロイできる状態になります。 最後に いかがだったでしょうか。今回はTailwind CSS の導入でしたが、Vue.jsやReactも同じように簡単な手順で導入することができます。 また、Laravel Mixからの移行もできるため、ぜひ試してみてください。
アバター
こんにちは 配配メール開発課 Jazumaです。 業務やプライベートでChatGPT等のAIを使いながらもより良い回答を得るためのプロンプトの作り方が分からないという方は多いのではないでしょうか。かく言う私もその一人です。 そこで今回は Best practices for prompt engineering with OpenAI API を見てプロンプトエンジニアリングのベストプ ラク ティスを整理します。 より良い出力を得るためのプロンプト 最新のモデルを使う プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る 具体的に指示を出す・説明する・詳細を記載する 例を提示して出力のフォーマットを明確に示す zero-shotから始め、few-shot, fine-tuneの順に試す あいまいな指示や不正確な指示を減らす 「してほしくないこと」ではなく「してほしいこと」を指示する コードを生成させる際は特定のパターンに誘導するための文言を含める 終わりに より良い出力を得るためのプロンプト 最新のモデルを使う より良い回答を得るために最新のモデルを使うことが推奨されています。2023年4月時点においてはテキスト生成:GPT-4, 画像生成: DALL・E, 音声変換:Whisper が有力な選択肢になると思われます。 1 プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る # 悪い例 Summarize the text below as a bullet point list of the most important points. {text input here} # 良い例 Summarize the text below as a bullet point list of the most important points. Text: """ {text input here} """ OpenAIのドキュメントでは上記のように文脈部分を区切り文字で囲う例が紹介されていましたが、別の資料では指示の部分を「###」で囲っていました。 どちらがより良いプロンプトかは未確認です。 具体的に指示を出す・説明する・詳細を記載する コンテキスト・出力結果・出力の文字数・出力形式等はなるべく具体的に詳しく指定します。 # 悪い例 経費精算システムを紹介するメールを作成してください。 # 良い例 経費精算システムを宣伝するためのメールの本文を作成してください。 詳細:""" 形式:HTML 対象:企業の経理担当者 容量:20KB程度 内容:https://www.rakurakuseisan.jp/ を本文に含める """ 例を提示して出力のフォーマットを明確に示す 出力のフォーマットを具体的に示してあげるとモデルからより良い回答が得られます。 また、出力形式が定まることにより、出力結果をプログラムで解析しやすくなります。 # 悪い例 以下のテキストで言及されているエンティティを抽出します。次の 4 つのエンティティ タイプを抽出します: 会社名、人名、特定のトピックとテーマ。 テキスト: {text} # 良い例 以下のテキストで言及されている重要なエンティティを抽出します。最初にすべての会社名を抽出し、次にすべての人名を抽出し、次にコンテンツに適合する特定のトピックを抽出し、最後に一般的な包括的なテーマを抽出します 望ましい形式: 会社名: <comma_separated_list_of_company_names> 人名: -||- 特定のトピック: -||- テーマ: -||- テキスト: {text} zero-shotから始め、few-shot, fine-tuneの順に試す 前提事項 zero-shot: プロンプトに例を示さずに指示だけを出すこと few-shot: プロンプトに何件か例を提示して指示を出すこと fine-tune: 特定のタスクをより高精度に実施するために、既存のモデルにデー タセット を与えて再学習させること まずは簡単なzero-shotプロンプトで指示を出し、上手くいかない場合はより高度なプロンプトを使います。 あいまいな指示や不正確な指示を減らす 指示を出す際は「なるべく」「少なく」等の形容詞ではなく、具体的な数値を使うことが望ましいとされます。 # 悪い例 楽楽精算についてなるべく短く、いくつかの文で一文が長くなりすぎないように説明してください。 # 良い例 楽楽精算について3 ~ 5分程度で説明してください。1文あたりの文字数は40字程度です。 「してほしくないこと」ではなく「してほしいこと」を指示する # 悪い例 以下は、エージェントと顧客の間の会話です。ユーザー名やパスワードを聞かないでください。繰り返さないでください。 顧客: アカウントにログインできません。 エージェント: # 良い例 以下は、エージェントと顧客の間の会話です。エージェントは問題の調査と解決策の提案を試みますが、個人情報に関する質問は控えます。ユーザー名やパスワードなどの情報を尋ねる代わりに、ユーザーにヘルプ記事 www.samplewebsite.com/help/faq を案内します。 お客様: アカウントにログインできません。 エージェント: ※ 良い例でも「控えます」のようにしてほしくないことを指示しているように見えますが... 否定文ではなく肯定文で指示をすることが重要なのでしょうか。 この点についてはOpenAIのドキュメントには記載されていませんでした。 コードを生成させる際は特定のパターンに誘導するための文言を含める 例えば SQL を生成したい時は「SELECT」、 Python や JavaScript のコードを生成したい時は「import」をプロンプトの末尾に記載すると 期待するコードをモデルが生成しやすくなると言われています。 # 悪い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers # 良い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers import 終わりに OpenAIのドキュメントでは「なんとなくこうすると上手くいく」という漠然とした経験則が 言語化 されていました。 業務でAIを使う場合、やはりコードを生成するのが主な用途になると思います。 今後コード生成に特化したプロンプトのベストプ ラク ティスも 調べてみたいと思います。 Models - OpenAI API ↩
アバター
2回目の投稿です。rks_mnkiです。 インフラエンジニア的なことをやっています。 さて今回は、「ブラウザ環境上の操作のみでシステムログを取得する環境構築」について、実際の設定内容などを踏まえながらご紹介したいと思います。 目次: 1.導入を進める背景、課題 2.システム要件 3.利用ツールの選定 4.システム全体の構成イメージ 5.各種コンポーネントの説明 Jenkinsジョブ Promtail Loki Grafana Nginx 6.Docker Composeによる実装 7.操作の流れをご紹介 8.まとめ 1.導入を進める背景、課題 弊社では、日々のシステム運用業務の中でインフラ以外の開発やカスタマーサポート部門でも、エラー調査や顧客からの問い合わせ対応などでログを必要とすることがあります。 その際、従来はシステムが稼働する本番環境上にある専用サーバに対して特殊な経路でリモートログインし、以下のような流れでログを取得していました。 取得したい対象サーバ名を記載したリストを作成 定期的に「なんらかの処理」が走って、対象サーバから決められたログが取得されている その中身を確認 ここで「なんらかの処理」と表現したのは、このログを取得する仕組みが大昔に作成されたものであり、正直あまり詳しく理解できていない為です。 また、 Linux 環境上での操作となるために、あまり慣れてない部門のメンバーでは対応しづらく、そのほかの問題もあって利用頻度は低い状態でした。 結局は、インフラ部門に対してログの提供依頼があり、その都度インフラにてログを取得するという対応が発生しています。 課題をまとめると、このようになります。 用意されているログ取得の仕組みが古すぎてメンテナンスされていない 現行の仕組みでは取得対象サーバを登録してから最長5分の待ち時間が発生 Linux 環境( コマンドライン 操作)に慣れていないメンバーには使いづらい ログの容量が大きい環境では処理が タイムアウト になることも  ↓↓ 用意された仕組みは使われず、インフラへのログ提供依頼ケースが多くなる そのため、ログ取得という「生産性の高くない」業務稼働がインフラ部門で一定の割合を占めてました。 あわせて、昔の仕組みを理解して改修するのは嫌だったので、それならイチから新たにログを取得できる仕組みを構築しようと考えました。 2.システム要件 構築するにあたり、まずは以下のようなシステム要件を検討しました。 基本的にブラウザ環境での操作で完結させる 取得したい対象期間を指定可能 利用ユーザが本番環境へのログインは不要 その他、以下の前提条件を定義。 対象サーバは1台のみ指定 各部署単位で必要なログの種類を選定し、それ以外は選択できないようにする これ以外のケースでログが必要な場合は、これまで通り別途インフラへの対応を依頼頂く運用を想定しています。 3.利用ツールの選定 既にインフラでの業務で利用しているJenkinsでのジョブ実行をベースに検討。 Jenkins+各種 スクリプト promtail+loki+Grafana Nginx 今回は、とりあえず取得したログの中身をそのまま閲覧できればいいので、比較的シンプルに導入できそうなツールを検討しました。 4.システム全体の構成イメージ 以下のような構成を検討し、構築を進めました。 システム構成イメージ ログデータを取得するための ダウンロードサイト と、ログ閲覧用サイトは別々で提供するようにしました。 なお、このイメージ図では掲載できていないが、JenkinsジョブのURLなども含めてアクセス先URLリンクをまとめたサイトを1つ構築して、Nginxにて提供しています。 5.各種 コンポーネント の説明 今回、長期間のログに関する傾向分析ではなく、必要な期間に限定したログの内容を参照する目的にフォーカスして構成しています。 Jenkinsジョブ 対象ホストやログ、期間を指定できる ユーザインタフェース を提供 サービス環境内の管理サーバに設置された スクリプト を実行して、必要なログを取得 取得したログを「ログサーバ」に格納 【補足】 指定されたパラメータは、 スクリプト に引き渡されて実行されます。 その中で、以下のコマンドにて「対象期間」に該当するログファイルを抽出しています。 find {対象ログのフルパス*} -newermt '{開始日付} 0:0:0' -and ! -newermt '{終了日付} 23:59:59' この時、ログファイルの「タイムスタンプ(最終更新日時)」をもとに検索をかけるので、ログの中身で記録されている日時ではないことに注意です。 例)1週間単位でログローテーションされているケースが多い -rw------- 1 root root 346K 3月 12 04:02 messages.5.gz -rw------- 1 root root 2.1M 3月 19 04:02 messages.4.gz -rw------- 1 root root 1.9M 3月 26 04:02 messages.3.gz -rw------- 1 root root 2.0M 4月 2 04:02 messages.2.gz -rw------- 1 root root 2.2M 4月 9 04:02 messages.1.gz -rw------- 1 root root 33M 4月 15 18:03 messages 上記における問題あるケースとして、対象期間を【4/4~4/6】で指定してしまうと、該当するタイムスタンプのログファイルが存在しないので、何もアクションされない事になります。 当然、ログの中身としては該当期間のものが出力されていますが、ファイル単位での検索処理となっているため、このような動きとなってしまいます。 そのため、ジョブ実行時にログが存在しないといったエラーになると、以下のようにメッセージを返すようにしています。 #### WARNING #### No data. Please check HostName or change the specified period and try again. Promtail 所定の位置に格納されたログを収集し、Lokiに転送 各ログの種別に応じてタグ付け Timestampの設定 サンプルコード server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: messages static_configs: - targets: - localhost labels: job: messages __path__: /mnt/data/collect_logs/*/*/messages* デフォルト設定では、ログを収集した時刻(promtailが読み込んだ時刻)をタイムスタンプとして扱う事になります。これでは、ログが出力されている日時で検索したいケースに対応することができません。 そのため、ログに出力された日時をタイムスタンプとして扱えるように設定します。 具体的には、 timestamp に関する設定によりデータを解析し、Lokiにて保存されたログエントリのタイムスタンプを上書きします。 例)/var/log/messagesの場合:このログに出力されている日時をタイムスタンプとして認識させる。 Apr 9 05:54:04 {ホスト名} systemd[1]: session-51560.scope: Succeeded. pipeline_stages を以下のように設定。 pipeline_stages: - match: selector: '{job="messages"}' stages: - regex: expression: "^(?s)(?P<time>.+? \\d\\d:\\d\\d:\\d\\d):? (?P<content>.*)$" - timestamp: source: time format: 'Jan _2 15:04:05' location: Asia/Tokyo regex にて日時のタイムスタンプを 正規表現 で指定し、 time 変数に格納。 その下部にある timestamp の source で先ほど設定した time 変数を指定し、 format でタイムスタンプ形式を指定。 この辺りは、公式サイトなどを参考にしながらトランアンドエラーにて設定した為、あまり詳しく理解できていませんので、適切な設定方法があればご指摘ください。。 grafana.com Loki ログの集約と検索が行える Grafanaと連携 ※基本的には、configはデフォルトのまま運用。 Grafana 可視化プラットフォーム Lokiから連携された時系列データを処理する Nginx Webによるログの ダウンロードサイト を提供 URLリンクの管理サイトを提供 デフォルトのconfigを利用し、必要に応じてHTMLファイルを作成。 6.Docker Composeによる実装 今回、上記の各種 コンポーネント に関する複数のコンテナを、Docker Composeにより一括で管理しています。 サンプル)docker-compose.yml version: '3' services: nginx: image: nginx container_name: nginx environment: TZ: Asia/Tokyo ports: - "80:80" volumes: - /data/sample-log_collect/dev:/usr/share/nginx/html/download/sample-develop - /data/sample-log_collect/cs:/usr/share/nginx/html/download/sample-support - /log_aggregation/nginx/config/default.conf:/etc/nginx/conf.d/default.conf - /log_aggregation/nginx/config/index.html:/usr/share/nginx/html/index.html - /log_aggregation/nginx/config/main.css:/usr/share/nginx/html/main.css - /log_aggregation/nginx/config/manual.html:/usr/share/nginx/html/manual.html networks: - sample-log_aggr promtail: image: grafana/promtail:2.3.0 container_name: promtail environment: TZ: Asia/Tokyo command: -config.file=/mnt/config/promtail-config.yaml volumes: - type: bind source: /data/sample-log_collect target: /mnt/data/collect_logs - type: bind source: /log_aggregation/promtail/config/promtail-config.yaml target: /mnt/config/promtail-config.yaml networks: - sample-log_aggr loki: image: grafana/loki:2.3.0 container_name: loki environment: TZ: Asia/Tokyo ports: - 3100:3100 command: -config.file=/mnt/config/loki-config.yaml volumes: - /log_aggregation/loki/config/loki-config.yaml:/mnt/config/loki-config.yaml networks: - sample-log_aggr grafana: image: grafana/grafana:latest container_name: grafana environment: TZ: Asia/Tokyo hostname: grafana volumes: - ./data/grafana:/var/lib/grafana networks: - sample-log_aggr ports: - 8080:3000 user: "0:" networks: sample-log_aggr: name: sample-log_aggr ipam: driver: default config: - subnet: 10.***.***.0/24 なお、デフォルトだとdockerのログは docker-compose logs コマンドで確認が必要ですが、正直使いづらく過去ログも保管しておきたいので、syslog経由でログを出力するように設定しています。 以下の設定を各コンテナごとに追記。 logging: driver: syslog options: syslog-facility: daemon tag: log-aggregation/{{.Name}}/{{.ID}} すると、このようなログが生成されます。 ファイル名:/var/log/docker/log-aggregation.log ログの中身: Apr 15 17:46:07 HOST log-aggregation/loki/197fdf4139ac[207994]: level=info ts=2023-04-15T08:46:07.056567716Z caller=table_manager.go:476 msg="creating table" table=index_19073 準備が整い docker compose up -d コマンドにて起動すると、以下のようなプロセスが生成されます。 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ab60c21fffff grafana/grafana:latest "/run.sh" 34 hours ago Up 34 hours 0.0.0.0:8080->3000/tcp grafana ce89712d582a nginx "/docker-entrypoint.…" 34 hours ago Up 34 hours 0.0.0.0:80->80/tcp nginx 197fdf4139ac grafana/loki:2.3.0 "/usr/bin/loki -conf…" 34 hours ago Up 34 hours 0.0.0.0:3100->3100/tcp loki 691b10c43729 grafana/promtail:2.3.0 "/usr/bin/promtail -…" 34 hours ago Up 34 hours promtail 7.操作の流れをご紹介 まず、対象サーバや期間を、取得したいログの種別を選択してJenkinsジョブを実行します。 Jenkinsジョブの入力画面 ジョブが正常実行されたら、ブラウザ上より欲しいログファイルをダウンロードできるようになります。 対象ログファイルをクリックすると、ダウンロードされます。この場合、利用ユーザのローカル環境にログをダウンロードし、 テキストエディタ 等でログを確認するケースを想定しています。 ダウンロードサイト または、Grafanaによるブラウザ上でのログの閲覧も可能です。 ダッシュ ボード画面も用意していますが、左メニューから Explore を選択すると、取り込んだログファイルを指定して、その内容を閲覧することができます。 Grafana_explore さらに、対象期間の絞り込みや、キーワードによる対象ログの抽出も可能ですので、特定ログを調査したい場合に活用できます。 以下の例では、 systemd のワードで抽出しています。 キーワード検索 8.まとめ 使い勝手の良さを意識して、 ブラウザ上での操作で完結する本番システムログの取得環境 の構築に取り組み、なんとか思い描いた形に仕上げることができました。 これにより、 従来の仕組みと比べてお手軽に使えるようになり、今まで一定の頻度で発生していた 「インフラ部門へのログ提供依頼」 の件数が少しでも減少することを期待 しています。 そのためにも、少しでも見やすいマニュアルを作成して、他部門へ展開しています。 ただし、 対象サーバが複数であったり、対象期間が1ヵ月を超えるようなケースにおいては、本システムを使っての取得はNG としているので、まだ一定数以上の依頼は発生する想定です。 また、リリースしたばかりのシステムであるため、今後利用されていく中で様々な不具合や改修ポイントが見つかると考えており、より良い形になるよう今後もメンテナンスしていきたいと思います。 (実際、「あれ…」ってなる箇所を逐次発見しては改修している状況です。) 以上です。本記事が少しでもお役に立てれば嬉しいです。
アバター
はじめに こんにちは、imamoto です。 今年も プロ野球 が開幕し、すっかり春だなぁと感じる今日この頃です。 さて今回は、Go言語でのWeb アプリ開発 をした際のチーム内のノウハウを、 GitHub 上で公開してみた話を書いていきたいと思います。 目次 はじめに 目次 公開したGitHubリポジトリの紹介 資料の構成について モジュラーモノリスで実装した背景 ノウハウ公開に至った経緯 おわりに 公開した GitHub リポジトリ の紹介 私はSRE課に所属しているのですが、SRE課ではGo言語でWebアプリを開発することが多いです。 独立した機能を持った複数のGo Moduleを、1つの モノリス なアプリケーションとしてデプロイする、 いわゆる モジュラー モノリス という設計での アプリ開発 にもチャレンジしているのですが、 その開発中にチームとして蓄積した実装ルールやノウハウのうち、汎用化できそうな部分をまとめて、 以下の GitHub リポジトリ 上に公開したので紹介させてください。 github.com この記事では、以下の3点について説明していきたいと思います。 資料の構成について モジュラー モノリス で実装した背景 ノウハウ公開に至った経緯 資料の構成について まとめた内容は、以下の GitHub 上のREADMEファイルを出発点にして学習できるようになっています。 github.com 前提として、Webフレームワークは gin-gonic/gin 、ORMは GORM 、DI ツールは google/wire を使用しています。 また、 3.推奨する学習順序 の項にも記載してありますが、資料は以下の9つの領域で構成されています。 (2023年4月時点での内容であり、予告なく構成を変更する可能性があります) Basics : DDD の基本説明、 ディレクト リ構成等、当資料を参照するにあたって認識いただきたい前提知識を記載しています。 Multi Module Project : モジュラー モノリス 構成のアプリに対して、Go の Workspace 機能を用いて名前解決する方法を記載しています。<3. Layered Architecture : 単一の Go Module はレイヤード アーキテクチャ の構成を取っています。各レイヤーの説明と基本的な実装方法を記載しています。 Dependency Injection : 各レイヤーをまたいだオブジェクトのファクトリ関数を google/wire で生成する手法を記載しています。 Transaction : DB トランザクション の制御の実装方法を記載しています。 Routing And Middleware : Web フレームワーク Gin を用いたルーティングの実装方法を記載しています。 Row Level Security : PostgreSQL の Row Level Security を、ORM の Gorm を用いて実装する方法を記載しています。 Goroutine Job : Goroutine を使った バッチ処理 の実装方法を記載しています。 Unit Test : 単体テスト の基本的な記述方法を記載しています。 DDD 、 レイヤード アーキテクチャ 、 モジュラー モノリス 等の全体的な設計に関わるトピックだけでなく、 DB トランザクション 、 RLS 、 DI 等の具体的な実装に関わるトピックも含まれています。 多くの方に少しでもお役に立てていただくことができますと幸いです。 モジュラー モノリス で実装した背景 今回まとめたノウハウはモジュラー モノリス のアプリケーションであることが前提となっていますが、 設計としてモジュラー モノリス を選択した背景は以下の3点に集約されます。 業務ロジックの複雑度 単一チームでの開発のためMSA化の必要性が低い Go Moduleを使えばシンプルにモジュラー モノリス 構成を実現可能 実現したい業務ロジックがかなり複雑なアプリを開発する必要があったので、 単純な モノリス のアプリケーションにすると複雑度が高くなりすぎるというリスクが開発当初からありました。 そのため、コンテキストマップの設計をすることで適切なモジュール分割を実施し、 実際のアプリケーションの構成と一致させることを目指しました。 ただ、単一チームでの開発で複数のマイクロサービスに分けて開発するとメンテ 工数 だけ増加しメリットがあまり無い点、 Go言語のGo Moduleを連携させればコンテキストマップの構成とアプリの構成を一致させることができる点を踏まえて、 モジュラー モノリス という形を取ることにしました。 ノウハウ公開に至った経緯 私が所属するSRE課は、私を含めメンバー全員がほぼGo言語の開発経験がないところからのスタートだったので、 有識者 がみんなを引っ張っていくスタイルを取ることができませんでした。 そのため、学習・試行錯誤・軌道修正をを繰り返しながらより良い実装の仕方を固めていくという形で開発を進めました。 一方、設計や実装の背景と、それらに付随する用語を理解する必要がある領域が多い構成になっているにも関わらず、 それらの ドキュメンテーション が追い付いていなかったため、 途中から参画してくるメンバーに対するオンボーディングにおいては口頭で説明せざるを得ない状況になっており、 既存メンバーと新規メンバーの間で、重要な概念に対する理解度に開きが生まれてしまうリスクが生まれてしまっていました。 そのリスクを解消するべく、必要なノウハウを汎用的にまとめてチームで共有することにしました。 また、自分たちの試行錯誤の結果を一般に公開することで、他のエンジニアの助けになれば良いなという思いと同時に、 より良い実装方針のフィードバックも外部からいただけるかもしれないという思いもあり、 今回は GitHub 上で一般公開するという結論に至りました。 おわりに 今回は、SRE課におけるGo言語のWeb アプリ開発 のノウハウを公開したお話について書かせていただきました。 もしご興味があれば、公開させていただいた資料をご一読いただき、感想をいただけると嬉しいです。 それでは、また!
アバター
楽楽精算開発部の id:smdr9p です。主に Java を使ったサーバーサイドを担当しています。 前置き GoF の デザインパターン はご存知でしょうか。 ご存知の方も多いかと思いますが簡単に説明すると、 GoF の デザインパターン とは Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides の4人、通称 Gang of Four 、略称 GoF によって書かれた書籍、Design Patterns: Elements of Reusable Object-Oriented Software(邦題: オブジェクト指向 における再利用のための デザインパターン )に掲載されている23の デザインパターン のことです。 GoF パターンや単に GoF と呼ばれることもあります。(この記事では以降は GoF パターンと呼びます。) これらは本のタイトルにも示されているとおり オブジェクト指向 プログラミングの領域における デザインパターン をまとめたものですが、この本、そしてこれに掲載された デザインパターン があまりにも有名であるため、単に「 デザインパターン 」と言った場合にこの GoF パターンを思い浮かべる人も数多くいるほどです。 GoF パターンのうち、特に有用性の高いものは言語仕様自体に取り込まれたり、メジャーな ミドルウェア でサポートされたりしているものもあります。 このように GoF パターンは多くの人々によって長年活用され続けてきているものの、 デザインパターン はこれだけ知っていれば十分、というわけにはいきません。なにしろ GoF パターンは書籍が発行されたのが 1994 年、 Java も PHP もリリースされる前であり、それから オブジェクト指向 プログラミング界隈にも様々な知見が蓄積され、新たな常識やベストプ ラク ティスが作られてきました。 デザインパターン についても新たなパターンの発見や既存のパターンの再評価、ブラッシュアップがなされています。 そこで、それらの中から個人的によく見る、または使いやすいパターンを何回かにわけていくつか紹介したいと思います。 前置き Null Objectパターン パターン適用前 Null Objectパターンを適用してみる まとめ 関連するデザインパターン Strategyパターン Stateパターン Compositeパターン 参考文献 Null Objectパターン 今回は Null Objectパターンです。 このパターンを語弊を恐れずに簡単に言うと 「 null をオブジェクトとして扱う 」 です。 これにより null を ポリモーフィズム に組み込むことができ、null に関するさまざまな面倒や障害を避けることができます。 このパターンでは、実際のオブジェクトと同じインターフェースを持つ null オブジェクトを作成します。このオブジェクトは null 値の代わりに使用され、null 値のときに期待される振る舞いをします。これにより、今まで null チェックを行って null の場合はスキップ、null でない場合はオブジェクトのメソッドを実行、のような処理をしていた場面で、null チェックなしにオブジェクトのメソッドを実行することができるようになります。 パターン適用前 以下の ありがちな コードを Null Object パターンを使って改善してみます。 /** インターフェース */ public interface Shape { void draw(); int sumOfInteriorAngles(); } /** 実装クラス1 */ public class Triangle implements Shape { public void draw() { System.out.println( "△" ); } public int sumOfInteriorAngles() { return 180 ; } } /** 実装クラス2 */ public class Square implements Shape { public void draw() { System.out.println( "□" ); } public int sumOfInteriorAngles() { return 360 ; } } /** Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> null; } } } 図形を表す Shape インターフェースの実装として、 Circle クラスと Square クラスがあります。 図形を描画する draw() メソッドに加え、図形の内角の和を取得する sumOfInteriorAngles() メソッドも持っています。 またそれらの実装クラスを生成する ShapeFactory クラスもあります。角の数を指定するとそれに応じた図形クラスの インスタンス を返してくれますが、対応する実装がない場合は null を返します。 そして以下がそれらのクラスを利用する利用側のクラスです。 /** 利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { if (shape == null ) { // nullチェック continue ; } shape.draw(); } } public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } } 図形リストの shapes を渡してリストの図形を描画したり、リスト全体の内角の総和を取得できたりします。 ここで渡されるリストの生成方法については記載していませんが、リストの要素はすべて ShapeFactory クラスによって生成されたものが入っていると考えてください。 上記のとおり ShapeFactory クラスは null を返すことがあるのでリストには null が含まれている可能性があります。リストから取り出した Shape の実装クラスのオブジェクトはしっかり null チェック して、「 null の場合は何もしない 」ようにしています。 …のつもりだったのですが、実は一箇所 null チェックを忘れている箇所がありました。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このメソッドに渡されるリストにも null が含まれる可能性がありますので、ここでも null チェックが必要でした。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .filter(Objects::nonNull) // ここも要nullチェックでした .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このケースではここだけの修正で済みましたが、他の Shape を参照している箇所で同様のチェック漏れがないか確認する必要がありそうです。また、 Shape に新たなふるまいが追加された場合にも継続して注意していく必要がありますし、 Shape 以外のクラスでも類似の例がないか確認する必要もあるかもしれません。 このように、null を null のまま使い続ける限り、常に null を意識してコードを書いていく必要があります。 しかも null チェックを忘れていたとしても コンパイラ はエラーを吐いてはくれず、実行時に条件が合った場合に初めて NullPointerException が発生するのも頭の痛いところです。テストコードでチェックするとしてもテストケースから抜けていたら同じことです。 Null Objectパターンを適用してみる このような場合に Null Objectパターン が役立ちます。このパターンを適用することで、利用側で null チェックが不要になり、コードがシンプルで読みやすくなるだけでなく、今後の機能追加や変更に対しても安全性が向上します。 それでは、上記の実装に Null Objectパターンを適用してみましょう。 Null Objectパターンでは、null の場合に適用されるクラスを作成し、いままで各利用箇所に書かれていた「 null の場合のふるまい 」をあらかじめ持たせておきます。 Shape であれば draw() のときは文字どおり「何もしない」ふるまいをします。 sumOfInterirorAngles() であれば「何もない」数値である0を返すふるまいをします。 /** Null Object実装クラス */ public class NullShape implements Shape { public void draw() { // 何もしない } public int sumOfInteriorAngles() { return 0 ; // 何もない } } そして、 Factory クラスは適切な Shape の実装クラスの インスタンス を生成できない場合、Null Object である NullShape の インスタンス を返すようにします。 /** Null Object対応 Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> new NullShape(); // ここが変わった } } } ※ ここでは簡易的に都度 new していますが、可能であれば Singletonパターンで NullShape の インスタンス は1つのみ生成されるようにしておくのが望ましいです。 これによりリストには null の代わりにこの NullShape オブジェクトが入るようになりますので、リストから取り出した Shape の null チェックは不要になります。 /** Null Objectの恩恵を受けた利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { // if (shape == null) { // nullチェック不要! // continue; // } shape.draw(); } } public int countCorners(List<Shape> shapes) { return shapes.stream() // .filter(Objects::nonNull) // ここもnullチェック不要! .mapToInt(Shape::numOfCorners) .sum(); } } もちろんこの変更によってエラーは出ませんし、出力結果も変わりません。 いままでは利用側のコードで「オブジェクトが null であれば何もしないようにする、null でなければオブジェクトに命令する」判断が必要でした。 しかし Null Objectパターンを適用したコードでは、 利用側は「オブジェクトに命令する」のみ でOKです。オブジェクトが Null Object でなければ今までと同じふるまいをしてくれますし、Null Object であれば 「何もしない」をしてくれます 。 このように null を NullShape として Shape の派生とすることで、null を特別扱いすることなく他のクラスと同等の扱いができるようになります。将来的に Shape を利用するコードが追加されたとしても null に関する問題が発生する可能性を軽減できるでしょう。 まとめ このパターンは、null チェックが頻繁に行われるプログラムや、null が意図しないエラーやバグの原因となりやすい場面で特に有用です。 Null Objectパターンは null すらオブジェクトにすることによって ポリモーフィズム の活用範囲を広げる、非常に オブジェクト指向 らしいパターンだと思います。 あなたの既存のコードで Null Objectパターンが活用できる場所がないか、ぜひ探してみてください。 関連する デザインパターン Null Objectパターンに関連、あるいは活用できる他の デザインパターン をいくつか紹介します。 Strategyパターン Strategyパターンは アルゴリズム や実装を実行時に柔軟に切り替えることができるようにするパターンです。Strategy として何もしたくないケースがある場合に Null Object で Strategy を実装する事が可能です。開発やテストのときにログが出力されないようにするために NullLogger のようなクラス使ったことがあるかもしれませんが、それがまさに Null Object です。 Stateパターン Stateパターンはオブジェクトの内部状態にもとづいてオブジェクトのふるまいを変更するパターンです。State が何もない状態のときのふるまいを Null Object に実装することが可能です。初期化前などの状態に null を使用しているのであればそれを Null Object にしてみるといいかもしれません。 Compositeパターン Compositeパターンは階層構造を表現するパターンで、容器と中身を統一的に扱えるようにします。中身が何もないケースを Null Object で実装することで、容器の中身が何もない場合も変わらず統一的な操作を行うことが可能になります。 参考文献 リファクタリング -プログラムの体質改善テクニック Martin Fowler:著 児玉 公信 / 平澤 章 / 友野 晶夫 / 梅沢 真史:翻訳 ピアソン・エデュケーション オブジェクト指向 における再利用のための デザインパターン (改訂版) Erich Gamma / Richard Helm / Ralph Johnson / John Vlissides:著 吉田 和樹 / 本位田 真一:監修 SBクリエイティブ 増補改訂版 Java 言語で学ぶ デザインパターン 入門 結城 浩:著 SBクリエイティブ
アバター
配配メール開発課moryosukeです。 2023/03/23(木) ~ 03/25(土)の3日間に渡ってPHPerKaigi2023が開催されました。 今回も前回に引き続きハイブリッド開催となり、現地・配信ともに大盛況でした。 このイベントは 日本PHPユーザ会 主催のイベントで、 ラク スはスポンサーとして協賛させていただいています。 https://phperkaigi.jp/2023/ ラク スからは7人が登壇した他、多くのメンバーが参加しました。 そこで今回は参加者によるレポート、そして ラク スからの登壇者本人によるレポートを紹介させていただきます。 3/23(木)前夜祭 名著「パーフェクトPHP」のPart3に出てきたフレームワークを令和5年に書き直したらどんな感じですかね? レポート ある日オレオレフレームワークを作りたくなったぞ!! レポート 名付けできない画面を作ってはならない - 名前を付けるとは何か レポート 3/24(金) 1日目 作って理解するバックドア レポート 時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる レポート 防衛的 PHP: 多様性を生き抜くための PHP 入門 レポート 詳説「参照」:PHP 処理系の実装から参照を理解する レポート Composerを「なんとなく使う」から「理解して使う」になる レポート PHP Parserで学ぶPHP レポート 安全にプロセスを停止するためにシグナル制御を学ぼう! レポート パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 発表者本人によるレポート 不幸を呼び寄せる命名の数々  ~君はそもそも何をされてる方なの?~ 発表者本人によるレポート stdClassって一体何者なんだ?! 発表者本人によるレポート 特徴、魅力を知って、各PHPフレームワークを使いこなそう! レポート 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解とRDBMSの上手い使い方 レポート いろいろなフレームワークの仕組みを index.php から読み解こう レポート 約10年もののPHPアプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて レポート 計測できるレガシーさを捉え、コード改善に対処する レポート 【実録】「PHP_CodeSniffer」で始める快適コードレビューライフ 発表者本人によるレポート PHPマジックメソッドクイズ! 発表者本人によるレポート フレームワークが存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 レポート まとめ PHPerのためのコミュニティ PHPTechCafe 3/23(木)前夜祭 名著「パーフェクト PHP 」のPart3に出てきた フレームワーク を令和5年に書き直したらどんな感じですかね? report by id:rakuinoue きんじょうひできさん (@o0h_) による発表です speakerdeck.com レポート 2010年頃、同様に MVC フレームワーク を作成し社内に広めていた身からすると、非常に刺さる内容でした。 当時と比べると、 Composerの登場・PSR-xxなどの デファクト 化が進む PHP のバージョンアップに伴う型に関するサポート・静的解析の普及 フルスタ ックより、様々なライブラリを組み合わせていく これに伴う抽象化やDIの概念の必要性 等が変わってきた内容として挙げられていました。 改めて設計技法等含めて、おさらいできる良い内容だったと思います。 ある日オレオレ フレームワーク を作りたくなったぞ!! report by id:akikuchi_rks 果物リンさん (@FruitRiin) による発表です レポート 独自で作成した フレームワーク 、いわゆるオレオレ フレームワーク を作成した過程をお話しいただきました。 composer init で フレームワーク の雛形を作成するところからPakegistに公開するところまでプロジェクト作成の全体の流れを知ることができ、とても勉強になりました。 また、発表の冒頭ではオレオレ フレームワーク の歴史的背景が語られていたのですが、2000年代初頭の今普及しているような フレームワーク が登場していない頃には 当たり前のようにオレオレ フレームワーク が作られていたというお話には衝撃を受けました。 登壇者もお話ししていたようにオレオレ フレームワーク はセキュリティの問題や引継ぎが大変という理由から現代のプロダクションに活用するのは難しいと思いますが、 今回のように フレームワーク を1から作ってみることは普段意識していない フレームワーク の内部的な仕組みを知ることができ、かなり役立つのではないかと感じました。 名付けできない画面を作ってはならない - 名前を付けるとは何か report by id:mrstsgk_rks ちゃちいさん (@chatii) による発表です speakerdeck.com レポート トーク 名は「名付けできない画面を作ってはならない」ですが、画面についての話ではなく 命名 についての話になります。 名前をつける大切さが伝わる発表でした。特に名前を付けて認識を共有するについて、 ユビキタス 言語の例は納得できました。 また、技術的な発表で心理学の概念(愛着理論)が出てきたのは面白かったです。 最後に、リーダブルコードはやっぱり良書でした。 3/24(金) 1日目 作って理解する バックドア report by id:hirobex Rokuさん (@ad5jp) による発表です speakerdeck.com レポート バックドア を設置されるとどうなるのか なぜ バックドア が設置されてしまうのか バックドア の攻撃手法 バックドア を設置されないようにするには といった、 バックドア に関する知識が詰まった トーク でした 中でも、実際に採集された バックドア のサンプルは一見の価値ありだと思います! 時間を気にせず普通に カンニング もしつつ ISUCON12 本選問題を PHP でやってみる report by id:hirobex sji さん (@sji_ch) による発表です speakerdeck.com レポート ISUCON12優勝チームのレポジトリを参考にしつつ、 PHP でハイスコアを目指すという内容でした データベースの ボトルネック 解消後、徐々にGoと引き離されて行く中で、 いかに PHP としてアプリケーション側のスコアを上げていくのかという挑戦が非常に面白かったです Webアプリケーションパフォーマンスに興味のある人は、ぜひ見るべき トーク だと思いました! 防衛的 PHP : 多様性を生き抜くための PHP 入門 report by id:hirobex しけちあさん (@s6n_jp) による発表です レポート 多様性を生き抜くためにどのような防衛手法が取れるのか、という話から始まり PSR-12などの規約や、様々な静的解析ツールの使い方を紹介していただきました コード品質を担保するために、静的解析を使ってみたい人におすすめの トーク です! 個人的に、この トーク で紹介されていた「Rector」というツールが気になりました✍ 詳説「参照」: PHP 処理系の実装から参照を理解する report by id:mrstsgk_rks nsfisisさん (@nsfisis) による発表です github.com レポート PHP における参照の不思議クイズから始まり、 PHP の内部でどのような処理が行われているかの解説を挟んで クイズでの参照の挙動を説明していただき、 PHP における参照を少し理解できました。 特に、 PHP 処理系は C言語 で書かれていますが、 C言語 の知識はあまり必要ではなかったので C言語 を触ったことのない私からすれば、発表が聞きやすかったです。 Composerを「なんとなく使う」から「理解して使う」になる report by id:hiro_ji あすみさん (@asumikam) による発表です speakerdeck.com レポート Composerではどのように依存関係を管理しているかや、 DockerにおけるComposerの取り扱いについてお話しいただきました。 Composerを理解して使うためにはまずcomposer. json /composer.lockがいつ更新されるかを理解する 本番環境に余計なものを入れないためには Copmoser install に[--no-dev]オプションをつける マルチステージビルドでDockerイメージを作成 タイトルにもあるようにComposerを「なんとなく」使っていた身としては必見の内容でした! PHP Parserで学ぶ PHP report by id:takaram inouehiさんによる発表です speakerdeck.com レポート PHP コードを 構文解析 して抽象 構文木 (AST) に変換できるライブラリ PHP -Parser ( nikic/php-parser ) を通して、 PHP 自体について学ぼうという発表です。 PHP -Parserは PHP 本体のコントリビューターでもある Nikita Popov さんが開発しており、PHPStanやPsalmといった広く使われている静的解析ツールも PHP -Parserの上に成り立っています。 普段何気なく書いたり読んだりしている PHP ですが、文と式の違い、関数と言語構造の違い、 スカラー 値の種類など、把握している方はどれくらいいるでしょうか? 私はそれなりに知っているつもりでいましたが、「そんな書き方できたんだ!」ということもあり 目から鱗 でした。 PHP 自体は C言語 で実装されていて、なかなか中身を読むのはハードルが高いですが、 PHP で実装された PHP -Parserは、 PHP を詳しく学びたい人にはちょうどいいと感じました! 安全にプロセスを停止するためにシグナル制御を学ぼう! report by id:hirobex やまもとひろやさん (@HiroyaYamamoto1) による発表です speakerdeck.com レポート プロセスとシグナルの関係を、なんとなくで理解している人におすすめの トーク です! 私自身、とりあえず Ctrl+C でプロセスが止まるんでしょ?といった程度の理解だったのですが この トーク を聞いてプロセスの止め方に適したシグナルがあることや、プロセスの止まり方によって PHP プログラムが制御できることを知りました! パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 report by id:taclose 弊社 前田啓佑 (@taclose) による発表です speakerdeck.com 発表者本人によるレポート パフォーマンス改善の失敗談と成功談を話した内容です。 改めて「計測するのが大事なんだな」と理解してもらえたかと思います。 スライドのほとんどは非エンジニアでも読めるような構成をとっており、若手やマネージャーの方でも読める構成になっています。 今一度パフォーマンス改善のやり方をチーム内で話し合って、共通認識を持ってもらえればと思います。 不幸を呼び寄せる 命名 の数々  ~君はそもそも何をされてる方なの?~ report by id:yamamuuu 弊社 山村光平 (@yamamu096454848) による発表です speakerdeck.com 発表者本人によるレポート 開発をしていると「この変数は何を表しているのだろう?」とか、「この関数は何をしているのだろう?」とか、一目見ただけでは何を伝えたいのかが分からない 命名 を目にすることがあります。 意図が伝わらないだけであれば調べれば済むかも知れませんが、時には 命名 が誤解を招き大問題に発展することもあります。 そんな不幸を呼び寄せる 命名 をあなたも知らず知らずのうちにしているかも知れません。 スライドでは「君はそもそも何をされてる方なの?」と言いたくなる 命名 の数々を紹介し、改めて 命名 の重要性を感じていただきます。 stdClassって一体何者なんだ?! report by id:daina_rks 弊社 寺西帝乃 (@dainabook) による発表です speakerdeck.com 発表者本人によるレポート stdClassを使う場面はよくあるかと思いますが、「そもそもこのクラスは一体何者なのか」と質問されると答えに困るPHPerは多くいるのではないでしょうか。 この発表ではstdClassの生成方法 ⇒ stdClassにできること・できないこと ⇒ stdClassに関する議論の順番で、「stdClassとは一体何者なのか」の答えに近づく内容です。 PHP8.2で導入される「動的プロパティの廃止」にも関連する内容となっております。 stdClassがよくわからない初心者PHPerや、今までなんとなく使ってきた中堅PHPer、stdClassのあり方について考えるベテランPHPer達の理解の手助けになると思います。 特徴、魅力を知って、各 PHP フレームワーク を使いこなそう! report by id:radiocat 弊社 浅野 仁志 (@hitoshi_a0) による発表です speakerdeck.com レポート フレームワーク 選定は「冒険を共にするパートナー選びのようなもの」とのことで冒険のパートナーになぞらえて PHP フレームワーク が紹介されました。 Laravel:多くの人に頼られる人気者、能力も高く王道の主人公 CakePHP :真面目、曲がったことが大嫌い、正義感が強い委員長 Yii:玄人好みの影の立役者、 切れ者 軍師 Slim:最低限の荷物だけで旅をする自由人 永遠の 宗教戦争 的テーマなので「あの フレームワーク は?」という意見も多々あり、反響のあまり発表者が「 Symfony はすみません」とコメントするなど、会場一体の議論を巻き起こすキラーLTとなっていました。また、会場だけでなくオンラインからもコメントが多く寄せられて、会場で流されていたニコ生のコメントともコラボした盛り上がりを見せていました。 SNSでの盛り上がり も合わせてお楽しみ頂ければと思います。 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解と RDBMS の上手い使い方 report by id:hirobex 曽根 壮大さん (@soudai1025) による発表です レポート 実際にやりがちな設計はこうだけど、こう変更が加わったときに辛いよね、だからこう設計すると楽だよね といった、実例から正しいテーブル設計を紹介する流れが非常に勉強になりました 今楽だからとりあえず既存テーブルにカラム追加しちゃう といったことは、私自身やりがちなので気をつけようと思いました いろいろな フレームワーク の仕組みを index. php から読み解こう report by id:hirobex おかしょい/岡田 正平さん (@okashoi) による発表です speakerdeck.com レポート 「 フレームワーク がなぜWebページを表示できるのか」という話を PHP でWebページを表示する方法から始まり、 CakePHP 、Laravel、Slim、 Symfony と様々な フレームワーク のindex. php を読み解いてWebページの表示の仕組みを解説する トーク です 意外と、 フレームワーク しか触ったことのない人におすすめの トーク だと思いました! 約10年ものの PHP アプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて report by id:Jazuma たけてぃさん (@takeokunn) による発表です レポート 物流システムという商材の特性上、システム障害を起こしてはいけないという要件が存在します。 その制約をクリアするためにSREチーム主導で以下のような取り組みが行われました。 Datadogによるシステムモニタリング。 アラートが発生した場合、Slackに通知する仕組みも構築 JobサーバをDockerizeしてECS化。 これによりMWバージョンアップ等のメンテナンスコストが低下した gitと AWS の権限管理 PHPUnit の高速化 <感想> メトリックスの監視やMWバージョンアップ等システム運用に必須の作業を効率化するための 施策に先手を打って取り組まれているという印象を受けました。 計測できるレガシーさを捉え、コード改善に対処する report by id:Jazuma 大橋 佑太さん (@blue_goheimochi) による発表です レポート コードを「計測」することで効果的な リファクタリング ができるという視点で コードのレガシーさを測定する方法が紹介されました。 重複コードはどれくらいあるか phpcpdというツールにより計測可能。重複コード = 悪というわけではないため検知されたコードの目視確認が必要 循環的複雑度はどのくらいか terryyin/ lizard により計測。(if/else, for, switch等の数を計測する。 これらはコードがどれだけ複雑化の指標となる) 未使用コードの検出 ツールは様々なものがあるため複数のツールを試して比較してみると良い ファイルの変更回数 Gitコマンドでファイルの変更回数を測定可能。変更回数が多い = 責務過多の可能性がある、あるいは価値検証の場合がある Gitコマンドでファイルの変更回数を測定するのは特別なツールを導入することなく実現できるので測定の第一歩として適切だと思いました。 【実録】「 PHP _CodeSniffer」で始める快適コードレビューライフ report by id:mrstsgk_rks 弊社 森下 繁喜による発表です speakerdeck.com 発表者本人によるレポート 私の所属する配配メール開発課の事例をもとに PHP _CodeSnifferを導入したときの恩恵と思わぬ落とし穴を説明しました。 オンライン登壇で ニコニコ生放送 と発表時の音声を片耳ずつ聞いていたので、 音声のラグでスタートに戸惑いましたので、次はオフライン登壇でリベンジしようと思います。 次回「 PHP _CodeSnifferをPhpStormに設定したら実装 工数 が減少できた件」でお会いしましょう!!! PHP マジックメソッドクイズ! report by id:Y-Kanoh 弊社 加納悠史 (@YKanoh65) による発表です speakerdeck.com 発表者本人によるレポート 「このメソッド、定義されていないはずだけど... あ、マジックメソッドを呼び出しているのか」Laravel のコードを読んでいるときに、一瞬迷ったのがこの発表の元でした。 知っていれば「ああ、こういうことね」と脳内で変換できますが、知らないと関係性がわからなくなってしまうのがマジックメソッドだと思います。特に一般的な開発時には自分で実装することが一部を除き多くはないため、まずは知ることが重要です。 このクイズで興味を持った方は、ぜひ一度調べてみてください。 フレームワーク が存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 report by id:radiocat 弊社 廣部 知生 (@tomoki2135) による発表です speakerdeck.com レポート 20年モノのレガシープロダクトの新UI導入をきっかけにLaravelを導入した事例の紹介です。 Laravelに”載せる”実装戦略として、 アーキテクチャ に ADR パターンを採用して既存コードをできる限りそのまま維持してLaravel上で動作させる戦略が紹介されました。 この戦略によって、載せ替える前の構造を維持したままスピーディーな移植を実現できたようです。また、移植によってデータが返り値となる仕組みになったことでテストが書きやすくなりました。あくまで移植しただけなのでまだまだ課題はあるようですが、レガシープロダクトにモダンな フレームワーク を導入する参考事例のひとつとして大きな反響があり、LTではなくレギュラー枠でじっくり聞きたいという意見もあがっていました。 まとめ 初心者向けの内容からマニアックな内容まで幅広い人が聴いていて楽しいイベントとなっていました。LTの時間は現地ではペンライト、配信では色付きのコメントが流れるなど大盛りあがりでした。 PHPerのためのコミュニティ PHPTechCafe ラク スでは PHP に特化したイベントを毎月開催しております。その名も「PHPTechCafe」!! 次回は4/25(火)に『「PhpStormを語る」』 をテーマに開催します! まだまだ参加者を募集していますので、ぜひお気軽にご参加ください。 👉 PHPerのための「PhpStormを語る」 PHP TechCafe 参加申込は以下フォームよりお願いします! rakus.connpass.com 最後までお読みいただきありがとうございました!
アバター
1. はじめに 2. Google Homeのしくみ Home Graph について 3. Local Home SDK とは Local Home SDK を使って、やろうと思ったこと 4. Local Home SDKの導入手順 ①あなたが始める前に ②入門 ③スターター アプリを実行する ④クラウド フルフィルメントの更新 ⑤ローカル フルフィルメントを構成する ⑥ローカル フルフィルメントの実装 ⑦スマートウォッシャーを開始する ⑧TypeScript アプリをデバッグする ➈おめでとう 5. 実装手順 任意のタイプの家電を登録する デバイスに備わってる機能を変更する SYNC:追加された機能の詳細 Query:現在の家電の状態を報告する EXECUTE:追加される「EXECUTE」リクエスト に対応する 6. さいごに 1. はじめに 家電にはリモコンが欠かせません。 テレビ、エアコン、照明など、私たちの生活に密接に関わっている家電製品は、その操作のために専用のリモコンが必要になります。 私の場合、テレビにブルーレイレコーダーとスピーカーを繋いでいたため、リビングの机の上に3つのリモコンを常設していたのですが、リモコンを置く場所をとったり、リモコンを見失ったり と、解決したい問題が発生していました。 そこで、SwitchBotのリモートリモコンを使い、リモコンをアプリ管理に変更することにしてみました。 SwitchBotのリモートリモコンとは、家電製品を スマートホーム に対応させるための便利なデ バイス です。 このリモコンは Wi-Fi に接続してネットにつながることができ、事前にリモコンの赤外線を学習させることで、対応する スマートフォン のアプリを介して家電を操作できるようになります。 ただ、SwitchBotを使っていても、アプリをタップしてからリモコン操作が可能になるまでに時間がかかり、不便を感じていました。 そこで、 Google Home を使って 声で命令を出せるようにしたのですが、それでも、下記のような不満を抱えてしまいました。 (微妙な言葉の揺れを吸収してくれたりしたので、機能性に関しては満足しました。) 私が使用している照明は、リモコンの「電源」ボタンを押下すると、ON→常夜灯→OFF→ONのサイクルで状態が変わります。 Google Home は、ONとOFFのステータスしか管理しないため、電気を消したいと思ったとき、 ・「OK、 Google 、照明を消して」-> 照明が常夜灯になり、 Google Home 上のステータスは「OFF」になる ・もう一度「OK、 Google 、照明を消して」 -> 照明がOFFになるが、 Google Home 上のステータスは「ON」になる といったことが起きました。 照明を消すために、「OK、 Google 、照明を消して」を2回いう必要があり、その上、ステータスもずれてしまうのです。 ほかにも、スピーカーの音量を調節できない、扇風機の風量を変更できない等、細かい部分で不満点が残りました。 調べていると、 Google Assistantのプロジェクトを自作することで、家電を操作することが可能であることを知りました。 自分で家電の操作のロジックを作ることができるのであれば、抱えている不満点を解消できるのではと思い、実践してみました。 本記事では、Local Home SDK を用いて、実装を行ったので、その手順を紹介しようかと思います。 詳細は後述しますが、上記を利用すると 家電を操作するために作成するコード、アプリを外部に公開する必要がなくなり、私にとって大きなメリットに感じたため採用しました。 もし悪用されて、家の家電が見知らぬ人によって勝手に動き出すことを考えると、公開するべきじゃないと思いました、、 また、サンプルのGit プロジェクトがあるため、迷う時間を短縮できるとも思いました。 該当のサンプルは、Local Home SDK を使わないパターンのコードも含まれているため、「コードを外部に公開することに躊躇いがない」場合も、そちらを流用できます。 2. Google Home のしくみ まず、 Google Home がスマート家電を操作するときの仕組みを理解しておく必要があるので、簡単に説明します。 ①例えば、 Google Home に「OK、 Google 、照明を消して」と命令したとします。 ② Google Home は受け取った音声を、 クラウド 上の Google Assistant に投げます。 ③ Google Assistantが家電操作の命令と判断した場合、受けとったワードに関連するスマート家電情報をHome Graphから取得します。 Home Graph について 急に現れたワード、"Home Graph" についての説明を簡単に挟みます。 Home Graphとは、端的に言うと Google Home が操作可能な家電を管理するデータベースのようなものになります。 Google Home と対応したプロジェクトの連携を行ったとき、そのプロジェクトが管理している スマートデバイス リストを取得し、保持します。 調べたところ、「ライト」「照明」 などといった言葉の揺れを吸収しているのが、Home Graphのようです。 複数のライトが登録されている時、「リビング」の Google Home から「ライトをつけて」とだけ命令しても、暗黙的に「リビングのライト」を選択してくれます。 Home Graph の詳細 developers.google.com ④Home Graph から受け取った情報と紐づくプロジェクトに対して、実際に家電を動かすように「EXECUTE」リク エス トを送ります。  例えば、受け取った家電情報が SwitchBot で登録した照明だった場合、紐づいているプロジェクトの設定に従って、SwitchBot に向かってリク エス トが飛びます。 ⑤「EXECUTE」リク エス トを受け取ったSwitchBot サービスがからリモートリモコンに対して、「照明の電源ボタンを1回押す」といった命令を渡し、 ⑥それをリモコンが実行することで、照明が消灯(私の場合は常夜灯)します。 もし、ライトがスマート家電だった場合は、リモートリモコンを介さずに照明に対して直接「照明を消す」命令が飛ぶということになります。 以上のような原理で、 Google Home を使って家電を音声操作することができます。 3. Local Home SDK とは Local Home SDK とは Google が提供する技術で、こちらを使うことで Google Home がローカルのネットワークを使って家電に対して、 操作コマンドを直接送信することができるようになります。 developers.home.google.com 図を使って、簡単に説明します。 まず前提として、プロジェクトはあらかじめ「Local Home SDK 」を用いて実装した Google Home 上で動くプログラムファイル(言語はTypeScript系)を用意しておく必要があります。 Google Home は、プロジェクト連携時にそのプログラムファイルを取得します。 ※プロジェクトを紐づけるか、端末を再起動することで自動的に取得する模様 命令を受け取り、Home Graph から家電情報を受け取るところまでは同じですが、ここから先のフェーズが異なります。 ④ Google Assistant は、受取った情報とともに「EXECUTE」リク エス トを" Google Home "に返します。 ⑤「EXECUTE」リク エス トを受け取った Google Home は、あらかじめ取得していたプログラムファイルに従って、 操作コマンドをローカルネットワーク経由で家電に対して直接実行します。 ⑥リモコンが家電を操作する仕組みは同じです。 Local Home SDK を使って、やろうと思ったこと Google Home は定期的にローカルネットワークに対して、操作可能なデ バイス がないか、スキャンして確認しています。 上記に対して、HomeGraph に登録されている家電情報に紐付くIDを返すデ バイス を発見すると、 Google Home はそのデ バイス を該当のスマート家電と認識します。 つまり、同じローカルネットワーク内にあるPCなどで Google Home のスキャンにして、該当するIDを返す仕組みを実装すると、 以降、 Google Home はそのPCをスマート家電と認識するので、「EXECUTE」リク エス トを受け取れるようにすることができるようになります。 ※ラズパイ等でも代用は可能ですが、知識がない(そもそも持っていない)ので、PCを採用しています。 「EXECUTE」リク エス トを任意のデ バイス で受け取ってしまえば、そこから先の処理は自作することができます。 図の通り、SwitchBot API を叩いてスマートリモコンを経由で家電の操作も可能になるということです。 上記のような仕組みを利用することで、先述した通り 家電を操作するアプリを外部に公開する必要がなくなります。 4. Local Home SDK の導入手順 最初に話した通り、Local Home SDK のサンプルコードを使って実装の準備を進めます。 Google 公式の CodeLabs もあるので、その手順に従って、準備します。 developers.home.google.com 本記事では、上記に書いてある実装・操作手順的な内容はばっさりそぎ落とし、簡単な補足情報をまとめようかと思います。 ①あなたが始める前に 本記事の「2. Google Home のしくみ」「3.Local Home SDK とは」にあたる内容が記載されています。 ②入門 準備として Action on Google と Firebase の初期化を行います。 Firebase を使って、「 Google Assistant のプロジェクト」のアプリ部分を作成します。 Action on Google の設定を完了することで、上記で作成したアプリを「 Google Assistant のプロジェクト」として登録するイメージです。 ③スターター アプリを実行する ここから用意されているサンプルコードを用いて、実際に実装していくフェーズです。 github.com コードの更新がしばらく行われておらず、node のバージョンが古くなっていました。 私の場合、node のバージョンが環境と合わず、バージョンの更新とそれに伴う依存関係の修正を行いました。 ここまでの準備が完了すると、Firebase Functions にデプロイしたプログラムに対して、 Google Assistant から「EXECUTE」リク エス トが送信されるようになります。 既存では、パラメータに応じて Firebase Realtime Database のステータスを変更する 処理が走るのみですが、コードを変更すれば実装可能です。 Local Home SDK の仕組みを使用していませんが、家電を操作する実装を行うことができるようになります。 Local Home SDK の機能を使わなくてもいいのであれば、ここまで準備すれば Google Home を使って家電操作できます。 ・functions > index.js を実装することで、家電操作が可能になります 具体的な実装方法の詳細は、後述します。 Local Home SDK を使った仕組みを目指すのであれば、次へ進みます ④ クラウド フルフィルメントの更新 Local Home SDK を使用するための準備フェーズになります。 Google Home からのスキャンに対して、otherDeviceIds に指定したIDを返すことで、 Google Home は該当のデ バイス だと認識します。 ⑤ローカル フルフィルメントを構成する ここで設定した内容に従って、 Google Home がローカルネットワークのスキャンを行います。 ⑥ローカル フルフィルメントの実装 ここで、Local Home SDK を使った実装になります。 identifyHandler Google Home がローカルネットワークをスキャンし、その結果を処理する箇所になります。 対応しているデ バイス は、otherDeviceIds に対応した ID を返してくるので、それをそのまま返すようにしています。 executeHandler Home Graph からきた「EXECUTE」リク エス トを処理する部分です。 基本的に、 Google Assistant から受け取ったコマンドをそのまま仮装デ バイス に渡すだけで良いと思うので、 ここのコードは公式の内容から特に変更はしなくて良いかと思います。 ここでSwitchBot API を叩くという荒技もできそうですが、やめておきました。 ⑦スマートウォッシャーを開始する node 上で動く 仮想のスマート家電を起動します。 Local Home SDK を使った音声での命令が成功すると、ログにそのことが出力されます。 また、併せて Firebase Realtime Database のステータスも変更します。 ⑧TypeScript アプリを デバッグ する 記載の通り Google Home 上で動くコードは、同じネットワークにつないでいるPCの Chrome ブラウザ > inspect でデバックすることが可能です。 また、 スクリプト を更新した場合、 Google Home を再起動しないとその変更は反映されません。 ➈おめでとう ここまで完了すると、 Google Home と仮想スマート家電との疎通が完了します。 サンプルコードはログを出力するだけですが、コードを変更すれば家電操作ロジックを実装できます。 実装方法の詳細は、次の章で解説します。 5. 実装手順 既存のサンプルコードの状態で扱うことができるスマート家電は「洗濯機」で、その機能も限られています。 もちろん、上記は変更可能です。 ここではその方法と、実装手順を紹介いたします。 任意のタイプの家電を登録する まずは、任意の種類を 家電を登録する方法です。 Google Home からくる、「Sync」リク エス ト に応答することで、任意のスマート家電を登録することが可能です。 サンプルコードでいうと、functions > index.js の onSync で返すオブジェクトが、これにあたります。 家電の種類は、payload.devices.type の値で報告できます。 公式のドキュメントに報告できる種別がすべて乗っています。 developers.home.google.com 上記から一つだけ選択して、報告します。 例えば、照明に変更したい場合は、"action.devices.types.LIGHT"を指定します。 payload: { agentUserId: USER_ID, devices: [{ id: 'washer' , type: 'action.devices.types.LIGHT' , // ここを変更する .... これでアプリ上のロゴもライトに変わり、Home Graph で解釈する言葉の揺れも、照明用に代わります。 言葉の揺れ に関して、payload.devices.name.nicknames を変更することで、任意のワードを登録することができるようになります。 .... name: { defaultNames: [ 'My Washer' ] , name: 'Washer' , nicknames: [ '間接照明' , 'テレビ台の上のライト' , ] , // ここを変更する } , .... 上記のように設定すると、「OK、 Google . 間接照明をつけて」や「OK、 Google . テレビ台の上のライトを消して」と話しかけた場合も、登録した機器が選択されるようになります。 「Sync」リク エス ト で報告する内容の詳細は下記を参考にしてみてください。 developers.home.google.com デ バイス に備わってる機能を変更する こちらは、payload.devices.traits の値を変更することで解決することができます。 デ バイス タイプと同様に、公式対応表を参照します。 developers.home.google.com こちらは、複数指定することができます。 例えば、"action.devices.traits.Brightless"を指定することで 、明るさを調整するための命令ができるようになり、対応する「EXECUTE」リク エス トが送られるようになります。 余談ですが、、 私は、"action.devices.traits.Modes" を乱用しています。 任意の「モード」を追加することができ、様々なことに汎用することができます。 「テレビ」に対して、「 NETFLIX モード」「 YouTube モード」「 Amazon Prime モード」を設定すれば、「OK、 Google . テレビをAmazo Primeモードにして」の一言で、 テレビで Amazon Prime を再生させることも可能になります。 SYNC:追加された機能の詳細 実は、traits で任意の機能を報告するだけでは、機能を追加することができない場合があります。 Volume (音量調整) がわかりやすいので、例としてみてみたいと思います。 developers.home.google.com 「デ バイス の属性」の章を確認すると、「Sync」リク エス ト で次の情報を報告する必要があることがわかります。 volumeMaxLevel:ボリュームの最大値 volumeCanMuteAndUnmute:ミュート機能に対応しているかどうか volumeDefaultPercentage:デフォルトのボリューム値 levelStepSize:ボリュームを相対的に調整するときの変化量 commandOnlyVolume:現在のステータスを報告できるかどうか traits = action.devices.traits.Volume と併せて、上記を報告することで初めて、ボリューム調整機能を追加することができます。 Query:現在の家電の状態を報告する 現在の状態やステータスを、HomeGraph に報告することで Google Home アプリに現在のスマート家電の状態を表示することができます。 これがなくても動かすことは可能ですが、実装することをお勧めします。 サンプルコードでは、functions > index.js の onQuery に実装されています。 return { on: data.on, isPaused: data.isPaused, isRunning: data.isRunning, currentRunCycle: [{ currentCycle: 'rinse' , nextCycle: 'spin' , lang: 'en' , }] , currentTotalRemainingTime: 1212, currentCycleRemainingTime: 301, } ; 報告が必要な key は、報告している traits によって決まります。 traits の詳細のページで、確認することができます。 'action.devices.traits.OnOff'(電源のオンオフ切り替え)と'action.devices.traits.Brightless'(明るさ調整)機能を報告した照明を登録した場合を考えてみます。 action.devices.traits.OnOff https://developers.home.google.com/cloud-to-cloud/traits/onoff?hl=ja#device-states action.devices.traits.Brightless https://developers.home.google.com/cloud-to-cloud/traits/brightness?hl=ja それぞれを確認すると、 on と、 brightness を返す必要があることがわかります。 return { on: true , brightness: 50 } ; ※電源がついており、その明るさが 50% であることを報告している EXECUTE:追加される「EXECUTE」リク エス ト に対応する payload.devices.traits を追加して機能を増やすと、その機能に対応した「EXECUTE」リク エス トを処理する必要が出てきます。 サンプルコードでいうと、app-start > functions > index.js の onExecute、もしくは、virtual-device > washer.js の state(既存の関数名だと微妙ですが)で「EXECUTE」リク エス トを処理しています。 公式のドキュメントを確認することで、どのような「EXECUTE」リク エス トが来るかを確認することができるので、 それを参考に、実装を行います。 "action.devices.traits.OnOff"を例に、私の実装を例として簡単に紹介します。 developers.home.google.com 「ライトをオンにして」と Google Home に話しかけたら、下記のような「EXECUTE」リク エス トを受け取ることがわかります。 { "command" : "action.devices.commands.OnOff" , "params" : { "on" : true } } params > on の値を参照し、下記のように実装すれば、家電の電源を切り替えることができます。 // req = 受け取った「EXECUTE」リクエスト const data = req.body; // コマンド種別を取得する const command = data.command switch (command) { case 'action.devices.commands.OnOff' // 切り替えたいステータスを取得 const isOn = data.params.on; if (isOn) { // 電源を入れるケース switchBotApi.on(); } else { // 電源を切るケース // 電源ボタンを2回押下する switchBotApi.off(); } case 'action.devices.commands.BrightnessAbsolute' // 省略 } オフの時だけ電源ボタンを2回押下するように実装できるので、初めに挙げていた照明のステータスがずれる問題を解決することができました。 ※SwitchBot API の叩き方の詳細は、下記をご覧ください。 github.com 6. さいごに 「OK、 Google テレビをつけて」の指示で、テレビとスピーカーの電源を同時に入れるようにしたり、 「OK、 Google 広告をオフにして」の指示で、 YouTube の広告をスキップさせたりと、好きなように家電をコン トロール することができるようになります。 単に Google Home と、SwitchBot サービスを連携させるだけでは上記は実現できませんし、最初に上げたような不具合が発生したりします。 そこを自分で解決することができたので、試して良かったと感じています。 また、コードを動かすトリガーを声にすることができるので、SwitchBot API に限らず、様々なこと(家電操作以外も)ができそうです。 他になにかできないか模索し、より便利な スマートハウス の構築を目指していこうかと思います。
アバター
技術広報の syoneshin です。 いつも ラク スエンジニアブログをお読みいただき、ありがとうございます! ! 今回は2023/2/8に開催した 「RAKUS Tech Conference 2023」の内容を当日発表資料 と共にご紹介します! ◆ 関連記事 昨年度(2022年)の開催レポートは以下をご覧ください tech-blog.rakus.co.jp 【目次】 イベント概要 発表の紹介 オープニングセッション 短納期でも進化をあきらめなかった新規プロダクト開発 フロントエンド横断組織のチームトポロジー ベテラン社員が抜けても若手が成長できるエンジニア組織づくり デザイン組織が社内下請けから脱却するためにやったこと ゼロから始めるクラウドネイティブ 「開発優先」の中で取り組む組織的な新技術への挑戦 クロージング 終わりに イベント概要 日時 :2022/2/8(火) 14:00-18:00 会場 :オンライン 参加費:無料 主催 : 株式会社ラクス開発本部 Twitter ハッシュタグ : #RAKUSTechCon techcon.rakus.co.jp 【イベントメッセージ】 株式会社 ラク スは「ITサービスで企業の成長を継続的に支援します!」をミッションに掲げ、 メール共有・管理システムの「メールディーラー」や、経費精算システムの「楽楽精算」など、 延べ80,000社を超えるお客様に自社開発したサービスを提供してきました。 『RAKUS Tech Conference2023』はこうした今までの取り組みや知見を紹介する、 ラク ス開発本部主催の技術カンファレンスです! 当社ではローンチ20年を超えるプロダクトもあり、決して目新しい技術ばかりを扱っているわけではありませんが、 一人ひとりが地道な" カイゼン "のための努力、そして"挑戦"を続けています。 「日本を代表する」組織を目指し、失敗を恐れずに成長を続けるエンジニア/デザイナーの生の声をお届けします。 発表の紹介 それではここから各発表内容と資料を共有させていただきます! オープニングセッション 登壇:公手 真之 [ 執行役員 開発本部長] オープニングセッションでは、 ラク ス開発本部長の公手より、 以下内容を紹介させていただきました! ラク スについて エンジニア組織について CTOが取り組んできたこと ラク スのエンジニア組織のこれから 開発組織の成長過程で遭遇したさまざまな課題と取組み事例、また進行形の課題と取組み、今後のチャレンジをご紹介しました。 speakerdeck.com 短納期でも進化をあきらめなかった新規プロダクト開発 登壇:松浦 孝治 [楽楽明細開発2課 課長]     川上 正博 [楽楽明細開発2課] 2022年12月現在、約25,000社の方に申込頂いている『楽楽電子保存』サービスですが、初期開発に与えられた猶予は弊社のプロダクト開発史の中で最短となる「半年」という、非常にタイトなスケジュールの中で完遂させる必要がありました。 短期間でサービスインまで導いたマネジメント 中長期的なサービス運用を見据えたゼロベースでの アーキテクチャ ー選定 を中心に、初期開発で特に力を入れた取り組みと今後の展望をご紹介しました。 speakerdeck.com フロントエンド横断組織のチーム トポロジー 登壇:國枝 洋志 [フロントエンド開発課 課長] 設立20年目にして作られたフロントエンド開発組織は発足から2年経ちました。 発足時2名しかいなかったフロントエンドエンジニアは18名に増えましたが、まだまだ人手不足。課題も山積みです。 ベスト・オブ・ブリード戦略で成長してきた様々なサービスに対して、各開発課と共にサービス開発を行うストリームアラインドチームになるのか、フロントエンドの スペシャ リストとして技術支援を行うイネイブリングチームになるのか、横断組織ならではの悩みにどのように取り組んで行くのか、これからの組織設計についてお話ししました。 speakerdeck.com ベテラン社員が抜けても若手が成長できるエンジニア組織づくり 登壇:大塚 正道 [配配メール開発課 課長]     荒巻 拓哉 [配配メール開発課]     久山 勝生 [配配メール開発課] ラク スの開発組織には毎年多数の新卒社員が入社しており、私たちのチームにもたくさんの若手社員が所属しています。 一方で スペシャ リスト職などのベテラン社員は不足気味です。 重要度の高いプロジェクトに専念するためチームから抜けていくこともあります。 しかし、私たちは逆にこれを若手社員の成長の機会と捉えて積極的にチャレンジできる環境をつくり、若手が活躍し成長できるチームを目指して取り組んできました。 実際に取り組みを行ったメンバーからその事例をご紹介するとともに、マネジメントサイドからそのための組織の向き合い方についてお話ししました。 speakerdeck.com デザイン組織が社内下請けから脱却するためにやったこと 登壇:小林 肇 [プロダクトデザイン課 課長] デザイナーが少なく、 開発プロセス の中でデザイナーの役割の定義が曖昧で、デザイナーって何をしてくれる人なのか、プロダクト開発においてデザイナーがいるとどんなメリットがあるのかがちゃんと認識されていませんでした。キレイなUIを作ってくれるということは認知されていても、そのためにはどのようにデザイナーがプロダクト開発に関わるべきか不明瞭なために、下請け的な仕事になっていました。 課題解決をミッションとするデザイナーが、プロダクト開発チームの一員になるために、どのようにして役割を認識してもらい、役割を広げていっているのかをご紹介しました。 speakerdeck.com ゼロから始める クラウド ネイティブ 登壇:見形 親久 [SRE課 課長]     松本 隆二 [東京インフラ開発1課] クラウド ネイティブな開発をやってはみたいが、機能開発が忙しくなかなかそのチャンスが生まれない、そんな時に新しいサービスを立ち上げるチャンスが舞い込んできました。 これ幸いと、イメージしていた構成を構築しようとしてみましたが、あれやこれやとハマりポイントや考慮できてないモノがあり思っていたほど簡単なものではありませんでした。 Kubernetes など クラウド ネイティブ技術が一般的になっては来ていますが、実際に導入するにはエコシステムの構築など、周辺のシステムも考慮する必要があり、どこから手を付けるべきか悩んでいる方もいらっしゃるのではないでしょうか。 システム構成の紹介/その背景 環境構築時のハマりポイント/考慮点 今後やっていきたいこと を中心に、ゼロからどの様に環境を構築していったのかと今後の展望をご紹介させていただきました。 speakerdeck.com 「開発優先」の中で取り組む組織的な新技術への挑戦 登壇:堀内 泰秀 [技術推進課 課長]     鈴木 勇 [技術推進課] 「機能開発を優先しているため リファクタリング ができない」この状態に陥っている方、多いのではないでしょうか。 サービス開発では立ち上げから売れ始めるまでの間、プロダクトマーケットフィットを目指して最短距離を走ることを求められます。 その中で行われる意思決定では必ずしもすべてが最適解を得られるわけではありません。また時を経るにつれて最適解が最適解でなくなることもあるでしょう。 「サービスがある程度軌道に乗り、開発体制も整ってきたら リファクタリング やリアーキテクトしたい」――しかしベテランエンジニアの方はそんなタイミングが訪れないと知っているはずです。 そんな厳しい現実を直視して、 ラク スではどのような対応を取ってきたのかをご紹介しようと思います。 本発表では以下のような内容をお話しします。 ラク スにおける取り組みの概要 取り組みの立ち上がりから継続していくための仕組みづくり 取り組み実施のサイクル 自社で検討することのメリット speakerdeck.com クロージング 登壇:公手 真之 [ 執行役員 開発本部長]     大塚 正道 [配配メール開発課 課長]     小林 肇 [プロダクトデザイン課 課長]     見形 親久 [SRE課 課長]     堀内 泰秀 [技術推進課 課長] クロージングセッションでは、 ラク ス開発本部長の公手と 当日登壇者複数名で 本編では伝えれなかったこと 自身のチームで今後やろうとしていること テーマにパネルディスカッションを実施しました。 イベント当日はたくさんの方にご視聴、そしてコメントやご質問をいただきました。 お申し込み、ご参加いただいた皆さま本当にありがとうございました! 終わりに ラク スMeetupでは現場最前線のエンジニア/デザイナーから ラク スの SaaS 開発ならではの技術・運用ノウハウや、 新しい取り組みの成果や失敗談、プロダクト開発/運用で得た知見等の技術情報をお届けしております。 今後も定期的なイベントを計画しております、ぜひご参加ください。 最後までお読みいただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申し込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申し込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、技術推進課の uemura_rks です。 私が所属する技術推進課では「各商材の開発チームから課題を ヒアリ ングし、その解決に向けた調査を行う」という取り組みをしています。 取り組みの一環で Java のパフォーマンス分析、診断ツールである JDK Flight Recorder(JFR) について、その機能と導入することによるアプリへの影響を調査・検証しました。 「小さいオーバーヘッドで使えるらしいけど本当にそうなのか?」 を確認してみたので、その結果を報告します。 JFR を使えばトラブルの 原因調査に役立つ情報 を 手軽に収集 できます。 Java を扱っている方であればどなたにとっても有益なツールとなりますので是非最後までお付き合いください。 目次 目次 JDK Flight Recorder(JFR)の特徴 オーバーヘッドが小さい 手軽に使い始められる JVM やアプリ動作に関する情報を取得できる トラブルの再発を待たなくていい 取得情報はカスタマイズ可能 イベントログのデータフロー オーバーヘッドによる影響度検証 検証結果 CPU Javaヒープ使用量 プロセスが消費する最大メモリ量 ディスク吐き出し時のアプリ動作への影響 ダンプサイズ まとめ 参考文献 JDK Flight Recorder(JFR)の特徴 検証の前にまずはJFR がどんなツールなのか、その特徴を紹介します。 オーバーヘッドが小さい アプリケーションを分析、診断するためには多くの情報を計測する必要があります。 JFR は JVM に組み込まれている ため、その計測のオーバーヘッドは小さく、1,2%程度だといわれています。 商材に導入したらアプリ全体のパフォーマンスが悪化した、なんてことが起きにくいです。 (と、言いつつこの部分を後半で検証します) 手軽に使い始められる JFR は各ベンダーの JDK に実装されているので、現在使っている JDK はそのまま、 機能を有効化するだけ で利用可能です。(有効化には JVM の起動オプションや jcmd を使います) アプリサーバに何かしらの ミドルウェア をインストールしたり、 ソースコード に手を加えたりすることなく使い始められます。 JVM やアプリ動作に関する情報を取得できる JFR のログからは JVM のリソース状況 GC の情報 スレッドダンプ コンパイル やコード・キャッシュの情報 ログを自動分析したレポート など様々な情報を得ることができます。 JVM の動作に関するような低レベル情報を集めてくれるので、トラブル調査に役立つと思います。 いくつか例を載せておきます。 < JVM のリソース状況> CPU 使用率やヒープ使用量、オブジェクトのメモリへの割当てなどを確認できます <自動分析のレポート> ログの包括的な分析レポートです。 潜在的 な問題をスコア化して警告してくれます。 <スレッドダンプ> 1分間隔でスレッドダンプを記録してくれています。 ◆ログの可視化 JFR のログ自体はバイナリになっています。 JDK Mission Control(JMC) というビューアーを使うと上記キャプチャのように可視化できます。 その他、JFR で得られる情報やその見方についてはこちらの チュートリアル が分かりやすいです。 トラブルの再発を待たなくていい JFR は基本的に 常に有効化しておく ツールです。 これは「オーバーヘッドが小さいこと」と後ほど紹介する「イベントログのデータフロー」から成っている強みになりますが、それ故に JFR は 「何かトラブルが起きてデータが欲しくなったから計測開始」 ではなく、 「何かトラブルが起きたときに備えてずっと計測しておく」 という使い方ができます。 航空機のフライトレコーダーから名前を取っているようなのですが、まさしくその Java 版といったところです。 (もう少し身近なところで、 ドライブレコーダー の Java 版 と言った方が馴染みがあるかもしれません。) 取得情報はカスタマイズ可能 JFR は事前に定義された イベント という単位で情報を集めます。 *1 数あるイベントのうち、どれを取得するかは設定ファイルでカスタマイズすることができます。 独自の設定を作成することもできますし、テンプレートとして2種類の設定ファイルが JDK に同梱されていますので、それをそのまま使うこともできます。 default.jfc 本番環境での継続的な記録に向いたテンプレート設定 オーバーヘッドは1%ほど profile.jfc 検証向けのテンプレート設定 オーバーヘッドは2%ほど <設定ファイルの一部> <event name="jdk.ThreadAllocationStatistics"> <setting name="enabled">true</setting> <setting name="period">everyChunk</setting> </event> <event name="jdk.ClassLoadingStatistics"> <setting name="enabled">true</setting> <setting name="period">1000 ms</setting> </event> 設定ファイルは xml 形式になっています。 enabled 属性の true / false を切り替えることで取得イベントの選択ができます。 イベントログのデータフロー JFR が取得するイベントがログファイルとして出力されるまでの流れを紹介します。 登場人物の多くは JFR のオプションでサイズなどの調整が可能です。 *2 流れを把握し、環境に合わせて適切なオプションを指定することで、導入によるサーバリソースへの負荷を抑えることができます。 ◆ アーキテクチャ JFR ではイベントを「スレッドローカルバッファ」「グローバルバッファ」の2つのメモリ領域と「 リポジトリ 」と呼ばれるディスク領域で保管しています。 ◆データフロー ①拾ったイベントを「スレッドローカルバッファ」に格納  スレッドローカルバッファはスレッド単位の専用バッファのため、他のスレッドと競合することがありません。  初動でこのバッファを使うことで高速なイベント収集を実現しているそうです。 ②「スレッドローカルバッファ」から「グローバルバッファ」にコピー  スレッドローカルバッファは小さいためすぐに一杯になります。溢れた情報はグローバルバッファに移ります。  グローバルバッファはスレッド間で共有するプロセスのメモリで、循環バッファの形になっています。 ③「グローバルバッファ」から「 リポジトリ 」にファイル出力  グローバルバッファからも溢れた情報は リポジトリ というディスク領域に .part ファイルとして  出力されます。 ④part ファイルからチャンクダンプに変換  グローバルバッファから溢れた情報はどんどん .part ファイルに溜まっていきます。  こちらも一定のサイズたまると、 .jfr ファイルに変換されます。  (これをこの記事では チャンクダンプ と呼びます)  チャンクダンプは JFR のログファイルとして完全なバイナリになっており、JMC などのビューアーを使って  可視化することができます。  また、 リポジトリ 領域はログの 最長保管期間 や 最大保管サイズ をオプションで指定することができます。  ストレージを圧迫しすぎないような設定をおすすめします。 ⑤jcmd やプロセスの停止などによって JFR ファイルを出力  ③④で出力されるチャンクダンプはチャンクサイズごとに細切れになったファイルです。  jcmd によるダンプ出力の指示やプロセスの停止をトリガーとして、すべての記録が一つにまとまった  JFR ファイルを生成することができます。 *3  (これをこの記事では フルダンプ と呼ぶことにします)  フルダンプと一つひとつのチャンクダンプの中身は基本的に一緒ですが、フルダンプにはまだチャンクダンプに  吐き出されていなかったメモリ領域のイベントログも出力されます。 オーバーヘッドによる影響度検証 オーバーヘッドが小さく、故に常に有効化しておくことのできる JFR ですが、 そのオーバーヘッドが実際にアプリにどれぐらいの影響を与えるかを CPU・Javaヒープ使用量・プロセスが消費する最大メモリ量・ディスク吐き出し時のアプリ動作への影響・ダンプサイズ(ストレージへの影響) の5つの観点で検証してみました。 検証結果 少し長くなるので結果だけ先に述べておきます。 5つの観点ですが... 「 Java ヒープ使用量」への影響のみ要注意 その他は無視できるか、オプションで調整可能な範囲の影響 だと言える結果になりました。詳細をこれから記していきます。 CPU CPUへの影響は次の観点とメトリクスで見ていきます リクエスト処理中 の影響 → CPUが高稼働になっていた時間 を計測 待機中(リクエストを処理していない時) の影響 → CPU使用率 を計測 ◆計測方法 リク エス ト処理中にひたすらCPU負荷をかけるサンプルを用意してリク エス トを繰り返し送りました。 次の3パターンで試行し、2つのメトリクスを計測結果から抽出します。 JFR 無効 JFR 有効 / 本番向け設定ファイル(default.jfc)を使用 JFR 有効 / 検証向け設定ファイル(profile.jfc)を使用 ◆計測結果 各パターンの計測値を比較した結果です。 <リク エス ト処理中の影響(CPUが高稼働になっていた時間)> CPU( JVM ) 高稼働時間 CPU(マシン) 高稼働時間 ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 56.88 秒 - 56.90 秒 - JFR 有効(default.jfc) 57.15 秒 + 0.27 秒 [+ 0.47 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] JFR 有効(profile.jfc) 57.21 秒 + 0.33 秒 [+ 0.58 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] ※平均:20リク エス トの平均 ⇒ 増加率1%未満 の範囲でわずかに増加 という結果になりました。 <待機中の影響(CPU使用率)> CPU( JVM ) 使用率 CPU(マシン) 使用率 ※平均 JFR 無効との差 ※平均 JFR 無効との差 JFR 無効 0.986 % - 1.207 % - JFR 有効(default.jfc) 1.640 % + 0.654 % 1.738 % + 0.531 % JFR 有効(profile.jfc) 1.830 % + 0.844 % 1.869 % + 0.662 % ※平均:19期間の平均 ⇒ 増加量1%未満 の範囲でわずかに増加 という結果になりました。 Java ヒープ使用量 Java ヒープへの影響を見ていきます。 こちらも リクエスト処理中 と 待機中 の2つの観点で使用量の変化を確認しました。 ◆計測方法 リク エス ト処理中にメモリ負荷をかけ続けるサンプル と シンプルなDBアクセスのみをするサンプル の2種類のサンプルを用意して、それぞれでヒープの変化を計測しました。 また、こちらは正確な関係がわかっていないのですが、JFR を有効化していると初回のリク エス ト終了後にフル GC が走るという動きが見られました。 ( Java ヒープの New 領域や Old 領域に余白があってもフル GC が走っていました) そのため、 リクエスト処理中 待機中 それぞれで フルGC前 フルGC後 の2地点の計測を行いました。 こちらもCPUと同じく JFR の有無で結果を比較します。 ◆計測結果 こちらはいくつか条件を組み合わせて計測したのですが、その条件の違いや同一条件内でも試行ごとで計測値のブレが大きい結果となりました。 結果の一例を載せますが、こちらの結果では 一部(平均値で)20%近い増加率 も記録されています。 サンプルアプリの範囲での結果ですので実商材でも同じ結果になるとは言い切れませんが、 導入を検討する際の要注意項目となりました。 <リク エス ト処理中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 247.8 MB - 32.9 MB - JFR 有効(default.jfc) 270.6 MB + 22.8 MB [+ 9.2 %] 36.8 MB + 3.9 MB [+ 11.9 %] JFR 有効(profile.jfc) 279.5 MB + 31.7 MB [+ 12.8 %] 34.5 MB + 1.6 MB [+4.9 %] ※同一条件で3回計測した平均 <待機中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 205.2 MB - 30.6 MB - JFR 有効(default.jfc) 244.1 MB + 38.9 MB [+ 19.0 %] 34.7 MB + 4.1 MB [+ 13.4 %] JFR 有効(profile.jfc) 238.4 MB + 33.2 MB [+ 16.2 %] 32.5 MB + 1.8 MB [+5.9 %] ※同一条件で3回計測した平均 プロセスが消費する最大メモリ量 JFR がイベントログを溜める上で、プロセスとしてどこまでメモリを使用するか確認しました。 おそらくグローバルバッファのサイズに比例するだろうと仮定しつつも、際限なく食い潰される危険がないかを検証します。 計測メトリクスとしては「3日間アプリを稼働させた中で記録した RSS の最大値」を見ていきます。 検証でいろいろ試していく中で、 JFR のイベントログはCヒープ領域に保持されていそう だと理解したため、 RSS を取っています。( JVM に組み込まれているという特徴も相まってそう判断しています) ◆計測方法 サンプルアプリを立ち上げ、3日間ひたすらリク エス トを送り続けました。 ◆計測結果 ⇒ 想定通り、 グローバルバッファのサイズに準じて増加する という結果が得られました。 グローバルバッファのサイズは JVM 起動時のオプションなどで指定できます。 サイズの大きさとディスク吐出しの頻度は トレードオフ ですが、 環境に合わせてサイズを指定してあげれば影響は小さくできると言えます。 ディスク吐き出し時のアプリ動作への影響 JFRはそのデータフローの中で3種類のディスク吐出しを行います .part ファイル出力 チャンクダンプ出力 フルダンプ出力 それぞれのファイル出力時において、アプリに遅延が発生しないかを確認します。 リク エス トの処理時間をメトリクスとし、ファイル出力の有無による時間の差を見ます。 ◆計測方法 サンプルアプリに対して定期的にリク エス トを送ります。 .part ファイルとチャンクダンプは一定リク エス トを送れば自動で出力されますので、そのときのリク エス ト処理時間を計測します。 フルダンプは jcmd で出力指示を出して、そのときのリク エス ト処理時間を計測します。 ◆計測結果 ディスク吐出し時の処理時間と、平均処理時間を比較した結果です。 < .part ファイル出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 39.98 MB 8.809 秒 8.728 秒 - 0.081 秒 [- 0.92 %] 79.96 MB 9.090 秒 + 0.281 秒 [+ 3.19 %] 119.69 MB 8.917 秒 + 0.108 秒 [+ 1.23 %] ※partファイルの増加量 <チャンクダンプ出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 40.4 MB 8.883 秒 8.927 秒 + 0.044 秒 [+ 0.50 %] 80.7 MB 8.708 秒 - 0.175 秒 [- 1.97 %] 119.0 MB 8.917 秒 + 0.034 秒 [+ 0.38 %] ※(出力されたチャンクダンプのサイズ) - (直前のpartファイルのサイズ) <フルダンプ出力時> フルダンプサイズ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 111.7 MB 8.564 秒 9.645 秒 + 1.081 秒 [+ 12.6 %] 226 .3 MB 8.582 秒 9.912 秒 + 1.33 秒 [+ 15.5 %] 456.0 MB 8.677 秒 10.505 秒 + 1.828 秒 [+ 21.1 %] ⇒ 気にするべきは「フルダンプ出力時」になります。 フルダンプ出力時は、指示を出したタイミングで別プロセスが大量に立ち上がり、 アプリ( JVM )の使えるCPUが30%ほど減っていました。 結果、増加率 21.1 % という大きな数値につながっています。 とはいえ、フルダンプ出力は普段プロセスが動いているときに勝手に実行される機能でもありません。 フルダンプ出力の命令タイミングだけ気を付けることで回避可能 な影響ですのでクリティカルなものではないと考えています。 また、 .part ファイルやチャンクダンプ出力時の増加率は1桁%となっており、その影響は十分小さいと言えます。 ダンプサイズ JFR のダンプサイズがどれぐらいになるか計測します。 自社の商材ではログはアプリサーバからログサーバに転送しているのですが、 そのログサーバを圧迫するようなことがないかを確認します。 ◆計測方法 15時間アプリを稼働させ、次の観点の組み合わせで15時間後のダンプサイズを計測します。 設定ファイル default.jfc / profile.jfc リク エス ト回数 15時間で... 0リク エス ト / 約26,500 リク エス ト / 約53,000 リク エス ト / 約106,000 リク エス ト ◆計測結果 ⇒ ダンプサイズは リク エス ト回数に応じて微増 しました。 同じリク エス トを投げていたため同じイベントを大量に収集したことになりますが、同一イベントであれば JFR がいい感じに圧縮してくれていそうです。 計測結果から1日当たりのダンプサイズを試算してみると、 数万~数十万リク エス ト/日 規模のシステムであれば 、本番運用向けの default.jfc 設定でざっくり 300MB/日 程のサイズになると見込めます。 また、ダンプファイルは gzip や bzip2 で 圧縮をかければ3割のサイズにまで小さく できました。 実質 90MB/日 程です。 このサイズをどう捉えるかは各々の環境次第ではありますが、商材の持つ リク エス ト数 や アプリサーバの台数 なども加味して、場合によってはログローテートを見直すなどの対応が必要になりそうです。 まとめ JDK Flight Recorder の特徴と、オーバヘッドによるアプリへの影響を検証した結果を紹介しました。 アプリへの影響としては、 Java ヒープへの影響のみ要注意 だが、 その他は無視できるか、オプションで調整可能な範囲 だと言える結果になりました。 また、 機能を有効化するだけで使い始められる手軽さ と 収集できる情報の豊富さ は大きな魅力だと思っています。 小さくはじめられて大きなリターンを期待できます ので、この記事を読んでくださった方も是非 JFR を使ってみてください。 参考文献 OpenJDK での JDK Flight Recorder の使用 OpenJDK 11 | Red Hat Customer Portal フライト・レコーダを使用したパフォーマンス問題のトラブルシューティング JDK Flight Recorder入門 - Speaker Deck 目次 · 入門: JDK Flight Recoder 使ってみよう!JDK Flight Recorder OpenJDK 8uでJDK Flight Recorderの入門 - 赤帽エンジニアブログ GitHub - thegreystone/jmc-tutorial: A hands-on-lab/tutorial for learning JDK Mission Control 7+. *1 : 独自イベントを実装することもできます 参考: カスタムイベント API の定義と使用 *2 : オプションの参考: コマンドラインを使用した JDK Flight Recorder の設定 *3 : リポジトリ 領域の最長保管期間・最大保管サイズを越えて削除されたログは出力されません
アバター