rhanda | 元銀行員Web系エンジニアの日記

実務未経験からWeb系受託開発企業に転職したひよっこエンジニアが覚えたことや日々の感情を残すブログ

『実践Claude Code入門』読みました

感想

ざっくりとClaude Codeの概要を体系的に知りたくて読みました。 色々な用語、できること、Tips、実際に自律的に動かして開発を進める様子を見ることができてよかったです。 コンテキストエンジニアリングの話、それに付随して永続的・一時的なドキュメントを残していく方法が参考になりました。

MCPの設定

方法は2つ

  • claude mcpコマンドを利用する
  • 直接 .mcp.jsonファイルを編集する

CLAUDE.md

メモリとして機能する。

  • 機能開発以外のルールも必要に応じてメモリに指示しておくと良い。
  • それらの指示から生成されるものはすべて、AIが計画・ドキュメント化して人間が監視するフローを徹底すると良い

Claude Code Action

GitHub Actionsのワークフロー内のステップでClaude Codeを実行するためのアクション

アンビエントエージェント

  • イベントに応じて動作するAIエージェント
  • アンビエントエージェントの考え方では、イベントに応じてAIエージェントが人間より先回りして動き、必要に応じて人間に通知・質問・レビューといった関与を求める

AIエージェント

環境・知覚・メモリ・推論・行動という5つの要素から成り立つ。
これらが連携することで、指定されたゴールに対して自律的に行動するような振る舞いを実現する

コンテキストウィンドウ

LLMが新しいテキストを生成する際に参照できるテキストの総量と、生成する新しいテキストの全体。
CLAUDE.mdをロードしたりファイルをReadしたりWebから情報をWebFetchしたり、Bashによるコマンドの実行結果を保持したり、これまで行ってきたあらゆる動作を記録するために消費される

コンテキストウィンドウの自動圧縮

一定の閾値(デフォルト95%)を超えると、自動圧縮(auto-compaction)が実行される。これはコンテキストウィンドウがいっぱいになることを防ぐための自動的な情報処理プロセス。
過去の詳細な情報は、大幅に要約される可能性がある。

Context Rot

入力トークン量の増加につれて、LLMが情報を正確に参照・理解する能力が低下する現象

コンテキストエンジニアリング

  • AIエージェントの持つ限られたコンテキストウィンドウを管理するための技術
  • Claude Codeが長時間のタスクで過去の指示を忘れてしまう課題があり、この根本的な原因は「コンテキストウィンドウの容量制限」と「Context Rotによる性能低下」だった。

アプローチ

1. ツール結果のクリア

  • 過去のツール呼び出し結果をコンテキストから削除しながら、ツール呼び出し自体の記録は保持する方法
  • e.g. Claude Codeで実行するコマンドを設計する際、詳細はログファイルに書き出し、標準出力には概要のみを表示する

    2. マイルストーンごとのでの履歴削除

  • 手動で会話履歴を削除(/clear)することで、これまで消費されたコンテキストウィンドウを開放する

    3. 圧縮前の外部化

  • 圧縮によって失われる可能性のある重要な情報を、外部ファイルに記録する
  • CLAUDE.mdに構造化ノートのルールを定義しておけば、自動的に重要な情報を外部ファイルに記録してくれる容易になる
  • 構造化ノートは、重要な情報をコンテキストウィンドウの外部にあるファイルとして永続化する戦略
    • 実装中に重要な設計判断や技術選択をおこなった場合、その理由をdicisions.md(例)に記録することをCLAUDE.mdでルール化しておくと良い
    • 後から「なぜこの方法を選んだのか」を振り返ることが可能になる

サブエージェント

  • 親エージェント(メインのClaude Code)から独立したコンテキストウィンドウを持つ、専門化されたAIアシスタントのこと。
  • 親エージェントは、サブエージェントに作業を委譲し、結果だけを受け取る
  • Claude Codeのサブエージェントは、マークダウンファイルとして定義される

制約と効果的な使い方

  • 新しいサブエージェントを作成する際は、Claude Codeに生成させてから、カスタマイズすることが推奨される
  • 親エージェントのコンテキストは参照できない
  • サブエージェントに具体的な実装などの作業を実施されるのは、あまりうまくいかないパターン
  • そのため、コンテキストの圧迫を避けたいタスクを実施させるのが効果的
  • サブエージェントは、詳細な作業を行った後、その結果を簡潔なレポートとしてまとめる

Planサブエージェント

  • 組み込みのサブエージェント。プランモード(実行せずに計画のみを立てるモード)で利用される

スキル

効果的に使うための3つのポイント

  1. 単一責任の原則
  2. 1つのコマンドは1つの目的に集中
  3. 適切な粒度
  4. 明確な実行フロー

フック

  • Claude Codeのライフサイクルの特定タイミングで、スキルやスクリプトを実行する仕組み
  • プロンプトではなくプログラムとして動作するため、100%確実にルールを適用できる
  • スキルはプロンプトベースの仕組みであるため、Claude Codeが従うかどうかはLLMの推論内容に依存する。そのため、「〜してはいけない」と指示しても守られるとは限らない

フックの種類

10種類用意されている(ツール実行前、サブエージェント完了時、セッション開始時など)

Sandboxing

  • 組み込みのセキュリティ機能
  • Bashツールは強力だが、適切な制限がないと機密情報の漏洩や、システムファイルの改変が意図せず行われるリスクがある
  • OSレベルの仕組みを使って、ファイルシステムとネットワークへのアクセスを制限することで、これらのリスクを軽減する
  • サンドボックス内での安全なコマンドは承認なしで実行でき、サンドボックス外にアクセスしようとすると即座にブロックされる

