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

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

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

テスト

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

その他

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

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

複数あるエンドポイントについてデフォルトで実行したいフィルタを、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)内に全てのストア関係を記述。基本的な方式