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

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

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

テスト

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

その他

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

親コントローラに入れる処理をテストしやすくするための設計

複数あるエンドポイントについてデフォルトで実行したいフィルタを、before_actionとして親コントローラに登録したいことがありました。

当初は親コントローラでメソッド定義とフィルタの登録をして、テストは各エンドポイントのテストで行うやり方を考えていました。
しかしその方針では、今後エンドポイントを追加したい時にも毎回同じテストを書き続けることが必要になってしまい、将来的に追加漏れが発生する恐れがあることに気づきました。
仮に追加が漏れて問題が無かったとしても「なぜこのテストがあるファイルとないファイルがあるのだ」という余分な迷いを生んでしまいそうに思われました。


そこで次のような方法で実装することにしました。

# concern

module RequireActiveUser
  extend ActiveSupport::Concern

  included do
    before_action :user_must_be_active
  end

  private

  def user_must_be_active
    if user.inactive?
      # ...
    end
  end
end
# 親コントローラ

class ParentController < ActionController::API
  include RequireActiveUser

  # ...
end
# concernのテスト

require 'rails_helper'

RSpec.describe RequireActiveUser, type: :controller do
  describe '#user_must_be_active' do
    controller do
      include RequireActiveUser

      def index
        head :ok
      end
    end

    context '有効なユーザーのとき' do
      # ...
    end

    context '有効でないユーザーのとき' do
      # ...
    end
  end
end
# 親コントローラがconcernをincludeしていることのテスト
RSpec.describe ParentController, type: :controller do
  # ...

  describe 'RequireActiveUser' do
    it 'RequireActiveUserモジュールをincludeしている' do
      expect(controller.class.ancestors).to include(RequireActiveUser)
    end
  end
end


まとめ・感想

上記のような設計にすることで、今後この親コントローラの下に新たなエンドポイントを追加する時に都度このフィルタに関するテストを追加することが不要になりました。
コントローラごとに挙動が変わるわけではないコードについて全てのコントローラでテストをすると、線形にコードが増えていって好ましくなさそうと学びました。

またこの後に別のサービスで同様の実装をしたい機会があった時には、子コントローラの中にいくつか「フィルタを実行したくないエンドポイント」があったことから、フィルタを実行したい子コントローラでconcernをincludeし、各子コントローラのテストでconcernをincludeしていることを検証するという設計にしました。

なおその時には「今後このアプリケーションが拡張される可能性が少ない(将来的な追加漏れを考慮する必要が少ない)」という点も考慮されていたので、今回のやり方を一つの引き出しとして、その時々のアプリケーションの状態を考えながらうまく使えたら良いなと思いました。

『Vue.js&Nuxt.js超入門』読みました

構成

Chapter1 Vue.jsを使ってみよう
Chapter2 Vue.jsの基本をマスターしよう
Chapter3 コンポーネントを使おう
Chapter4 コンポーネントをさらに掘り下げる!
Chapter5 Nuxt.jsにアップグレード!
Chapter6 外部サービスを利用しよう!
Addendum JavaScriptオブジェクト超入門

感想

プロジェクトでNuxt.jsを扱うのでキャッチアップのために読みました。
Vue.jsの最も基礎的な書き方からスタートしてNuxt.jsへ入る流れで、非常に体系的に学べました。
JavaScriptも私はほとんど触ったことがないのですが、そういう話題もビギナー向けに書かれていたのがありがたかったです。
各種プロパティやディレクティブについても、「これで〜〜ができるけど更に〜〜機能を作りたい時もあってそのためにこれ」のような感じで説明がなされていて、1つ1つ順序立てて理解が深まったように感じました。

メモ

仮想ノード

  • v-htmやv-bindは「仮想ノード」のための属性
  • v-〇〇〇のvは「virtual」のv
  • Vue.jsでは実際のDOMの上に仮想DOMが用意され、その中に仮想ノード(VNode)というオブジェクトでHTMLのタグがそのまま仮想的に構築されている。
  • Vue.jsでは直接DOMを操作するのではなく、仮想DOMにある仮想ノードを操作することで、実際に表示されているDOMを更新する。
  • v-〇〇は仮想ノードに対して属性を設定するもの

Nuxt.js?

  • Vue.jsにある「コンポーネントタグがあるのみコードの中身がない」「データ管理機能が足りない」「ルーティングの仕組みがない」などの弱点を解決する
  • SSR、Vuexプラグインを使ったデータ管理、Vue Routerによるルート管理機能などが追加された

ミューテーション

  • ストアの値を変更するための機能
  • ストアの値を直接書き換えることもできるが推奨されない
  • 様々なコンポーネントから共用されるため、各コンポーネントから直接書き換えると問題を引き起こす可能性がある
  • ミューテーションは値を操作する処理をストア側に用意し、それをコンポーネント側から呼び出すというやり方で安全に処理をする