DevContainer

開発環境全体をコンテナ化し、分離された環境でClaude Codeを実行できる。これにより、ホストシステムへの影響を遮断しながら自律的に動作させることが可能になる。

MCPとコード実行の組み合わせによる効率化

複数のMCPサーバーをClaude Codeに接続すると、ツールが数十、数百と増えて、数十万トークンを消費する可能性がある。 この対応策として、「コード実行環境を開始てMCPサーバーにアクセスする」方法が紹介されている 具体的には、MCPサーバーのツールをTypeScript関数としてラップし、それらをファイルシステム上に配置し、エージェントは必要な関数だけを読み込んで実行することで、内部的にMCPサーバーを呼び出す。

自動実行

  • 自動実行を前提とする場合、例外処理ルールを明示的に定義することが大切
  • ヘッドレスモード
    • Claude Codeを、対話なしでプログラム的に実行する機能
    • 使用するには、-p または --print オプションを指定
    • e.g. claude -p "src/ディレクトリのコードを説明してください"
  • REPLモード
    • Claude Codeが承認が必要なツールの使用時にユーザーに承認を求める
    • ただし、Shift + Tabで権限モードを切り替えられる
      • normal mode:承認が必要なツールの使用時に確認を求める
      • accept edits:編集操作は自動承認、その他の承認が必要なツールの実行は都度確認
      • plan mode:実行はせず、計画のみ作成

【Rails】仮想カラムを使って論理削除を考慮したユニーク制約をする

やりたいこと

あるテーブルにおいて、特定のカラムの組み合わせ(e.g. user_idcommon_id)がユニークになるように制約を設けていました。

またこのテーブルではacts_as_paranoid のgemを用いた論理削除が導入されています。

ここで、利用上は論理削除されたレコードはもう無くなったものとして、削除済みのレコードと同じ common_id を使って新しくレコードを作れるようにしたいです。

しかし、テーブルには論理削除されたレコードを考慮せずにユニーク制約を設定していたため、DBのユニーク制約に引っかかってエラーになってしまうという問題がありました。

あった課題

  • テーブルでは user_idcommon_id の組み合わせに対するユニークインデックスが設定されていた。
  • アプリケーション側でも user_idcommon_id の組み合わせをユニークにするバリデーションを設定していた。
  • アプリケーション上では論理削除済みのレコードが無視されるため、削除済みのレコードと同じ common_id で新しいレコードを作成しようとするときはバリデーションエラーにならなかった。
  • DBのユニーク制約は論理削除を考慮しないため、INSERT時にエラーが発生していた。

やったこと

  1. deleted_at が NULL の場合にのみ値を格納する仮想カラム active_common_id を追加した。
  2. DB側でuser_idactive_common_id の組み合わせでユニーク制約をした。
class ChangeTaskUniqueIndex < ActiveRecord::Migration[7.1]
  def up
    change_table :tasks, bulk: true do |t|
      t.remove_index name: 'index_taskss_on_user_id_and_common_id', column: %i[user_id common_id], unique: true
      t.index %i[user_id common_id], name: 'index_tasks_on_user_id_and_common_id' # 元の組み合わせでも、インデックス自体はあった方が良さそうなので残した
      t.string :active_common_id, as: 'IF(deleted_at IS NULL, common_id, NULL)'
      t.index %i[user_id active_common_id], unique: true, name: 'index_tasks_on_user_id_and_active_common_id'
    end
  end

  def down
    change_table :tasks, bulk: true do |t|
      t.remove_index name: 'index_tasks_on_user_id_and_active_common_id', column: %i[user_id active_common_id]
      t.remove :active_common_id
      t.remove_index name: 'index_tasks_on_user_id_and_common_id', column: %i[user_id common_id]
      t.index %i[user_id common_id], unique: true, name: 'index_tasks_on_user_id_and_common_id'
    end
  end
end
# schema.rb

++ t.virtual "active_common_id", type: :string, as: "if((`deleted_at` is null),`common_id`,NULL)"

これによって未削除のレコード同士でのみ common_id のユニークか検証できるようになり、論理削除済みのレコードと同じcommon_idを使って新規レコードを作成できるようになりました。
(📝 削除済のレコードではactive_common_idの値がNULLになり、NULL同士の比較ではユニーク制約に引っかからない)

まとめ

この話の中で「そもそも安易に削除フラグを使わない」という考え方があることを知って勉強になりました。 ( cf. SQLアンチパターン 幻の第26章「とりあえず削除フラグ」 )

あと今回、ChatGPTにテーマだけ共有してガッと書いてもらったブログを手直しするやり方を試してみて、一旦いい感じの大枠の文章が速くでき上がったので、構成を考えたりする時間が減って良いなと思いました。

『達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践 』読みました

構成

Chapter1 チューニングの基礎知識
Chapter2 モニタリング
Chapter3 基礎的な負荷試験
Chapter4 シナリオを持った負荷試験
Chapter5 データベースのチューニング
Chapter6 リバースプロキシの利用
Chapter7 キャッシュの活用
Chapter8 押さえておきたい高速化手法
Chapter9 OSの基礎知識とチューニング

感想

パフォーマンスチューニングの世界について知ってみたくて読みました。

とにかく

の手順を地道に踏むことが大事という印象でした。
対処の方法にはキャッシュを使う・クエリを改善する・リバースプロキシを使うなど、色々なアプローチがあることを知れて面白かったです。

