『達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践 』読みました
構成
Chapter1 チューニングの基礎知識
Chapter2 モニタリング
Chapter3 基礎的な負荷試験
Chapter4 シナリオを持った負荷試験
Chapter5 データベースのチューニング
Chapter6 リバースプロキシの利用
Chapter7 キャッシュの活用
Chapter8 押さえておきたい高速化手法
Chapter9 OSの基礎知識とチューニング
感想
パフォーマンスチューニングの世界について知ってみたくて読みました。
とにかく
- ボトルネックを見つける
- 対処してみる
- 対処前と比べて評価する
の手順を地道に踏むことが大事という印象でした。
対処の方法にはキャッシュを使う・クエリを改善する・リバースプロキシを使うなど、色々なアプローチがあることを知れて面白かったです。
学んだメモ
- メトリクス
- レイテンシなど状態を定量的に示した値
- モニタリンググラフ
- メトリクスの変化を時系列で可視化
- エージェント
- Webサービスが動作している環境で起動させてメトリクスの収集とモニタリングを行うデーモン
- スロークエリログ
- 期待通りの実行計画にならないとき
- 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つずつ理解しながら読み進められてよかったです。
TwitterやGitHubなど、世のサービスはどうなっているかが例に挙げられていて、面白みを感じながら読み進めることができました。
また「なぜデファクトスタンダードに寄せるのが良いのか」みたいなところから、それがどうして良いのかの根拠を言語化して示してくれていたので、自分の知識として活用できそうでした。
実際に以前プロジェクトの中で追加したAPIと同じパスが例に出てきて、その時は先方のエンジニアが提案してくださってその名前になったのですが、デファクトスタンダードになっていたのだなあと改めて思いました。
(この本もその方が薦めてくださったもの)
学んだメモ
- Web APIは簡単にいえば「HTMLの代わりにプログラムがより処理しやすいデータ形式を返すWebページの一種」
- APIエコノミー
- Web APIを公開することで外部サービスとの連携を容易にして新たな価値が生まれ、サービスやビジネスが発展していくこと
- サードパーティJavaScript
- スマートフォンアプリケーションにおいてサーバとクライアントを繋ぎこむWeb APIについて、モバイルアプリケーションでは一度インストールされたらアップデートされるまで古いコードが動作を続けてしまうので、APIのアップデートなどを戦略的に行う必要がある。
- (「スマートフォンアプリの旧バージョンがこのコード使ってるから変えられない」みたいなのあったなあ💭)
- Web APIを美しく設計するには
- 仕様が決まっているものに関しては仕様に従う
- そうでないものはデファクトスタンダードに従う
- メリット:他のAPIを利用してきた開発者が利用方法を類推しやすくなったり、既存のクライアントライブラリの流用が可能になるなど
エンドポイント設計
- 改造しやすいHackableなURIは望ましい
- e.g.
v1/items/123
- 「このアイテムのIDが123だな」と予想できる。またこの数字を変えると別アイテムにアクセスできると予想できる
- applicaiton/x-www-form-urlencoded以外の形式で送ることができないデメリット
- e.g.
- URIで単語をつなげる必要がある場合はハイフンかアンスコか
- 1スクリーン1APIコール、1セーブ1APIコール
- HATEOAS
{ "friends": [ { "name": "Taro", "link": { "uri": "https://hogehoge", "rel": "user/detail" } ] }
- REST LEVEL3 API
レスポンスデータの設計
- データフォーマットはJSONがデファクトスタンダード
- JSONP ( JSON with padding )
- JSONをラップするJSを付け加えたもの
- JSONはトップレベルに配列ではなくオブジェクトを置く方が好ましい
- 巨大な数値は文字列で返す
- IDなど巨大な数を扱うとき、数値をそのままJSONとして返してしまうと問題が起こる可能性がある。これを回避するには、数値ではなく文字列で返す方法がある。
- エラーレスポンス
HTTPを最大限利用する
- プロキシサーバ
- クライアントとサーバの間に位置してやり取りを仲介する。キャッシュの情報を扱ったり
- Content-Type
- クライアントの多くはContent-Typeの値を使ってデータ形式をまずは判断するので、正しくデータを読み出せるよう適切なメディアタイプを指定しよう
- 同一生成元ポリシー ( Same Origin Policy )
- XHTTPRequstでは、異なるドメインに対してアクセスを行って、レスポンスデータを読み込むことができない。
- CORS ( クロスオリジンリソース共有 )
設計変更しやすいWeb APIをつくる
- APIの変更は難しい。ではどうするか→
- 新しいものを提供する
- 新しいものはパスでそのことがわかるようにする(v1/, v2.0/ etc.)
- バージョン管理
- 提供終了時の対応
- 継続的な告知、Blackout Test
- あらかじめ提供終了時の使用を盛り込む
- 410と公開が終了した旨のメッセージを返すなど
堅牢なWeb APIを作る
- HTTPSを使えばほとんどの攻撃は防げるが100%ではない
- XSS
- ユーザーの入力を受け取ってそれをページのHTMLに埋め込んで表示する際に、JSなどを実行できてしまうというもの。セッションクッキーetc.ブラウザに保持された情報にアクセスできたり、同一生成元ポリシーの制限を受けずにサーバにもアクセスできる
- ユーザーからの入力はそれがどう使われるかに関わらずチェックしましょう
- XSRF
- JSONハイジャック
- 大量アクセスへの対策
- 各ユーザーごとのレートリミット(最大アクセス回数/単位時間)を決める
- サービスの性質に合わせて良い感じにできると良さそう
- 単位時間、現在公開されているAPIを見ると1時間くらいが多いらしい
- 制限を超えた時は429と制限内容を返すと良さそう
Playright + reg-suit + GitHub Actionsでビジュアルリグレッションテストを定期実行する
ビジュアルリグレッションテストとは
スクリーンショットの比較によって、見た目に差分が生じていないかを検証するテストのことです。
意図しない差分が検知されれば、コードの変更で予期せぬ影響が出ていることに気づけます。
なぜやりたかったのか
既存のRSpecによるE2Eテストによって、開発者がテストケースとして書いた箇所は期待通り表示されていることが担保できていましたが、それ以外の箇所でも差分が発生していたら気づけるようにしたいとなったのがきっかけでした。
また今回は
- Playwrightを使ってブラウザを操作したい
- できればNode.jsではなく、Rubyを使ってPlaywrightを動かしたい
- CIツールを使って定期実行したい
- 常に前回実行時に撮影したスクリーンショットを、次回の期待画像としてテストしたい
という要件がありました。
構成
以下の構成でビジュアルリグレッションテストを実行するようにしました。
- Playwright
- ブラウザを操作して、スクリーンショットを撮影する。
- playwright-ruby-client
- Google Cloud Storage
- reg-suit
- Playwrightとの組み合わせでビジュアルリグレッションテストを実装した事例が多く見つかったことから、導入しやすいのではないかと考えて選定しました。
- 利用したプラグイン
- reg-simple-keygen-plugin
- reg-publish-gcs-plugin
- reg-notify-slack-plugin
- テストの結果をSlackへ通知するためのプラグイン。
- GitHub Actions
- スケジュールイベントでワークフローをトリガして定期実行できるようにします。
実装
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
- どれだけ細かい差分を検知するかの閾値の設定。0だと意図しない差分まで検知されたので0.1にしています。(0〜1で指定)
- cf. https://github.com/reg-viz/reg-suit#core
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 Actionsでsecretsを扱うとき、秘匿情報がマスクされるようになっている」ことで、
JSONを扱う時 {}
のみがマスクされるようになっているために
- 本当に隠したいはずのkey:valueはログなどに露出されてしまう
- JSONとして扱えない
- cf. https://github.com/actions/runner/issues/1488
という問題がありました。
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にすべきではない
- 処理を共通化して良いかどうか判断するには、同じビジネス概念かを見分ける必要がある
- ビジネス理解が進んだら「あるビジネス概念が実は複数の異なるビジネス概念だった」というのはありがち。それがわかった段階で共通化を解除するなど構造を見直すのがベター
- 例えば、
- 「通常割引メソッド」と「夏季セール割引メソッド」が割り引く値以外差異がなくロジックはほぼ同じな時に「重複コードではないか?」と思うかもしれない
- しかし夏季セール割引の仕様が変わったとしたら、夏季セール割引のロジックは通常割引とは違うものになる
- このように同じようなロジックが複数があるからと言って責務を考えず無理にひとまとめにすると責務が多重になる。
- マジックナンバー
- ロジック内に直接書き込まれている意図不明な数値。説明なき数値は開発者を混乱させる
- 影響範囲を最小化するよう設計する
- ラバーダッキング
- 意図がわからない名前
- 理解の難しさから翻訳作業が都度必要になる(💭超わかる)
- 例えば仕様変更の依頼があったとき、対応するメソッドや変数がなんなのか頭の中で翻訳作業が必要になる
- 新しくジョインしたメンバーへの説明コストUP
- スプレッドシートで用語集などが作られるケースがあるが、この手の資料はメンテナンスがおろそかになりがち
- 人間の注意力には限界があるので、仕様とロジックを相互にいつでも正確に翻訳できるわけではない。不注意により誤って解釈する可能性がある。
- また意図不明な名前は解釈の誤りをさらに増大させる(💭ついこの間起こった気がする)
- 技術駆動命名
- 連番命名
- 基本的に名前は省略しない
- 再説明コメントで命名をごまかす
- 意味が伝わりにくいメソッドでは再説明コメントが書かれがち(💭わかる)
- あまり可読性に貢献しない上、メソッドが変わるたびにコメントの更新作業も発生
- ロジックをなぞるだけのコメントは退化しがち
- (実装が変わったのにメンテナンスされず実装を正しく説明しなくなること)
- ダメなメソッド名をコメントで補足説明するのではなく、メソッド名自体をブラッシュアップしましょう
- 情報システムとは
- 現実世界の概念のみをコンピュータの世界へ投影した仮想現実といえる
- 現実世界の概念をコンピュータの仮想世界へ変換し、意味を対応づけ、そして概念的なやり取りをコンピュータによって高速化することで高速化していると考えられる
- 現実世界での物理的な存在と情報システム上のモデルが1:1になるとは限らず、1:多になるケースが多いのが特徴
- GitHubのユーザー設定では、目的ごとにアカウント・プロフィールなどと目的ごとにモデルが分かれて表現されている
- 設計しないと開発生産性が低下する
- レガシーコード
- 変更が困難で壊れやすいコード。またレガシーコードが蓄積している状態を技術的負債と呼ぶ
- 開発生産性が低下する要因は主に次の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
に切り出す - フィルタのテストは一時的なコントローラを作ってテストする
- 親コントローラがその
concern
をincludeしていることをテストする
# 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)内に全てのストア関係を記述。基本的な方式