アクション

  • ミューテーションを呼び出す仕組み。複数ミューテーションを続けて呼び出す場合などに使われる

vuex-persistedstate

  • ストアの値をリロード後も保持し続けるためのプラグイン
  • ストアの値はページのロード時に生成されるため、リロードすると全て初期化される

クラシックモードとモジュールモード

  • クラシックモード
    • store/ ディレクトリの中に複数のファイルを設置し、それぞれにストアの内容を記述。発展的な方式
  • モジュールモード
    • 1つのファイル(index.js)内に全てのストア関係を記述。基本的な方式

福岡Rubyist会議03の参加レポート

概要

2/18に開催された福岡Rubyist会議03に出席しました。
その時の学んだメモや感想を簡単にまとめます。🏃‍♀️

感想

RubyKaigi2022に続いて2度目のカンファレンスオフライン参加でした。

事前にタイムスケジュールを見た時から「RubyKaigiよりも理解できる内容が多そう」と思っていたのですが、実際に親しみやすい内容が多くて、理解しながら聞けることも多かったと感じました。(RubyKaigiはなるほどわからんが多め)

hsbtさんのRubyのリリースにまつわる話では、とてもすごい方々が一つずつ問題に対処して進めているというのを聞いて「自分もシュッとやろうと思ったりせず目の前の問題をクリアできるように頑張りたいし新たなリリースが出たら感謝しよう」と思いましたし、hachiさんの「テストに不要なコードがある」話についても、最近大きめのテストを扱うタスクに取り組んだりしていたのもあって非常に共感しながら聞いていました。

またぽっけさんの外部コマンドの話も非常に勉強になりました(最近の業務でぽっけさんの記事を参考にすることがあって、説明がわかりやすすぎてなんて日本語がお上手なのだと感じたりもした)し、ぺんさんのTRICKは何回見ても凄いなあと見入ってしまいました。

あとは会場で八女茶が配布されたり、ランチやディナーでご当地のものを食べて福岡の良いところをたくさん感じることができたのもとても良かったなあと思いました。
RubyKaigi以外の地域コミュニティイベントも、また機会があれば参加したいです。

セッション

RubyistによるRubyistのためのカンタン動画制作 / yasulabさん

  • 動画制作は思いのほか簡単にできる
  • 動画にすることで、できること・やれることが増える
  • 機材などのこだわりについて「音は丁寧に画はほどほどに」
    • 視聴者は音のクオリティの方が影響大きそう
  • 配信は制作するよりもっと簡単!
    • StreamYardおすすめ(今回の配信もこれ)。月20hまで無料で、サファリから開いて簡単に使える
  • 編集では音量調整だいじ
  • 推しの箇所があれば文字起こし
    • vrewを使っている。 文字起こしソフトもごまんとある

感想

実際に動画を作っている方の肌感というか感想を聞くことができて、動画コンテンツを作ることのハードルがこれまでより下がったように感じた(結構難しそうに思っていた)。こういう道具を揃えれば良いなどの解像度が上がった。もし今後動画を作ることが検討されたときに、それを選ぶことのハードルが下がった

Ruby のリリースを爆速にするための方法 / hsbt(柴田博志)さん

  • バックポート: rubyリポジトリのmasterブランチのコミットをメンテナンスブランチ(ruby_3_2 etc.)へcherry-pickすること
  • パッケージ:masterブランチまたはメンテナンスブランチから作成された、tar.gzなど配布用のアーカイブファイル
  • パッケージング: パッケージを作る作業。リポジトリソースコードからbisonなどを利用してコードやスクリプトを生成する一連の作業
  • リリース担当、Rubyは全員野球体制。他言語によっては担当者みたいなのを置いている場合もあるそう
  • 2019年にパッケージが壊れていたことが判明した
    • znzさんがgithubactionsを使ってパッケージを使う仕組みを作ってactionsを動かすだけで良くなった。自動化
    • make-snapshotがリリース直前にいじられていたことから、パッケージに含むべきファイルが消えていることを発見
    • 直した後にまた同様の問題が。Makefileとmake-snapshotが変わっていた。
  • パッケージングは3年ぐらいに渡る作業
  • リリース直前に特定の環境でビルドできない問題、ubuntu22.04のbisonが新しくなっていた
    • 2年後の変化を予測してコードを書くのは厳しい、、(😱)
  • セキュリティリリースが特に大変。パッチをコミットできない
  • リリース作業が大変になる→仕事が大変だと延ばし延ばしに→間隔を開けるとリリース作業の失敗確率が上がるという地獄ループ
    • 「年1回、数ヶ月に1回しか実行しない作業の安定化」が課題
  • 結論がんばるしかない
    • 事象1つ1つの自動化を行なっている

感想