学んだメモ

  • メトリクス
    • レイテンシなど状態を定量的に示した値
  • モニタリンググラフ
    • メトリクスの変化を時系列で可視化
  • エージェント
    • Webサービスが動作している環境で起動させてメトリクスの収集とモニタリングを行うデーモン
  • スロークエリログ
    • 設定した閾値より実行時間の長いクエリを、かかった秒数や処理じた行数とともに出力したログ。実行されたクエリを集めて解析するのに使われる。設定ファイルやMySQLのコンソール等から有効化できる
  • 期待通りの実行計画にならないとき
    • FORCE INDEXなどを用いてどのインデックスを使うのかヒントを与える方法がある
  • 高速であることの価値
    • 単位時間あたりに処理できる要求数が多くなる
  • レイテンシ
    • 待機時間
  • スループット
    • 同時並行処理性能
  • キャパシティ
    • その時に利用可能な計算機資源の量
    • 垂直スケーリング(性能)
    • 水平スケーリング(数)
  • チューニングで必要なのはボトルネックの解消。ボトルネックの特定はデータ入出力の流れの一番外側から
  • nginxはC10K問題を解決するために開発されたWebサーバ
    • C10K問題:Apache HTTP ServerなどのWebサーバソフトウェアとクライアントの通信において、クライアントが約1万台に達すると、Webサーバーのハードウェア性能に余裕があるにもかかわらず、レスポンス性能が大きく下がる問題
  • ブロッキング
    • 書き込み・読み込みなどのI/O処理を待つ状態
  • KVS(Key Value Store)
    • 1つのキーに対して1つのデータを格納するDBの一種。memcachedなど
  • キャッシュを使ってミドルウェアとの通信コストをカットして高速化したり
  • キャッシュ
    • CPUへの負荷が大きな処理の実行回数削減
    • 大量のリクエストに耐えられる仕組みが比較的容易に作れる

『Web API The Good Parts』読みました

構成

1章 Web APIとは何か
2章 エンドポイントの設計とリクエストの形式
3章 レスポンスデータの設計
4章 HTTPの仕様を最大限利用する
5章 設計変更をしやすいWeb APIを作る
6章 堅牢なWeb APIを作る

感想

基礎的な用語から解説がなされていたので、1つずつ理解しながら読み進められてよかったです。

TwitterGitHubなど、世のサービスはどうなっているかが例に挙げられていて、面白みを感じながら読み進めることができました。
また「なぜデファクトスタンダードに寄せるのが良いのか」みたいなところから、それがどうして良いのかの根拠を言語化して示してくれていたので、自分の知識として活用できそうでした。

実際に以前プロジェクトの中で追加したAPIと同じパスが例に出てきて、その時は先方のエンジニアが提案してくださってその名前になったのですが、デファクトスタンダードになっていたのだなあと改めて思いました。
(この本もその方が薦めてくださったもの)

学んだメモ

  • Web APIは簡単にいえば「HTMLの代わりにプログラムがより処理しやすいデータ形式を返すWebページの一種」
  • APIエコノミー
    • Web APIを公開することで外部サービスとの連携を容易にして新たな価値が生まれ、サービスやビジネスが発展していくこと
  • サードパーティJavaScript
    • Facebookのいいね」など他のページに貼り付けるウィジェットなどのJavaScriptファイル
    • ブラウザを使って他のページからAPIへのアクセスが行われるため、クロスドメインでのアクセスのサポートなどを行う必要がある。
    • クライアント側のコードは誰でも読むことができてしまうため、なりすましや不正アクセスにより注意
  • スマートフォンアプリケーションにおいてサーバとクライアントを繋ぎこむWeb APIについて、モバイルアプリケーションでは一度インストールされたらアップデートされるまで古いコードが動作を続けてしまうので、APIのアップデートなどを戦略的に行う必要がある。
    • (「スマートフォンアプリの旧バージョンがこのコード使ってるから変えられない」みたいなのあったなあ💭)
  • Web APIを美しく設計するには
    • 仕様が決まっているものに関しては仕様に従う
    • そうでないものはデファクトスタンダードに従う
      • メリット:他のAPIを利用してきた開発者が利用方法を類推しやすくなったり、既存のクライアントライブラリの流用が可能になるなど

