TECH PLAY

株式会社ラクス

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

927

始めに 弊社では、 数行と画像1枚の静的ページを表示させるためだけ に、1台サーバーを構築し保守運用してました。 それだけのために、1台のサーバーを保守運用するの馬鹿らしくね?????? \\\うん!馬鹿らしい/// ということで、 AWS 上に移行すること となりました にしました。 今回は、S3とCloudFrontを利用して静的ページを表示させる設定をご紹介させていただきます。 初歩的な内容となりますので、これから静的ページを作りたいんだけど!といった方向けの内容となります。 始めに 要件 構成について AWSの利用サービス 構成図 実際の設定手順 S3設定 1. バケット作成 2. indexファイルのアップロード CloudFront設定 1. ディストリビューションを作成 2.ポリシー設定 3.接続確認 Route53 設定 1. カスタム SSL 証明書発行 2. CNAME登録 3. ルーティング設定 4.カスタム SSL 証明書設定 5.確認作業 HTTPレスポンスの変更 1. Cloud Front関数 2. Cloud Front紐づけ 3.確認作業 おわりに 要件 今回の要件は下記です。 弊社の ドメイン が利用可能 HTTPレスポンスコード(501 や 503 番台等) が任意のものに変更が可能 https 通信で静的ページが表示可能 弊社のシステム側からのFWの許可設定を追加の必要がない 構成について AWS の利用サービス Route53 : 独自ドメイン を登録するために利用 CloudFront : 独自ドメイン の利用及びレスポンスコードを任意のものに変更するために利用 S3 : 静的ファイル(HTML) を配置 構成図 構成は下記を想定しています。 実際の設定手順 それでは早速設定手順に入らせていただきます。 S3設定 静的ページを保存するためだけに利用しております。 本番サービスで稼働する際には、ログ保存の設定 や 権限の設定などが色々かかわってきますが、 今回は最低限の設定だけとなります。 1. バケット 作成 下記の設定手順で バケット を設定する バケット 名: rakus-tset 他の設定: デフォルト 2. indexファイルのアップロード 下記のファイルを作成した バケット に保存します。 index.html <!DOCTYPE html> <html lang="ja"> <head> <title>hello rakus!</title> </head> <body> <h1>hello rakus!</h1> </body> </html> CloudFront設定 独自ドメイン の利用 及び レスポンスコードを任意のものに変更するために利用します。 まずは、 独自ドメイン を利用せずにhttp通信が可能なところまで設定します。 1. ディストリビューション を作成 ディストリビューション は下記のように設定します。 オリジン ドメイン : 作成したS3 を選択 オリジンアクセス : Origin access control settings (recommended) Origin access control : コン トロール 設定を作成 ※デフォルトで作成 ウェブアプリケーション ファイアウォール (WAF):保護しない 料金クラス : 北米と欧州のみを使用 IPv6 : オフ 他の設定: デフォルト 2.ポリシー設定 該当オリジンのページに戻った際に下記のようにでているため、作成したS3の バケット ポリシーにコピペで設定をします。 3.接続確認 ここまでで下記にアクセスできるようになったため、確認します。 https://ディストリビューションドメイン名/index.html Route53 設定 DNS にCNAME や ルーティングの設定を行います。 この設定を行うことで、CloudFront と 独自ドメイン が紐づき、 独自ドメイン での https 通信を行えるようになります。 1. カスタム SSL 証明書発行 こちらもCloudFrontの画面から設定します。 該当の ディストリビューション の設定から カスタム SSL 証明書 の配下にある [証明書をリク エス ト] を押下します。 設定画面の通りに進んでいきます。 下記の通りで設定していきます。 完全修飾 ドメイン 名: 任意 他の設定: デフォルト 2. CNAME登録 登録後下記のようにでるため、 証明書の表示を押下します。 その後、Route 53 でレコードを作成を押下してRoute53に登録します。 検証が終わるまで待機します。 3. ルーティング設定 Route53 のサーバーへアクセスし、該当ホストゾーンに遷移し、下記の設定を登録します。 レコード名: 設定した ドメイン 名 レコードタイプ : A エイリアス : はい トラフィック のルーティング先 : 作成した ディストリビューション を選択 4.カスタム SSL 証明書設定 該当の ディストリビューション の設定を下記のように変更します。 代替 ドメイン 名 (CNAME) : 先ほど登録したCNAME カスタム SSL 証明書 : カスタム SSL 証明書発行で作成したもの 5.確認作業 下記のコマンド実施します。 curl -I https://ドメイン名/index.html 現状では、レスポンスコードが200番で返ってきます。 HTTPレスポンスの変更 httpレスポンスを400番台や500番台で返したいため、そちらの内容を設定していきます Viewer Response Eventで HTTPレスポンスコードを書き換えます 1. Cloud Front関数 Cloud Frontの関数項目より下記を作成します。 名前: 任意 説明: 任意 説明: 開発コードは下記 ※レスポンスコードが200だった場合 503 番台に変更するようなものとなっております。 function handler(event) { var response = event.response; var contentType = response.headers['content-type'].value; if (response && response.statusCode === 200) { response.statusCode = 503; response.statusDescription = 'test mode'; } return response; } 発行タブより発行を行います。 2. Cloud Front紐づけ 作成した ディストリビューション の画面からビヘイビアを編集します。 関数の関連付け を下記のように設定してください。 ビューワーレスポンス: CloudFront function 先ほど作成した関数 を指定 3.確認作業 下記のコマンド実施します。 curl -I https://ドメイン名/index.html 結果(一部抜粋) HTTP/1.1 503 test mode Content-Type: text/html Content-Length: 143 Connection: keep-alive おわりに S3を静的ページで採用されることは結構あるかと思います。 ドメイン や レスポンス内容の制約により、利用されることを断念するケースもあるかもしれません。 CloudFront functionは非常に便利で色々なことができますので、よろしければ掘ってみてもおもしろいかもしれません。 次は、署名付き URL と署名付き Cookie あたりを触りたいと思います。 最後まで読んでいただきありがとうございました。 皆さまの参考になれば幸いです。
アバター
こんにちは!フロントエンド開発課所属の koki _matsuraです! 今回はものすごく今更感が否めないのですが、Reactのv18で発表された「Suspense」とVercel社が提供しているReact Hooksライブラリの「SWR」によって何を解決してくれるのか、 コンポーネント の表示と実装を例に紹介します。 目次は以下のようになっています。 Suspenseとは SWRとは Suspense・SWRが解決すること Suspense・SWR導入におけるコンポーネント表示の変化 Suspense なし SWR なし Suspense あり SWR なし Suspense あり SWR あり Suspense・SWR導入におけるコンポーネント実装の違い Suspense なし SWR なし Suspense なし SWR あり Suspense あり SWR あり まとめ 終わりに Suspenseとは React16.6で実験的な機能として追加され、React18で正式に追加された機能で、「 コンポーネント のローディング状態をハンドリングする」ことが役割となっています。 基本的にこれだけなのですが、これにより コンポーネント 単位でのロードを可能にします。 SWRとは データ取得のためのライブラリです。 名前の由来はHTTPキャッシュ無効化戦略の"stable-while-revalidate"です。 こちらもやっていることはシンプルで特定のデータをキャッシュし、もう一度、必要になった際にキャッシュからデータを返します。また、定期的に裏側でフェッチをし、最新のデータに更新してくれます。 Suspense・SWRが解決すること 結論からいきます。 Suspenseは以下のことを解決します。 データ取得のローディング状態を宣言的にする コンポーネント 単位でのロードを可能にする コンポーネント の責務を明確にする SWRは以下のことを解決します。 キャッシュにより、更新時の長期のローディングがなくなる API 通信が簡略化される フェッチしたデータの管理をしなくてよくなる よくSuspenseは「ローディング状態をハンドリングするもの」のように思ってしまっている方がいます。もちろん間違ってはいませんが、それだけではないということです。 むしろ、Suspenseのいいところは「 コンポーネント の責務を明確にする」部分です。これが本質だと思っています。 SWRはキャッシュの管理や非同期処理状態の管理をしてくれるため、データのロードを待つ必要がなかったり、データの取得を非常にシンプルにしてくれます。 ここからは コンポーネント の表示と実装を例に、実際にSuspenseとSWRが解決していることを紹介していきます。 Suspense・SWR導入における コンポーネント 表示の変化 表示においてSuspenseとSWRを取り入れることでどのような変化があるのかを紹介します。また、更新時においての違いも見ていきます。 下記のパターンで変化を見ていきます。 Suspense なし SWR なし Suspense あり SWR なし Suspense あり SWR あり 下図のようにページに3つの コンポーネント を表示する例で比較します。それぞれの コンポーネント は上から1秒間、2秒間、3秒間かかるフェッチ処理を行っているため、表示するにも1秒間、2秒間、3秒間以上かかります。 Suspense なし SWR なし まずはSuspenseもSWRも使っていない例です。初期表示時と更新時の動画を載せています。 ・初期表示時 ・更新時 初期表示時は「Suspenseを利用しない」というボタンを押したときに該当のページをリク エス トします。そこから3秒間経過したのち、3つの コンポーネント が揃った状態で画面が表示されています。 更新時も3秒後に「◯秒後に表示される コンポーネント _」の後ろについている文字列が一気に変更されていることがわかります。 これはサーバが完全にHTMLを構築してからクライアントに返していることを表しています。 図で表すと以下のようになります。 ページをリク エス ト API サーバへリク エス ト(3つ) 1,2,3秒後にレスポンスをWEBサーバへ返却 WEBサーバは全て揃ってからHTMLを構築 クライアントへHTMLを返却 HTMLを描画し、JSをロード、HTMLにハイドレーションを実行 少し雑に紹介していますが、大まかな流れを表せていると思います。初期表示や更新時に毎回3秒待たされるのは問題です。また、JSのロードやハイドレーションのことを考えると操作できるようになるには3秒よりもかかると考えられます。 ここにSuspenseを導入し、ページのロード単位を コンポーネント 単位にすることで大きな待ち時間を解決してくれます。 Suspense あり SWR なし Suspenseを導入すると以下のように変化します。 ・初期表示時 ・更新時 初期表示時は「SWRなし」というボタンを押すとページがリク エス トされ、すぐにページが遷移しています。そして、1,2,3秒後にそれぞれの コンポーネント が順に表示されていくことが確認できます。取得中は代わりの コンポーネント (今回はローディング コンポーネント )が表示されます。 更新時も同様に、全体が取得中になり、1,2,3秒で コンポーネント が表示されていきます。 WEBサーバはHTMLが未完成の状態でも返してくれることがわかると思います。 図で表すと以下のようになります。少し見にくくなっています。すみません。 ページをリク エス ト API サーバへリク エス ト(3つ) レスポンスがいらない部分だけのHTMLを構築 クライアントへ3で作成したHTM Lを返却 HTMLを描画、JSをロード、ハイドレーション。取得中の部分は代わりの コンポーネント を表示 「1秒後の コンポーネント 」のレスポンスをWEBサーバへ 「1秒後の コンポーネント 」のHTMLを構築 「1秒後の コンポーネント 」を返却 「1秒後の コンポーネント 」を表示し、その部分のJSをロード、ハイドレーション 以下略... 10〜16は6〜9と同じ流れになるので省略しました。 Suspenseを使うことで コンポーネント 単位でのロード(JSのロード・ハイドレーション)を可能にすることにより、初期表示の大きな待ち時間を解決 してくれています。 しかしながら、更新するたびに完全に表示し直すのに結局3秒以上かかるのは問題です。 ここをSWRのキャッシュ管理により解決します。 Suspense あり SWR あり SWRを導入すると、以下のようになります。 ※ SWRを導入すると、 SSR はできません。 CSR になります。 ・初期表示時 ・更新時 初期表示はSuspenseのみを導入した時と見た目は変わりません。 見た目は変わらないのですが裏側ではフェッチと共にデータをキャッシュしており、更新時にはそのキャッシュを返すことですぐに表示することができています。つまり、SWRにより 更新時の長期のローディングが解決 されていることがわかります。 また、裏で再フェッチしてくれているので最新情報を取得できればすぐに コンポーネント が更新されていることが文字列が変更されていることで確認できます。 コンポーネント の表示で比較することでSuspenseの コンポーネント 単位のロードによる表示速度の解決 とSWRの キャッシュ管理による更新時の表示速度の解決 を実際に見て、理解できたと思います。 次は、コード側から見てSuspenseとSWRが解決してくれることを紹介します。 Suspense・SWR導入における コンポーネント 実装の違い こちらも下記の3パターンで変化を見ていきます。先ほどとは違って、SWRだけを導入した場合とそこにSuspenseを導入した場合で紹介させていただきます。 Suspense なし SWR なし Suspense なし SWR あり Suspense あり SWR あり ユーザ情報を表示する コンポーネント を例にします。 Suspense なし SWR なし 下記はSuspense・SWRを共に使っていないUser コンポーネント で、ユーザ情報を表示するためだけのシンプルな コンポーネント です。 const User = () => { const [ user , setUser ] = useState < User >( null ); useEffect (() => { fetchUser () .then (( res ) => setUser ( res )); } , [] ); if ( user === null ) return < Loading / >; return < Contents user = { user } / >; } ; データがnullの間、つまり取得中の間はローディングを表示、nullじゃなければ、ユーザ情報を表示するようになっています。そのユーザ情報はuseEffect内のfetchUser()で取得され、stateにより管理されています。 まず、問題として挙げられるのは、useEffect内でフェッチをしている点です。本来、useEffectは副作用処理を書くためのものであって、冪等性のない処理を書かない方が望ましいです。 また、User コンポーネント の中でLoadingとContent コンポーネント を出し分けが行われている点も問題です。今回は例に挙げなかったのですが、フェッチのエラー処理もすることがあります。そうなると、Error コンポーネント も出し分けに加わることになります。 本来はユーザ情報を表示することのみが仕事のUser コンポーネント がローディング、エラー、ユーザ情報を表示する コンポーネント になってしまいます。 これらの問題をSWRを導入した時、どのように変わるか見ていきましょう。 Suspense なし SWR あり 先ほどの コンポーネント 表示ではSWRによってキャッシュが働くことを確認できたと思います。 コードは以下のようになります。 const User = ()=> { const { data: user , isLoading } = useSWR ( "user" , fetchUser ); if ( isLoading ) return < Loading / >; return user && < Contents user = { user } / >; state管理・useEffectがなくなり、その代わりにuseSWRフックを使っています。 useSWRは API 通信を簡素化してくれるため、 本来書くべきではないuseEffect内でのフェッチ処理やフェッチ後のデータ管理、キャッシュの管理の問題を解決 することができます。 今回は書いていませんが、useSWRはエラーにも対応しています。 SWRだけでも非常にシンプルになりましたが、まだUser コンポーネント はローディングとユーザの出し分けという複数の責務を持ってしまっています。ここにSuspenseを取り入れてみます。 Suspense あり SWR あり Suspenseを取り入れたUser コンポーネント は以下のようになります。 const User = ()=> { const { data: user } = useSWR ( "user" , fetchUser , { suspense: true , } ); return < Contents user = { user } / >; SWRでSuspenseを使いたい時はuseSWRの第三引数でsuspenseオプションをつけるだけです。 変更点はローディングの出し分けがなくなった部分です。 SuspenseがLoading コンポーネント を出す責務を担ってくれました。たった一行減っただけですが、これで コンポーネント の責務の不明確化 が解決できました。 ちなみに、Suspenseを使うときは該当の コンポーネント をSuspense コンポーネント で囲むだけで実装できます。 今回の場合は以下のようになります。 < Suspense fallback = { < Loading / > } > < User / > < /Suspense > fallbackの中にローディング中に表示したい コンポーネント を入れるだけです。 ローディング状態が命令的なものから宣言的なもの になっています。 Suspense・SWRを取り入れることにより従来の コンポーネント の問題点を解決できることが理解できたと思います。 まとめ コンポーネント の表示による違いと実装の違いを例にSuspense・SWRが何をしてくれるのかを説明してきました。 まとめると、SWRは今まで コンポーネント 内で行っていたデータの管理やフェッチの処理をuseSWRフックのみで完結させ、キャッシュまでも柔軟に管理してくれるライブラリです。 そして、Suspenseはローディング状態を宣言的にしてくれることにより、 コンポーネント 自体を非同期処理として扱え、責務を明確にするとともに、 コンポーネント 単位のロードを可能にしてくれました。 終わりに ここまで読んでいただき、本当にありがとうございます。 間違っている点などがあれば、ぜひコメントでおしえてください! この記事がSuspenseやSWRに興味を持つきっかけになってくれれば幸いです。
アバター
はじめに こんにちは、 MasaKu です。 弊社では、 PHP に関する最新ニュースの発信や気になるお題について議論するイベント「 PHP TechCafe」を毎月開催しております。 本日は、 PHP TechCafe とはどんなイベントなのかのご説明と、過去開催したイベントの中で特に盛り上がったイベントをご紹介させていただきます。 Web × PHP TechCafe はじめに PHP TechCafeの目的 立ち上げからの経緯 参加対象者とその理由 運営メンバー テーマ選定方針 コンテンツ作り 特に評判の良かったテーマ10選 PHPerのための「PHPと型定義を語り合う」 PHP TechCafe PHPerのための「PHPのリーダブルなコード」を語り合うPHP TechCafe PHPerのための「Laravel10の新機能」を語り合う PHP TechCafe PHPerのための「PHPDoc相談会」PHP TechCafe PHPerのための「PHP8.2の新機能」を語り合うPHP TechCafe PHPerのための「Composer」を語り合うPHP TechCafe PHPerのための「PHPフレームワーク」を語り合うPHP TechCafe PHPerのための「静的解析」を語り合うPHP TechCafe PHPerのための「Xdebugの活用方法」を語るTechCafe PHPerのための「PHPUnit の始め方」について語りあう PHP TechCafe おわりに PHP TechCafeの目的 PHP TechCafe は月に一度 ZOOM で開催されているオンラインイベントです。 エンジニア同士の交流の機会を提供する、エンジニアと技術が交差する憩いの場(カフェ)になれるようなイベントを目指しています。 PHP TechCafe というイベントそのものが学びの場となり、運営メンバーも含め、参加者全員がエンジニアとしてレベルアップしていけるように支援することを目的として開催しております。 立ち上げからの経緯 立ち上げ当時は PHP をテーマにした勉強会ではなく、弊社のカフェスペースを利用した社外向けの もくもく会 でした。 もくもく会 で社外のエンジニアと交流を深めていく中で、技術的なテーマで交流できることの楽しさに気づきました。 もっと技術的なテーマで語り合う方法はないかと検討したところ、自分たちの強みである PHP を軸にしたイベントにしてみてはどうかという方向性に至り、以来模索を重ねてきました 最初は PHP のニュースを紹介するだけのイベントでしたが、イベントに参加してくださった参加者にもっと価値を提供できないかと考え、特集コーナーを設けるなどして現在の形に進化してきました。 参加対象者とその理由 PHP TechCafe の参加対象者 PHP TechCafe の参加対象者は PHP 入門後の初級エンジニア ~ シニアエンジニア としています。 理由としましては、 PHP TechCafe を運営しているメンバーのレベル感や弊社の PHP エンジニアの技術領域もこの辺りに位置しているため、イベントを通して参加者と一緒に成長していきたいと考えているためです。 PHP TechCafe のイベントにはエキスパート以上のエンジニアが参加してくださることもあります。 そういった際には積極的に質問するなどして運営メンバーのレベルアップにもつなげていきたいと考えています。 運営メンバー 運営メンバーは実際にイベントに参加するメインメンバーとイベントのコンテンツ作成に協力していただくサポートメンバーで構成されています。 現在以下のメンバー構成で実施しています。 メインメンバー Y-Kanoh , MasaKu サポートメンバー 8名 弊社の PHP 系プロダクトから各2名ずつ参加しています 上記のうちイベントサポーターは2班に分けて隔月で参加してもらっています。 そのため、普段のイベントはメインメンバー2名とイベントサポーター4名の合計6名で作業をしています。 テーマ選定方針 基本的には運営メンバーが学んでみたい PHP 関連の技術をテーマにしています。 直近の業務で扱っていた部分や、業務に取り入れてみたい技術などがテーマに上がることがあります。 他のパターンとしては、以下のようなものがあります。 新しいバージョンの PHP や フレームワーク が公開されたタイミング 新機能の紹介 各種 PHP 関連イベント( PHPカンファレンス 等)が開催されたタイミング ふりかえり会などを開催 ※イベントのふりかえり会を実施する際は事前にイベントの運営元に許可をいただいて実施しております 参加者が多く集まったり、 SNS やZOOMのチャットで盛り上がった会については、イベントのブラッシュアップを行うなどして繰り返し実施することもあります。 テーマが決まったら、イベント当日はどのような流れで話をすれば盛り上がりそうか、ということを考えながら アジェンダ を作成するようにしています。 コンテンツ作り 選定したテーマを元にサポートメンバーと共にイベント当日に公開する ShowNote を作成します。 担当範囲はテーマ選定の時に作成した アジェンダ を元に分担しています。 各々が調べてきた内容をイベント当日までに持ち寄って情報共有を行い、メインメンバーがしっかりとインプットして当日のイベントに備える、という流れで準備をしています。 特に評判の良かったテーマ10選 以下では、特に評判の良かった回をピックアップさせていただきましたので、ご紹介いたします。 PHPerのための「PHPと型定義を語り合う」 PHP TechCafe rakus.connpass.com 概要 動的型付け言語である PHP は、手軽にコードを記述できる反面、 保守性の担保、または意図しない挙動を防ぐため、常に型を意識してコーディングを行う必要があります。 しかし、手軽に書ける反面、 PHP を始めたばかりの人の中には、あまり型を意識せず記載している人もいるのではないでしょうか? そんな方のために型との付き合い方を語り合いました! ShowNoteはこちら! hackmd.io PHPerのための「PHPのリーダブルなコード」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP で読みやすいコードとはどのようなコードでしょう? 同じ処理でも書き方によって可読性、ひいては保守性は大きく変わります。 ビギナーPHPerに伝えたい「可読性の高いコード」について語りました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Laravel10の新機能」を語り合う PHP TechCafe rakus.connpass.com 概要 Laravel の初版リリースから 11年目に Laravel10 がリリース予定されました。 Laravel の基礎的な内容をおさらいしつつ、Laravel10 の新機能について取り上げました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPDoc相談会」PHP TechCafe rakus.connpass.com 概要 PHPDocは数が多く、ツールによって対応有無も異なります。 そんなPHPDocについて、イベント運営メンバが疑問に思ったことを中心に議論しました! PHP で型定義されている場合、PHPDocでも型を書いた方がいいか? PhpStorm最新版は配列の型、 連想配列 のkey, value の型を検知してくれる? レガシーシステム とPHPDocの向き合い方 など ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHP8.2の新機能」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP8.2 は実用的な機能から破壊的な機能まで、様々な機能が追加されました。 PHP8.2 で実装される機能がどのようなものなのか、どういった用途があるのかについて語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Composer」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP の依存性管理ツールである "Composer" について深掘りしました。 何に使うものなのか、どのように使うのか、Packagist とは何なのか 等について語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPフレームワーク」を語り合うPHP TechCafe rakus.connpass.com 概要 Laravel、 Symfony 、Cake、Slim など、 PHP の フレームワーク について有名どころをリストアップしました。 主催者一同、触ったことがない フレームワーク が多数存在する中、開催までにしっかりと調査して語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「静的解析」を語り合うPHP TechCafe rakus.connpass.com 概要 静的解析とは、コードを実行することなく行うコード検証のことです。 PHP にも PHP _CodeSniffer や PHPStan などの静的解析ツールが存在します。 PHP でなぜ静的解析が必要なのかや、各静的解析ツールの特徴について、深堀して語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Xdebugの活用方法」を語るTechCafe rakus.connpass.com 概要 Xdebug は PHPer による デバッグ のお供であり、よく使われる "ステップ実行" だけでなく、さまざまな機能を提供する拡張ツールです。 実は、 Xdebug は "ステップ実行" だけでなく、様々な機能を有していますので、便利な使い方について語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPUnit の始め方」について語りあう PHP TechCafe rakus.connpass.com 概要 PHPUnit に興味はあるけど何から始めればいいの? 学ぶ前に全体像を把握したい!といった方を対象に、 PHPUnit の基本的な知識などを題材にしました。 PHP 初心者の方だけでなく、ベテランエンジニアの方も学び直し・気づきの発掘などの機会になればと思い語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp おわりに PHP TechCafe は現在のオンライン形式になってから 約3年 イベントを継続しています。 イベント運営の苦労もありますが、 PHP 関連のテーマで社外のエンジニアと交流できることは貴重な経験になっています。 これからも PHP TechCafe をどうぞよろしくお願いいたします。 次回の PHP TechCafe は 9月26日 に開催します。 ご参加お待ちしております。 rakus.connpass.com
アバター
はじめに こんにちは akihiyo76 です。現在、私のチームではレビュー ガイドライン を明文化して、レビュアーは ガイドライン に従ってコードレビューを行なっています。この ガイドライン は、チームで運用を開始して2年になりますが、チームでも浸透しレビュー時に必ず利用するようになりました。 はじめに コードレビューの課題感 課題改善に向けて 採用したコードレビュー観点 1. Design(設計) 定義 具体例 2. Simplicity(理解容易性) 定義 具体例 3. Naming(命名) 定義 具体例 4. Style(コードスタイル) 定義 具体例 5. Functionality(機能要求) 定義 具体例 6. Test(テスト) 定義 具体例 7. Document(文章) 定義 具体例 指摘対応の要否 具体的な利用方法 指摘例 最後に コードレビューの課題感 私は現在モバイル開発チームに所属しておりますが、メンバーは若手エンジニアが中心です。一方、弊社のサービスは SaaS が中心であるため、これまでモバイル開発の経験者が少ない状況でした。そのため「モバイル技術のセオリーが分からない」という課題がチームにあり、コードレビューに苦労する状況でした。 その結果として、 メンバーの技術力の伸び悩み リリース後に一定数のバグが発生する といった状況でした。 課題改善に向けて そこで、この課題改善に取り組むことにしましたが、 レビュー指摘を類型化して、メンバーの技術力を定量化できないか と考えました。その手段として、コードレビューに対する ガイドライン を作成して、レビューコメントを類型化・ 定量 化することにしました。 一定の観点を持ってレビューコメントを類型化することで、KPIとして計測が可能( 見える化 )になり、弱点分析をすることができるからです。 ガイドライン を作成する上で、 Google が公開するレビューガイドライン を参考にして以下の7つのレビュー観点を設けることにしました。 採用したコードレビュー観点 では、実際にレビュー ガイドライン で採用した観点を紹介します。 No 観点 概要 1 Design 設計が適切か 2 Simplicity 理解容易性 3 Naming クラス、メソッド、変数名などの 命名 4 Style コードスタイル 5 Functionality 機能(要件)を充足しているか 6 Test テストの記述、パターンが適切 7 Document コメント、ドキュメントに関連 特にNo.1 ~ No.4は、 オブジェクト指向 の観点で非常に重要な観点といえます。しかし、これらの観点と概要だけでは判断が難い場面もあるかと思うので、もう少し具体的にコードベースで説明します。 1. Design(設計) 定義 システムにとって適切な責務・振る舞いになっているか。システムとして アーキテクチャ を遵守できているか。また、システム全体として 一貫性ある設計になっているか。 具体例 基本的には、以下のような オブジェクト指向 の基本である SOLID 原則に反するような場合、指摘の対象になります。 関心の分離原則違反(≒ 単一責任原則違反) 密結合 低凝集 DRY 原則違反 etc. 例えば、以下のコードの場合 add() で様々処理を行なっており、責務超過といえるため Design 指摘の対象になります。 class HogeDiscountManager { lateinit var manager: DiscountManager /** * 商品を追加する */ fun add(product: Product): Boolean { if (product.id < 0 ) { // バリデーション 1 throw IllegalArgumentException () } if (product.name.isEmpty()) { // バリデーション 2 throw IllegalArgumentException () } val temp: Int = if (product.canDiscount) { // 条件分岐 1 manager.totalPrice + manager.getDiscountPrice(product.price) } else { manager.totalPrice + product.price } return if (temp < 3000 ) { // 条件分岐 2 manager.totalPrice = temp manager.discountProducts.add(product) true } else { false } } } 2. Simplicity(理解容易性) 定義 システムとして可読性あるコーディングになっているか。 処理ができるだけシンプルな振る舞いになっているか。 具体例 以下のように実装が複雑になる場合、指摘の対象になります。 ネストが深い if 分 複雑な 三項演算子 文 冗長な SQL stream, filter, map を多用したObject整形文 etc. このように分岐が多い if 分は、Simplicity の指摘の対象になります。 // Before.kt val powerRate: float = member.powerRate / menber.maxPowerRate var currentCondition: Condition = Condition.DEFAULT if (powerRate == 0 ) { currentCondition = Condition.DEAD } else if (powerRate < 0.3 ) { currentCondition = Condition.DANGER } else if (powerRate < 0.5 ) { currentCondition = Condition.WARNING } else { currentCondition = Condition.GOOD } return currentCondition 実際のレビューコメントでは、以下のようにネストを解消するように指摘をする場合などに使用します。 // After.kt val powerRate: float = member.powerRate / menber.maxPowerRate if (powerRate == 0 ) { return Condition.DEAD } if (powerRate < 0.3 ) { return Condition.DANGER } if (powerRate < 0.5 ) { return Condition.WARNING } return Condition.GOOD 3. Naming( 命名 ) 定義 変数やクラス、メソッドに責務を意図した明確な名前が付けられているか。英語文法に誤りがないか。 typo もこれに含まれる。 具体例 このような 命名 に関する指摘をする場合に使用します。 振る舞いと一致しない変数名、関数名 責務と一致しない関数名 英文法の誤り etc. 例えば iOS アプリ開発 時においては、Swift Foundation や Cocoa の 命名規則 に準拠しない場合、Naming の指摘対象になります。基本的な 命名規則 は、利用している フレームワーク や言語の特性によるものが判断基準になります。 4. Style(コードスタイル) 定義 コードスタイル言語仕様に準拠しているか。 具体例 コードスタイルも同様に言語仕様や フレームワーク に準拠させることが基本になるため、これに反する場合に使用します。 静的解析違反 不適切なアクセス修飾子 表記違反(スネーク、キャメルなど) etc. 他にもモバイル開発では、公式( Apple 、 Google 等)で公開している ガイドライン 違反している場合もこれに含まれます。コードスタイルの判断はその人の経歴などの主観的な部分も影響するので、コードフォーマッターを導入し、 機械的 な判断基準を設けることもこの指摘点を減らす有効な手段です。 5. Functionality(機能要求) 定義 システムとして外部仕様を充足しているか。作者が意図した通りの振る舞いであるか。 また、システムの通信量、パフォーマンスに懸念がないか。 具体例 主な観点としては、外部機能を充足しているかという点が対象になります。 外部仕様の未充足(不具合) 概要設計書の フローチャート と異なるフローになっている 不要データを送信している etc. 6. Test(テスト) 定義 システムとして適切な自動テストを兼ね備えているか。自動テストの内容で品質を担保できているか。 また、システムを担保するパラメータ群を備えているか。 具体例 テストコードが期待になっていない場合や、テストでのパラメータに考慮漏れがある場合、指摘の対象になります。 対象のメソッドがテストされていない テストパターンが網羅できていない(パタメータテスト、 閾値 テストの不足) 分岐がパターンが網羅されていない 実装上宣言している静的定数値が直接ハードコードされている アーキテクトに準じたテストになっていない 7. Document(文章) 定義 ソースコード 上に記載されている doc、コメントが適切な内容であるか。 また、関連するドキュメントは更新されているか。その内容は適切か。 具体例 ソースコード に関連するコメントだけでなく、プロジェクトで管理している関連ドキュメント(README)も対象になります。 関連ドキュメントの更新漏れ(README など) doc やコメントの内容が不適切、内容が不適切 指摘対応の要否 更にコードレビューの現場では上記の7つの観点に加えて、指摘修正の要否を4つの累計に分けてコメントしています。 観点 概要 MUST PR、MR をマージするためには必ず修正が必要 SHOULD 修正なしにマージすることはできるが、リリースまでには修正が必要 IMO レビューアー観点の意見。修正不要 NITS IMO より細かい意見など。修正不要 このように、コードレビューでマージするために必要な修正は MUST 指摘となります。MUST と SHOULD の使い分けは難しい部分もありますが、これまでのレビュー ガイドライン の運用では、 SQL のパフォーマンスをより良くするための指摘やテストコードの最適化の指摘などで SHOULD は利用される場面もあります。その場合、修正タスクを Issue に積んだ上で(修正スコープの合意)、マージするようにしています。一方、IMO や NITS は修正は不要ですが、修正しない場合はその旨をコメントに返信してもらい、コメントを閉じてからマージする運用をしています(レビュアーとの合意)。 具体的な利用方法 実際にコードレビューをするとき、上記の7つの観点と修正の温度感をこのように交えた Prefix を付けて、コメントをします。 指摘例 MUST(Design): ドメイン ロジックが Controller クラスに実装されてます。 domain 層の対象 package に新しくクラスを作成して実装を移してください。 このとき Prefix の入力を手入力にしてしまうと、入力の手間や入力がミスが生じることもあるので、カスタム script で入力をサポートするようにしています。 最後に 以上のように、私のチームではコードレビュー ガイドライン を作成してルールを明文化することで、技術力を 見える化 させて課題改善を進めています。レビューコメントをこのように分析することで、個人の弱点に合わせたアプローチ方法も見えてきます。このように技術力に対するアプローチとして PDCA サイクルを回すことで、チームメンバーの技術育成を進めております。 最後に簡単にまとめると、コードレビュー ガイドライン を明文化した場合、 指摘数に応じて技術力を 見える化 できる コードレビューで オブジェクト指向 が学べる スキルアップ のためのアクションプランが検討しやすい といった恩恵を受けることできるので、ぜひチームに合ったコードレビュー ガイドライン を作成してみてはいかがでしょうか。
アバター
はじめに はじめまして。インフラエンジニアの rkyohei です。 Linux サーバの運用やモニタリングにおいて、性能チューニングや トラブルシューティング にはさまざまコマンドを使用すると思います。その中でも、特にリソース使用状況を詳細に分析するために便利なツールの1つが「vmstat」となります。 vmstatコマンドの存在自体は知っていたけど、オプション、実行結果の見方についてあまり知らなかったのですが、先日業務で使用する機会があり、vmstatコマンドについて調べましたのでこのエンジニアブログでみなさんにご紹介したいと思います。 はじめに vmstatとは何か? vmstatコマンドの基本的な使い方 vmstatコマンド実行結果の見方 vmstatコマンドの実行例 1. vmstatコマンドのみでの実行例 2. 更新間隔、表示回数を含めた実行例 3. -sオプション(メモリ統計情報の表示)を使用した実行例 4. -dオプション(ディスクI/O統計情報の表示)を使用した実行例 最後に 参考文献 vmstatとは何か? vmstatとは Virtual Memory Statistics の略であり、 Linux システム上で 仮想メモリ の統計情報を表示するコマンドです。 vmstatコマンドの基本的な使い方 vmstatコマンドの基本的な使い方についてご紹介します。 vmstat [オプション] [間隔(sec) [回数] ] ※[ ] は省略可能です オプション: ここではvmstatコマンドのオプションについて一部ご紹介します。 オプション 説明 -a 仮想メモリ の詳細情報を表示します。プロセスのステート(実行中、スリープ中など)、ページング、メモリ情報などが含まれます。 -s 仮想メモリ の統計情報のみを表示します。各種メモリスタット、ページング、 スワップ 情報などが表示されます。 -d ブロックデ バイス のIO統計情報を表示します。IOのバイト数、リク エス ト数、転送時間などが表示されます。 -D ディスクの統計情報を1項目1行で表示します。 -p < パーティション > 指定した パーティション に関する情報を表示します。 パーティション を指定して詳細情報を取得することができます。 -S 単位 単位をk,K,m,Mで指定します。 -t タイムスタンプを表示します。 これらのオプションを使用することで、さまざまな情報を取得することができます。例えば、 仮想メモリ の詳細情報や統計情報、ブロックデ バイス のIO統計情報などを利用して、システムの性能やリソース使用状況をより詳細に分析できます。 上記以外にもオプションはありますので、オプションの詳細について興味がある方は、 man コマンドを使用してマニュアルページを参照していただければと思います。 更新間隔: デフォルトでは1秒ごとに情報が表示されますが、必要に応じて変更できます。 表示回数: 指定回数だけ情報を表示した後にコマンドが終了します。 vmstatコマンド実行結果の見方 vmstatコマンドの実行結果の見方について、以下にご紹介いたします。ここではvmstatコマンドをオプション無しで実行した結果を例としています。 # vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st  0  0 123088 154496      0 1168484    0    0     0     1    1    1  0  0 100  0  0 区分 値 意味 procs r 現在実行待ちのプロセス数。CPUが過負荷であるかどうかを示す指標です。 b スリープ中のプロセス数。スリープ中のプロセスはI/Oの完了を待っています。 memory swpd スワップ されたページの数。 スワップ の量を示す指標です。 free 使用されていないメモリの量。大きな値が望ましいです。 buff ファイルの読み取り結果としてキャッシュされているメモリ量。 cache ファイルシステム がキャッシュしているページの量。メモリ使用効率の指標。 swap si スワップ 領域からメモリにページが転送された回数。 so メモリから スワップ 領域にページが転送された回数。 io bi ブロックデ バイス から受け取ったブロック。(blocks/s) bo ブロックデ バイス に送られたブロック。(blocks/s) system in 1秒あたりの割り込みの数。ハードウェアの負荷を示す。 cs 1秒あたりの コンテキストスイッチ (プロセスの切り替え)の数。 cpu us ユーザープロセスが消費したCPU時間。 sy カーネル プロセスが消費したCPU時間。 id アイドル状態のCPU時間。高いほどCPUがアイドルであることを示します。 wa ディスクI/Oの待機時間。ディスクへのアクセスが遅い場合に増加します。 st 仮想マシン から盗まれた時間を示します。 vmstatコマンドの実行例 vmstatコマンドの実行例と結果についていくつかご紹介します。 1. vmstatコマンドのみでの実行例 vmstatコマンドをオプション無しで実行すると、1回のみ結果が表示されます。 # vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st  0  0 123088 154496      0 1168484    0    0     0     1    1    1  0  0 100  0  0 2. 更新間隔、表示回数を含めた実行例 以下のように実行することで更新間隔、表示回数を指定することができます。ここでは1秒間隔で5回実行されるように指定しています。 また  -t オプションを併せて使用することでタイムスタンプを結果に表示させることもできます。おそらく トラブルシューティング の際には問題となる事象が再発するまでコマンドを継続して実行する必要があり、cronで定期的に実行したり、引数を使用して実行状態のまま経過監視すると思います。 TeraTerm 等のログ保存設定にタイプスタンプを付与することも可能ですが、 -t オプションを使用しタイムスタンプを表示することで問題が発生した時間のログを探しやすくなります。 # vmstat 1 5 -t procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- -----timestamp-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st                 JST  0  0 123088 154320      0 1168484    0    0     0     1    1    1  0  0 100  0  0 2023-08-28 12:17:49  1  0 123088 154224      0 1168484    0    0     0     0  141  202  1  0 100  0  0 2023-08-28 12:17:50  0  0 123088 154224      0 1168484    0    0     0     0  150  218  0  0 100  0  0 2023-08-28 12:17:51  0  0 123088 154256      0 1168484    0    0     0     0  128  189  0  0 100  0  0 2023-08-28 12:17:52  0  0 123088 154256      0 1168484    0    0     0     0  139  192  0  1 100  0  0 2023-08-28 12:17:53 3. -s オプション(メモリ統計情報の表示)を使用した実行例 -s オプションを使用することでメモリの統計情報を1項目1行で表示することができます。 # vmstat -s 1728000 K total memory 405420 K used memory 621268 K active memory 539020 K inactive memory 153972 K free memory 0 K buffer memory 1168608 K swap cache 2097148 K total swap 123088 K used swap 1974060 K free swap 3091882 non-nice user cpu ticks 135539 nice user cpu ticks 1694588 system cpu ticks 2524043051 idle cpu ticks 59367 IO-wait cpu ticks 1517573 IRQ cpu ticks 1268497 softirq cpu ticks 487644 stolen cpu ticks 9009973 pages paged in 20842448 pages paged out 74510 pages swapped in 207951 pages swapped out 1668051957 interrupts 2180661177 CPU context switches 1680577615 boot time 393160 forks 4. -d オプション(ディスクI/O統計情報の表示)を使用した実行例 -d オプションを使用することでディスクI/Oの統計情報を表示することができます。ディスクの性能評価や トラブルシューティング に役立つ情報を確認することができます。 # vmstat -d disk- ------------reads------------ ------------writes----------- -----IO------ total merged sectors ms total merged sectors ms cur sec sda 238129 21641 18019946 506118 893700 503648 41682672 4461928 0 2138 sr0 9 0 3 2 0 0 0 0 0 0 dm-0 181053 0 17319481 448718 1182573 0 40014120 8384242 0 2099 dm-1 74608 0 600776 58836 207951 0 1663608 8075290 0 42 ディスクI/Oの統計情報では3つの項目が表示されます。それぞれ読み取り(reads)と書き込み(writes)、実行中のI/Oです。各項目中のmsとsecは合計時間を示しています。 最後に vmstatコマンドは、 Linux サーバエンジニアにとって重要なモニタリングツールです。リソース使用状況の トラブルシューティング や最適化にとても有効だと考えます。 ただし、オプションや実行結果を理解し、覚えることはなかなか難しいと思います。 コマンドを実行する環境があるのであれば、実際にコマンドを実行することで、オプション、結果にふれてみるのも良いと思います。 参考文献 How to read Vmstat output
アバター
はじめに 昨今 書籍や各社Blog記事などでプロダクトマネージャー(以下PdM)の業務内容について記載された媒体が多数でている状況です。 ですが、複数の媒体を参照された方は、こう思われることが多いと考えております。 「見るものによって役割、業務内容違くないか?」 実際、企業・プロダクト・チームといった単位で、PdMの業務内容は変わっていると私も考えております。 弊社 ラク スにも、以下のようにさまざまなプロダクトがございますが、各プロダクトによってPdMの業務内容は異なっています。 その中でも今回は、 「楽楽精算」のPdM業務内容をご紹介します。 スコープ はじめに プロダクト体制 楽楽精算のPdM業務内容 事業KPI貢献に沿った優先順位 PRD(要求仕様書)作成 今後の展望 ラクスのPdMとして活躍してみませんか? プロダクト体制 さっそくPdMの業務内容を説明したいところですが、 まずは楽楽精算を提供・開発する上での体制(概略図)を説明させてください。 (その方が後続の理解がしやすくなるためです) 楽楽精算もARR100億を達成してきたこともあり組織として、大きくなっております。 その中でも特筆する部分は、開発と事業をつなぐ役割としてPdM/PMMを配置している部分です。 楽楽精算プロダクト体制概略図 楽楽精算のPdM業務内容 大きく以下の業務がございます。 事業KPI貢献に沿った優先順位で案件を推進すること 顧客解像度を高めた上で、PRD(要求仕様書)を作成、開発へ渡すこと 事業KPI貢献に沿った優先順位 基本的に、以下図のようにCSから見える「解約原因」営業から見える「失注原因」から財務効果を割り出し案件の優先順位を決めていきます。 会社戦略上 優先することが決定している案件、EOLなどの維持管理案件などはこれの限りではございません。 基本優先順位ロジック PRD(要求仕様書)作成 楽楽精算では、PRDに盛り込む要素は、以下画像のオレンジ部分と定義しており、調査対象に定めています。 (こちらは各PdMによって様々なご意見があるかと思いますが、一旦楽楽精算ではそうしています) 画像は プロダクトマネジメントのすべて から抜粋させていただいております。 (全PdMが読むべき良書と私は思っています) プロダクト4階層  出典:「 プロダクトマネジメント のすべて」p.52の記載を引用し、オレンジ枠は筆者追記 また、調査するための計画Agendaは、現状以下のように定めています。 (ブラッシュアップは続けています) 調査計画Agenda 最終的には、以下のAgendaの内容は最低でもPRDに盛り込むようにしています。 PRD Agenda(一部) 今後の展望 現在以下のようにDACIという フレームワーク を利用して、PdM/PMMの役割分担を決めています。 今後は、インタビュー等の収集業務、プロダクト指標の決定(共に現状0ではないのですが)にも踏み込んでいきたいと考えております。 DACI表(一部) ラク スのPdMとして活躍してみませんか? 今後の展望にも記載の通り、PdMとして役割を広げていきたいと考えております。 そのためにも楽楽精算PdMは、人材を募集しております。 是非カジュアル面談からお申し込みいただけると幸いです。 プロダクトマネージャー | エンジニア職種紹介 | 株式会社ラクス キャリア採用
アバター
はじめに こんにちは akihiyo76 です。Swift Concurrency が WWDC で発表されてから 2 年になりました。各プロダクトではサポートバージョンがアップデートされ、実際に導入が進み始めているプロダクトも多いのではないでしょうか。一方で新規で開発する場合は、前提となる技術だと考えています。弊社でも Swift Concurrency への移行対応を行いましたが、今回は実際に行った導入戦略を紹介したいと思います。 はじめに 導入するメリット 1. 並行処理を簡潔・安全に記述できる 2. データの競合やデッドロックを回避(品質向上) async / await Sendable Actor Task 既存プロジェクトへの導入 1. PoCコードの実装 Strict Concurrency Checking の設定 実装方針の決定 2. スコープ分割 3. 横展開・テスト まとめ 参考 導入するメリット では、動いている既存コードを修正して Concurrency を導入するメリットは何でしょうか。 実際に対応を進める場合、実装コストだけではなく、品質を担保するためのテストコストも必要になります。更にプロダクトによってはリリースコストが必要になるため、そのコストに合うメリットが要求されます。 そこで、実際に Concurrency を導入するメリットについて考えてみたいと思います。 1. 並行処理を簡潔・安全に記述できる まず、Concurrency のメリットとして「並行処理を簡潔・安全に記述できる」という点が挙げられます。 実際のコードを比較してみましょう。以下は従来の Block 構文で実装した通信周りのコード例です。 func fetchImageData (request : URLRequest , completionHandler : @escaping ( UIImage? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in // Image をロードする self .loadImage(data : data ) { image in // Image サイズが適切かチェックする self .checkImage(image : image ) { completionHandler(image) } } }.resume() } この実装例では completionHandler() でコールバックを繋げる実装になっており、それぞれの処理がネスト構造を形成しています。これによりコードの複雑性が増し、分岐処理やエラー処理が追加されると更に実装が複雑になり、可読性も低下し、品質にも影響を及ぼす可能性があります。複雑性のために completionHandler() の記述を忘れた場合、特定の処理でコールバックが得られずアプリの処理が止まるリスクも考えられます。 それでは、この Block 構文で記述されたコードを Concurrency を使用して書き換えてみましょう。 func request (url : URL ) async throws -> UIImage? { let (data, response) = try await URLSession.shared.data(from : url , delegate : nil ) let image = try await loadImage(data : data ) let result = try await checkImageSize(image : image ) return result } async / await を使って書き換えることで、Block 構文の多段ネストが解消され、非同期の実装を簡潔に記述できるようになります。 ただし、コードスタイルや可読性の向上だけで Concurrency 移行のコストを検討するのは、割に合わないかもしれません。 2. データの競合や デッドロック を回避(品質向上) Concurrency 導入のメリットは、データ競合や デッドロック を回避できることです。具体的には Sendable や Actor といった機能の恩恵によるものですが、これについては後ほど具体的に説明します。Concurrency を導入し、これらの機能に準拠することで、品質の確保・改善に期待できる点が大きなメリットだと考えています。 async / await 関数の先頭に async(async throws) を定義することで、その関数を非同期関数として定義できます。定義した非同期関数を実行するためには、 await を使用する必要があります。 func execute () { Task.detached { do { let url = URL(string : "https://api.example.com" ) ! // 実行完了まで待機する let response = try await self .request(url : url ) // 後続処理 } catch { print(error.localizedDescription) } } } // 非同期関数として定義する func request (url : URL ) async throws -> HTTPURLResponse { // 通信処理 return result } このように、 async / await を使って定義することで、非同期関数の定義と実行が可能になります。 Sendable Sendable はデータ競合が起こらないことを コンパイル 時に保証してくれる型で、データ競合を避けて安全に渡せるデータを表す概念として導入されました。Sendable プロトコル に準拠することによって、その型が Sendable であることが コンパイラ に伝えられます。 final class Valid : Sendable { // 定数定義 let name : String init (name : String ) { self .name = name } } final class Invalid : Sendable { // 変数定義 var name : String init (name : String ) { self .name = name } } Valid と Invalid は共に Sendable プロトコル に準拠していますが、name については定数と変数で定義しており、Invalid の name は公開されているため変更が可能であり、データ競合が生じる可能性があります。この状態で コンパイル すると、 Stored property 'name' of 'Sendable'-conforming class 'Invalid' is mutable という コンパイラ のデータ競合警告が発生します。 Actor Swift 5.5 から導入された Actor は Concurrency の一部として、データ競合を防ぐ型です。Actor により作成された インスタンス は、一つのデータへのアクセスが同時に行われないように制御されます。これを Actor isolated と呼び、複数のタスクがデータにアクセスする際にもデータ競合を防ぐことができます。 actor MainActor { // 変数定義 var number : Int = 1 } func increment () { let act = MainActor() Task.detached { // number を更新 await act.number = 100 } } このように Actor で生成された インスタンス の number を変更しようとすると、 Actor-isolated property 'number' cannot be mutated from a Sendable closure という コンパイル エラーが発生します。 Task Task は並行処理を実行する単位で、複数のタスクの構造化(Task Tree)も可能になります。以下のコードのように、 withTaskGroup を使用してタスクグループ(親タスク)を作成し、要素となる子タスクを追加・実行することができます。 func getUser () async -> UserInfo { // 親タスク await withTaskGroup(of : DataType.self ) { group in // 子タスク A group.addTask { let friends = await self .getFriends() return DataType.friends(friends) } // 子タスク B group.addTask { let notes = await self .getNotes() return DataType.notes(notes) } } return UserInfo(friends : friends , notes : notes ) } こうした async / await 、 Sendable 、 Actor 、 Task といった機能を使用することで、Swift Concurrency を活用した効果的な非同期処理を実現できるようになります。 既存プロジェクトへの導入 ここからは既存プロジェクトへの導入方法について紹介します。導入の主な流れとしては、以下のようになります。 PoCコードの実装(Concurrency 設定と実装方針の決定) スコープ分割 各スコープごとの対応 リグレッション テスト 大まかな対応方法としては、実装方針をFIXしてチーム全体で一気に対応していくという流れです。 1. PoCコードの実装 Strict Concurrency Checking の設定 導入に際しては、実装方針(Concurrency の記述方法)を決定する必要があります。まず最初にプロジェクトの設定として、 Strict Concurrency Checking の設定を行います。 設定には Minimal、Targeted、Complete の 3 種類がありますが、 Targeted に設定します。 Minimal Sendable の制約を、明示的に採用された場所にのみ強制し、コードが並行性を採用した場所では actor-isolated チェックを実行します Targeted Sendable の制約を強制し、コードが並行性を採用した場所( Sendable を明示的に採用した場所も含む) actor-isolated チェックを実行します Complete モジュール全体で Sendable の制約と actor-isolated の分離チェックを強制します Complete はモジュール全体に大きな影響を及ぼすため、導入時の制約としては厳しすぎると判断しました。 参考: https://developer.apple.com/documentation/xcode/build-settings-reference#Strict-Concurrency-Checking 実装方針の決定 Strict Concurrency Checking の設定後は、基本的な Concurrency の実装パターンを作成します。 通信実装などのコアな部分が多く対象になると考えられるため、同じ責務の Concurrency 関数を準備します。最終的には既存の実装を削除するため、 @available で deprecated を指定しておきます。以下は withCheckedThrowingContinuation を使用して Concurrency に変換していますが、async / await を使用しても同様に変換可能です。 @available ( * , deprecated , message: "This function is deprecated. Use the requestAsync () instead." ) func request (request : URLRequest , completionHandler : @escaping ( Data? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in completionHandler(data) }.resume() } func requestAsync (request : URLRequest ) async throws -> Data? { return try await withCheckedThrowingContinuation { continuation in self .session.dataTask(with : request ) { data, response, error in if let error = error { continuation.resume(throwing : self.handleNSURLError (error : error as NSError )) return } continuation.resume(returning : data ) }.resume() } } また、requestAsync() の呼び出し元クラスも変更します。以下の fetchUserData() は View 側から実行される想定なので、 @MainActor を付けて Main Thread から実行できるようにし、また nonisolated を指定して非分離メソッドとして指定します。 @MainActor class ViewModel { private let apiService = ApiService() nonisolated func fetchUserData () async throws -> Data? { do { let urlRequest = await self .createUrlRequest() return try await apiService.requestAsync(request : urlRequest ) } catch { throw await self .handleError(error) } } } 最後に fetchUserData() の呼び出しを Task 内で行います。Task の定義は、プロジェクトの アーキテクチャ に合わせて行うのが良いでしょう。 override func viewDidLoad () { super .viewDidLoad() Task { do { let userData = try await viewModel.fetchUserData() self .updateUser(userData) } catch { // エラー処理 } } } } 2. スコープ分割 導入の基本方針が確定したら、具体的な進行方法を検討します。大規模なプロジェクトでは修正が多岐にわたることが予想されますが、一度に行う変更が大きいと、レビューや品質管理が難しくなる可能性があります。そのため、一定の粒度で分割し、対応を進めることで迅速な対処が可能です。チーム全体で進行する場合、各メンバーが実装とレビューを同時に行うことが望ましいでしょう。もし修正すべき箇所が多い場合は、 スプレッドシート で一覧化しておき、ファイルごとやクラスごとなど明確な区分け方法にすることで、作業の分担が容易になります。 3. 横展開・テスト 修正すべきスコープが確定したら、あとは一気に修正を進めます。チームの状況によっては、徐々に移行する方法ありますが、他の機能の実装も進行中の場合、コンフリクトが発生する可能性があるため、一気に対応する方法をオススメします。最後に、@available deprecated を指定した関数を削除することを忘れずに行いましょう。導入実装が完了したら、 リグレッション テストを実施して機能 デグレ がないことを確認します。 まとめ Swift Concurrency の主要な機能に焦点を当てながら、既存のプロジェクトにおける導入戦略を紹介しました。今回紹介した導入戦略は、私自身が実践した方法ですが、既存の実装を保ちながら進行したため、多少冗長な箇所があるかもしれません。これはあくまで一つの戦略として、Swift Concurrency の導入を支援するための手助けとなることを願っております。 参考 developer.apple.com
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 弊社では先日 テスト駆動開発 (以降、TDDと呼ぶ)ハンズオン勉強会を開催しました! 今回の記事の内容はズバリ2つ 誤解してる!? テスト駆動開発 の良さ!学ぶ事の意味! TDDハンズオン勉強会を開催する意図や実施内容、感想! 読者のターゲットは TDDを誤解している人 TDDハンズオン勉強会を弊社でもやろう!とか思ってる人 を想定していますっ。 誤解されがちなTDD、記事にするには書ききれないTDD...なるべく小難しい内容は省いて興味を持ってもらうための記事を書いてみようと思います! テスト駆動開発(TDD)は良い物だ! テスト駆動開発(TDD)とは何か? TDDに対する誤解 TDDハンズオンについて TDDハンズオンの趣旨 TDDハンズオンの計画 事前準備 スケジュールと概要 TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった TODOリストの作成にもう少しゆとりを持つと良い まとめ 最後に 参考文献等 テスト駆動開発 (TDD)は良い物だ! テスト駆動開発 (TDD)とは何か? プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き、そのテストが 動作する必要最低限な実装をとりあえず行なった後、コードを洗練させる、という短い工程を繰り返すス タイルである (Wikipediaから抜粋) よくTDDについて調べてみると、だいたい上のような説明に始まり、Red/Green/RefactorのサイクルがTDDであると学んで終わってしまう。でも、この記事に辿り着く読者は「そんな事はわかっている」という人が大半だと思うので、あえてここではこう説明しておきます。 「動作するきれいなコード」。Ron Jeffriesのこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。 動作するきれいなコードはあらゆる意味で価値がある。 (「テスト駆動開発」著 Kent Beck 訳 和田卓人のまえがきから抜粋) TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング 2020年にあった和田さんによる基調講演も「TDDとは何か?」という話はここから始まります。是非この動画は見て頂きたいのですが、抜粋して言うと、 「動作するきれいなコードを書く事は、品質・コスト・開発速度どの側面から見ても重要な要素であり、それを実現するのにTDDは1つの解である」 といった内容に感じました。 このゴールから始まる「まえがき」の書き方も、よくよく考えるとTDDっぽくて感慨深いですね!グレイトっ(笑) では、「そんなにTDDが良いのならなぜもっと普及しないのか?」少しだけTDDに対する誤解について話しておこうと思います。 TDDに対する誤解 テストコードを書く事が一般的になった昨今ですが、そんな中で 「TDDは先にテストコードを書く事」という偏った認知 が広がっていったようです。これは誤った認識だという事は声を大にしてお伝えしたいです。 TDDが テストファースト である事には複数の意味がありますが、私が一番しっくりきた解釈としては、 詳細設計と実装の狭間に「どんな風にクラスを分けて、どんなメソッドを定義してあげると良いだろうか?」というのをテストコードという形で箇条書きしている開発フロー だと感じました。 思いつきで書いた動作するコードは、得てしてテストコードが書きづらく、既存実装に対して書きやすさで実装が進んでしまいます。なので、先にどのクラスにどんなメソッドが存在していてほしいかをテストコードとして記載していく事で、本来あるべき実装・振る舞いを実装できる。 また、 「TDDはテストコードの開発に時間がかかる」 というのもよく聞く話ですが、先ほど紹介した動画の中でも最後に解説されていますが、テストの経済性というのは実際そんなに悪くはなく、少なからず担当者が離職する間隔よりも短いタイミングで元は取れるようです。 また、ここであえて「離職する間隔よりも短い」という言い回しをさせてもらったのは、実体験的に当事者意識を持てるか?というのが案外大事なのではないかと思ったから補足させてもらいました。内部品質の高いコードを書く事の見返りは自分にもある事を知っていてほしいです。 xUnit Test Patternsより抜粋 ※上の画像は、先ほど解説した動画の最後の方で出てくるグラフです。 無駄にメンテナンスが困難なテストコードを量産しているとメンテナンスコストが高くなる事もあるので注意です。 ※本来の テスト駆動開発 をやればこうはなりませんよ!深く考えずテストを後から書くような開発してるとこうなっちゃう現実が待ってるという注意喚起です。 TDDハンズオンについて ある意味、ここからが本題ですねっ! TDDハンズオンの趣旨 「TDDの良さを知ってもらって、普段の開発に活かしてほしい。」という事で開催されました。 先にも記載した通り、TDDというのは名前 からし ても誤解されがちな傾向にあり、本当の良さって伝わりにくいものです。 実際に手を動かしてはじめて「あ、確かに開発の流れがしっくりくる!安心感がある!」そんなTDDだからこそハンズオンをやる意味があるのでは? と考えました。 TDDハンズオンの計画 事前準備 TDDを実施する開発環境として Java が動く開発環境が必要になりますが、手間であれば Codinggame.com のようなサイトでやってもらっても良いかと思います。 www.codingame.com ※弊社も一部の社員はこちらを使いました。普段 PHP で開発していて Java 環境がない社員等。 スケジュールと概要 計画 1日目: 弊社では1日目を TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング を一緒に視聴する時間に費やしました。ハンズオン本編以外の部分をスキップしたり、1.25倍速で再生して1時間半に無理矢理短縮しました^^; まずは基礎知識としてTDDの流れについてちゃんと理解してもらいたかったためです。 2日目: うるう年問題は以下としました。 入力した整数がうるう年かどうかを判定するプログラムを書け。 うるう年のときは「true」、それ以外は「false」とプリントすること。 うるう年の定義 ・西暦年号が4で割り切れる年をうるう年とする ・ただし、西暦年号が100で割り切れ、かつ400で割り切れない年は平年とする。 私の所感としては、1時間半でやるハンズオンであれば、丁度いい難易度かなと感じました。 タイムスケジュールとしては以下としました。 うるう年についてのTODOリストの整理(10分) 2チームからTODOリストを発表(6分) ※これを参考に後半タスクを進めてもらう テストコードのサイクル開始(Red/Green/Refactor)(30分) 2チームからTDDの結果を発表(6分) バッファ(20分) TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった 参加者から頂いたコメントにも 「開催回数が増えてもよいのでビデオは早回し再生ではなく通常再生が良かったです。」 といったご意見がありました。 確かに1.25倍速で抜粋して見てもらったのは苦肉の策で、計画段階でも悩んだポイントでした。 「参加者は事前にこれを見て来てください(倍速で見るとかはお任せ)」とした上で、重要ポイントだけピックアップして解説してから始めるぐらいにした方が、円滑に進んだかも! TODOリストの作成にもう少しゆとりを持つと良い 動画を見てもらうとわかるかとは思いますが、TODOリストとは「どんな振る舞いをしてほしいのかを箇条書きにした仕様書のようなもの」です。 TODO作成時間に時間を割いても良かったように感じました。 TODOがしっかりできていれば後はそれ通りに実装するだけなので、 割合的にはTODO作成時間が多くなるイメージだと個人的にはもう少しうまくできたのかなと思いました。 上記のような意見も後日アンケートで頂きました。 確かに10分なんかじゃ全然足りなかった!初回のTODOリスト作成は20分ぐらいはせめてあるべき、30分でも良いと感じました。 3分クッキングみたいに「出来上がったTODOリストはこちらです」って提示するのは一案かもしれないんですが、ハンズオンの意味が色褪せちゃいますよねOrz せっかくのハンズオンなのでもう少し時間にゆとりがあれば良かったです!反省! まとめ 反省すべき点はありますが、30名程参加頂き、総じてTDDハンズオンの実施で新しい学びがあったという意見は多く、有意義な時間だったなと思います! TDDハンズオン風景(大阪) TDDハンズオン風景(東京) エンジニアは万能ではないので(少なからず私は)ミスする事は多々あります。TDDはそういった普通の人でも、正しい実装を安心して開発していける手法です。 是非、ブログ記事1つで理解したつもりにならず、本を読んだり、TDDハンズオンを実際に体感して、一考してもらえればなと思います。 最後に 和田さんに感謝!TDDブートキャンプの基調講演の動画とても参考になりました。 和田さんの前で「ちゃんとテスト書いてます!」って言えるようにがんばるます! 参考文献等 「テスト駆動開発」著 Kent Beck 訳 和田卓人 TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング xUnit Test Patternsから学ぶユニットテストの6つの目指すべきゴール Codinggame.com
アバター
こんにちはサッカー大好き@neroblubrosです。 ラク スでは月に一度ですが、定時後にビアバッシュを行っています。 開発部隊は東京オフィスと大阪オフィスにいますが、それぞれでビアバッシュを開催しています。 今回は大阪オフィスで開催しているビアバッシュについて紹介いたします。 ビアバッシュとは 6月レポート 1人目「社会人1年目を振り返ろう」 2人目「社会人1年目の振り返り」 3人目「やらかし事例集」 4人目「PHPMDでコード品質を計測する」 5人目「RSSで手軽に更新通知」 6人目「ChatGPTでブログを書いてみた」 7人目「おもしろいDB設計本ミッケ!」 7月レポート テーマ枠は「新卒に伝えたいこと」 4人目「新卒の方々にお伝えしたいこと」 5人目「君たちはどう生きるか」 6人目「新卒に伝えたいことLT」 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 9人目「PHPを支えるコミュニティたち」 次回の8月ビアバッシュの予告 ビアバッシュとは まずはここで軽くビアバッシュの説明をします。ビアバッシュとは、 シリコンバレー が発祥で ビア(beer):ビール バッシュ( bash ): どんちゃん 騒ぎ を組み合わせた造語で、ビールと軽食を食べつつアウトプットする場です。 ラク スの大阪オフィスでは2015年から始め、途中コロナ禍では開催できなかったときやオンラインで開催していましたが、毎月継続して開催しています。 コロナが5種になったことから今年度からアルコール+オフラインでの開催が再開しました。 軽食とアルコールとソフトドリンクが振る舞われますが、それらの費用は会社持ちです! 出席は自由で発表や出席をすると人事評価につながります。 大阪オフィスでは各課の代表者がビアバッシュ推進委員を構成してビアバッシュの運営しています。 以下、 ラク ス大阪オフィスのビアバッシュの内容です。 みんなで語り合うのではなく、希望者が発表をして聞く、質問あり。 発表内容はテーマ枠と自由枠がある。 テーマ枠は新入社員の自己紹介、トラブル事例など 自由枠はITや仕事に関することならなんでもOK。仕事に関係のない趣味の発表とかはNG 発表時間はテーマ枠と自由枠ともに5分~10分 ここから大阪オフィスで6月と7月に開催したビアバッシュの内容をざっと紹介します。 6月レポート 軽食はピザ+ナゲット+ポテトフライ。 最初の3名はテーマ枠の「社会人1年目の振り返り」です 1人目「社会人1年目を振り返ろう」 1人目は「社会人1年目を振り返ろう」です。 コロナ禍に入社した新卒3年目の発表で、新卒研修を終えて、配属部署での研修の話でした。 緊急事態宣言下に研修をすることの苦労話ややらかしたこと、逆に良かったことの発表でした。 2人目「社会人1年目の振り返り」 2人目は「社会人1年目の振り返り」で、前職で社会人1年目に経験したことの発表です。 スマホ アプリの機能追加で最新版をインストールすると起動できなくなり、リリース直後にユーザからのクレームが殺到したなど、いくつかの苦い経験を暗くなりすぎないように、面白おかしく発表してくれました。 個人的には 東日本大震災 の 計画停電 で、フロアの照明が定時で消灯されるため、デスクライトをつけて残業したことが印象に残りました。 3人目「やらかし事例集」 3人目は「やらかし事例集」で、2人目と同じような内容ですが、タイトル通り「自分がやらかしたこと」の発表です。 検証環境のつもりが接続先を間違えて本番環境を変更した 本番環境にテストデータを登録してしまった など、やらかし事例集をクイズ形式での発表でした。 不謹慎ですが、他人の不幸は蜜の味ということで、一番盛り上がった発表となりました。 次からは自由枠です。 4人目「PHPMDでコード品質を計測する」 4人目は「PHPMDでコード品質を計測する」です。 ソースコード の分岐や繰り返し処理がどれくらいあるかを表した数値が循環的複雑度といい、 オープンソース の静的解析ツールで配配メールの循環的複雑度を計測した結果の発表でした。 5人目「 RSS で手軽に更新通知」 5人目は「 RSS で手軽に更新通知」。 RSS を題材にシステム間連携で扱い易いフォーマットを考える発表でした。 6人目「ChatGPTでブログを書いてみた」 6人目は「ChatGPTでブログを書いてみた」で私の発表です。 ChatGPTを使った ライフハック に興味を持っていて、ChatGPTを使ってブログを書く方法を発表しました。 ブログの記事をChatGPTに生成させるにはどのようなプロンプトを作成すればいいかを、実際に使ったプロンプトと完成したブログを用いて説明しました。 7人目「おもしろいDB設計本ミッケ!」 7人目は「おもしろいDB設計本ミッケ!」でERDの本の紹介で、大阪オフィスのテッ クリード による発表です。 単なる本の紹介ではなく、本に書かれている方法で実際に領収書をつかってテーブル設計を行うという、さすがテッ クリード という発表で、参加者はうなずきながら聞いていました。 6月はテーマ枠が「社会人を1年目の振り返り」でしたが、自由枠も実体験に基づいたリアリティに溢れた発表内容でした。 7月レポート 軽食はサンドウィッチ(3種類から選択)+ポテチ。 テーマ枠は「新卒に伝えたいこと」 7月から新卒で入社した新人が配属されたため、新卒の自己紹介とテーマ枠が「新卒に伝えたいこと」でした。 最初は新卒で入社した3名の自己紹介で、それぞれ工夫をこらした発表でした。 以降4人目からの発表を紹介します。 4人目「新卒の方々にお伝えしたいこと」 4人目は「新卒の方々にお伝えしたいこと」で私の発表です。 現在は退任しましたが、取締役のメールを紹介しました。 内容は紹介できませんが、 ラク スの文化を象徴するメールの内容だったので新卒に皆さんに紹介しました。 5人目「 君たちはどう生きるか 」 5人目は「 君たちはどう生きるか 」というタイトルでちょっと身構えてしまいそうですが、自分を守る仕事の仕方の紹介です。 ブラックな環境で働いてきた経験から ラク スがいかに恵まれた環境か、そしてその環境を生かして「楽しく健やかなエンジニアライフ」を過ごす方法の説明で、 ラク ス愛あふれた発表でした。 私も経験がありますが、修羅場をくぐり抜けてきた人の「人生は長い。心身の健康が一番大事。」という言葉に重みがあります。 6人目「新卒に伝えたいことLT」 6人目は「新卒に伝えたいことLT」で「みなさんはプロですか?」という問いかけから仕事に対する姿勢を新卒入社に説明。 「プロの仕事とは?」という問いかけから「プロフェッショナル」な仕事の仕方についての発表でした。 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 自己研鑽をやっておけという当たり前のような内容からなぜ必要なのか?という説明です。 メールディーラー開発の実装チームのリーダの発表で、説得力があり大阪オフィスのテッ クリード から「すごくいい発表だった」と称賛のことばがもらえました。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 8人目は「新卒の皆さんに知ってほしい!チャットディーラー開発課」で、チャットディーラー開発課から異動になり、チャットディーラー開発課のいいところの紹介です。 タイトルからもわかるようにチャットディーラー愛にあふれる発表でした。 9人目「 PHP を支えるコミュニティたち」 9人目は「 PHP を支えるコミュニティたち」ということで、タイトル通り PHP の歴史とそのコミュニティの紹介です 7月の発表テーマは「新卒に伝えたいこと」だったので、仕事に対することや 自己啓発 的な発表が多かったです。 次回の8月ビアバッシュの予告 大阪オフィスの8月のビアバッシュのテーマは「納涼! ヒヤリハット 特集」です。 暑い夏を吹き飛ばすようなヒヤリとしたエピソードを共有することで、同じようなミスをしないようにする内容ですね。 次回のビアバッシュレポートもお楽しみにしててください!
アバター
はじめに こんにちは akihiyo76 です。先日 Android 14 Beta 5 がリリースされ、最終リリースまであと僅かとなりました。そこで、今回は Android 14 で提供される新機能の概要をまとめてみました。 はじめに 機能と API の概要 国際化(Internationalization) アプリ固有の言語設定 Grammatical Inflection API grammatical gender の設定方法 ユーザー補正(Accessibility) ユーザー体験(User experience) 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショットの検知 予測型「戻る」アプリ内アニメーション まとめ 参考 機能と API の概要 Android 14 の「新機能と API の概要」は、 公式ページ に記載されており、日本語の翻訳も徐々に進んでおりますが、ニュアンスが微妙なところもあるので、可能であれば 英語版 で読むとより理解が進むかと思います。 Android 14 の新機能を大別すると、以下の 3 つに分類されるので、それぞれを順に紹介したいと思います。 国際化(Internationalization) ユーザー補正(Accessibility) ユーザー体験(User experience) API の追加、変更、削除の一覧については、 API 差分レポート でご確認ください。また、新しい API についての詳細は、 Android API リファレンス でご確認ください。 国際化(Internationalization) 国際化の新機能としては、 アプリ固有の言語設定の自動化 と Grammatical Inflection API(文法的な語形変化) が追加になります。 アプリ固有の言語設定 Android 13( API レベル 33)で導入されたアプリ別の言語機能が拡張され、以下の機能が追加されます。 アプリの localeConfig の自動生成 アプリの localeConfig の動的アップデート インプット メソッド エディタ( IME )によるアプリの言語の確認 Android 14では、locales_config. xml の生成を以下の設定を追加することで、自動生成にすることが可能になりました。自動生成には、 Android Studio Giraffe Canary 7 および AGP 8.1.0-alpha07 以降が必要となっています。 1. androidResources > generateLocaleConfig を定義する。 gradle:build.gradle.kts android { androidResources { generateLocaleConfig true } } 2. resources.properties を作成し、デフォルトの言語を設定する。 res/resources.properties unqualifiedResLocale=en-US このように設定するだけで、locales_config. xml の生成を自動生成にすることができます。 Grammatical Inflection API Android 14 では、性別で文法が変わる言語に合わせてユーザー中心の UI を構築するため、アプリを リファクタリング せずに文法上の性別への対応を追加できる Grammatical Inflection API が導入されています。 日本語の場合、文法的な語形変化はなかなかイメージしにくいと思いますが、フランス語での一例を紹介します。 🇫🇷 冠詞の性別 ( 英語 : the ) 男性形単数 : le 女性形単数 : la 🇫🇷 動詞の過去分詞 ( 英語 : spoken ) 男性形単数 : parlé 女性形単数 : parlée Grammatical Inflection API では、このような文法的言語変化に対応することができます。 grammatical gender の設定方法 Grammatical Inflection API で設定できる文法上の性別の設定は以下の 3 パターンになります。 Configration. java public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 中性的 public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 女性的 public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 男性的 アプリで Grammatical Inflection API を利用するには、対象となる Activity に configChanges の指定します。 AndroidManifest. xml <activity android:name=".MainActivity" android:configChanges="grammaticalGender" android:exported="true"> </activity> 実際に grammatical gender を GRAMMATICAL_GENDER_FEMININE に設定する場合は、 setRequestedApplicationGrammaticalGender() API を利用して設定します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) gIM.setRequestedApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_FEMININE) また、設定された grammatical gender を取得するには、 getApplicationGrammaticalGender() API を利用します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) val grammaticalGender = gIM.getApplicationGrammaticalGender() このように設定した grammatical gender によって、 -neuter -feminine -masculine の suffix を付けた resources ファイルでリソースを使い分けることができるようになります。 res/values-fr-neuter/strings. xml <resources> <string name="example_string">Abonnement à...activé</string> </resources> res/values-fr-feminine/strings. xml <resources> <string name="example_string">Vous êtes abonnée à...</string> </resources> values-fr-masculine/strings. xml <resources> <string name="example_string">Vous êtes abonné à...</string> </resources> このように Grammatical Inflection API では文法上の性別によって表現を変えることができるため、性別を扱うことが多い SNS アプリなどで活用できる場面もあるかと思いますが、ローカルでのリソース設定に止まるため使える場面は限定的になりそうです。 ユーザー補正(Accessibility) Android 14 では、 非線形 フォントスケーリングが 200% までサポートされます。SP 指定の場合、追加のオプションとスケーリングの改善がアプリのテキストに自動的に適用されるようになります。 (引用: The first developer preview of Android 14 ) このようにスタンダードスケーリングだと表示崩れが生じるため、 200%のフォントサイズで UI テストで確認すること が重要となります。 200% のフォントサイズを有効にする手順は以下の通りです。 設定アプリを開き、[ユーザー補助] > [表示サイズとテキスト] に移動] [フォントサイズ] オプションで、最大フォントサイズの設定が有効になるまで、プラス(+)アイコンをタップ また、アプリで sp 単位を使用している場合、 Android はユーザーが選択するテキストサイズを適用して適切にスケーリングできるよう、テキストサイズは必ず sp 単位 で指定しましょう。 ユーザー体験(User experience) ユーザー体験では、以下の機能が追加になりました。 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショット の検知 予測型「戻る」アプリ内アニメーション 共有シートのカスタムアクションの追加 共有シートのカスタムアクションを追加するには、カスタム ChooserAction から ChooserActions のリストを作成します。 MainActivity.kt fun buildCustomShareSheetActions(context: Context): Array<ChooserAction> { val pendingIntent = PendingIntent.getActivity( context, 0, Intent(Intent.ACTION_WEB_SEARCH).apply { putExtra(SearchManager.QUERY, "Search on browser") }, PendingIntent.FLAG_IMMUTABLE ) val actions = mutableListOf<ChooserAction>() for (i in 0 until 5) { val customAction = ChooserAction.Builder( Icon.createWithResource(context, R.drawable.ic_launcher_foreground), "Action${i+1}", pendingIntent ).build() actions.add(customAction) } return actions.toTypedArray() } 次に、作成した ChooserActions のリストを Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS として指定し、共有シートを開きます。 MainActivity.kt fun buildChooserIntent(chooserActions: Array<ChooserAction>): Intent { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, "dummy text") } val chooserIntent = Intent.createChooser(intent, "Android 14").apply { putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions) } return chooserIntent } fun showActionSheet() { val chooserActions = buildCustomShareSheetActions(this) val chooserIntent = buildChooserIntent(chooserActions) startActivity(chooserIntent) } このように複数のカスタムアクションを設定することができます。 アプリストアの改善 Android 14では、ユーザの操作を中断せずにアプリのアップデートが可能になります。具体的には、 以下の操作を API で実現できるようになります。 ダウンロードする前にインストールの承認をリク エス ト 今後の更新に責任を移譲 影響が少ないタイミングでアプリを更新 アプリインストール操作の事前承認では、targetSdk 34 で PackageInstaller が追加され、 requestUserPreapproval() を実行することで、事前承認を行うことができます。 アプリストアの改善機能は、スムーズなアプリインストールを促すことが期待できるため、ユーザーにとっても嬉しい機能になるでしょう。 スクリーンショット の検知 API level 17 以降では、 FLAG_SECURE を設定することで スクリーンショット を無効にすることができましたが、 Android 14 では スクリーンショット の検知ができるようになります。 スクリーショットの検知をするには、 DETECT_SCREEN_CAPTURE の permission を定義します。DETECT_SCREEN_CAPTURE の Protection level は normal です。 AndroidManifest. xml <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" /> 検知用の Callback は ScreenCaptureCallback を作成して、ライフサイクルに合わせて登録・登録解除を行います。 MainActivity.kt val screenCaptureCallback = ScreenCaptureCallback { // Add logic to take action in your app. } override fun onStart() { super.onStart() registerScreenCaptureCallback(mainExecutor, screenCaptureCallback) } override fun onStop() { super.onStop() unregisterScreenCaptureCallback(screenCaptureCallback) } このように Android 14 では スクリーンショット の検知が非常に簡単に実現できます。アプリ内で スクリーンショット の検知したら、安易に共有させないような注意喚起を行うケースなどで有用かと思います。 予測型「戻る」アプリ内アニメーション バックアクションに対して、事前検知することでカスタムアニメーションなどの処理を追加することが可能になりました。 @RequiresApi(34) で指定している Implement Menbers が Android 14 で追加になります。 MainFragment.kt val callback = object: OnBackPressedCallback(enabled = false) { @RequiresApi(34) override fun handleOnBackStarted(backEvent: BackEvent) { // Create the transition } @RequiresApi(34) override fun handleOnBackProgressed(backEvent: BackEvent) { // Play the transition as the user swipes back } override fun handleOnBackPressed() { // Finish playing the transition when the user commits back } @RequiresApi(34) override fun handleOnBackCancelled() { // If the user cancels the back gesture, reset the state } } this.requireActivity().onBackPressedDispatcher.addCallback(callback) 実際にアニメーションを追加実装サンプルは 公式ページ にあるので、実際の実装例を見てみたい方はそちらをご覧ください。 まとめ Android 14 の新機能概要を紹介させて頂きました。アプリによっては有用な機能もあると思うので、targetSdk 34 対応時に合わせて、アプリの新機能として検討してみてはいかがでしょうか。 参考 Features and APIs Overview Android Developers Blog
アバター
はじめに はじめまして、新卒1年目のTKDSです! 先日、Spring Bootの入力値チェックについて触れる機会があったため、入力値チェックの使い方について調べてました。 今回は、調べた内容と簡単な使いかたについてご紹介したいと思います。 はじめに 入力値チェック アノテーションの実践 ネストされたformの入力値チェック まとめ 参考文献 入力値チェック Spring Bootでは、入力値の検証を行うための便利な機能が提供されています。 これにより、入力データがアプリケーションの要件を満たしているかどうかを確認できます。 入力値チェックはフォームクラスにチェックする内容を示す アノテーション をつけると行えます。 フォームクラスにつける アノテーション にはさまざまなものがあります。 一部を記載します。 アノテーション チェック内容 @NotNull Nullでないか @Max() 最大値以下か @Min 最小値以上か @Pattern 正規表現 に一致するか @Size 要 素数 が最小以上かつ最大以下であるか アノテーション の実践 サンプルアプリを用いて、実際に アノテーション を行ってみます。 検証用にPOSTリク エス トをおくると json を返すアプリを定義します。 まだこのサンプルアプリでは入力値チェックは行われていません。 コントローラー // 一部抜粋 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody Todo todo,) { List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } レスポンスボディを受け取る兼返信用クラス public class Todo { private Long id; private String usrName; private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // Getter, Setter, コンストラクタ省略 } このサンプルアプリはタスク名と時刻を JSON に含めてリク エス トを送信するとTodo型のオブジェクトに当てはめて、 JSON 形式で返却します。 リク エス トの送信には、 VS Code 拡張のREST Clientを使用しています。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:20:54 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } ] 現状のコードではバリデーションチェックがなにも行われていないため、Nullなどの値を送ってもそのままエラーチェックをすり抜けます。 例として、taskNameをリク エス トボディに含めずリク エス トを送信します。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:11:12 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : null, " date " : " 2023-08-02T22:20:00 " } ] リク エス トに含まれていない "taskName" が null になっているのが確認できます。 では、入力値のバリデーションチェックを行い、taskNameが指定されていない場合にエラーを返すようにします。 TodoクラスのtaskNameに @NotNull アノテーション を付けます。 @NotNull private String taskName; これで、Null値を許容しない設定にすることができました。 次にエラーハンドリングができるようにします。 コントローラーを次のように変更します。 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody @Validated Todo todo, BindingResult result) { if (result.hasErrors()) { return new ResponseEntity<Object>( "bad request" , HttpStatus.BAD_REQUEST); } List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } 引数の部分に アノテーション を追加し、エラーハンドリングの処理を追加します。 先ほどと同じ、taskNameがないリク エス トを送信します。 結果は次の通りです。 HTTP/ 1 . 1 400 Content-Type: text/plain; charset =UTF-8 Content-Length: 11 Date: Wed, 02 Aug 2023 15:33:01 GMT Connection: close bad request エラーハンドリングの部分で設定した bad request が返ってきたのが確認できました。 ネストされたformの入力値チェック 前項では、入力値を受け取るクラスのフィールド変数に対して簡単にバリデーションを行うことができることが確認できました。 しかし、フォームクラス内に自作クラスを使用してフィールド変数を持つ場合、従来の方法だとバリデーションチェックが正常に機能しないケースがあります。 具体的には、フォームクラスの変数自体は アノテーション でチェックできるものの、そのフィールド変数が自作クラスであり、かつ空の配列が入力された場合、入力値のチェックがうまく行われないことがあります。 実際に要素を追加して確認してみましょう。 public class Todo { private Long id; private String usrName; @NotNull private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // 追加部分 private List<SubTask> subTasks; public static class SubTask { @NotNull private String taskName; // Getter Setter コンストラクタ省略 } // Getter Setter コンストラクタ省略 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ {} ] } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Sun, 06 Aug 2023 15:34:33 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ { " taskName " : null } ] } ] subTaskのtaskNameが指定されていないにもかかわらず、バリデーションエラーが返ってきません。このことから、SubTask内のtaskNameに対して、 @NotNull は働いていないことがわかります。 この問題は、親クラスでフィールド変数を宣言する際に@Valid アノテーション を付与することで解決できます。 @Valid private List<SubTask> subTasks; これにより、オブジェクトのフィールド変数に対しても アノテーション チェックが行われるようになります。 リク エス ト POST http: //localhost:8000/todo-app/todo-list/1 Content-Type: application/json { "taskName" : "" , "date" : "2023-08-02T22:20:00" , "subTasks" : [ {} ] } レスポンス HTTP/ 1.1 400 Content-Type: text/plain;charset=UTF- 8 Content-Length: 11 Date : Wed, 02 Aug 2023 15 : 33 : 01 GMT Connection : close bad request バリデーションチェックが行われていることが確認できました。 まとめ この記事では、Spring bootにおける入力値のバリデーションについて、調べて結果を紹介しました。 また、サンプルアプリを用いて、入力値のバリデーションを実践しました。 Spring Bootのバリデーションの理解の一助になれば幸いです。 参考文献 https://access.redhat.com/documentation/ja-jp/red_hat_jboss_enterprise_application_platform/7.4-beta/html/development_guide/jakarta_bean_validation https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/package-summary.html https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/constraints/package-summary.html
アバター
こんにちは。 ラク ス インフラチーム所属のas119119です。 今回は、タイトルの通りビジネスの場面で重要なスキルといわれている ロジカルシンキング 研修を開催した件について取り上げてみたいと思います。 今回のブログ構成については以下の通りとなります。 ロジカルシンキングとは 研修実施の背景 研修の内容 研修を実施してみて ロジカルシンキング とは 世間でよく聞く ロジカルシンキング とはそもそもどのようなものなのでしょうか。 日本語をそのまま当てはめると「論理的思考」となるようです。 しかし、「論理的思考」といってもあまりにも漠然としていますね。 少なくとも個人的にはそう思います。 巷にあふれる情報の中から論理的思考とは何かと調べてみると、 概ね次にあるような類似の定義にたどり着きます。 「論理的思考」とは、つまるところ直感や感覚的なところで物事を捉えるのではなく、筋道を立てて内容に矛盾や破綻がないように理屈が合うように考え、 結論を導くことという定義に集約できると思います。 研修実施の背景 部署内には新卒や中途、ベテランなど多種多様なメンバーがいる中で課題設定や課題解決能力にばらつきがあり、メンバー間の連携共有をよりスムーズに実施するためにはどうすればよいかという点に端を発しています。 そこでビジネスをする上で重要なスキルである ロジカルシンキング 研修をメンバー向けに実施することで、メンバー間のビジネススキルの底上げを図るとともに、チーム内部で共通のスキルを身に着けることで情報連携力の向上は図ろうとしたのが主催の背景となります。 その意味では、このようなインフラに関連していない試みでもチャレンジ(スモールスタートと言えるでしょう)できる土壌があるという点は ラク スの強みでもあると言えます。 研修の内容 さて、何を研修で扱ったかについて、ここでは取り上げていきたいと思います。 研修としては、業務時間も有効に活用しつつ、1回あたり90分を全3回、2週間から3週間おきに実施し、おおよそ2ヵ月以内で全内容を消化した形となります。 毎回、研修中にハンズオン形式の課題に取り組みつつ、各回の終了後、次回までに必須課題があるため、参加者にとっては通常業務等々をこなしつつ、なかなかヘビーな内容であったと思います。 イメージしやすいように、参考までに使用したテキストのサンプルをご紹介したいと思います。 ロジカルシンキング を検討する上で、様々なツールを使用すると便利であることがわかっているので、研修では一般的なビジネス概念である MECE やロジックツリーなどのツールを用いて、様々はハンズオンを実施しました。 今回は、ベテランから若手まで多様なメンバーが参加していたこともあり、ハンズオンにおける意見交換は活発に実施され、とりわけ若手にとっては色々な意見に触れるよい機会になったのではないかと思います。 研修を実施してみて 研修を ファシリテーター として実施するのも初めてで、研修を実施するための準備について、それなりに別のベテランから指導を受けたつもりでしたが、時間管理やハンズオンのコン トロール などはなかなかスムーズにいかないところも多々ありました。 しかし、全体を通しては実施する価値はあったと確信しています。 実際、参加者に対して毎回、研修の最後にアンケートを依頼していたのですが、概ね好評で、他のエンジニアにも受講を薦めるという意見が多く出ていました。 ある程度、 定量 化できる効果も出せたのではないかと感じています。 というのも、研修の成果物の一環として、参加者に「前後確認シート」と呼ばれるものを研修前と研修後に作成を依頼したところ、当研修で学んだことを活用して業務内における半年先までの目標設定に活用している成果が見受けられたためとなります。 今回、 ロジカルシンキング 研修を内製化するような形で実施したことを踏まえ、比較的習得したいスキルや知識はやりかた次第でできる環境が整備されていると思いました。 他方で新卒などの若手やベテランの分け隔てなく必要な研修を提供できた点は、個人的にも貴重な経験を積む機会となったのではと感じました。 ここまで目を通して頂き、ありがとうございました。
アバター
ユーザーによるカスタマイズ 型と実体(クラスとインスタンス) Type Object パターン 例 申請書クラス 既定の申請書のクラス カスタム申請書のクラスの検討 Type Object パターンを適用 まとめ 関連するデザインパターン Flyweight パターン Interpreter パターン、Command パターン 参考文献 ユーザーによるカスタマイズ 楽楽精算開発部の id:smdr3s です。主に Java を使ったサーバーサイドを担当しています。 弊社のサービスである 楽楽精算 は、その名のとおり経費精算のサービスです。 交通費や出張費、交際費といったさまざまな経費を申請でき、上司や 経理 担当部署による承認を経て、 経理 処理の完了までをサポートしています。 基本的には経費精算に関わる処理が主な機能ですが、上記中の申請~承認の処理、いわゆる承認フローを経費精算以外の業務に使用することもできます。この機能は 汎用ワークフロー と呼ばれ、お客様(管理者ユーザー)が自由に申請書の種別を作成、中身をカスタマイズし、承認フローを関連づけておくことができます。 例えば稟議書、押印申請、重要資産アクセス申請など、さまざまな種別の申請書を作成できます。 そして(一般ユーザーが)それぞれの種別の申請書を使用して申請を行い、設定された承認フローに沿って承認、決裁を行うことができます。 このように、ビジネスアプリケーションではユーザーが自由にカスタマイズした雛形を作成し、その雛形から多数の書類を作成する、といった要求があることがあります。 特に B2B の SaaS プロダクトではユーザー企業ごとに規程が異なるものの、個別に実装を行うことはないため、ユーザー自身がカスタマイズを行える機能の必要性は高まります。 型と実体(クラスと インスタンス ) さて、多くのアプリケーションでは、決められた型に沿った実体を生成し、その実体を処理しています。 クラスベースの オブジェクト指向言語 で書かれたプログラム上では、型がクラス、実体が インスタンス になります。 経費精算アプリケーションで言えば「交通費精算申請書」という「型」を表すクラスが実装されており、個々の申請ごとにこのクラスの実体である インスタンス を作成( new )して処理します。 一方、先ほどの汎用ワークフロー機能で作成する申請書の種別のような、ユーザーが自由に作成する「型」は、そのままクラスとして実装することはできません。 クラスは実装時に作成する必要がありますが、その仕様は実行時にユーザーが指定するまでわからないためです。 Type Object パターン このような場合に役立つのが Type Object パターン です。 Type Object パターンでは、実行時に作成された「型」の情報を「オブジェクト」に入れて保持し、そのオブジェクトを他のオブジェクト( インスタンス )から参照させることで「型」としての情報を与えます。型のメタ情報をオブジェクトにしているとも言えます。 例 経費精算アプリケーションを例に、Type Object パターンを使用してみます。 アプリケーションの要件は以下のとおりです。 既定で交通費精算と経費精算の申請書を作成できる。 交通費精算の申請書には「行先、 交通機関 、金額」の入力項目がある。 経費精算の申請書には「品名、単価、数量、金額」の入力項目がある。 自由に申請書の種別を追加できる。(カスタム申請書) カスタム申請書には、申請書名を設定できる。 カスタム申請書には、任意の数の入力項目を追加でき、それぞれに項目名を設定する。 申請書クラス まずは各申請書の共通の構成を抜き出し、それをもとに申請書の親クラスを作成します。 申請書名(申請書の種別ごとに決まっている) 複数の入力項目 項目名(申請書の種別ごとに決まっている) 項目値(個々の申請書ごとに入力する) public abstract class Application { private Map<String, Object> fields = new HashMap<>(); // 入力項目 public abstract String getApplicationName(); // 申請書名 public abstract String[] getFieldNames(); // 項目名リスト // 項目値設定 public void setField(String name, Object value) { if (Arrays.asList(getFieldNames()).contains(name)) { fields.put(name, value); } } // 項目値取得 public Object getFieldValue(String name) { return fields.get(name); } } 既定の申請書のクラス 申請書の種別=型、と考えると、既定の種別の申請書であれば型の仕様はすでに確定しているため、クラスを実装することが可能です。 交通費精算クラスや経費精算クラスは以下のように実装できます。 // 交通費精算クラス public class TransportApplication extends Application { @Override public String getApplicationName() { return "交通費精算" ; } @Override public String[] getFieldNames() { return new String[] { "行先" , "交通機関" , "金額" }; } } // 経費精算クラス public class ExpenseApplication extends Application { @Override public String getApplicationName() { return "経費精算" ; } @Override public String[] getFieldNames() { return new String[] { "品名" , "単価" , "数量" , "金額" }; } } もちろん、これらのクラスは new で インスタンス を作成することができ、その インスタンス は当然そのクラスの情報を持っています。 public class Main { public static void main(String[] args) { // 交通費精算の申請書インスタンスを作成 Application app1 = new TransportApplication(); // 交通費精算の申請書インスタンスは交通費精算の型の情報を持つ System.out.println(app1.getApplicationName()); // "交通費精算" System.out.println(app1.getFieldNames()[ 0 ]); // "行先" // 経費精算の申請書インスタンスを作成 Application app2 = new ExpenseApplication(); // 経費精算の申請書インスタンスは経費精算の型の情報を持つ System.out.println(app2.getApplicationName()); // "経費精算" } } カスタム申請書のクラスの検討 次にカスタム申請書の実装を行います。 当然、カスタム申請書は実装時には申請書の型の仕様が決まっていないため、種別ごとの値をハードコードすることはできません。 // カスタム申請クラス(実装不可) public class CustomApplication extends Application { @Override public String getApplicationName() { return ????; // 実装時に申請書名は不明 } @Override public String[] getFieldNames() { return ????; // 実装時にどんな項目があるか不明 } } 申請書の実体生成時にカスタム申請書の型の情報を渡して直接 インスタンス を生成するのはどうでしょうか。 // カスタム申請クラス(直接生成) public class CustomApplication extends Application { private final String applicationName; // カスタム申請書の申請書名 private final String[] fieldNames; // カスタム申請書の項目名リスト // 生成時に型の情報を渡す public CustomApplication(String applicationName, String[] fieldNames) { this .applicationName = applicationName; this .fieldNames = filedNames; } @Override public String getApplicationName() { return applicationName; } @Override public String[] getFieldNames() { return fieldNames; } } これでも インスタンス にカスタム申請書の型の情報を持たせることはできていますが、これは単に「カスタム申請クラスの インスタンス 」を作成しているだけになっています。その インスタンス がどのカスタム申請書の種別か、すなわち「型」自体の情報がなくなってしまっています。 (この例では applicationName でカスタム申請書の種別を判別できる可能性がありますが、それはたまたまそうなっているだけで、型の識別子としては不十分です。) Type Object パターンを適用 そこで、Type Object パターンを適用します。 まず、「型」を表すクラスを作成します。ここではカスタム申請書の種別ごとの「型」ですので、その設定を入れられるクラスにします。 // カスタム申請書の型クラス public class CustomType { private final String applicationName; // カスタム申請書の申請書名 private final String[] fieldNames; // カスタム申請書の項目名リスト // 生成時に型としての情報を渡し、保持する public CustomType(String applicationName, String[] fieldNames) { this .applicationName = applicationName; this .fieldNames = fieldNames; } public String getApplicationName() { return applicationName; } public String[] getFieldNames() { return fieldNames; } } そして、この型クラスのオブジェクトへの参照を持つカスタム申請書クラスを作成します。 // カスタム申請書クラス public class CustomApplication extends Application { private final CustomType customType; // 型オブジェクトへの参照を持つ // 生成時に型オブジェクトを渡し、型を持たせる public CustomApplication(CustomType customType) { this .customType = customType; } @Override public String getApplicationName() { return customType.getApplicationName(); } @Override public String[] getFieldNames() { return customType.getFieldNames(); } } 基本的なクラスの準備は以上です。実際にカスタム申請書の種別を作成し、その申請書の インスタンス を作成してみます。 public class Main { public static void main(String[] args) { // カスタム申請書の種別「稟議書」を作成 CustomType proposal = new CustomType( "稟議書" , new String[] { "件名" , "内容" }); // 稟議書の申請書インスタンスを作成 CustomApplication app1 = new CustomApplication(proposal); // 稟議書の申請書インスタンスは稟議書の型の情報を持つ System.out.println(app1.getApplicationName()); // "稟議書" System.out.println(app1.getFieldNames()[ 0 ]); // "件名" // 同じ種別の複数のインスタンスを作成可能 CustomApplication app2 = new CustomApplication(proposal); // 別の種別「押印申請」を作成可能 CustomType stamp = new CustomType( "押印申請" , new String[] { "書類種別" , "相手方社名" }); // 押印申請の申請書インスタンスを作成 CustomApplication app3 = new CustomApplication(stamp); // 押印申請の申請書インスタンスは押印申請の型の情報を持つ System.out.println(app3.getApplicationName()); // "押印申請" } } CustomType の インスタンス に注目してください。 これが、型を表すオブジェクト Type Object として CustomApplication の インスタンス にカスタム申請書の種別という「型」を与えています。 これにより CustomApplication の インスタンス は与えられた型の情報を持つようになっています。 型を表すオブジェクトである CustomType の インスタンス は使い回しが可能です。 (むしろ同じ型であれば CustomType の インスタンス は同一であることが望ましいです。システム全体でユニークとなるよう管理が必要です。) このコードでは説明のため CustomApplication の インスタンス を CustomApplication として扱っていますが、もちろん親クラスである Application として扱うこともできます。そうすればカスタム申請書の各種別も、既定の申請書も同一のインターフェースで扱うことができ、 ポリモーフィズム が捗ります。 まとめ Type Object パターン を使用すると、実行時に動的に型を作成し、その型のオブエクトを作成することができます。 作成した型は一般的なオブジェクトですので容易に管理、再利用が可能です。 アプリケーションに動的にひな形を作成するカスタマイズ性が求められたときには、Object Typeパターンで型管理を導入できないか、ぜひ検討してみてください。 関連する デザインパターン Flyweight パターン 共通の インスタンス を他のクラスの複数の インスタンス から参照するという点は Flyweight パターンと共通です。 また、型オブジェクトを型ごとにシングルトンにする際に Flyweight パターンの実装が参考になるかもしれません。 Interpreter パターン、Command パターン 型ごとにクラスを実装する際には実装クラスごとにメソッドに自由にロジックを記述することができますが、Type Object パターンでは個別にメソッドを記述することはできないため、型オブジェクトごとにロジックをカスタマイズすることはできません。 型オブジェクトを作成する際に設定できる情報はコンスト ラク タメソッドに渡せるような第一級オブジェクトに限られますので、型ごとにロジックを変えたい場合は Interpreter パターンや Command パターンなどと組み合わせるのが有効です。 参考文献 Nystorm, Robert. Game Programming Patterns. Genever Benning, 2014, 354p. https://gameprogrammingpatterns.com/ Johnson, Ralph; Woolf, Bobby. "The Type Object Pattern". 1996. http://www.cs.ox.ac.uk/people/jeremy.gibbons/dpa/typeobject.pdf Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John. オブジェクト指向 における再利用のための デザインパターン (改訂版). 吉田和樹, 本位田真一監修. SBクリエイティブ , 1999, 418p.
アバター
こんにちは、neige_ gnome です。 プライベートでは2児の母で、子どもに自宅の壁をボッコボコにされています。 会社では開発管理課という部署で、PSIRT(※)のようなことをやってます。 開発管理課は、開発部の中の1部署で、「エンジニアが働きやすい環境を提供し、成果の最大化に貢献する」をMissionに、当社の提供するサービス開発におかるセキュリティ・品質面のサポートを行っている部署です。 今回は、 ラク スにおけるプロダクトセキュリティ対策について、私たちの活動内容 をチョコっとだけご紹介します。 ※PSIRTとは:製品(Product)レベルのセキュリティ担当者のことを意味します。  会社レベルのセキュリティ担当であるCSIRTよりもスコープが開発寄りな分、  開発に特化したセキュリティ対策を行う人とイメージいただければと思います。 はじめに 前提説明 ・ラクスの組織体制ご紹介 ・OSSとは 本題 ①ライセンスチェックのながれ: ②脆弱性チェックのながれ: ③EOLチェックのながれ: 今後の課題 課題その1.人力対応が多い 課題その2.ステークホルダが可変式 課題その3.ウォッチすべき技術領域が広がりすぎ さいごに はじめに 昨今、残念なことに マルウェア やフィッシング被害のインシデントニュースを聞かない日はありません。 自社サイトが マルウェア の発信源にされていた、という話も聞きます。 そんな状況の中で、私たちは攻撃者たちからどのように自社を守れば良いのでしょうか。 今回は我々(以下、PSIRT担当と記載)のコミットメントが大きい「 OSS の 脆弱性 、EOL、ライセンスのチェック」に絞って ラク スの事例をご紹介します。 前提説明 本題の前に2点、 ラク スの組織体制と、 OSS について説明を入れさせて頂きます。 何やってるかだけ知りたい人は飛ばして『本題』を見てくださいネ! ・ ラク スの組織体制ご紹介 当社は企業の成長を支援するITサービスを提供する会社として、バックオフィス系の SaaS サービスを展開しています。 さて問題です。当社が公開している SaaS サービスの数をご存じでしょうか。 楽楽精算、楽楽明細あたりはTV CMを御覧になった方も居ると思いますが、他にも沢山あるんです。 また、公開しているサービスだけではなく、より良いサービスを世に出すために日々研究や開発を重ねています。 詳しくはコチラ→ クラウド事業 | 株式会社ラクス そしてそれらの SaaS サービスを開発するには、プログラム言語をはじめとした開発知識だけではなく 関連法や、対象製品に対する深い理解が必要になります。 そのため、 ラク スではサービスごとに専門の開発部隊が存在します。 しかも!ベスト・オブ・ブリードで開発を進める方針の都合上、 各サービスに最適な技術を取り入れているため、サービス間でシステム構成が異なります。 私たちPSIRT担当はそういった、人もシステム構成も異なる全サービス開発担当者に向けて、 横断的にセキュリティやライセンスの方針を作ったり、方針適用のサポートをしています。 体制図 ・ OSS とは OSS は一般公開されているプログラムのことを言います。 サービス開発を家の建築に例えるなら、 OSS は洗面台パーツやトイレパーツのようなものです。 パーツを持ってきて組み込むことで、トイレを自分で1から作るよりも、遥かに効率的に家を作ることが出来ますよね。 プログラムも同じです。 しかも有償の洗面台やトイレとは違い、 OSS は特定の条件の基であれば無償で利用できるケースがあることが特徴です。凄いですよね。 そんな素敵な OSS ですが、善意で公開されている性質上、開発の進みが遅かったり、 EOL(=開発がストップ) する可能性もあります。 酷いケースだと、明らかに大きな 脆弱性 (=セキュリティ的な不具合) があっても、放置され続ける場合もあります。 更に、毎日新しいパターンの 脆弱性 が発見されているため、いま 脆弱性 の無い OSS でも明日にはどうなるか分かりません。 そういった新しい 脆弱性 を持つ OSS や、EOLした OSS はクラッカーたちの格好の攻撃ターゲットになります。 つまり、 OSS を安心して使い続けるには、 OSS がEOLしたり 脆弱性 が発生していないかを、コンスタントにチェックして対応する力が必要になるのです。 勿論、 ラク スでも数百にも上る OSS を利用していますので、継続的に監視・対応しています。 なお、 OSS は"特定の条件の基であれば"無償で利用できると書きましたが、 その"特定の条件"というのが、"ライセンスと利用条件"です。 特に ライセンス は、よくあるライセンスだけでも600以上の種類があり、それぞれでどんな利用方法がOK/NGなのかが異なります。 独自のライセンスを持つ OSS も多いため、使いたい OSS が本当に使えるのかを確実にチェックすることが重要です。 ラク スでは、NGな利用方法をしないよう、PSIRT担当がチェックしています。 本題 さてここでは、 ラク スにおける OSS のライセンス、 脆弱性 、EOLのチェックのながれをご紹介します。 主な登場人物は、PSIRT担当とサービス開発担当の2者です。 ①ライセンスチェックのながれ: ライセンスチェックフロー サービス開発担当が利用 OSS 情報をPSIRT担当に共有 → PSIRT担当がライセンスチェック → サービス開発担当へフィードバック となっています。 新出パターンのライセンスが出てきた場合は、このフローに法務担当部署が加わり、PSIRT担当と認識をすりあわせます。 ② 脆弱性 チェックのながれ: 脆弱性 チェックフロー (①でPSIRT担当が利用 OSS を把握) → PSIRT担当が情報収集・サービス開発担当へ通知 → サービス開発担当にて影響確認・対応・情報共有 となっています。 サービス開発担当は、対応方針を決めたら開発・PSIRT担当全体に共有しますが、 その際 『対応方針を決めるのは通知から最遅でもX時間以内、 クリティカルな 脆弱性 があった場合に OSS のアップデートを適用した製品をリリースするのはX日以内』 という ラク ス独自のルールに従います。細かい数字はセキュリティに影響するため言えませんが、 Xに入る数字は相当短い、とだけ書かせて頂きます。(サービス開発担当者の技術力が伺えますね。) なおPSIRT担当の情報収集源は多岐にわたります。 商用ツールだけでなく、 脆弱性 の一覧サイト(NVD, JVN )、 Apache などベンダーのサイト、 RSSフィード から直接拾うこともあります。 ツールも利用していますが、機械チェックでは誤検知が多いため、人の目である程度フィルタリングした後にサービス開発担当へ通知するようにしています。 (実はこの部分に、PSIRT担当・サービス開発担当の双方が少し前まで多大な 工数 をかけていました。 それを先日自動化し、年間数百時間の 工数 削減に成功しました★そのお話はまた後日。) ③EOLチェックのながれ: 脆弱性 チェックと同じですので、図は割愛させていただきます。 PSIRT担当がEOLの情報収集をする上でトリッキーなポイントがあります。 実はEOL予定というものは、 OSS の公開ページに直接記載されているものから一切書かれていないものまで、様々です。 そこで、 ラク スでは独自に『X年以上開発がストップしている OSS はEOLと見做す』という基準を設けて、 EOL予定が近い OSS を定期的にリストアップし開発へ通知しています。 サービス開発担当は、EOLが来る時期を見据えて別の OSS に乗り換えたり、小さい機能であれば自社開発するなどして備えます。 以上が、 ラク スにおける OSS のライセンス、 脆弱性 、EOLのチェック体制です。 ちなみに上記3つの体制において、当社では情報共有に当社製品『楽楽販売』を利用しています。 楽楽販売は API 対応、ノーコードで色々な処理が自動化できて、承認プロセスもケアできる便利なツールです。 ご利用・ご検討頂いてるお客様へ CRM 以外にもご活用頂けますよ、ということでご紹介させていただきました。 今後の課題 ここまで、当社のライセンス・ 脆弱性 ・EOL管理についてご紹介してきましたが、 我々もPSIRT担当として他社の例に漏れず、課題を抱えています。 個人的に大きいと思っている課題を3点を挙げてみました。 課題その1.人力対応が多い 文中にも若干記載した通り、自動化は適宜進めていますが まだまだ手動の箇所が多く、PSIRT担当として生産的な活動に廻せるリソースが少ない状況です。 そのため、品質は担保したまま、ツールなどを活用しライセンスチェック~ 脆弱性 検知をトータルで効率化できないか検討中です。 課題その2.ステークホルダが可変式 セキュリティルールの策定・変更に対して、10以上の関連組織(総勢300名~)に100%コンセンサスを得るのは無理です。 かといって運用との乖離は出したくないので、ある程度はネゴりたいところです。 しかしながら、要件次第でどの組織の誰に訊けば情報があるのか/誰に承認貰えば良いかが変わるため、 いろいろな現場で相異なるご意見をいただいた場合は、方針を取り纏めるのに苦労することもあります。 (もういっそ開き直って、分かる範囲で調整した上でサッサとルールを全体周知してしまい、 指摘を受ければ即是正する方が当社の変化のスピード感に合っているかも・・と思っています。) 課題その3.ウォッチすべき技術領域が広がりすぎ その時々で理想とされるシステム構成や世間のニーズに追随するため、 ラク スのサービスで扱う技術領域は日々広がっています。 古き良きオンプレの ファイルシステム 、 クラウド 、モバイルアプリ、Dockerと、セキュリティ面でカバーすべき範囲もその都度広がってきました。 各技術に共通するセキュリティルールは多いものの、新技術独自のルールも整備していく必要があります。 ただ、新しくルールを作るとしてそれをチェックできる体制を敷かねばなりません。 その一方で、新技術の確立で陳腐化してしまったルールがあれば、早急に改善する必要があります。 こういった技術トレンドの変化に全てオンタイムでルールを整備するのは厳しいものがあります。 特に、多数のサービス開発者にコンセンサスを得る場合は余計に時間がかかります。 優先度を決めて順次対応していますが、それまでは個別に問合せや連絡対応をする必要がありサービス開発担当の方に申し訳なく思っています。 生成系AIと少しのコーディングで、そういったルールのアップデートや ネゴシエーション を 大幅に自動化できるのではないかと妄想はしていますが、今はまだ、手を出せていません。 さいごに セキュリティ対策は、サービス開発効率との トレードオフ の側面があります。 やり過ぎた結果、開発効率やサービスレベルを下げてしまうのは可能な限り避けるべきです。 とはいえ、 セキュリティインシデント のニュースは増える一方ですので、MAXに警戒する必要があります。 それを踏まえて、私たち開発管理課は、サービス開発担当との情報共有を密に行い、時流と事業規模に合った標準化/方針策定をすることで、自社に最適なセキュリティ強化を図っていきたいと思っています。 と、真面目なことをつらつらと書きましたが、現場では毎日、和気あいあいと改善に向けて取り組んでいます。 特に、失敗があっても許容してくれる社風のお陰で気楽にトライ&エラーが出来て助かっています。 チャレンジとお喋りと勉強が好きな方、いつでもお待ちしています。 ★開発管理課では一緒に働くメンバーを募集してます!!★ ↓ ↓ ↓ プロダクトセキュリティエンジニア の項をご参照願います。 募集職種 | 株式会社ラクス キャリア採用
アバター
はじめに PHP_CodeSnifferとは 運用フロー 問題点 PHP_CodeSnifferの指摘をまとめて受け取る PHP_CodeSnifferの指摘を確認するために、都度CIを実行しなければならない 改善 PhpStormにPHP_CodeSnifferを設定する 導入 PHP_CodeSnifferをインストールする PhpStorm の設定画面を開き、PHP_CodeSniffer の実行設定を行う インタプリタの設定を追加 その後 実装中にPHP_CodeSnifferの確認ができるようになった CIの自動チェック完了までにタイムラグがあり、待ちの時間ができる問題について まとめ はじめに こんにちは。配配メール開発チームに所属しているmrstsgkです。 2023年3月23日から3月25日に開催されたPHPerKaigiに参加しました! (LT枠で登壇しました!とても楽しかったです!) speakerdeck.com 発表後、 PHP _CodeSnifferについて進展があったので、このブログで紹介いたします。 PHP _CodeSnifferとは コーディング規約の違反を検出するライブラリです。 PSRや PEAR などの様々なコーディング規約を指定して検査を実施しています。 PSR とは: PHP Standards Recommendationsの略で、 PHP -FIG(The PHP Framework Interop Group)が策定している PHP の規約です。 PEAR とは: PHP Extension and Application Repositoryの略で、 PHP で利用する事ができるライブラリ(パッケージ)を提供しているサービスのことです。 似た名前で PECL がありますが、こちらは C言語 で書かれた拡張ライブラリ (extension) を提供するサービスのことです。 独自の規約を追加することも可能です。 私たちのチームでは、独自のルールセットを採用しています。 原則 PSR12 に準拠するルールセットになっていますが、 else if の代わりに elseif を使用するなど既存コードの影響で大量に指摘が出そうなルールは除外しています。 運用フロー CIでの自動チェック。 親ブランチと比較して増加したエラーのみが出力されます。 \ 問題点 現状の運用フローでの問題点 PHP _CodeSnifferの指摘をまとめて受け取る チームの開発の流れとして、実装完了時に今までの commit をまとめてGitLabに push することが多くありました。 なので、 PHP _CodeSnifferの指摘をまとめて受けるため、対応で 工数 が増加する問題が発生しました。 PHP _CodeSnifferの指摘を確認するために、都度CIを実行しなければならない 「 PHP _CodeSnifferの指摘をまとめて受け取る」という問題を解決するために、こまめにGitLabに push するようになりました。 しかしCIでの自動チェックが完了するまでにタイムラグがあり、待ちの時間ができてしまいました。 CI実行中にほかの作業に着手するのでスイッチングコストの増加が見られ、実装に集中しにくい環境になっていました。 改善 PhpStormに PHP _CodeSnifferを設定する 現状の運用フローだと、 PHP _CodeSnifferを確認できるのがGitLabに push することだけだったので、 確認できる段階をもっと早くするためにPhpStormに PHP _CodeSnifferを導入することになりました。 また、PHPerKaigi後に寄せられたフィードバックを確認していると、以下のフィードバックをいただきました。 (フィードバックありがとうございます!!) IDE に組み込めばCIにあげる前に手元でチェックできるので、フィードバックサイクルを早められてよいと思いました。 PhpStormに PHP _CodeSnifferを設定する際は、公式のドキュメントを参考にすれば導入しやすいと思います。 pleiades.io 導入 PHP _CodeSnifferをインストールする 公式のドキュメントによると PHP _CodeSnifferをComposerと共にインストールすると、PhpStormは必要な スクリプト を自動的にダウンロードし、 IDE に登録し、オプションで対応するコードインスペクションを有効にして構成します。 とのことなので、composerをインストールします。 php composer.phar install PhpStorm の設定画面を開き、 PHP _CodeSniffer の実行設定を行う Settingsを開きます。 PHP > Quality Tools内に PHP _CodeSnifferがあるので、そこから設定することができます。 インタプリタ の設定を追加 ※設定例 PHP _CodeSniffer path: /usr/local/phpcs/vendor/bin/phpcs Path to phpcbf: /usr/local/phpcs/vendor/bin/phpcbf その後 実装中に PHP _CodeSnifferの確認ができるようになった PhpStormに PHP _CodeSnifferを導入することで、問題点であった「 PHP _CodeSnifferを確認できるのがGitLabに push することだけ」を解決することができました。 PHP _CodeSnifferをCIに組み込んだときに出た良い副産物として、 開発中に細かいCommit粒度でGitLabへpushするようになり、「開発→CIでコードを確認→修正」のサイクルで実装に集中しやすい状態になりましたが、 PhpStormに PHP _CodeSnifferを導入することで、「開発→静的解析→修正」のサイクルを回すことができるようになり、より実装に集中しやすい環境になりました!! CIの自動チェック完了までにタイムラグがあり、待ちの時間ができる問題について こちらの問題は、PhpStormに PHP _CodeSnifferを導入しても解決することはできません。 根本的な原因として、CIのジョブがいつ完了しているかわからないので定期的にGitLabを確認しなければなりませんでした。 なので、CIのジョブが完了したら連絡が来る仕組みを作って対応しています。 まとめ 静的解析ツールはやはり最高でした!! コーディング時の早い段階でエラーを検出できるため、バグ修正コストを抑制できることや、共通のコーディングスタイルを推進できるため、コードの可読性や保守性が向上するなどの多くの利点があります。 皆さんも是非、静的解析ツールを利用して快適な開発ライフをお過ごしください。 ただプロジェクトにもよりますが、 PHP _CodeSnifferは「 IDE への導入」「CIでの自動チェック」「CIのジョブ連絡」この3つが揃うことで真の快適さが得られると感じました。
アバター
はじめに はじめまして、 ラク スフロントエンド開発課の斉藤です。 普段フロントエンド開発課では、一部のプロダクトにおいて新しく開発した機能を実装した画面や、パフォーマンスの劣化が懸念される画面に対して、性能計測を行っています。今回はフロントエンド開発課が どのようなWebパフォーマンス指標を計測しているのか 、 なぜその指標を採用したのか 、 どのように計測しているのか を紹介したいと思います。また、パフォーマンスを計測している中で 不便に思っていること や 改善していきたいこと についてもご紹介します。 注意点として今回紹介する指標や計測ツールは、あくまでも ラク スのプロダクトに合わせて選定したものであるため、他社様、あるいは個人にとって最適なものと言えるわけではありません。参考程度にお読み頂ければ幸いです。 はじめに 対象読者 この記事で扱わないこと 結論 計測する指標と目標スコアの一例 計測ツール 選定理由 各指標の概要と選定理由 FCP (First Contentful Paint) 概要 選定理由 参考スコア LCP (Largest Contentful Paint) 概要 選定理由 参考スコア TTI(Time to Interactive) 概要 選定理由 参考スコア TBT(Total Blocking Time) 選定理由 参考スコア CLS(Cumulative Layout Shift) 選定理由 参考スコア Response 概要 選定理由 目標 Animation 概要 選定理由 目標 計測手順 webパフォーマンス計測環境の準備 計測対象画面 devtoolsの設定 view portの設定 スロットルの設定 各webパフォーマンス指標の測定 LCP, TTI, LCP, CLSの測定 TBTの測定 Responseの測定 Animation パフォーマンス測定シート 現状問題に感じていること パフォーマンス計測を始めてよかったこと おわりに 参考文献 対象読者 フロントエンドのパフォーマンスを計測したいが、どの指標を計測すればよいかわからない方 フロントエンドのパフォーマンスを計測したいが、どのツールを使って計測すればよいかわからない方 フロントエンドのパフォーマンス測定について興味がある方 この記事で扱わないこと Webパフォーマンスの改善方法 結論 計測する指標と目標スコアの一例 WANT MUST FCP <= 1.8s <= 3.0s LCP <= 2.5s <= 4.0s TTI <= 3.8s <= 7.3s TBT <= 200ms <= 600ms CLS <= 0.1 <= 0.25 Response <= 100ms <= 1000ms Animation <= 16.7ms(60FPS) <= 33.3ms(30FPS) ※目標スコアはあくまでも一例になります。実際にはプロダクトごとに異なる値となっています。 計測ツール パフォーマンス指標計測には以下ツールを用いる Chrome devtools: Performance Insight 測定対象:FCP, LCP, TTI, CLS Chrome devtools: Performance 測定対象:TBT, Response, Animation 選定理由 devtoolsに内包された機能のため実施コストが低い ネットワーク・CPUのスロットルが行えるため実ユーザー環境に近い計測が行える web.dev で紹介されている指標に加え、 RAILモデル の測定も行えるため 各指標の概要と選定理由 ユーザーがパフォーマンスをどのように認識しているかに関する指標として、以下の項目が 挙げられています。 使用可能性 :ユーザーがページを操作できているか。ビ ジー 状態になっていないか。 快適さ :遅延やエラーがなく、スムーズで自然な操作ができているか。 知覚される読み込み速度:  ページがすべての視覚要素を読み込み、画面に レンダリング する速度。 読み込みの応答性:   コンポーネント がユーザーの操作に対してすばやく応答するために、ページが必要な JavaScript コードを読み込んで実行する速度。 実行時の応答性:  ページの読み込みが完了した後、ページがユーザーの操作にどの程度すばやく応答できるかを示す指標。 視覚的な安定性:  ユーザーが予期しないような手法でページ上の要素が移動し、ユーザーの操作に支障をきたす可能性があるかどうかを示す指標。 滑らかさ:   トランジション やアニメーションが一定のフレーム レートで レンダリング され、ある状態から別の状態へと流れるように移動しているかどうかを示す指標。 フロントエンド開発課では上記を網羅的に測定することを目指し各指標を選定しています。 FCP (First Contentful Paint) 概要 FCPは、ユーザーがアクセスしたウェブサイトで最初に表示されるコンテンツがどれくらい早く描画されるかを示す指標です。この指標における "コンテンツ" は、テキスト、画像 (背景画像を含む)、 <svg> 要素、白以外の <canvas> 要素のことを指しています。 引用元: https://web.dev/fcp/ 上記のタイムラインでは2フレーム目にコンテンツが初めて表示されています。このようにFCPはwebサイトを読み込んでから一部のコンテンツが表示されるまでの時間を計測します。全てのコンテンツが レンダリング される時間では無いことに注意してください。 選定理由 読み込みの応答性を測定するため 参考スコア MUST:3秒以下 WANT:1.8秒以下 参考: https://web.dev/fcp/#fcp-における良いスコアとは? LCP (Largest Contentful Paint) 概要 LCPは、ビューポート内で最も大きなコンテンツがどれだけ早く描画されるかを示す指標です。この指標における "コンテンツ" は、以下の要素を指します。 <img> 要素 <svg> 要素内の <image> 要素 <video> 要素 (ポスター画像が使用されます) テキスト ノードやその他のインラインレベルのテキスト要素の子要素を含むブロックレベル要素 h1, h2, h3 p articleなど LCPとしてレポートされる要素のサイズは、通常ビューポート内でユーザーに対して表示されるサイズとなります。要素がビューポートからはみ出していたり、要素の一部が切り取られていたり、画面に表示されないオーバーフローが発生したりしているような場合、そういった部分は要素のサイズには含まれません。あらゆる要素において、 CSS を介して適用されているマージン、パディング、ボーダーはすべて考慮されません。 引用元: https://web.dev/lcp/ 上記の例ではLCPは5フレーム目で発生しています。最後の大きな画像が表示されるまではテキスト要素がLCPのターゲットとなっていましたが、ページの読み込みが進み、最終的なLCPのターゲットが変化した例です。 遅れて読み込まれたコンテンツの方がすでにページ上に表示されているコンテンツよりもサイズが大きいといったケースはよく見られますが、必ずしもそうなるわけではありません。次の 2 つの例では、ページが完全に読み込まれる前に LCPが発生しています。 引用元: https://web.dev/lcp/ 選定理由 知覚される読み込み速度を測定するため 参考スコア MUST:4秒以下 WANT:2.5秒以下 参考: https://web.dev/lcp/#lcp-における良いスコアとは? TTI(Time to Interactive) 概要 TTI は、ページの読み込みが開始されてから主なサブリソースの読み込みが完了するまでの指標です。改善することでページがユーザーの入力に対してすばやく確実に応答できるようになります。 TTIは以下の手順に沿って計測されます。 FCPから時間の計測を開始します。 少なくとも 5 秒間の「落ち着いている期間(以下quiet window)」を時間の経過順に探していきます。quiet windowは以下のように定義されています。 LongTask(50msを超えて実行されるタスク)がない 実行中のネットワーク GET リク エス トが 2 件以下 quiet windowより前の期間内で、一番最後に現れる長く時間がかかっているタスクを見つけ出します。長く時間がかかっているタスクが見つからない場合には、FCP まで遡ります。 quiet windowより前の期間内で一番最後に現れる長く時間がかかっているタスクの終了時間が、TTI となります。(長く時間がかかっているタスクが見つからない場合には、FCP と同じ値になります)。 以下の図は、上記の手順を視覚化したものです。 引用元: https://web.dev/tti/ 選定理由 実行時の応答性を測定するため 参考スコア MUST:7.3秒以下 WANT:3.8秒以下 参考: How Lighthouse determines your TTI score TBT(Total Blocking Time) TBTは、読み込みの応答性を測定するために重要となる指標です。ページが確実に操作可能になるまでの間の操作不可能性の重大さの数値化に役立ち、TBT が低ければ低いほどページが確実に使用可能となることを示しています。 メインスレッド上にLongTask(50msを超えて実行されるタスク)が存在する場合、そのメインスレッドは"ブロックされた" とみなされます。"ブロックされた" と表現されるのは、 ブラウザー が進行中のタスクを中断することができないからです。したがって、Long Task実行中にユーザーがページを操作した場合、 ブラウザー は応答する前にタスクの終了を待たなければなりません。これによりユーザーから見るとページが遅い、または質が低いと感じてしまう可能性があります。 TBTは、FCPとTTIの間で発生する各長いタスクの ブロック時間の合計により計算されます。 たとえば、ページを読み込んでいる最中の ブラウザー のメイン スレッドの図は、以下のようになります。 引用元: https://web.dev/tbt/ 上記のタイムライン上には 5 つのタスクがあり、そのうちの 3 つは継続時間が 50 ms を超えているため、長く時間がかかっているタスクとなります。以下の図は、長く時間がかかっているタスクそれぞれのブロック時間を示しています。 引用元: https://web.dev/tbt/ このため、メイン スレッドでのタスク実行の総時間は 560 ミリ秒ですが、そのうちの 345 ミリ秒のみがブロック時間としてみなされます。 タスクの継続時間 タスクのブロック時間 タスク 1 250 ミリ秒 タスク 2 90 ミリ秒 タスク 3 35 ミリ秒 タスク 4 30 ミリ秒 タスク 5 155 ミリ秒 合計ブロック時間 345 ミリ秒 選定理由 使用可能性、読み込みの応答性を測定するため 参考スコア MUST:600ミリ秒以下 WANT:200ミリ秒以下 参考: how-lighthouse-determines-your-tbt-score CLS(Cumulative Layout Shift) CLSは、視覚的な安定性を測定するための指標です。画面に表示されたコンテンツのユーザーの予期しない突然の移動やズレをレイアウトシフトと呼びます。このレイアウトシフトに遭遇する頻度の数値化に役立つ指標がCLSであり、CLS が低ければ低いほど、そのページが快適であることが保証されます。 CLSスコアは以下の計算式で算出されます。 レイアウトシフトの影響を受けた面積 × 実際にずれて動いた距離 以下のようにテキストが表示されたあとに「Click Me」ボタンが表示され、要素のズレが起こった場合を考えます 引用元: https://web.dev/cls/ レイアウトシフトの影響を受けた面積(赤点線部に囲まれた分)はビューポート全体の50%を占めるため0.5となります。また実際にずれて動いた距離(紫色の矢印の長さ)はビューポート全体の15%を占めます。したがってCLSスコアは次のように計算できます。 0.5(レイアウトシフトの影響を受けた面積)× 0.15(実際にずれて動いた距離)=0.075 レイアウトシフトが複数箇所で発生する場合は上記計算式の合計値がCLSスコアとなります。 参考: https://gmotech.jp/semlabo/seo/blog/cwv_cls/ 選定理由 視覚的な安定性を測定するため 参考スコア MUST:0.25以下 WANT:0.1以下 参考: https://web.dev/cls/#cls-における良いスコアとは? Response 概要 Responseは、ユーザーがアクションを起こしたときWebサイトが反応するまでを測定する指標です。100ms以内に完了することで、ユーザーはやりとりが瞬時に行われていると感じます。例えば以下のアクション時間を測定します。 チェックボックス にチェックを入れてからチェック状態が反映されるまで リンクをクリックしてから画面が遷移するまで ボタンをクリックしてからダイアログが表示されるまで API を叩いてから何らかの通知(スナックバーやトースト等)が表示されるまで 選定理由 快適さ、実行時の応答性を測定するため 目標 MUST:1000ms以下 WANT:100ms以下 参考: https://web.dev/i18n/ja/rail/#ユーザーに焦点を合わせる Animation 概要 Animationは、アニメーション動作の各フレームの動作時間を測定する指標です。この指標はアニメーションの視覚的な滑らかさを数値化したものです。 選定理由 滑らかさを測定するため 目標 MUST:16.7ms以下 WANT:33.3ms以下 ※ それぞれ30FPS/60FPSを満たす目標 参考: https://web.dev/i18n/ja/rail/#アニメーション:フレームを10ミリ秒で生成する 上記以外にも重要な指標としてFIDが挙げられます。FIDは、ユーザーが最初にサイトを操作したとき から、その操作に実際に応答するまでの時間を測定する指標です。この指標は 実際のユーザー環境で測定する指標である ため測定難易度が高くなります。したがって、FIDの代わりにTBTを測定することで対処する事とします。 TBTはFIDと相関性が高く、かつ検証環境での測定が可能 なことが理由です。 計測手順 webパフォーマンス計測環境の準備 webパフォーマンスを計測するために以下の条件を満たす環境を用意します 検証環境 本番ビルドかつBEと疎通が行える環境を用意します ただしフロントエンドのパフォーマンス測定のみに焦点を当てる場合、バックエンドはモック環境でもOKとします。その場合でもフロントエンドは本番ビルドを行った環境で測定してください。 理由:ユーザーが使用する実際の環境に近い条件で測定するため 理論上表示できる最大数のデータを用意する 例えば1つのページで最大500件の検索結果を表示できるような仕様の場合、ダミーデータを500件以上用意しかつ、その全てを表示した上で計測を行います 最大表示件数が決まっていないページに関しては、ビューポート全体に表示できる数のダミーデータを用意すればOKとします 理由:予測される最大の レンダリング コストがかかる環境のパフォーマンス改善を行うことができれば、その画面全体のwebパフォーマンス向上が期待できるため 計測対象画面 新しく機能を追加した画面において、パフォーマンスの低下が懸念される画面 新しく作成した画面 類似した画面を複数実装する場合いずれか1つでOKとします devtoolsの設定 webパフォーマンスの計測を行う前にdevtoolsの設定を行います。 view portの設定 view portによりwebパフォーマンスの測定結果は変化するためview portを固定します。 devtoolsを開き「Toggle device toolbar」を押下します。 画面サイズを任意の値に設定します。基本的には提供するアプリケーションの推奨する画面サイズに設定します。 スロットルの設定 商材を利用するユーザーが用いるPCはエンジニアの開発用PCと比較して性能が低いことが懸念されます。したがってユーザーの実環境に近い条件でwebパフォーマンスの測定を行うため、devtoolsでスロットルの設定を行います。 devtoolsの「Performance Insights」パネルを開きます。 セレクトボックスを押下しNetwork、CPUを想定するユーザーの環境に合わせて設定します。ユーザーは基本的にキャッシュされた状態のページを参照するので、「Disable cache」のチェックは外します。 同様にdevtoolsの「Performance」パネルを開きます。 設定ボタンを押下します。 CPU、Networkを設定します。 各webパフォーマンス指標の測定 LCP, TTI, LCP, CLSの測定 LCP, TTI, LCP, CLSは Chrome DevToolsの「Performance Insights」パネルで測定することができます。 Chrome DevToolsを開き、「Performance Insights」パネルを押下します。 「Measure page load」を押下しパフォーマンス測定を行います。 devtoolsに表示されるFCP,LCP,TTIにマウスホバーすると各値が表示されます。 CLSは画面にレイアウトシフトが発生している場合のみ自動で計測されます。「Insights」タブから値を確認できます。 TBTの測定 TBTは Chrome DevToolsの「Performance」パネルで測定します。 Chrome DevToolsを開き、「Performance」パネルを押下します。 画面左上の「Start profiling and reload page」を押下します。 devtools下部に表示される「Total blocking time」の値がTBTです。 Responseの測定 Responseは Chrome DevToolsの「Performance」パネルで測定します。 今回はボタンを押してからダイアログが開くまでの 応答時間 の測定を例にとります。 Chrome DevToolsを開き、「Performance」パネルを押下します。 「Screenshots」にチェックを入れます。 devtools左上の「record」を押下します。 アプリケーション上でボタンを押下し、ダイアログを表示させます。 devtools左上の「stop」を押下します。 スクリーンショット を確認しながらボタンを押してからダイアログが表示されるまでの範囲をドラッグして絞り込みます。 TotalがResponseの測定値になります。 Animation Animationは Chrome DevToolsの「Performance」パネルで測定します。 今回はダイアログが開く際のアニメーションを測定します。 1~5まではResponseの測定と同様 スクリーンショット を確認しながらアニメーション動作中の各フレームの動作時間を記録する 2で記録した各フレームの動作時間の平均値がAnimationの測定値になります。(1フレームずつ確認するのは非効率なためぱっと見でもOK) パフォーマンス測定シート フロントエンド開発課では各webパフォーマンス指標の測定結果を以下のような スプレッドシート にまとめています。 全測定値のうち75パーセンタイル以下の値が合格基準を満たしていれば、その指標は合格となります。例えば測定回数が全部で4回の場合、そのうちの3回が合格基準を満たしていればOKということになります( 参考 )。合格基準に満たない場合でも「訳あり合格」として合格とすることもできます。現実的には全ページ、全機能で合格基準を達成することは難しいです。達成が難しく改善できない合理的な理由を説明できるのであれば「訳あり合格」とすることができるようにしています。 現状問題に感じていること ここまで読んでいただいている場合すでにお察しかもしれませんが、各パフォーマンスを手動で計測しているため 実施コストが高い です。少しでも測定コストを減らすため、Animationの指標は測定してなかったり( SaaS プロダクトにおいて重要度の高くない指標のため)、BEを MSW でモックした環境で測定を行うこともあります。信頼性の高いデータを測定することとパフォーマンスを測定するコストは トレードオフ の関係にあるので、 工数 と相談しながら計測しているのが現実です。今後の展望として Lighthouse CI や Sentry 等を用いて自動的にパフォーマンス測定&レポート作成ができないか検証を進めていきたいです。 パフォーマンス計測を始めてよかったこと 各Webパフォーマンスの指標に基づいて具体的な数値を計測するため、パフォーマンス劣化の ボトルネック がどこにあるかを特定しやすくなったので良かったです。「なんとなくページが遅い気がする」といった感覚値から「LCPの値が基準値を◯%下回っているためパフォーマンスが悪い」のように実測値で判断できるようになりました。これにより、このページのパフォーマンスを改善するためにはチャンク分割が効果的なのではないか、と具体的な根拠を持って仮説を立てることができるようになり、効率的に改善に取り組めています。またパフォーマンス改善を行なった後もう一度計測を行うことで、具体的にどの項目がどれだけ改善されたのかがわかるようになったため、パフォーマンス改善のために行なったことが本当に効果があったのか検証するのにも役立っています。 おわりに ラク スのフロントエンド開発課が測定するWebパフォーマンス指標と測定方法を紹介しました。計測はまだ始めたばかりで非効率な部分も感じています。少しずつ改善しながら実施コストを下げつつ、定期的にパフォーマンス測定を行うことで、より品質の高い製品をリリースしていきたいです。 参考文献 ユーザーを中心としたパフォーマンス指標 RAILモデルでパフォーマンスを評価する Core Web Vitals を測定するためのツール Lighthouseの計測結果を見ていく コアウェブバイタルのCLSとは? The Science Behind Web Vitals How Percentile Approximation Works Web Vitals Webフロントエンド ハイパフォーマンス チューニング
アバター
PHP で「文字列に特定のキーワードが含まれているか」や「文字列中に特定の文字列を含むか」を確認したい場合、どのようなコードを書くだろうか? もし、あなたが strpos() や strstr() を使う方法を思いついたのなら、これだけは覚えて帰ってほしい。 文字列検索には str_contains() を使え 。 結論:文字列検索には str_contains() を使う サンプル 昔はstrpos()やstrstr()などを使っていた strpos() や strstr() ではダメなのか? strpos() を使うべきでない理由 strstr() を使うべきでない理由 preg_match()を使うべきでない理由 「いや、キーワードが先頭にあるかを知りたいんだ」という人は 「日本語でも使えるの?」との疑問について もっと詳しく? これでわかっただろう 結論:文字列検索には str_contains() を使う str_contains() は、「指定した部分文字列が、文字列中に含まれるかを調べる」ための関数である。この関数はPHP8.0 で追加された。 php .net: str_contains str_contains(string $haystack, string $needle): bool needle が haystack に含まれるかを調べます。 大文字小文字は区別されます。 〜中略〜 haystack に needle が含まれていた場合 true そうでない場合、false を返します。 サンプル <?php if ( str_contains ( "なんだお前たち!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら、" ; // 表示される!! echo "答えてあげるが世の情け" ; } if ( str_contains ( "誰だお前たち!!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら、" ; // 表示されない echo "答えてあげるが世の情け" ; } <?php $ result = str_contains ( "なんだお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(true) $ result = str_contains ( "誰だお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(false) 上記の通り、検索結果が bool で返ってくる。とても明確な仕様だ。 名前もすばらしい。 contains (含む、包含する)とあるので、読めば何をする関数かわかりやすい。 少なくとも strpos() 、 strstr() よりは直感的だ。 よって、文字列検索には str_contains() を使え。 昔は strpos() や strstr() などを使っていた しかし、 Google 検索で以下のような検索を行うと strpos() や strstr() (もしくは mb_strpos や mb_strstr )を提案するサイトが出てくる。 PHP で文字列を検索する PHP で文字列を含むか検索する PHP で文字列の有無を確認する なぜか? それは str_contains() が 2020/11/26 にリリースされた PHP8.0 で導入された関数だから だ。 *1 *2 それ以前の PHP には str_contains() が存在せず、 strpos() や strstr() を代替品として使う手法が一般的だったのだ。 しかし、2023年7月現在サポートされている PHP8.0、8.1、8.2 には str_contains() が存在する。 つまり、 str_contains() が使えないバージョンの PHP はもうEOLを迎えているのだ。 *3 そのため文字列検索には str_contains() を使え。 strpos() や strstr() ではダメなのか? 今まで使っていた strpos() や strstr() がなくなるわけではないから、今まで通り使っても問題ないでしょ?? ひょっとしたら、あなたはそう思うかもしれない。確かにもっともだ。 しかし、よくよく考えて欲しい。もともと strpos() は「文字列内の部分文字列が最初に現れる位置を見つける」ための関数だ。 *4 また、 strstr() も「文字列が最初に現れる位置を見つけ、そこから文字列の終わりまでを返す関数」である。 *5 つまり、 これらの関数は、文字列が含まれているかを確認する関数ではない 。本来の目的が純粋な検索ではないのだ。 紙を切るために包丁を使っている人を見たら「いやハサミを使いなよ!」と言いたくなるだろう?それと似た感覚だ。 違和感が伝わったのであれば、文字列検索には str_contains() を使え。 strpos() を使うべきでない理由 もっと現実的な理由がある。 それは、 strpos() などを文字列検索に使うと、バグを生み出す可能性が高くなってしまうことだ。 strpos() などの戻り値は、FALSE だけでなく 0 を返すこともあるのがその原因だ。 まず、 strpos() の定義を確認しよう。 php .net: strpos strpos(string $haystack, string $needle, int $offset = 0): int|false 文字列 haystack の中で、 needle が最初に現れる位置を探します。 〜中略〜 needle が見つかった位置を、 haystack 文字列の先頭 (offset の値とは無関係) からの相対位置で返します。 文字列の開始位置は 0 であり、1 ではないことに注意 しましょう。 needle が見つからない場合は false を返します。 ドキュメントには上記の説明に合わせて、以下のように警告も記載されている。 つまり、 strpos() 関数は、 「文字列の先頭にキーワードが存在する場合は 0」を返し、「文字列にキーワードが存在しない場合は FALSE」を返す のだ。 これがどれだけ危険なことか、あなたが PHPer であればわかるだろう。 ❌ strpos() が意図しない動きをするコード <?php if ( strpos ( "お前たちは誰だ!" , "なんだ" ) ) { echo "なんだかんだと聞かれたらああああ、" ; // 表示されない echo "答えてあげるが世の情け" ; // (意図通り) } if ( strpos ( "なんだお前たち!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら \n " ; // 表示されない!! echo "答えてあげるが世の情け" ; // ("なんだ"を含むのに...) } <?php $ result = strpos ( "お前たちは誰だ!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(false) ← "なんだ"がないので FALSE が返る $ result = strpos ( "なんだお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // int(0) ← "なんだ"が文字列の先頭に存在するので 0 が返る! 上記の通り、文字列の先頭にキーワードが存在する場合は 0 が返ってくるので、 そのまま条件式として利用すると、"暗黙の型変換"が行われ意図していない比較結果になってしまう。 この問題はさまざまな記事で取り上げられているように、 !== による「厳密な比較」を行うことで回避は可能だ。 これにより、厳密に FALSE が返ってきた時のみ検知することができる。 ✅ strpos() を意図通りに動かすコード <?php if ( strpos ( "なんだお前たち!" , "なんだ" ) !== FALSE ) { echo "なんだかんだと聞かれたら \n " ; // 表示される echo "答えてあげるが世の情け" ; // (意図通りの挙動) } もちろん、このとき != は使ってはいけない。0 が返ってきたときに true と判定してしまうからだ。 いかがだろうか? strpos() を文字列検索目的に利用する場合、これだけのことを理解した上で利用する必要がある。 また、これらの考慮をうっかり忘れると、「キーワードが文字列の先頭にあるときだけ意図しない挙動をするバグ」を生み出すことになるだろう。 余計なことを考えながら実装するぐらいだったら、文字列検索には str_contains() を使え。 strstr() を使うべきでない理由 strstr() も同じだ。比較結果が意図しない値になる可能性がある。 strstr() の定義は以下のとおり。 php .net: strstr strstr(string $haystack, string $needle, bool $before_needle = false): string|false haystack の中で needle が最初に現れる場所を含めてそこから文字列の終わりまでを返します。 〜中略〜 部分文字列を返します。 needle が見つからない場合は false を返します。 strpos() の場合は以下のようなコードを書くことで意図しない動きをしてしまう。以下の例では、 検索対象である "0" が文字列中に存在するのに関わらず、文字列が表示されてしまう。 ❌ strstr() が意図しない動きをするコード <?php if ( strstr ( "今すぐ買うべき技術書トップ10" , "0" ) ) { echo "文字列中に 0 があるよ!" ; // 意図せず表示されない!! } <?php $ returnVal = strstr ( "今すぐ買うべき技術書トップ10" , "0" ) ; var_dump ( $ returnVal ) ; // string(1) "0" 何より strstr という名前が検索っぽくない。それより文字列検索には str_contains() を使おう。 preg_match() を使うべきでない理由 単純な文字列検索であれば、 preg_match() も使うべきでない。 php .net: strpos preg_match( string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0 ): int|false pattern で指定した 正規表現 により subject を検索します。 〜中略〜 preg_match() は、pattern が指定した subject にマッチした場合に 1 を返します。 マッチしなかった場合は 0 を返します。 失敗した場合に false を返します ここでの「失敗した場合」とは、指定した 正規表現 のパターンが不正な場合も含まれる。 つまり、 preg_match() では 指定した 正規表現 が間違っていた場合は FALSE を返し、文字列にマッチしなかった場合は 0 を返す のだ。 よって前述の関数と同じように結果の比較で誤った実装を行なってしまう可能性が高くなってしまう。黙って文字列検索には str_contains() を使うべきだ。 「いや、キーワードが先頭にあるかを知りたいんだ」という人は 少なくとも strpos() は危ない。文字列の先頭にキーワードがあるときは 0 、キーワードが存在しない場合は FALSE を返すため比較時にバグが発生しやすくなる。 この場合は、同じく PHP8.0 で追加された str_starts_with () を使おう。戻り値が bool のみであり安全になるだろう。 *6 「日本語でも使えるの?」との疑問について もちろん使える。 str_contains() の RFC にマルチバイト文字列についての記載がある。 *7 安心して文字列検索には str_contains() を使え。 もっと詳しく? この関数が PHP8.0 で実装されることになった詳しい経緯は、当時の RFC や マージリク エス トを確認するといいだろう。 スモールスタートにするため大文字小文字を区別する仕様のみ実装したことや、マルチバイト文字への対応についての意見などが確認できる。 PHP RFC: str_contains Internals Mailing Lists 提案と議論 投票開始 投票完了 マージリクエスト しっかりと議論が行われたことを確認したら、文字列検索には str_contains() を使え。 これでわかっただろう 文字列検索には str_contains() を使うべき理由がわかっただろう。 str_contains() の方が他の関数を使うより直感的であり、なにより無用な心配をしなくて済む。 なぜ今までこの関数がなかったのか不思議だが、実装されたからには積極的に str_contains() を使え。 ん?なんだって? str_contains() が undefined function でエラーになる?? ... PHP のバージョンが 7.4? なるほど。それじゃあ、しかたがない。諦めは心の養生というものだ。 www.php.net Written by: Y-Kanoh *1 : https://www.php.net/archive/2020.php#2020-11-26-3 *2 : https://www.php.net/manual/ja/function.str-contains.php *3 : https://www.php.net/supported-versions.php *4 : https://www.php.net/manual/ja/function.strpos.php *5 : https://www.php.net/manual/ja/function.strstr.php *6 : https://www.php.net/manual/ja/function.str-starts-with.php *7 : https://wiki.php.net/rfc/str_contains
アバター
はじめに みなさんこんにちはインフラエンジニアのa_renrenです。 日々、サーバを運用される方であれば、サーバの不具合や高負荷などで引き起こされるアラートの対応しているかと思います。 その対応手順は手順書としてまとまっていることが少なく、今までの経験則に基づいて対応を行ったり、ベテランメンバーに頼ったりしているところも少なくないかと思います。 自分のチームも各々で手順やノウハウをまとめたりしていますが、共通の手順がほぼない状況でした。そのような現状を打開するため、アラートの対応手順を作成したので、今回はその時の振り返りをしようかと思います。 文字ばかりになってしまい、読みにくいかと思いますがご容赦ください。 ※ 障害が発生した際は別途対応フローや手順がありますので、対象外としています ※ 一部、実際の運用方法と変えて記載したりしていますので、ご了承ください はじめに アラートの対応方法 なぜ取り組んだのか 作成したもの アラート対応手順を作成してみて 今後について アラートの対応方法 まずは、自分のチームでどういう感じでアラートを対応しているか軽くご紹介します。 自分が所属するチームでは2つの商材を担当しており、その2つの商材のアラートの対応をおこなっています。 各サービスごとサーバ監視システム(Zabbix)を使用してサーバの運用管理を行っており、そのシステムからのアラートをトリガーに対応を開始しています。 もし、アラート対応方法がわからない、判断できない場合は、チーム内で確認し、詳しいメンバーに判断仰ぎながら対応を行っています。 なぜ取り組んだのか この取り組みをする前は以下のような課題がありました。 新しいメンバーがアラートの対応を行いにくい 人によってアラートの対応方法に差がある アラート対応のノウハウがあまり共有できていない 一部のアラートの対応が属人化している チーム外の人がアラートの対応を行いにくい 経験が浅いとすぐに対応が必要なものとそうでないものの判断が難しい これらの課題を克服するために、アラートの対応手順を今回作成しました。 目的をまとめると以下のような感じです。 アラートの対応方法を統一化し、対応する人によるばらつきをなくす 各メンバーが持っているアラートの対応手順を共有できる場をつくる 新しいメンバーやチーム外のメンバーでもアラートの1次対応がスムーズに行えるようにする ターゲットを新しいメンバーやチーム外などのあまり商材の知識がない人にし、なるべく判断に迷わない具体的な手順を意識して作成を行いました。 また、対応手順と言っても時と場合によって対応方法が変わるため、最低でもこの手順を見れば1次対応が行えるようにしました。 作成はある程度、自分の経験などをもとにたたき台をつくり、情報が足りなければ詳しい人に確認しつつ作成を行いました。 作成したすべてのアラートの対応手順をチーム内でレビュー会を開いて意見を募り、最終的な形にしていきました。 作成したもの 今回、アラート対応手順は、 Markdown 形式で作成し、それを社内のGitLabにあげて管理するようにしました。 Markdown にしたのは、シンプルに作成でき、ある程度雛形を作成しておけば、今後他のメンバーが作成する際も あまり負担がないかと思ったからです。 GitLabで管理したのは、変更の差分が管理しやすく、 Markdown 形式で表示されて見やすいからです。 今回作成したフォーマットは以下のような感じです。 実際のアラート対応手順はお見せできないので、フォーマットとサンプル情報でお許しください。。。 ()内のは説明で実際には記載していません。 アラートの大まかな分類以外は目次にならないように Markdown 上で別途設定しています。 ## ディスク関連 (アラートの大まかな分類 ディスク関連のアラートはこの分類の中にまとめる) ### ・Free disk space is less than 10% on volume /test (通知されるアラートのタイトル) #### 【対象サーバ】 Webサーバ #### 【アラート内容】 /testのディスク容量が10%以下の状態 (アラートの発報条件やアラートが発報されることでどういう状態なのかを記載) #### 【アラート発生原因】 - 無駄なファイルが配置され続けている - Bの処理で不具合が発生している (考えられる原因を記載) (調査の際にここに記載した考えられる原因から調査が行いやすいようにする) #### 【アラート対応手順】 (アラートの対応手順を記載) 1. ディスク使用量を確認する 監視システムで直近の/testのディスクの使用量がいつからどのくらい増えているのか確認する 1. 削除してもよい不要なファイルを確認する 不要なファイルの判断ができない場合はチーム内に確認する ●削除してもよいファイル ・ファイル名にtestが含まれているもの ・最終更新日が1年前のもの ```bash /testの現状の空き容量を確認 $ df -h /test /test配下にあるファイルを確認し、不要なファイルをリストアップする $ ls -la /test ``` 1. 不要なファイルを削除する ```bash 上記で確認した不要なファイルを削除する $ sudo rm /test/不要なファイル ``` 1. Bの処理が正常に動いているログから確認する エラーが出ているようであれば、チーム内に共有し、原因の調査をおこなう ```bash Bの処理のログを確認し、エラーが出力されてないか確認する $ grep 'error' /var/log/B.log ``` GitLab上では、以下のように表示されます。 GitLabでの表示内容 アラート対応手順を作成してみて 今回アラートの対応手順を作成してから、まだ月日は経っていないため、あまり活躍はできてなさそうですが、 以前よりアラート対応方法が統一になったり、ノウハウの共有が行いやすくなったり、 新メンバーへのレクチャーも行いやすくなったかと思います。 上記以外にも、アラートの対応手順を作成していく過程でメリットがありました。 一つ目は、不要なアラートを洗い出し、削除することが出来たことです。 アラートの対応手順を作成していく中で、いろいろなアラートが見つかりました。。。 何のためのアラートなのかわからないアラート 今はもう不要になったが、削除されず残り続けているアラート 同じようなことを監視しているアラートなど 上記のように必要なアラート以外に様々なアラートが見つかりました。その見直しをこの機会に行えたことで、不要なアラートを削減することができ、不必要な対応する負担が減りました。また、より良い監視方法が見つかったりと、あまり注目されてこなかった箇所にメスを入れることができました。                                                                                                                                                                                                                  二つ目は、アラートがならないよう仕組みづくりの見直しをおこうことができたことです。 アラートの予測される発生原因を探っていく中で現状の設定値がベストでないことに気づき、設定値の改善をおこなったり、 ある条件で失敗する可能性がある スクリプト の処理を失敗しないように スクリプト の実装方法を変えるなどの見直しを行うことができました。 また、作成していくなかで対応しきれなかった課題もありました。 似たようなアラートの対応手順を一つにまとめきれず、アラートごとに一部似たような対応手順をそれぞれに記載したため、メンテナンス性が悪い状態になりました。 似たような手順を外出ししようかとも思ったのですが、対応するメンバーがいろんなファイルを行ったり来たりすると対応しにくいかと思い、あえて外出しはしませんでした。 今後、対応できるサーバを増えるにつれ、アラートの種類も増え、よりメンテナンス性が悪くなりそうなので、早めに作成者と対応者のどちらの負担も軽減できるように構成を変えていく必要がありそうです。 今後について 今回は、一部のサーバのアラート対応手順しか作成できなかったため、今後も上記の課題を解決しつつ、引き続き残りのサーバのアラート対応手順を作成をチームで進めていこうかと思います。 また、並行してアラートの削減やアラート対応の自動化などの根本的な対応も進めていければと思っています。 皆さんもこの機会にぜひアラートの見直しや対応手順の作成に取り組んでみてはいかがでしょうか。 最後までお読みいただきありがとうございました。
アバター
はじめまして。配配メール開発課所属Jazumaです。 本稿では昨年2022年に当社プロダクト配配メールにおいてCI/CDパイプラインを整備した過程やその結果についてご紹介します。 CIツールの使い方やCI/CDとは何かといった内容は取り扱いません。あらかじめご了承ください。 プロダクトについて 2022年開始時点の状況 施策1: 運用ルールの整備とCIツールの移行 施策1の結果 施策2: サブシステム構築に伴うユニットテスト・静的解析の整備 施策2の結果 施策3: サブシステムの検証環境への自動デプロイ 施策3の結果 施策4: メインシステムの検証環境への自動デプロイ 施策4の結果 施策5: アーキテクチャテストの導入・静的解析の拡充 施策5の結果 現時点の課題 1. 昔からあるコードにはテストを追加できていない 2. 自動デプロイのスコープがアプリケーションのみに留まっている 最後に プロダクトについて 弊社プロダクト配配メールは2007年サービス開始の長寿サービスです。 www.hai2mail.jp 長らくお客様にご愛顧いただいていましたが、CI環境が未整備・ ユニットテスト が少ない・静的解析がなく目視でコードレビューする等開発環境・体制に課題がありました。 そこで、開発体制を改善すべく2022年度にCI環境を拡充しました。 2022年開始時点の状況 ユニットテスト はちょっとある E2Eテストは整備されている 静的解析は存在しない Jenkinsでpushの度にテストを実行しているが、あまり運用されていない 施策1: 運用ルールの整備とCIツールの移行 まずは4つ目の問題を解消するためにCIの運用ルールを整備しました。 ルールと言っても複雑なものではなく、「テストが失敗したらメンバーのメールアドレスに通知が飛ぶ。通知を受けたら原因を調査する」というシンプルなものでした。 とはいえルールが整備されたことでテストが失敗した時の対応漏れが減りました。 また、合わせてCIツールをJenkinsからGitLabCI/CDに移行しました。 理由は以下の通りです。 Jenkinsfileよりもgitlab-ci.ymlの方が読み書きしやすいという意見があり、それに対する反論が特になかった JenkinsとGitLabを連携する手間が減る GitLabCI/CDの方がUIが洗練されており使いやすい 施策1の結果 テストが拡充したわけではないため、劇的な効果はありませんでした。 とはいえこの施策によってチームのCI基盤が整ったため、後の改善の土台となる重要な改善だったと言えます。 施策2: サブシステム構築に伴う ユニットテスト ・静的解析の整備 2022年8月に「業種業態・配信目的別スコアの確認」という機能をリリースすることになりました。この機能では【全サーバ全アカウントのスコア(メールの 開封 率や挿入されたURLのクリック率)を集計する】という要件が求められました。 www.hai2mail.jp この要件を満たすために、データ集計用の新規サブシステムを構築することになりました。 新規サブシステムは既存システムのコードや アーキテクチャ の影響を受けないということで、「静的解析と ユニットテスト をしっかりと作り込もう」という方針で開発を進めました。 新規サブシステムはメインシステムと同様に PHP で開発するため ユニットテスト には PHPUnit ・静的解析にはPHPStanを採用しました。 この辺りは極めてオーソドックスな技術選定だったのではないかと思います。 CIの実行ルールも特に変わったところはなく、pushやマージの度にパイプラインを実行するというものです。 施策2の結果 この施策は結果としては成功だったと言えます。具体的には2つの成果につながりました。 1つは「業種業態・配信目的別スコアの確認」機能を計画通りリリースできた上、この機能に関して2023年6月現在不具合が発生していないことです。 この機能は難易度が高く計画の遅れなどが懸念されていましたが、上記の通り大きな問題なくリリースすることができました。その要因の一つとして静的解析と ユニットテスト で ソースコード の品質を作り込むことができたこともあったのではないかと思います。 もう1つは「 ユニットテスト を書く」という文化がチーム内に広がったことです。「業種業態・配信目的別スコアの確認」機能の次のバージョンにて「フォローメール」という大規模な新機能がリリースされました。この機能の開発時にも可能な限り ユニットテスト が作成されました。 www.hai2mail.jp 施策3: サブシステムの検証環境への自動デプロイ パイプラインにサブシステムを検証環境に自動でデプロイする処理を追加しました。 この施策には明確な目的があったわけではなく「せっかくの機会だし自動化できる所はしておこう」くらいの意識で進みました。 仕組みとしてはごくシンプルでタグを作成する ⇒ アプリケーションに必要なファイル一式を含んだtar.gzファイルを作成 ⇒ 検証環境にデプロイする というものでした。 施策3の結果 この施策はあまり効果がありませんでした。サブシステム自体の変更頻度が低くデプロイが実行される機会がほとんどなかったことに加え、仕組みが完成した時期が遅く、活用できる場面が無かったことが原因です。 しかし、今までCI/CDパイプラインの実装経験が無かったメンバーが担当したことでチーム内に知見が広まった他、逆説的に以下のような教訓が得られました。 自動デプロイは変更頻度が高いシステムから優先的に実装すべきである 機能が完成してから自動デプロイの実装に着手しても遅い。機能開発と並行して進める必要がある 施策4: メインシステムの検証環境への自動デプロイ 2022年の秋に システムテスト を改善する取り組みを行いました。 ここでは「テストの品質を上げる( = バグを検知できるようにする)」「テストを効率化する( = 工数 を削減する)」という2つの目的の元改善作業を行いました。 自動デプロイは2つ目の目的を達成するために実施しました。 試算では システムテスト 中のデプロイおよびデプロイ作業漏れに起因する手戻りが年間10時間程度かかっていたため、自動化の効果が大きいと判断されました。 今回は施策3の教訓を踏まえて「変更頻度の大きいシステムを対象にする」「 システムテスト までに完了させる」という方針で進めました。 具体的には対象をメインシステムに絞り、デプロイ処理を追加しました。 今回は全自動化したいということで、タグの作成ではなく「リリース用のブランチへのマージ」をトリガーとしてデプロイ スクリプト を起動する実装としました。 施策4の結果 この施策は成功しました。 「定期的にリリースブランチへのマージを確認して検証環境へのデプロイを実行する」という雑務が無くなったことで稼働に空きができた他、割り込み作業がなくなりました。 また、検証環境が常に最新の状態に保たれていることが担保されるようになったため 不具合が見つかった場合の原因調査がスムーズになりました。 施策5: アーキテクチャ テストの導入・静的解析の拡充 2023年5月に添付ファイルを直接メールに添付せずに送信する機能をリリースしました。 www.hai2mail.jp この機能においてもサブシステムを新しく構築しました。 今回は施策2のような ユニットテスト ・静的解析に加えて新しく2つのことを試みました。 1つは アーキテクチャ テストです。 deptrac というツールを用いてクラス間の依存関係を検査するようにしました。 これにより、関数やクラス単位の品質だけではなく「クラス同士の依存関係が適切に設定されているか」という点まで担保できるようになりました。 もう1つは静的解析の拡充です。施策2で導入したphpStanでは主に型定義や未定義変数の検出等、一般的な観点を検査しました。 今回はコード 規約違反 の検出など、よりチームの実情に即した観点を検査することになりました。 静的解析ツールとしては PHP_CodeSniffer を採用しました。 (静的解析はこまめに実行できる方が手戻りのコストが小さく済むので、 PHP _CodeSnifferはCIに加えて IDE 上でも実行できるようにしました。本稿の趣旨からは逸れますので詳しくは取り上げません。) 施策5の結果 この施策も施策2と同様に成功しました。特に PHP _Codesniffer導入の恩恵が大きかったです。 今までは目視で確認していた項目を 機械的 に検出できるようになったため、コードレビューの負担が大きく減りました。 PHP _Codesniffer導入についてはこちらの記事でも触れていますのでぜひご一読ください。 tech-blog.rakus.co.jp 現時点の課題 2022年には配配メール開発チームにおいてCI/CDパイプラインが大きく拡充されました。 しかし、課題も多く残っています。 1. 昔からあるコードにはテストを追加できていない テストコードは主として新規に追加されるコードを対象に実装されました。 しかし、配配メールのコア機能を支え続けている古いコードにはほとんどテストコードを追加することができていません。 昔からあるコードは複雑度が高かったり密結合だったりしており、テストコードを書くことができない状態です。 テストを書くために リファクタリング しようとしても影響範囲が大きくなかなか手が出せないという(よくある) ジレンマに直面し続けています。 2. 自動デプロイのスコープがアプリケーションのみに留まっている 現状では自動デプロイの対象はアプリケーションコードのみです。環境設定やデータベース・ ミドルウェア の変更は手動で行う必要があります。 本来であれば環境・データベース・ ミドルウェア 含めて常に正しい状態を再現できるようになっているべきですが、稼働に余裕がない・ノウハウが足りないため手が回っていません。 最後に ここまでお読みいただきありがとうございました。 今後も配配メール開発チームではCI/CDパイプラインを活用して より高品質なプロダクトを開発できるように努めていきます。
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年10月のイベントでは「 PHP のリーダブルなコード」について語り合いました。 弊社のメンバーが事前にまとめてきたコードの書き方の事例にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com 特集:PHPのリーダブルなコード 初級 Sample 1 BADコード 良くない理由 解消例 Sample 2 BADコード 良くない理由 解消例 パターン1:早期リターンを活用 パターン2:条件を関数に閉じ込める Sample 3 BADコード 良くない理由 解消例 中級 Sample 4 BADコード 良くない理由 解消例 Sample 5 BADコード 修正前 修正後 良くない理由 解消例 パターン1:関数ごとに処理を分割 パターン2:デリミタ(区切り文字)を引数にする Sample 6 BADコード 良くない理由 解消例 上級 Sample 7 BADコード 良くない理由 解消例 Sample 8 BADコード 良くない理由 解消例 編集後記 特集: PHP のリーダブルなコード この回では弊社が用意したBADコードをトピックに、「どこが悪いのか」・「どうすれば良くなるか」を議論しました。 BADコードは難易度別に初級・中級・上級に分かれており、全部で8問ございます。 元ネタ: GitHub - piotrplenik/clean-code-php: Clean Code concepts adapted for PHP 初級 Sample 1 BADコード <?php if ( $ foo === $ bar ) { return true ; } else { return false ; } 良くない理由 bool 値を返したいときにif文を書くのは冗長 解消例 結果をbool値で返したい場合、明示的に if-else 文を書かなくとも、return 文に条件式を書くことで比較結果を戻り値にできます。 <?php return $ foo === $ bar ; 参加者からは次のようなご意見を頂きました。 あまり馬鹿にできなくて、何回か現場でも実際に見たことがあります 初学者は「"比較" = "IF文"」と錯覚しがちですね Sample 2 BADコード <?php if ( $ isOK && ! hasPermission && $ hoge !== “sample” ) //何らかの処理 } 良くない理由 if文の条件が複雑になると、単純に読みにくくなるだけでなく、不具合も起こりやすくなる isOK という変数名が微妙 解消例 パターン1:早期リターンを活用 <?php if ( !$ isOK ){ return "NG" ; } if ( hasPermission ){ return "OK" ; } if ( $ hoge !== "sample" ){ //なんらかの処理 } パターン2:条件を関数に閉じ込める <?php if ( 条件がわかりやすく言語化された関数名 ()){ // なんらかの処理 } Sample 3 BADコード <?php if ( $ input == 0 ) { echo ( ‘ 0 です!’ ) ; } 良くない理由 == の場合、判定が曖昧なのでバグのもとになりやすい PHP8.0以前の場合、文字列と数値の比較をする際に文字列が数値にキャストされるので危険 PHP の比較 演算子 については弊社のブログでも深掘りしておりますので、ご興味のある方はぜひ御覧ください tech-blog.rakus.co.jp 解消例 === に比較 演算子 を変更します。 これにより、左辺・右辺の値が等しく、かつデータ型も一致する場合のみ true を返すため、データ型の不一致による予期しない結果を避けることができます。 <?php if ( $ input === 0 ) {   echo ( '0です!' ) ; } 中級 Sample 4 BADコード <?php /** * お店が営業日かをチェックする * * @param $day 曜日の文字列 * @return bool */ function isShopOpen ( $ day ) : bool { if ( $ day ) { if ( is_string ( $ day )) { $ day = strtolower ( $ day ) ; if ( $ day === 'friday' ) { return true ; } elseif ( $ day === 'saturday' ) { return true ; } elseif ( $ day === 'sunday' ) { return true ; } return false ; } return false ; } return false ; } 良くない理由 このコードについて良くない理由として、以下の2点が考えられます。 どういうチェックが必要なのかがわかりにくい 分岐をすべて読まないとチェックしたい内容がわからない $day が以下の場合 true friday saturday sunday 分岐が進むにつれて、該当処理が実行される条件の数が多くなる これまでの分岐条件を記憶していかないといけない 引数に型指定されていない メソッド内で型チェックが必要 ロジックを読むまで$day が String 型であることがわからない 引数名からDateTimeクラスの インスタンス とも捉えられる だが実際はString型で受け取ることを前提とした処理になっている 参加者からは次のようなコメントが寄せられました。 「if-elseで条件分岐を作る際は、単 純化 できないか考えるチャンス」 その他、参加者からは「ネストがV字に広がっている様子がまるで 波動拳 のようだ」というコメント から有名な ミーム 画像が紹介され、「本格的な 波動拳 ですね」、「今度コードレビューで使いたい!」などコメントが寄せられ、かなり盛り上がりを見せていました。 解消例 解消例は以下になります。 <?php /** * お店が営業日かをチェックする * * @param string $day 曜日の文字列 * @return bool */ function isShopOpen ( string $ day ) : bool { // 値がセットされているか(これがガード節) if ( empty ( $ day )) { return false ; } $ openingDays = [ 'friday' , 'saturday' , 'sunday' ] ; return in_array ( strtolower ( $ day ) , $ openingDays , true ) ; } 主な修正点は以下のとおりです。 $day の引数型をString型で宣言することで、データ型のミ スリード をなくす 前提部分のチェックをガード節で対応することで、余計なネストを生まないようにする 営業日を$openingDays に入れることでいつが営業日かわかりやすくなり、変数名が説明変数を担っている こちらに関して、参加者からは次のようなコメントを頂きました。 match文 でもかけそう empty() はないほうが良い ※empty() の場合、変数が存在し、かつ値が空でない場合のみ false が返されます。ここで実施したいのは空文字チェックですが、empty() では仕様上、空文字でも true が返却されるため、空文字チェックとしてempty()を使用するのは適切ではありません。 PHP Sandbox - Execute PHP code online through your browser 列挙型(enum) も活用できそう Sample 5 BADコード このケースは少し特殊で、 修正前のコード を間違えて 修正後のコード に直してしまった、というケースを想定して作られています。 修正前 <?php public function getRecipeListString () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( "," , recipeList ) ; } 修正後 <?php public function getRecipeListString ( bool $ isSpace ) : string { $ recipeList = getRecipeList () ; //何かレシピのリストを配列で取得するもの if ( $ isSpace ) { return implode ( “ ”, $ recipeList ) ; } else { return implode ( “,”, $ recipeList ) ; } } 良くない理由 違う要件が来たときにまたif文が増える 仮に「レシピのリストを ハイフン区切り で表示する」といった仕様になった場合、新たに分岐を追加する必要がある 解消例 解消例として以下の2パターンが挙がりました。 パターン1:関数ごとに処理を分割 「カンマ区切り」・「スペース区切り」と、要件ごとに同じ処理を関数に区切ったパターンです。 <?php public function getRecipeListStringWithComma () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( "," , $ recipeList ) ; } public function getRecipeListStringWithSpace () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( " " , $ recipeList ) ; } パターン2:デリミタ(区切り文字)を引数にする <?php public function getRecipeListString ( string $ delimiter ) : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( $ delimiter , $ recipeList ) ; } またパターン2について、参加者からは 「$delimiter に初期値を入れてはどうか?」 という意見がありましたが、こちらについて以下のような反応がありました。 確実に必要ではない引数にはじめからデフォルト値をセットしないほうが良いと思う 既存のものを移行するなどのケースであれば、オプショナルにするのもあり データが収束しているように見えるからメソッドが気になる Sample 6 BADコード <?php //メルマガ購買顧客リストまたはYoutubeチャンネル登録会員リストを更新する function updateMailMagazineListOrYoutubeList ( $ accountId , $ MailMagazine , $ Youtube , $ isMailMagazine , $ isYoutube ) { if ( $ isMailMagazine ) { # code… } elseif ( $ isYoutube ) { #code… } if ( $ isMailMagazine ) { $ sql = “update mailMagazineList set …”; } elseif ( $ isYoutube ) { $ sql = “update youtubeList set…”; } } 良くない理由 別のビジネス概念を無理やり1つの関数の処理にまとめている フラグを引数で渡しているのでビジネス概念が増えるほど引数が増える 今後さらにビジネス概念が増えた場合、より分岐が複雑化する 実装者は共 通化 したいという意図を持っていたと思われるが、結局共 通化 できていない 参加者の方々からも次のようなコメントが寄せられました。 割りとよく見る 既存のものに焼き増しで追加した結果こうなってしまった? なかなかリアルなケース 等、現場でも見覚えのある方が多くいらっしゃったようです。 解消例 異なるビジネス概念を扱うなら、関数・クラスは分けるべき <?php function updateMailMagazineList ( $ accountId , $ Mail Magazine ) { #code… $ sql = “update mailMagazineList set,,,” #code… } function updateYoutubeList ( $ accountId , $ Youtube ) { #code… $ sql = “update mailMagazineList set…” #code… } 上級 Sample 7 BADコード <?php /** * PHPによる形態素解析処理 * * * @param string $code 文章 */ function parseBetterPHPAlternative ( string $ code ) : void { $ regexes = [ //… ] ; $ statements = explode ( ‘ ‘ , $ code ) ; $ token = [ ] ; foreach ( $ regexes as $ regex ) { foreach ( $ statements as $ statement ) { //… } } $ ast = [ ] ; foreach ( $ tokens as $ token ) { //lex… } foreach ( $ ast as $ node ) { //parse… } } 良くない理由 1つの関数で複数の処理を行っている 修正時の影響が大きくなる 修正箇所の後続処理への影響を考えて修正しなければならない 不具合が発生した際の問題箇所の特定が困難 複数の処理が組み込まれていると処理の前後関係を理解する必要がある 処理が分割していれば問題箇所を特定しやすくなる 内部処理の再利用ができない 処理の中に組み込まれてしまうと特定処理だけを他の処理でも利用したくても利用できない テストが書きにくい 処理が分割されていれば細かな条件のテストコードが書ける 参加者からは次のようなコメントを頂きました。 一度に一つ以上のことをやらないでほしい... サブルーチンに切り出してほしい 等の意見が挙がっており、やはり1つの関数で複数の処理を実行することに否定的な意見が多く寄せられました。 解消例 parseBetterPHPAlternative を3つのクラスに分割することで、可読性を高めつつ、各処理が依存していない状態に修正しました。これならば ユニットテスト も書けそうですね。 <?php /** * トークナイザ */ class Tokenizer { /** * 文章を単語に分解する * * @param string $code 文章 * @return array $tokens 単語のリスト */ public function tokenize ( string $ code ) : array { $ regexes = [ // ... ] ; $ statements = explode ( ' ' , $ code ) ; $ tokens = [] ; foreach ( $ regexes as $ regex ) { foreach ( $ statements as $ statement ) { $ tokens [] = /* ... */ ; } } return $ tokens ; } } /** * 字句解析器 */ class Lexer { /** * 単語の解析処理 * * @param string $tokens 単語のリスト * @return array $ast 解析結果のリスト */ public function lexify ( array $ tokens ) : array { $ ast = [] ; foreach ( $ tokens as $ token ) { $ ast [] = /* ... */ ; } return $ ast ; } } /** * PHPによる形態素解析処理 * * @param string $code 文章 */ class BetterPHPAlternative { /** @var Tokenizer */ private $ tokenizer ; /** @var Lexer */ private $ lexer ; public function __construct ( Tokenizer $ tokenizer , Lexer $ lexer ) { $ this -> tokenizer = $ tokenizer ; $ this -> lexer = $ lexer ; } /** * 形態素解析 * * @param string $code 文章 */ public function parse ( string $ code ) : void { $ tokens = $ this -> tokenizer -> tokenize ( $ code ) ; $ ast = $ this -> lexer -> lexify ( $ tokens ) ; foreach ( $ ast as $ node ) { // parse... } } } Sample 8 BADコード FavoriteRecipe クラス <?php class FavoriteRecipe { public function getRecipe ( string $ name ) : void { $ limit = 10 ; $ recipeRepository = new RecipeRepository () ; $ recipes = $ recipeRepository . findByName ( $ name , $ limit ) ; foreach ( $ recipes as $ recipe ) { var_export ( $ recipe ) ; } } } RecipeRepository クラス <?php class RecipeRepository { // Cookpadからレシピを取得する function findByName ( string $ name , int $ limit ) : Recipe { $ cookPad = new CookPad ( new \PHPHtmlParser\Dom ) ; $ result = $ cookpad -> search ( $ name , 1 , $ limit , false ) ; return $ result ; } } 良くない理由 FavoriteRecipeクラスがRecipeRepositoryクラスの実装に依存している RecipeRepository::findByName に何かしら影響が発生した場合、FavoriteRecipeクラスにも影響が及ぶ 例. findByNameメソッドの引数を増やした場合 <?php class RecipeRepository { // 引数を修正 - function findByName(string $name, int $limit):Recipe { + function findByName(string $name,int $pageNum, int $limit, bool $isRandom):Recipe[] { // ... } } class FavoriteRecipe { public function getRecipe(string $name):void { // ... // reposiotoryの修正を受けて引数を修正 - $recipes = $recipeRepository.findByName($name, $limit); + $recipes = $recipeRepository.findByName($name, $pageNum, $limit, $isRandom); } } 解消例 SOLID の原則の D:依存性逆転の原則(DIP) を活用して、 上位モジュールのFavoriteRecipeクラス が 下位モジュールのRecipeRepositoryクラス に依存しなくなるようにクラス構成を修正します。 RecipeRepositoryインターフェースを作成 <?php interface RecipeRepository { public function findByName ( string $ name , $ pageNum = 10 , $ limit = 10 , $ isRandom = false ) : Recipe [] ; } RecipeRepositoryクラスはRecipeRepositoryインターフェースを実装する <?php class RecipeRepositoryImpl implements RecipeRepository { public function findByName ( string $ name , $ pageNum = 10 , $ limit = 10 , $ isRandom = false ) : Recipe [] { $ cookpad = new Cookpad ( new \PHPHtmlParser\Dom ) ; $ result = $ cookpad -> search ( $ name , $ pageNum , $ limit , $ isRandom ) ; return $ result ; } } FavoriteRecipeクラスはRecipeRepositoryインターフェースを参照する <?php class FavoriteRecipe { private RecipeRepository $ recipeRepository ; public function __construct ( RecipeRepository $ recipeRepository ) { $ this -> recipeRepository = $ recipeRepository ; [ f : id : d - t - kong : 20230530144526j : plain ][ f : id : d - t - kong : 20230530144526j : plain ] } public function getRecipe ( string $ name ) : void { $ pageNum = 10 ; $ limit = 10 ; $ isRandom = false ; $ recipes = $ this -> recipeRepository . findByName ( $ name , $ pageNum , $ limit , $ isRandom ) ; foreach ( $ recipes as $ recipe ) { var_export ( $ recipe ) ; } } } こうすることで、FavoriteRecipeクラスがFavoriteRecipeクラスに依存しないクラス構成になりました。 こちらの解消例について、参加者の方から次のようなご指摘をいただきました。 RecipeRepositoryImpl::findByNameの ユニットテスト を実行するたびに Cookpad にHTTPリク エス トが飛ぶことになる 複数人で ユニットテスト を同時に実行したら、 Dos攻撃 になるのでは!? ユニットテスト 時には、外部 API へのHTTPリク エス トを避ける必要があります。参加者の間では以下の改善案が提案されました。 テストモックを準備する Cookpad の インスタンス 生成部分をもう一段階DI Cookpad が PSR-18 に準拠している場合、HTTP通信部分のみを外部から切り出す 編集後記 以上、 PHP のリーダブルなコードについてまとめました。 今回提示した アンチパターン を業務でやってしまった!という方も中にはいらっしゃったのではないでしょうか? 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております!
アバター