リリースされたらとにかく感謝したいと思いました。実際にリリースに関わっている人のお気持ちを聞けると面白いし、少し身近に感じられた

RubyKaigiのはなし / そらはさん

  • Ruby kaigi takeout、内製だった(外部サービスを使っているのではなかった)
    • オフライン会場がないのが面白みに欠けると感じた、何か面白いことを盛り込みたいという気持ちもあり
  • デプロイはHerokuで各種awsの各種サービスをモリモリ
  • AWS Chime SDKを使った事例になったという良い点があった
  • api/conference がほぼ全てのデータをJSONで返す
    • CDNにキャッシュさせてrailsにリクエストが来ないようにしていた
  • 管理画面を用意しなかった。rails consoleで良いか、使う人もエンジニアだし
  • 三重のRubyKaigiではLANケーブルを1.9km使った

感想

大規模なカンファレンスでリアルタイムに色々やるのはとても複雑で大変だということを少しだけ理解することができました。 いろんな調整必要だし問題も起こっていていろいろ対処しているのだろうなあと感じながら、次回参加してみたいと思いました。

mruby on IoT devices. / 岡嵜雄平さん

  • ポーティング
    • 基盤上で、別環境で動いたプログラムを動かす作業
    • 組み込みソフトウェアがどんな仕組みで動作するか前提知識をつけること

感想

組み込みソフトウェアの知識が無くあまり理解できることがありませんでした。🥲

Factorybot 改善ツール作成失敗と学び / hachiさん

  • 課題:テストが遅い
    • 100人ぐらい関わっている、全員が速度を意識して書いている保証はないし、遅いテストを書いてしまうことはある
    • 原因:対象処理が遅い・前提データの用意でN+1が発生・DBの読み書きが不要なテストでそれを行なっている・必要以上に何度もレコードに書き込む etc.
  • DBへ保存が必要なければ create -> build にする
  • 各コンテキストでcreate_listしているところ、そこで条件が違うテストではないので一度行えば良い(とても共感)
  • before_all を使うのどうでしょうかという提案
  • testProf gem、railsの遅いテストを診断できるgem
  • traitでまとめて作成されているがテストでは使用されていない不要なデータがあったり(とても共感)
  • レコード挿入後、一度もDBから呼び出していないコードを検知してお知らせしたり

感想

作られた後一度もDBから呼び出していないことを検知するなど、アプローチがわかりやすくとても良さそうと感じた。(実際には他にもいろいろな箇所で要不要の判断が難しく実用には至らなかった様子)
最近大きめのcontroller specをrequest specに移すタスクで「せっかくなら綺麗にしよう」と考えてやっていたときに、一度も呼び出されないletなど不要なコードがあるのをたくさん目にしたのでたくさん共感しながら聞いていました。

外部コマンド実行入門 / Pockeさん

  • ゴールはRubyで外部コマンドを実行する際に、適切なメソッドを適切に使えるようになる
    • 多くあるメソッドから、最適なメソッドを選べるようになる
    • 外部コマンド実行時䛾セキュリティリスクを知って、回避できるようになる
  • 外部コマンドとは?
    • ls, git, rubyなど
  • コマンドを実行する関数は、シェルを経由したりしなかったりする
    • system(“ls -l”) しない
    • system(“ls * “)する(globの*を使うため)
  • シェル経由でコマンドを実行しないよう意識する
    • コマンドラインインジェクション防止
    • 悪意あるユーザーはシェルの機能を使って意図しない動作を狙える
  • Rubyでは外部コマンドを実行するメソッドが、様々な用途に合わせて多く用意されている

感想

これまでコマンドでシェルを経由する・しないを意識していなかったので大変勉強になりました。 発表資料を読み返します。 https://pocke.hatenablog.com/entry/2023/02/19/004700

Rubyist Magazine Reboot / 西山和広さん

  • Rubyist Magazineが2年以上止まっているので、現状説明と将来どうなってほしいかの話
  • 創刊背景
    • ドキュメント不足、何か活動すると良いかも?という経緯
  • 人手不足によって発行が停滞(みよひでさんという方がやっている)
  • 編集・執筆なんでも手伝える方は  https://github.com/rubima/magazine.rubyist.net/issues

感想

コミュニティもそうですが、いろいろな活動があるのだなあと思って聞いていました。

メンテできないコードをメンテする技術 / ぺんさん

  • TRICK入賞コードをメンテ可能にした技術について
  • ルビー会議の水槽のやつ、アニメーションQuine

感想

発想が凄すぎました。「IRBでは扱えないかもしれない?ような使い方書き方をして、こういうこともできると発見できる点でRubyに貢献」というお話を聞いて「な、なるほど。。」となりました。 どこで一時停止してもそこからまた実行できるようになっているところが凄い〜と思いました。

おまけ

福岡のグルメ最高でした。