エンドポイント設計

  • 改造しやすいHackableなURIは望ましい
    • e.g. v1/items/123
    • 「このアイテムのIDが123だな」と予想できる。またこの数字を変えると別アイテムにアクセスできると予想できる
      • applicaiton/x-www-form-urlencoded以外の形式で送ることができないデメリット
  • URIで単語をつなげる必要がある場合はハイフンかアンスコか
    • ある程度好みだがハイフンが固そう
    • ドメインではハイフンが使えるがアンスコ使えないので、それに合わせる感じ
    • 実際は単語をつなぎ合わせることを避ける方が好ましい
      • popular_usersではなくusers/popularとするなどパスとして区切る方が、URIとして見やすい
  • 1スクリーン1APIコール、1セーブ1APIコール
    • 何度もAPIへのアクセスを繰り返すことは、速度の問題だけでなくデータの一部だけが表示・保存されたりするなどの問題に繋がる
    • APIのアクセス回数が増えると利用者にとって煩雑な上、HTTPのオーバーヘッドも上がってアプリケーション速度が低下する。サーバ側の負荷が上がる可能性もある
  • HATEOAS
    • APIの返すデータ中に次の行動・取得するデータのURIをリンクとして含めることで、データを見れば次にアクセスすべきエンドポイントがわかるような設計。
{
    "friends": [

        { "name": "Taro",
            "link": {
                "uri": "https://hogehoge",
                "rel": "user/detail"
      }
  ]
}
  • REST LEVEL3 API
    • HATEOASの概念を導入しているAPI
    • クライアントがURIを知る必要がないので、URIの変更がしやすくなるURIをHackableにする必要がなくなるというメリットがある
    • 人間が理解できないURIにもできる(アクセスされたくないURIを推測できないものにする)

レスポンスデータの設計

  • データフォーマットはJSONデファクトスタンダード
    • Twitterも1.1からJSONのみになった。
    • XMLよりJSONが広まった ← やりとりされるデータの多くは、軽量でシンプルJSONで表現可能だった。
  • JSONP ( JSON with padding )
    • JSONをラップするJSを付け加えたもの
  • JSONはトップレベルに配列ではなくオブジェクトを置く方が好ましい
  • 巨大な数値は文字列で返す
    • IDなど巨大な数を扱うとき、数値をそのままJSONとして返してしまうと問題が起こる可能性がある。これを回避するには、数値ではなく文字列で返す方法がある。
  • エラーレスポンス
    • 適切なステータスコードを使いながら、詳細をボディに格納すると良さそう。(ヘッダに含めるよりもクライアント側が処理しやすい)
    • Twitterはエラーが配列で返るようになっている。これは複数のエラーが同時に発生する場合に合理的といえる。

HTTPを最大限利用する

  • プロキシサーバ
    • クライアントとサーバの間に位置してやり取りを仲介する。キャッシュの情報を扱ったり
  • Content-Type
    • クライアントの多くはContent-Typeの値を使ってデータ形式をまずは判断するので、正しくデータを読み出せるよう適切なメディアタイプを指定しよう
  • 同一生成元ポリシー ( Same Origin Policy )
    • XHTTPRequstでは、異なるドメインに対してアクセスを行って、レスポンスデータを読み込むことができない。
  • CORS ( クロスオリジンリソース共有 )
    • これを利用すると、特定の生成元からのアクセスの場合のみ異なる生成元からのアクセスを許可できる。
    • クライアントからオリジンヘッダを送って、それがサーバ側で保持するアクセス許可対象の生成元一覧に含まれるかを確認する。
    • プリフライトリクエス
      • 生成元を跨いだリクエストが受け入れ可能かを事前に確認する。(HEAD, GET, POST以外のリクエストの時etc.)

設計変更しやすいWeb APIをつくる

  • APIの変更は難しい。ではどうするか→
    • 新しいものを提供する
    • 新しいものはパスでそのことがわかるようにする(v1/, v2.0/ etc.)
  • バージョン管理
    • パスの先頭に付けるのがURIに埋め込む場合には最も一般的でわかりやすそう
    • 「バージョンを日付で表す」という一般的ではないが伝統的な方法もある。
      • あまり使われていないのは長くて覚えづらいからや、古い日付をバージョンで使っていると古臭いAPIを公開しているという印象を与える恐れがあるためと考えられる。
  • 提供終了時の対応
    • 継続的な告知、Blackout Test
    • あらかじめ提供終了時の使用を盛り込む
      • 410と公開が終了した旨のメッセージを返すなど

堅牢なWeb APIを作る

  • HTTPSを使えばほとんどの攻撃は防げるが100%ではない
  • XSS
    • ユーザーの入力を受け取ってそれをページのHTMLに埋め込んで表示する際に、JSなどを実行できてしまうというもの。セッションクッキーetc.ブラウザに保持された情報にアクセスできたり、同一生成元ポリシーの制限を受けずにサーバにもアクセスできる
    • ユーザーからの入力はそれがどう使われるかに関わらずチェックしましょう
  • XSRF
    • サイトを跨いで偽造リクエストを送ることで、ユーザーが意図しない処理をサーバに実行させる攻撃
      • 勝手に投稿するetc.
      • 対策
        • サーバ側のデータが変化するようなアクセスに関しては、GETではなくPOST, PUT, DELETEメソッドを使う
          • ←IMG要素などを用いて攻撃用コードを埋め込むことができなくなる
        • XSRFトークンを使う(最も一般的な対策)
          • 正規のフォームにワンタイムトークンを埋め込みそれが無いリクエストを拒否するもの
  • JSONハイジャック
    • APIからJSONで送られる情報を悪意ある第三者が盗み取ること。SCRIPT要素に同一生成元ポリシーが適用されないために可能となる。
    • ヘッダをあれこれするなどの対策がある
  • 大量アクセスへの対策
    • 各ユーザーごとのレートリミット(最大アクセス回数/単位時間)を決める
    • サービスの性質に合わせて良い感じにできると良さそう
    • 単位時間、現在公開されているAPIを見ると1時間くらいが多いらしい
    • 制限を超えた時は429と制限内容を返すと良さそう

Playright + reg-suit + GitHub Actionsでビジュアルリグレッションテストを定期実行する

ビジュアルリグレッションテストとは

スクリーンショットの比較によって、見た目に差分が生じていないかを検証するテストのことです。
意図しない差分が検知されれば、コードの変更で予期せぬ影響が出ていることに気づけます。

なぜやりたかったのか

既存のRSpecによるE2Eテストによって、開発者がテストケースとして書いた箇所は期待通り表示されていることが担保できていましたが、それ以外の箇所でも差分が発生していたら気づけるようにしたいとなったのがきっかけでした。

また今回は

  • Playwrightを使ってブラウザを操作したい
  • できればNode.jsではなく、Rubyを使ってPlaywrightを動かしたい
  • CIツールを使って定期実行したい
  • 常に前回実行時に撮影したスクリーンショットを、次回の期待画像としてテストしたい

という要件がありました。

構成

以下の構成でビジュアルリグレッションテストを実行するようにしました。

実装

Playwrightを使ったスクリーンショットの撮影

実画像となるスクリーンショットの撮影を行うコードです。Page#gotoの引数に指定したURLのページを撮影し、Page#screenshotの引数に指定したパスに指定したファイル名で撮影した画像を保存します。

require "playwright"

Playwright.create(playwright_cli_executable_path: "./node_modules/.bin/playwright") do |playwright|
  playwright.chromium.launch(headless: true) do |browser|
    page = browser.new_page
    page.goto("https://www.baystars.co.jp/")

    page.screenshot(path: "./actual_screenshots/screenshot.png", fullPage: true)
  end
end

reg-suitの設定

次のような設定にしました。補足の説明を設定の下に書いています。

// regconfig.json

{
  "core": {
    "workingDir": ".reg", 
    "actualDir": "actual_screenshots", 
    "thresholdRate": 0.01, 
    "addIgnore": true,
    "ximgdiff": {
      "invocationType": "client"
    }
  },
  "plugins": {
    "reg-simple-keygen-plugin": {
      "expectedKey": "${EXPECTED_KEY}",
      "actualKey": "${ACTUAL_KEY}"
    },
    "reg-publish-gcs-plugin": {
      "bucketName": "vrt-screenshot"
    },
    "reg-notify-slack-plugin": {
      "webhookUrl": "${SLACK_WEBHOOK_URL}"
    }
  }
}

actualDir

thresholdRate

reg-simple-keygen-plugin

  • Google Cloud Storageから画像をダウンロード・アップロードするときのキーとなる文字列の設定。ここで指定した文字列のディレクトリがGoogle Cloud Storageで作られて、撮影したスクリーンショットや結果レポートファイルなどが格納される。
  • expectedKeyが期待画像(つまり前回実行時に撮影した画像)を取得するキーで、actualKeyが今回撮影した画像をアップロードするときのキー
  • 今回は定期実行実現のため、実行した日付がキーになるように実装をしたいので、このあとに記載しているGitHub Actionsのワークフローで環境変数に設定した値を使うようにしています。

GitHub Actionsの設定

name: Visual Regression Testing
on:
  schedule:
    # NOTE: 前回実行時に撮影したスクリーンショットを期待画像としてテストするため、
    #       実行時の年月日時間(分は含まない)をキーとして画像をアップロード・ダウンロードするようになっている。
    #       00分を指定すると実行時間が前後した時に時間がずれる可能性があるので回避する。
    - cron: "30 1 * * mon"
env:
  TZ: "Asia/Tokyo"

jobs:
  reg-suit:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set reg-suit snapshot keys
        run: |
          echo "EXPECTED_KEY=$(date -d "1 week ago" +'%Y-%m-%d')" >> $GITHUB_ENV
          echo "ACTUAL_KEY=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
      - name: Set slack webhook url
        run: echo "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" >> $GITHUB_ENV
      - name: Build docker container
        run: docker compose build
      - name: Set Credentials
        # NOTE: github actionsでsecretsを扱うとき秘匿情報がマスクされるようになっているが、JSONを扱う場合 {} のみがマスクされるようになっていることから、
        #       正しくJSONとして扱えないなどの問題がある。(cf. https://github.com/actions/runner/issues/1488 )
        #       これを回避するためsecretsにはbase64エンコードした値を入れておいて、それをワークフロー上でデコードして使う。
        env:
          ENCODED_JSON: ${{ secrets.CREDENTIAL_BASE64 }}
        run: echo $ENCODED_JSON | base64 --decode > ./credential.json
      - name: Run screenshot-script
        run: docker compose run app ruby take_screenshot.rb
      - name: Run reg-suit run
        run: docker compose run app npx reg-suit run

DockerfileとComposeファイル

FROM ruby:3.2.2

RUN apt-get update -y && \
    apt-get install -y --no-install-recommends locales-all fonts-ipafont fonts-ipafont-gothic fonts-ipafont-mincho

RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
    apt-get install -y --no-install-recommends nodejs

# Playwrightの依存関係をインストールする
RUN apt-get update && apt-get install -y \
    libnspr4 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdbus-1-3 \
    libdrm2 \
    libxkbcommon0 \
    libatspi2.0-0 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    libasound2 \
    libnss3

WORKDIR /var/app

COPY package.json ./
COPY package-lock.json ./

RUN npm install

# v1.38からPlaywrightのパッケージインストールだけではブラウザがダウンロードされなくなった。
# ドキュメントの推奨に従い、コマンドを使って明示的にブラウザをダウンロードする。
# cf. https://github.com/microsoft/playwright/releases/tag/v1.38.0
RUN npx playwright install

COPY Gemfile ./
COPY Gemfile.lock ./

RUN bundle install

COPY . .
docker-compose.yml

version: '3'
services:
  app:
    build: ./
    volumes:
      - ${PWD}:/var/app
      - /var/app/node_modules
    environment:
      GOOGLE_APPLICATION_CREDENTIALS: "./credential.json"
      # GitHub Actionsで作られた日付の環境変数を、Compose内の環境変数に格納する
      EXPECTED_KEY: ${EXPECTED_KEY}
      ACTUAL_KEY: ${ACTUAL_KEY}
      SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL}

実行した結果

1週間ごとに自動で実行されて、2度目の実行時には1週間前に撮影した画像と比較して無事にテストが成功することが確認できました。

詰まったところ

Google Cloud Storgeとやりとりするために、サービスアカウントの認証情報jsonを使ってADCという認証を通過する必要がありました。

また認証情報の渡し方が「認証情報が入っているjsonファイルのパスを指定する」であるため、

  • GitHubリポジトリのsecretsに認証情報jsonを格納
  • そこから取得した認証情報jsonを、所定のファイルに書き込む
  • reg-suitはそのファイルを使って認証を通過する

という実装にしたのですが、なかなかうまくいかないということがありました。

調べたところ原因は「GitHub Actionsでsecretsを扱うとき、秘匿情報がマスクされるようになっている」ことで、

JSONを扱う時 {} のみがマスクされるようになっているために

という問題がありました。

GitHub ActionsでJSON などの構造化データを扱いたい時は、base64エンコードしたものをワークフロー上でデコードして使うのが良いようで、なかなかこの問題に気づけず時間を要しました。

まとめ

今回はRailsアプリケーションにビジュアルリグレッションテストを導入し、Playright + reg-suit + GitHub Actionsで定期実行を行ったときの設定を書きました。

reg-suitは、差分派生時には送信されるHTMLレポートからわかりやすく確認ができるだけでなく、Google Cloud Storageとの連携やSlack通知の設定も簡単に行えて、非常に導入しやすく便利なツールなのではないかと感じました。

Dockerfileの設定などは、手探りでやったためにもう少し改善の余地があると思うので頑張りたい気持ちです。

『良いコード悪いコードで学ぶ設計入門』読みました

構成

第1章 悪しき構造の弊害を知覚する
第2章 設計の初歩
第3章 クラス設計〜すべてにつながる設計の基盤〜
第4章 不変の活用 〜安定動作を構築する〜
第5章 低凝集〜バラバラになったモノたち〜
第6章 条件分岐〜迷宮化した分岐処理を解きほぐす技法〜
第7章 コレクション〜ネストを解消する構造化技法〜
第8章 蜜結合〜絡まって解きほぐせない構造〜
第9章 設計の健全性を損なうさまざまな悪魔たち
第10章 名前設計〜あるべき構造を見破る名前〜
第11章 コメント〜保守と変更の正確性を高める書き方〜
第12章 メソッド(関数)〜〜良きクラスには良きメソッドあり〜
第13章 モデリング〜クラス設計の土台〜
第14章 リファクタリング〜既存コードを成長に導く技〜
第15章 設計の意義と設計への向き合い方
第16章 設計を妨げる開発プロセスとの戦い
第17章 設計技術の理解の深め方

感想

具体的なコードを伴ったアンチパターンを紹介してくれるので、「こういう事態を回避するためにこうする」という論拠となる考え方をたくさん知ることができました。特にどういうふうに開発の効率が悪くなるのか、どのように開発者の生産性を下げるのか、嬉しくないのかが言語化されていて、日々自分が感じる「なんか好ましくない」がこういうことだったのかと理解できる感じがする本でした。


「目的ベースの命名設計」に関する話の中で、例えばユーザーも目的で分けると出品者購入者クラスに分けることができる(クラスを分けることで無関係なロジックを排除し、影響範囲を小さく変更しやすくできる)という話が載っていたのですが、その粒度でクラスを分けるのは結構難しそうだなあと感じました。
「ビジネスルールとクラスが一致していると正確に素早く変更しやすい」というのは確かにそうなのかもなと思いました。

メモ

  • デメテルの法則
    • 利用するオブジェクトの内部を知るべきではない。(💭よく見るやつ)
    • 他のオブジェクトの内部状態を尋ねたり、状態に応じて呼び出し側が判断するのではなく、呼び出し側はただ命ずるだけで、命令された側が判断・制御するようにする
  • 深いネストによる可読性低下
    • 極めて慎重に読み解く必要がある。開発者の思考に高い負荷がかかり疲弊させる
    • コードの見通しが悪くなる。
  • 早期リターン
    • メソッドの冒頭とそれ以降とで、条件ロジックと実行ロジックを分離できる
  • ソフトウェア設計における責務
    • ある関心ごとについて、正常に動作するよう制御する責任
  • 単一責任の原則
  • DRYの誤用
    • 同じようなロジックや似ているロジックであっても、概念が違えばDRYにすべきではない
    • 処理を共通化して良いかどうか判断するには、同じビジネス概念かを見分ける必要がある
    • ビジネス理解が進んだら「あるビジネス概念が実は複数の異なるビジネス概念だった」というのはありがち。それがわかった段階で共通化を解除するなど構造を見直すのがベター
    • 例えば、
      • 「通常割引メソッド」と「夏季セール割引メソッド」が割り引く値以外差異がなくロジックはほぼ同じな時に「重複コードではないか?」と思うかもしれない
      • しかし夏季セール割引の仕様が変わったとしたら、夏季セール割引のロジックは通常割引とは違うものになる
      • このように同じようなロジックが複数があるからと言って責務を考えず無理にひとまとめにすると責務が多重になる。
  • マジックナンバー
    • ロジック内に直接書き込まれている意図不明な数値。説明なき数値は開発者を混乱させる
  • 影響範囲を最小化するよう設計する
    • グローバル変数(および巨大データクラス)は影響範囲が広すぎる
    • 呼び出し箇所が少なく、局所化されているほどロジックの理解が容易になる
    • 目的駆動名前設計
      • プログラミングにおける名前の役割は可読性の向上だけではない
      • ソフトウェアで達成したい目的をベースに名前を設計(命名)する
      • 名前設計が不十分 → 様々なユースケースのクラスと関係を持つ → ロジック増える → クラス巨大化が起こりがち
      • クラスが巨大化 → 仕様変更が発生した場合に影響範囲が大きい → 開発生産性DOWN
      • 大雑把で意味がガバガバな名前は、あらゆるロジックを惹きつけてしまう心理的引力が働く
  • ラバーダッキング
    • 誰かに説明すると自ら原因に気づいて解決するという手法。声に出して話すのは分析行為として理にかなっている
    • 書籍『ドメイン駆動設計』のユビキタス言語(チーム全体で意図を共有するための言葉)が由来。
    • 同じ意図の名前を会話・ドキュメント・クラス名やメソッド名で共通して使うことで、意図の減衰を防止し設計のいびつさ解消に役立つ
  • 意図がわからない名前
    • 理解の難しさから翻訳作業が都度必要になる(💭超わかる)
    • 例えば仕様変更の依頼があったとき、対応するメソッドや変数がなんなのか頭の中で翻訳作業が必要になる
    • 新しくジョインしたメンバーへの説明コストUP
    • スプレッドシートで用語集などが作られるケースがあるが、この手の資料はメンテナンスがおろそかになりがち
    • 人間の注意力には限界があるので、仕様とロジックを相互にいつでも正確に翻訳できるわけではない。不注意により誤って解釈する可能性がある。
    • また意図不明な名前は解釈の誤りをさらに増大させる(💭ついこの間起こった気がする)
    • 技術駆動命名
      • 技術ベースでの命名のこと。型名を表すintなど用語に基づいた名前が用いられたりして、意図がわかりにくくなりがち
      • 実現したい目的・意図がわかるように命名しましょう
    • 連番命名
      • クラスやメソッドに対して番号付で命名すること
      • 目的・意図が読み取れない点で技術駆動命名と似ているが、構造改革が困難な点において連番命名は悪質
      • 番号付で管理されているために連番命名以外で命名すると番号の秩序が失われることから、番号で管理したい側からの反発を招きかねず命名を見直しづらい
    • 基本的に名前は省略しない
      • 可読性が上がり他のメンバーや将来の自分を助ける(💭わかる)
      • 昔はタイプ文字数が多いときのタイポしやすさなどから、長い命名は嫌われたりしていた。最近はエディタが補完したりするのでその労力は気にならない
      • SNSやVIPのような、一般的にも省略されているものは例外と筆者は考える(💭わかる)
    • 再説明コメントで命名をごまかす
      • 意味が伝わりにくいメソッドでは再説明コメントが書かれがち(💭わかる)
      • あまり可読性に貢献しない上、メソッドが変わるたびにコメントの更新作業も発生
      • ロジックをなぞるだけのコメントは退化しがち
        • (実装が変わったのにメンテナンスされず実装を正しく説明しなくなること)
      • ダメなメソッド名をコメントで補足説明するのではなく、メソッド名自体をブラッシュアップしましょう
  • 情報システムとは
    • 現実世界の概念のみをコンピュータの世界へ投影した仮想現実といえる
    • 現実世界の概念をコンピュータの仮想世界へ変換し、意味を対応づけ、そして概念的なやり取りをコンピュータによって高速化することで高速化していると考えられる
    • 現実世界での物理的な存在と情報システム上のモデルが1:1になるとは限らず、1:多になるケースが多いのが特徴
      • GitHubのユーザー設定では、目的ごとにアカウント・プロフィールなどと目的ごとにモデルが分かれて表現されている
  • 設計しないと開発生産性が低下する
    • レガシーコード
      • 変更が困難で壊れやすいコード。またレガシーコードが蓄積している状態を技術的負債と呼ぶ
    • 開発生産性が低下する要因は主に次の2つ
      1. バグを埋め込みやすくなる
        • 正確に変更できるまで時間がかかる
      2. 可読性低下
        • ロジックの見通しが悪い、意図が読み取りづらいetc.
    • レガシーコードは高品質設計を妨げる
      • レガシーコードは極めてアンバランスでトリッキーがちなため、設計改善が困難になり、納期の都合などから諦めてしまうことが多い
      • → 高品質な設計実装の経験を積めなくなり、設計スキル向上を果たせなくなる
    • レガシーコードは開発工数を減少させる
      • レガシーコードは理解に多大な時間を要する(💭わかる)。一方で時間は有限
      • 本来もっと価値の高い仕事に充てられる時間が目減りしてしまう

『オブジェクト指向設計実践ガイド』読みました

構成

第1章 オブジェクト指向設計
第2章 単一責任のクラスを設計する
第3章 依存関係を管理する
第4章 柔軟なインターフェースをつくる
第5章 ダックタイピングでコストを削減する
第6章 継承によって振る舞いを獲得する
第7章 モジュールでロールの振る舞いを共有する
第8章 コンポジションでオブジェクトを組み合わせる
第9章 費用対効果の高いテストを設計する

感想

「振る舞いとデータをまとめたオブジェクトを使うこと」が、どのように変更のしやすさに寄与するのかという話をたくさん見ることができたと思いました。
目指すコードは「見通しが良く、合理的で、利用性が高く、模範的」なものであることと、 設計の目的は「今必要な役割を果たしながら、簡単に再利用できて、将来的にも変更しやすいコードを書くこと」であることを都度思い出させながら進む本だったので、何故こうしたいのかを意識しながら読めて良い感じでした。
新たにクラスを追加したい時やリファクタリングしたい時に使える知識が得られたと感じたのと、とりあえずもしクラスを追加する機会ができたら、単一責任のクラスになるように考えてみたいと思いました。

メモ

  • 設計とは
    • 「今必要な役割を果たしながら、簡単に再利用できて、将来的にも変更しやすいコードを書く」ためにやる
    • 繰り返しのフィードバックを頼りに進む。フィードバックは適度な時間ごとに行われるべき。
    • なのでアジャイルソフトウェア運動の反復的な手法は、適切に設計されたオブジェクト指向アプリケーションを作るために良い
    • 設計が早すぎる(つまり必要な調整がまだわからない時点で設計が行われる)と、初期の間違った理解がコードに埋め込まれる
  • アジャイルで正しいとされる「顧客が本当に求めるものを生み出すためのコスパの高い方法」は、顧客と共同作業をすること
    • ソフトウェアは一度に小さな単位で作り、その時にそれぞれの単位が次のアイデアを変える機会となることを目指す
  • BUFD(Big Up Front Design:詳細設計)
    • 提案されたアプリケーションの全ての機能の想定される未来の内部動作を全て特定し、完全に文書化すること
    • アジャイルが正しいならば、これは全く意味がないと言える
  • オブジェクト指向設計の目的は「変更にかかるコストを下げること

単一責任のクラスを作る

  • 変更が簡単なアプリケーションは再利用が簡単なクラスから構成される。2つ以上の責任を持つクラスは再利用しづらい
  • クラスが単一責任かどうかを見極めるには?
    • クラスの持つメソッドを質問に言い換えた時に、意味をなす質問になっているか
      • 「Gearさん、あなたの比を教えてくれませんか」
      • 「Gearさん、あなたのgear_inchesを教えてくれませんか」←しっくりこない
      • 「Gearさん、あなたのタイヤのサイズを教えてくれませんか」←タイヤに聞けよ
    • クラスを1文で説明できるかどうか
      • 「それと」が含まれたりすると複数責任を負っていそう
  • 凝縮度
    • クラス内の全てがそのクラスの中心的な目的に関連していれば「凝縮度が高い」
  • メソッドもクラスのように単一の責任を持つべき
    • 変更も再利用もしやすくなる。他のクラスへの移動も簡単に
    • メソッドの役割も1文で説明できると良さそう
    • クラスが行うこと全体がより明確になる

依存関係の管理

  • 一方のオブジェクトを変更したとき、他方のオブジェクトも変更せざるを得ない恐れがあるならば、片方に依存しているオブジェクトがある
  • 一定の依存関係が2つのクラス間に築かれるのは避けられない。結局共同作業は不可欠だから
  • 依存性の注入
    • あるメッセージに応答できるオブジェクト、つまりダックタイプのオブジェクトをクラスの初期化時に要求するようにすること
  • 自身よりも変更されないものに依存しなさい
    • あるクラスは他のクラスよりも要件が変わりやすい
    • 具象クラスは抽象クラスよりも変わる可能性が高い
    • 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ

柔軟なインターフェースを作る

  • クラスのパブリックインターフェースとプライベートインターフェース
    • パブリックインターフェース
      • そのメソッドによってクラスの主要な責任を明らかにする・他者がそこに依存しても安全etc.
    • プライベートインターフェース
      • パブリック以外のメソッド。実装の詳細に関わり、他のオブジェクトから送られることは想定されていない。変更されやすいし、他者が依存するのは危険
    • クラスの責任とパブリックメソッドは対応する。
    • 設計の目標は「今日の要求に応えるために十分なコードだけを書きつつ、将来的な柔軟性を最大限に保つこと」。良いパブリックインターフェースは、想定外の変更に対するコストを下げる
  • ドメインオブジェクト
    • アプリケーションにおいてデータと振る舞いの両方を兼ね備えた命名がされて表されるであろうオブジェクト
  • シーケンス図
    • これを書くと設計の重点が「クラスとクラスが誰と何を知るか」から、メッセージがどう送られるかに移る。
    • 「オブジェクトが存在するからメッセージを送る」のではなく、「メッセージを送るためにオブジェクトが存在している。」
  • 設計の目的は「今の役割を果たし、簡単に再利用でき、将来的な予期せぬ用途にも対応できるコードを書くこと
  • デメテルの法則
    • オブジェクトを疎結合にするためのコーディング規則の集まり
    • そこへメッセージを送ることができるオブジェクトの集合を制限する
    • 3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介することを禁じる
  • ポリモーフィズム
    • 多岐にわたるオブジェクトが同じメッセージに応答できる能力
    • メッセージの送り手は、受け手のクラスを機にする必要がなく、受けてはそれぞれが独自化した振る舞いを提供する
  • ダックでリファクタリングできる例
    • クラスで分岐するcase文
    • kind_of?とis_a?
    • respond_to?

継承

  • 共通の振る舞いを持つものの「いくつかの面においては異なる」という強く関連した型の問題を解決する
  • サブクラスは、そのスーパークラスを「特化」したもの
  • 正しい抽象を特定するのが簡単なのは、存在する具象クラスが少なくとも3つある時
  • リスコフの置換原則
    • スーパークラスが使えるところではサブクラスが使えるアプリケーションを作れる。派生型は上位型と置換可能
  • 抽象的な部分をスーパークラスに押し上げる方式が好ましい
    • 既存のクラスの具象 → 抽象変換を、具象的な部分のみを新しいサブクラスに押し下げてやろうとすると、具象的な振る舞いの一部を誤って置き去りにする恐れがある。
    • その定義からして、残された具象的な振る舞いが全てのサブクラスに当てはまるなどということはない
  • クラスの階層構造は浅くする
    • 深い階層構造が抱える問題は、メッセージを解決するための探索パスが長くなること
    • 自身より上に位置するオブジェクト全てに依存するので、階層が深いほどに大量の依存を初めから持つことになる。しかもそれは変更される可能性がある。

テスト

  • オブジェクトが受信する、もしくは送信するメッセージに集中すべき
    • 受信メッセージは、その戻り値の状態をテストする
    • 送信コマンドメッセージは、送られたことをテストする

その他

  • わかりやすいエラーメッセージを伴って失敗するコードを書くことは、書く時点では比較的小さな努力しか要しないが価値が永遠に続く