OBOEGAKI IN MY HEAD

主にIT関連の覚え書き

30歳でSIerのSEからスタートアップのソフトウェアエンジニアに転職して3年が経った

はじめに

忙しさにかまけて超久々の記事になってしまいました。

新卒で入社したSIerからWebサービスのスタートアップにソフトウェアエンジニアとして転職して3年が経過したのでやったことの振り返りをしようと思います。

自分と同じような境遇でWebサービス企業でエンジニアをやるかで悩んでいる方が仕事内容をイメージをするのに役立てればと思います。

転職前後の変化まとめ

3年経った結果

  • 入社時はバックエンド中心だったが、アプリ、フロントエンド、クラウドインフラ、チーム開発、運用などWebサービスを構築する業務について一通り経験できた
  • 新規サービスを立ち上げて継続的にリリースして運用するスキルと勘所が身についた
  • 少ないリソースで課題を解決することが求められる環境のため、開発の前に解決手段が正しいか議論したり、全体業務フローや将来を見越して取り入れるべき機能かどうかを検討する癖が身についた
  • サービスが成長するにつれ、どんなビジネス要求が現れるかを予測して設計に落とし込むべきか考えるようになった
  • Web系は未経験だったが、様々なプロジェクトを乗り越えたおかげで気づいたら技術リーダー的立ち位置になった

SIer時代

  • 数千人規模の会社で数十人規模の部に所属。扱うシステムは20年近く運用されてきたもの
  • 周りは業務委託のエンジニア含めGitHubすら知らない
  • 仕事は機能開発プロジェクトの要件定義(Excel)、設計(Excel)、テスト計画(Excel)、プロジェクト管理(Excel)など
  • リリースは数ヶ月に1度
  • ときどき開発もするが、開発マシンはWindows メモリ500MBでインターネット接続なし
  • 年収・福利厚生は良い。課長級で1000万、住宅補助フル活用なら30歳で600万半ばまで行く(自分は対象外だったけど...)
  • 労務管理はしっかりしている

Webサービス企業に転職するためにやったこと

  • 休日はだいたい自習室にこもって勉強・開発。平日も22時前に帰宅できたら自習室へ
  • VPSを借りて自作サービスをリリース
  • 自作サービスは他の人に譲渡してすぐに運用できるレベルを目指した。コードだけでなく、プロビジョニング(Vagrant、Ansible、Docker)、テストコード(単体テスト、E2Eテスト)なども準備
  • その他周辺ツール(エディタ、Gitなど)や設計などについて勉強
  • 勉強期間は1年半ほど

スタートアップ入社後

  • 入社当時は十数人ほど。エンジニアとしては5人目
  • 入社時に年収は100万以上下がったが、1年後の昇給で前職の年収を超える金額にはなった
  • リリースは1ヶ月に数回
  • 人が少ないのでいろいろやる。バックエンド、インフラ、アプリ、フロントエンド全部必要に応じてやる。顧客折衝も開発もサポートもやる
  • 入社当時はRailsの経験しかなかったが、都合よく人員がいるわけでもないのでフロント、アプリ、インフラも自分でやる
  • 開発マシンはMacBook Pro
  • 人員が少ないのでハードワークになりがち(最高月間残業時間 160h)
  • 大体自分でなんとかする必要がある
  • 勉強期間がかなり効いた。他のエンジニアとコミュニケーションを取るための前提知識が揃ったし、むしろその知識を活かして提案などができた。困ったのはAWS等のクラウド知識くらい。とはいえそこもVPSで色々構築したときの知識が理解に役立った
  • SIer時代の慣習もプロジェクトの確実性を上げるために役立ったりした。また、SIerのときにSQLを書きまくっていたおかげで開発・分析等で役立った
  • 鍵をもらって休日には自習室代わりにオフィスに行って勉強できた

転職1年目

toB向けSaaSのアプリデザイン刷新、ユーザ企業へのトライアル導入

転職後、開発開始したばかりのtoB向けSaaSの開発とユーザ企業へのトライアル導入の調整にアサインされました。

入社時点ですでにそのSaaSのネイティブアプリがあったものの、使い勝手が良くなかったのでデザインの刷新をしながら、同時にユーザ企業へトライアル導入することでフィードバックを得つつ開発をしていく、ということをしていました。

そのSaaSが利用される現場での業務内容を理解するためにユーザにヒアリングをしたり、現場に視察に行ったりとオフィスの外での作業も多くありました。 ヒアリング等で得た情報を整理して、現場での利用に適したアプリの画面フローをAdobe XDでプロトタイピングしてCEOと議論してフィードバックを得てプロトタイプを修正して...を繰り返してアプリをブラッシュアップしていきました。

現場に行ってアプリを使っているユーザの後を追いかけて使っている様子を観察したり、動画を撮らせてもらったり、ユーザ企業の事務所で作業させてもらっていたらコーヒーを淹れていただけたりとオフィス外での思い出が印象深いです。

このような現場目線での改善結果が評価され、このSaaSは後に大企業でのコンペを経て採用が決まりました。

外部コンサルを入れての技術導入

上記SaaSで数学的な問題を解決する必要があり、社内に適した人材もいなかったため、外部からコンサルを入れて取り組むことになりました。

コンサルの提案内容を理解し、効果のありそうなアプローチかを判断し、実装してもらい検収する...とすべきですが、このときは少しアプローチの精査が甘かったと反省しています。

最終的にコンサルから出てきたものは自分達でも実装できてしまうレベルであったため、外部の開発リソースを買うだけになってしまいました。提案内容に専門用語が出てこようと自分で中身を調べて理解しておくべきでした。

その後、現場からこの機能についての不満が出てきたため、改善を行うべく自分で調査していたら、この問題を解くライブラリがGoogleによってオープンソースで開発されていることがわかりました。

結局そのライブラリを使って自分で機能を作り直すことになってしまいました。結果としてコンサルが真にその分野の専門家ではなかったのだと思います。事前に自分で相談する予定の分野について調べておき、本当にその人がその分野の専門家であるかを見定めなければならないと認識した出来事でした。

とはいえ、このときの納品物は全くの無駄になったわけではなく、実は3年近く経った今でも一部使われていたりします。

新サービスA立ち上げ

toB向け新サービスを立ち上げすることが決まり、私はプロジェクト管理、要件定義、画面設計、開発を担当しました。これが初めてサービスの立ち上げから携わった初めてのプロジェクトでした。(この記事でやたらと新サービスが出てくるので新サービスAとします)

CEOの語るビジョンややりたいことをベースに議論しながら要件定義していき、どのような画面設計やフローにするかをプロトタイピングしつつ決めて開発する、というのは最初のプロジェクトとは変わらずです。

このときでは開発者が自分以外にももう2人いたのでタスク割り振りやスケジュール管理をしつつ運用していましたが、他のサービスの機能開発や運用をしながらだったので全然予定通りにいかず...。 結局ハードワークでなんとかするみたいな感じになっていました。 入社当時は1プロダクトのことだけしかやっていませんでしたが、このような新サービスが増えるたびに同時兼務が増えていきました。

このとき、実装する機能がどんな思想に基づいて実装されているかを明確にしたいという思いがあり、ドメイン駆動設計を勉強していたこともありプロジェクト憲章を作りました。この新サービスがどんなプロジェクトであり、どんな価値を提供するものなのかを文章にしてチーム内で合意を取りました。

プロジェクト憲章が機能検討時の判断の軸になることを期待しましたが、チーム内にあまり浸透させることができませんでした。定例会議の議事録の1行目にプロジェクト憲章へのリンクを貼っておいたり、短縮バージョンを貼っておいたりしたのですが、書いてあるだけでは見ないんですよね...。

プロジェクト憲章は残念ながらチームの役には立てませんでしたが、自分の役には立ちました。要件検討のときに、こんな機能をつけてはどうか、と提案があってそれの採用可否を決めるとき、その根拠がただの自分の感想ではなく、プロジェクト憲章で定義したこのサービスが提供しようとする価値に沿っているかどうかを根拠にしたことでメンバーの納得感が出ていたように思いました。

また、プロジェクト憲章を一番読んだ人間だったのでこのサービスがどんな価値を提供すべく立ち上げられ、どんな性質を持つべきなのかについて一番語れる人物になっていました。後に立ち上がる新サービスBでもプロジェクト憲章に近い文書を作って展開したら途中参加のエンジニア達に喜ばれました。そしてSlackのチャンネルにピン留めしてくれたのですが、やっぱり見てもらえず...。なんでこのプロジェクト始まったんですか?と聞かれてチャンネルのピン留めを紹介する、という感じでした。

1年目で使った技術・得た知識など

  • Railsでの開発(業務としては初)
  • AndroidJava)アプリの開発
  • Vue.jsでの開発
  • R言語での開発(コンサルの納品物を修正するために)
  • Pythonでの開発(コンサルの納品物を自分で再実装)
  • AWSでのインフラ構築(ElasticBeanstalk、EC2、S3、CloudFront)
  • LP制作(HTML+CSS+Googleフォーム+JavaScript
  • プロトタイピング(Adobe XD)
  • 新サービス立ち上げ時に必要なタスク(ロゴ制作、PR方法検討、LP制作、競合からの調査防止などを踏まえたサインアップ導線整備など)

2年目

新サービストライアル導入

前年に作った新サービスAをユーザ企業でトライアル導入しました。企業間を連携するための機能があり、3社もあったので調整が大変でしたが、どんな検証をするのか、どんな現場オペレーションにするかを整理してなんとか実現しました。

そしていつものように現場に張り付きオペレーションを観察したり、ヒアリングをしたりしました。 連携機能を使わない企業ではうまくサービスが使われたものの、企業間連携は業種が異なるため、A社にはメリットがあるもののB社のメリットが薄いということになり、トライアル以降は進展することはありませんでした。

Android用チャットライブラリ実装

新サービスAでAndroidアプリとブラウザ間でチャット機能を実装することが決まりました。

アプリにそのまま組み込むこともできたのですが、アプリとブラウザ間のコミュニケーションというのは社内のプロダクト全てに共通して必要とされる機能であると考えたため、ライブラリとして他のプロダクトにも組み込めるような形にして開発しました。

チャットのバックエンドは外部サービス任せにしてSDKを使って実装したものの、他のプロダクトに組み込めるような形にするのが大変でした。このライブラリは後の新サービスBに実際に利用されました。また、他のプロダクトでもチャット実装の要望が出てきたときに工数節約が期待されました(結局チャット実装そのものが見送られてしまいましたが)。

新サービスB立ち上げ

新サービスAは残念ながらクローズすることが決まりましたが、toB向けの新サービスBが立ち上がることが決まりました。私はいつもどおりプロジェクト管理、要件定義、画面設計、開発を担当しました。

ある事業領域における一通りの業務を機能として実装する必要があったため、まずはその事業領域における知識をつけるために業界団体が公開している資料を読み漁りました。また、その事業領域の現場視察の機会に現場の方に質問しまくり、写真撮影させてもらい、とにかく情報収集しました。

それらの情報を元に要件定義・画面の情報設計をしていきました。かなり複雑な要件になることが容易に想像できたため、テーブル設計も入念に行いました。その甲斐あって現在に至っても大きな問題なく機能拡張し続けられています。

また、当時社内では残念ながらテストがあまり書かれていませんでしたが、このプロジェクトではテストがないと絶対に破綻すると感じていたので、少なくとも全エンドポイントのリクエストテストは書く、ということを実践しました。

バックエンドはほぼ自分が書いていたのでテストを書くことは実践しやすかったですが、後に他のメンバーが加わったとき自然とテストが書かれるようになったのが印象的でした。すでにテストが整ってるとテストデータの用意が簡単なために書くハードルが下がったり、自分が整ってるものを乱すわけにはいかないみたいな心理が働いたのかもしれません。

このプロジェクトで業務領域の知識を元にテーブル設計したり、テストの整備などを頑張った甲斐もあって設計のスキルが向上したように思います。また、ユーザ企業にトライアルで使ってもらいる間にも絶えず機能拡張を続けていたため、バックエンド、フロントエンド、アプリ間のバージョン互換性にも敏感になりました。バージョンの組み合わせで問題が発生しないように互換性を保つように開発し、リリースの順番にも気をつけるようになりました。

ドキュメント整備

人が増えてきたこともあって、都度システム構成や業務知識を説明するのが大変になってきました。そろそろドキュメントを整備しないといけない、ということでドメイン駆動設計のコンテキストマップを参考に社内にどんなシステムがあってそれぞれがどんな業務領域をカバーしているかを表す図を作成してチームに展開しました。

リリース手順の自動化が甘いところは、一時しのぎですが手順のチェックリストを作ってミスを防ぐようにしました。その他にも機能追加をするときに見落としがちな影響箇所などを漏れなく確認できるようなチェックリストも作って属人性を排除するよう務めました。機能が多かったので要件定義時などに影響箇所のチェックとしてこのリストはとても役立ちました。

2年目で使った技術・得た知識など 

  • Railsでの開発
  • Vue.jsでの開発
  • Pythonでの開発
  • AWSでのインフラ構築(ElasticBeanstalk、EC2、S3、CloudFront、DynamoDB、Lambda)
  • プロトタイピング(Figma
  • ドキュメント整備(esa、draw.io、Plant UML
  • 互換性を保って継続リリースする方法
  • わからない業務領域でも業界団体が資料を公開してるので調べられる

3年目

新サービスBのユーザアカウントモデルのリファクタリング

ビジネス要求により新サービスBのユーザアカウントの考え方が変わりました。今までは業務1をやる人のためのアカウント、業務2をやる人のためのアカウントという考え方でしたが、アカウントを作ったらやる業務に応じて権限を変えることで業務1, 2の両方をできるようにすることになりました。

もともとユーザアカウントと権限という分け方をしていれば良かったですが、権限が違えばやることが全く違ってくるので業務1ユーザモデル、業務2ユーザモデルのように別々のモデルとして作っていました。そのため、統合されたユーザモデルを作り、それらに権限をつけ外しできるようにし、さらに権限に応じた機能制限を入れる必要が出てきました。

テストが無かったらその場しのぎで負債を残すところでしたが、幸い全エンドポイントにテストを書くことは継続されており、ちゃんとした設計へのリファクタリングをすることができました。やっぱりテスト大事。

新サービスCの立ち上げ

新型コロナウイルスの影響で緊急事態宣言が出されることが濃厚になった頃、この状況下で社会のために我々ができることをやろう、ということでtoC向けサービスを立ち上げることが決まりました。

緊急事態宣言内でのリリースを目標に最悪でも1ヶ月後にバックエンド、AndroidiOSアプリを用意しなければならず、さらには決済機能も実装しなければならないということでかつてないほどの短納期プロジェクトとなりました。

要件定義は企画チームが行い、私はバックエンドの設計、実装、さらにFlutterによるアプリ実装にも加わりました。
さすがにテストを書く暇がなかったものの、決済に関しては問題を起こすわけには行かなかったのでそこだけはテストを書いて後はとにかく実装に時間を割きました。これだけでもだいぶ役に立ちました。他のところは要件が変わったりしていたのでテストを書かずに正解だったかもしれません。

アプリ開発AndroidiOSアプリを個別に作るのは不可能と判断し、経験者がほとんどいないもののFlutterで開発することにしました。最後の方はエンジニア総出でFlutterを書いていました。
開発中の30日の間で休んだのは1日だけ、朝の4時まで開発、その5時間後に勤務開始して26時まで開発、みたいなだいぶぶっ飛んだ働き方もしつつ、4月の最終週にはなんとかリリースにこぎ着けました。

急ごしらえなので保守を心配しつつも、ゴールデンウィークはゆっくりできるかと思いきや、一本の電話により次のプロジェクトの始まりが告げられます...

新サービスCにtoB向け機能追加

新サービスCはtoC向けですが、そのユーザと接点をもつことができるようなtoB向け機能を追加することが決まりました。

緊急事態宣言下で業績の下がる小規模企業に向けた機能で、ある自治体との連携の予定が取れたということでその自治体の記者会見までに用意する必要があるため、これまた短納期でのプロジェクトとなりました。

私はtoB向けのサインアップ機能や出店機能、出店企業を検索するためにElasticsearchによる全文検索、緯度経度検索などを実装しました。toB向けiOSアプリ、Androidアプリも用意してゴールデンウィーク返上で実装し、リリースしました。

しかし、記者会見ではベンチャーと連携していくみたいなことを言ったものの、具体的な名前を出してもらえず肩透かしをくらったのでした。

新サービスCの料金計算方法を変更&キャンペーン機能追加

新サービスCの利用を促すために料金割引キャンペーンを実装することになりました(これまた短納期で)。さらに同時に料金の計算方法を全く異なるものに変えるというものです。このときも要件定義は企画チームに任せ、開発に全振りしました。

アプリの画面上に料金を出しているところが何ヶ所もあり、アプリ内を回遊しているユーザに影響を出すことなく料金の計算方法そのものを変えたり、キャンペーンを適用して旧アプリの動作に影響を与えず価格を変更できるようにするのは工数がかかりすぎるため、ユーザ告知をしてメンテナンス時間を入れました。

結果的にはメンテナンスを入れたものの、要件が確定するのを待っていたら開発が間に合わないので、メンテナンス無しでリリースすることも考え互換性のある形で開発していました。そのお陰で新バージョンのアプリがなくてもバックエンド実装を進めることができましたし、メンテナンスが必要になる場合の影響調査も進みました。

料金計算方法変更およびキャンペーン機能は無事リリースされ、短納期だったにも関わらず料金関係の問題も起こさずに済みました。

機能統合プロジェクト

いくつかの新サービスが立ち上がる中で、短納期のために同じユーザに向けた機能が別々のサービスに実装されており、その機能の改修に他のチームと連携を取る必要があったり、それぞれのサービスでデザインが異なっていたりと開発をする上で課題があったのでその機能を片方のサービスに統合することになりました。

統合するにあたっては、一番最初に統合後のデータ構造を設計してチーム内でレビューを受けておき、バックエンドとフロントエンドの開発者が同じデータ構造のイメージを共有しながら作業を進められるようにしました。今後も統合が必要と思われる機能は、以前コンテキストマップを作成したときにあたりがついていたので、将来の統合も踏まえて設計していきました。

しかし、一方のシステムの設計が非常にわかりにくく、さらには特殊なユーザアカウントにだけ発動するロジックなどがあり、その分析に時間がかかりました。その分析結果のドキュメントが膨大になってしまい、それを理解して何に気をつけて実装すればいいかを知っているのは自分だけみたいな状態になってしまいました。

そして不運なことに途中で私は他のプロジェクトを手伝いに行く必要が生じました。とはいえ、ドキュメントを書きつつタスクチケットへのリンクも都度行っていたので、引き継ぎ後に来る質問も大体はこのドキュメント見てください、と軽い説明で済みました。とは言え量がすごいので説明コストがかなりかかりました。

このプロジェクトは現在も継続中です。

3年目で使った技術・得た知識など

  • Railsでの開発
  • Vue.jsでの開発
  • Flutterでの開発
  • AWSでのインフラ構築(ElasticBeanstalk、EC2、S3、CloudFront、Elasticsearch Service)
  • ドキュメント整備(esa、draw.io、Plant UML
  • 決済系システムの知識

おわりに

3年分を一気に振り返ったため激長文章になってしまいました。ここまで読んでくださった方は本当にありがとうございます。

SIerからスタートアップに転職してからあっという間の3年でしたが、率直に言って転職して良かったです。SIerにいた頃は周囲は技術に興味がなかったのでシステムの設計は凄惨たる有様でしたし、Excelにテスト結果のスクリーンショットを貼り付けて印刷してファイルに閉じる、などの誰が喜ぶのかわからないしょうもない書類作成作業などとてもつらい時間が長かったです。

転職後はどうやったらユーザの課題を解決できるかを考えてみんなで議論したり、より良い設計を考えつつ実装したりとプロダクトのためになる行動が求められます。スピード感も全く違って1週間先すら読めないみたいなことがあって非常に濃い3年間であったと思います。

転職直後は、まずはWeb系のソフトウェアエンジニアとして1人前になることを目指していましたが、この3年間で様々なプロジェクトを乗り越え一通りの業務を経験したことで、とりあえずその目標は達成できたかなと思います。

スピードが求められ、設計に十分な時間が取れなかったりリリース時期は変わらないのに要件は変わったりとハードワークせざるを得ない場面も多数あり、健康に支障をきたすときもありました(1回目の緊急事態宣言下の超ハードワークにより腕を故障し、今でも影響が残っています)ので、良い事ばかりではないですが、これからは健康にも気をつけて引き続き頑張っていこうと思います。

個人開発のWebサービスを公開した&その振り返り

はじめに

 個人で開発していたToDo管理サービスOneProgressを公開しました。ソースコードGithub上で管理しています(https://github.com/shimgo/one-progress)。このサービスを個人開発する上で発生した問題や、得られた知見について書いていこうと思います。

サービスの紹介

 今回開発したOneProgressは、基本的な機能はシンプルなToDo管理のみですが、開始したタスクはすべてのユーザーに公開される、という特徴を持っています。これは、喫茶店などの人の目があるところで作業すると捗る効果をどうにかWeb上で実現できないか、と考えて作ったものです。

 以下の画像の右側が現在すべてのユーザーが取り組んでいるタスクの一覧です。左側は自分が作成したタスクの一覧で、これらは開始するまで公開されません。 f:id:mshingo:20171015163855p:plain

開発の目的

 今回開発をするにあたり、目標としたのは単にWebサービスを公開することではなく、何もない状態からサービスを開発・公開し、さらにプロジェクトとして継続して運用可能な環境を作れる能力を身につけることでした。そのために単純にRailsアプリを作るだけでなく以下の条件を満たすリポジトリを作りあげることを目標としました。

  • 外部サービスと連携する機能を実装すること(テストでハマる経験を得られると推測)
  • 誰でも実行可能なユニットテスト、E2Eテストを用意すること
  • 誰でも同じ開発環境上でアプリの動作が確認できること
  • 環境構築は自動化すること
  • workaround的な対処をできるだけ避けること

遭遇した問題

モチベーション低下問題

 個人開発でよく聞く問題だと思いますが、やはりこの問題は発生しました。よくある対策は、コードの綺麗さを考えずとにかく動くものを実装して短期間で作る、といった方法でしょうか。しかし、私の場合は技術力の向上のため、良い設計が思いつかないためにworkaround的な対処をしてしまうことを避ける、という目標を掲げていたので、良い設計や実装について考えるために時間をかけてでも取り組みました。そうすると当然、一日で得られる成果が減り、仕事から帰って来て休む間もなく取り組んだ割に1コミットもできなかったりすると結構ヘコみました。

 この対策としては、タスクを小さく刻んで少しでも進捗を得ている、という小さな達成感を得られるようにするのがいいのではないかと思いました。この経験があったために、OneProgressではタスクの目標時間の上限を60分までにして、タスクを小さく分解することを推奨する作りにしました。

環境構築自動化やりすぎ問題

 環境構築は自動化する、という条件をつけましたが、これを守ることに固執して時間を多く費やしてしまった点がありました。プロダクション環境の構築を完全に自動化させようとするあまり、サーバーを使い始める前に一度しか行わない設定も時間をかけて実現しようとしてしまいました。

 そのため、途中で効果の薄いものに関しては多少の手作業も許すことにしました。しかし、ここで時間をかけたことが無駄になったというわけでもありません。時間をかけて自動化の方法を模索したことでVagrantやAnsibleの使い方や連携方法について知見を得ることができました。

それでもやっぱり時間かけすぎでは?

 コミットログから、機能実装、E2Eテスト、環境構築自動化ごとの期間をざっくり出してみました。

工程 開始 終了 期間
機能実装とユニットテスト 2016/09/24 2017/04/10 約6.5ヶ月
E2Eテスト 2017/04/10 2017/05/28 約1.5ヶ月
環境構築自動化 2017/06/16 2017/10/01 約3.5ヶ月

…長いっすね。

 SIer勤めで残業もある中、余暇でこつこつ進めて1年かけたのにサンプルアプリレベルのToDoアプリリリースって、結果だけ見ると"1年かけてこれだけ?"って感じですね。とはいえ、一年前の自分とは比べ物にならないくらいの知識が得られたと思っていますし、環境構築の自動化と結構な期間向き合ったことでインフラの知識やVagrant、Docker、Ansibleの知識も強化されました。

まとめ

 開発の目的として、「何もない状態からサービスを作成・公開し、さらにプロジェクトとして継続して運用可能な環境を作れる能力を身につけること」を掲げていましたが、とりあえずは超小規模ながらも達成はできたかなと思います。それでも時間かけすぎた間は否めないので次はもう少し早く達成感を得られるようなゴール決めをしようと思います。モチベーションのコントロールはやっぱり大事です。

名著「理科系の作文技術」を今更ながら読んだ

 だいぶ前から読書リストに入れてあった「理科系の作文技術」を読みました。

理科系の作文技術(リフロー版) (中公新書)

理科系の作文技術(リフロー版) (中公新書)

 この本は、題名に"理科系"とある通り、多くの例が理系論文を元にしていたり、論文の書き方について解説していたりするところもありますが、報告書のような、文系・理系問わず仕事で作成する書類にも通じる知見を得ることができます。わかりやすい文書をどのようにして作成すればよいか、読みにくい文となる原因は何か、などを知りたい方におすすめの本です。論文を書く人には必読と言ってもいいのではないでしょうか。(大学生のときに授業でろくに使わない参考書を買わせるぐらいならこれを買わせてほしかった…)

 この本を読んで、今後文書作成のときに自分なりに気をつけておきたいことをまとめてみました。

  • 情報の順番は読者が期待する内容になっているか
  • この文書が、テーマに対して何を主張するものなのかを考える
  • その文は事実と意見のどちらなのかを意識する
  • 不要な言葉は一つも書かない
  • そのパラグラフはトピックセンテンス(そのパラグラフの概論)を含むか
  • そのパラグラフはトピックセンテンスに関係のない文を含んでいないか
  • トピックセンテンスだけを並べたとき、文書の要約になるか
  • 一つの文の中に二つ以上の長い前置修飾節を書かない
  • 修飾節の中の言葉に修飾節をつけない
  • 一つの文の中で主語が変わっていないか
  • 文末の述語が単調でないか

 これらに加え、ダッシュ(―)、セミコロン(;)、コロン(:)などの記号の使い方を理解してうまく取り入れれば、わかりやすい文を書くのに役立つと思います。本書ではそれらの用法についても解説しています。これから何度も読み返す本になりそうです。

Rubyでテストを書くときの方針をまとめる

はじめに

 「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んで、テストを書くときにこの書籍のテスト関係の話がすぐに思い出せるようにまとめが欲しいと思ったのでまとめてみます。

 この記事では、この書籍で語られている多くの背景が省略されているため、手放しでこのまとめにあることを実践するのはおすすめしません。また、私の言葉で述べているところがたくさんあるので、ぜひ一度書籍を読むことをおすすめします。テストだけでなく設計の話も大変参考になります。

簡易まとめ

 書いてみたら長くなってしまったのでサッと確認する用にまとめておきます。

  • テストを書く対象
    • テスト対象
      • 受信メッセージの戻り値
      • コマンドメッセージの送信(引数・回数)
    • テスト対象外
      • クエリメッセージ
  • テスト中に利用可能な情報
    • テスト対象オブジェクトから得られる情報のみ
  • 受信メッセージのテスト
    • パブリックインタフェースのテスト
    • 未使用インタフェースは削除(削除しないならテストを書く)
    • ロールのテスト
      • ロールを定義するためのテストを書く
      • テストダブルを使用(ロール定義テストをincludeしてロールの要件を満たすことを証明する)
    • プライベートメソッドはテストしない
  • 送信メッセージのテスト
    • コマンドメッセージのテスト
      • モックを使って正しい引数・回数で呼び出されることをテスト
  • 継承されたコードのテスト
    • スーパークラスのインタフェースのテスト
      • このテストが成功するサブクラスはどれでもスーパークラスのように振る舞うと判断される
      • スーパークラスの階層構造内のすべてのクラスのテストでincludeする
    • サブクラスに課される要件のテスト
      • サブクラスが満たすべき共通の要件をテスト
      • すべてのサブクラスのテストでincludeする
    • 具象サブクラス固有のテスト
    • 抽象スーパークラス固有のテスト
      • インスタンス化が難しいならテスト用にスタブしたサブクラスを使用する
      • スタブしたサブクラスのテストにサブクラスに課される要件のテストをincludeして、サブクラスの要件を満たすことを証明する

 以下から各項目について詳細を書いていきます。

何をテストするか

 あるオブジェクトに対してテストを書く対象を決定する方針は以下の通りです。

  • テスト対象
    • 受信メッセージの戻り値
    • 副作用を持つ送信メッセージ(コマンドメッセージ)が適切な引数・回数で送られたこと
  • テスト対象外
    • 副作用を持たない送信メッセージ(クエリメッセージ)

 コマンドメッセージは戻り値を気にせず送られたことだけをテスト対象としています。これは、送信メッセージは送信先オブジェクトにとっての受信メッセージであり、その戻り値に関するテストは送信先オブジェクトでテストされているべきだからです。

テスト中に利用可能な情報

 テストで利用できる情報はテスト対象オブジェクトから取得できる情報のみです。テスト対象オブジェクト以外に関する情報については可能な限り無知であり続けることで、テスト対象ではないオブジェクトのコード変更の影響を受けずに済みます。

受信メッセージのテスト

パブリックインタフェースをテストする

 考えられるすべての状況において、受信メッセージを実行することで戻される値や状態が正しいことを証明します。

使われていないインタフェースは削除する

 受信メッセージがどのオブジェクトからも依存されていなさそうな場合、それは存在しない要件を推測した実装です。そのメッセージを残すことはテストとメンテナンスのコストを増加させるだけなのでそのメッセージは削除します。削除しないのならば必ずテストを書きます。

ロールをテストする

 テスト対象が特定のロールに依存するとき、テストをする際にそのロールを担う実際のオブジェクトをテスト対象に渡してテストしていれば、そのロールのインタフェースが変わったとき(ロールを担うオブジェクトのメソッド名が変わったとき)、テストは当然失敗します。ロールを担うオブジェクトが1つしかないのであればそのようなテストでも問題はありません。しかしロールを担うオブジェクトが複数存在する場合には、そのロールの存在を示すようにテストを書くことも検討すべきです。
 例えば、以下のように渡されたwheelがdiameterメソッドに応答することを期待するGearクラスがある場合を考えてみます。

class Gear
  def gear_inches(wheel)
    ratio * wheel.diameter
  end
end

diameterメソッドを持つDiamaterizalbeロールが存在(Diamaterizalbeがコード上に宣言されているのではなく、diameterメソッドに応答するオブジェクトが存在するという意味)していることを示したい場合、テストダブルを利用します。 下記のようなテストダブルのオブジェクトをテストコード内で定義し、gear_inchesメソッドに渡すことでテスト対象はdiameterメソッドを持つロールに依存していることを示すことができます。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.5780-5781). 株式会社技術評論社. Kindle 版. 
class DiameterDouble
  def diameter
    10
  end
end

 しかし、この方法には1つ問題があります。Diamarerizableロールを実装するアプリケーション内で実際に使われるメソッド名が変更されても、テストダブルはdiameterメソッドに応答できるためにテストが成功してしまい、実際のアプリケーションではエラーになることに気づけません。そこで、ロールのインタフェースをテストするモジュールを定義します。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6208-6210). 株式会社技術評論社. Kindle 版. 
module DiameterizableInterfaceTest
  def test_implements_the_diameter_interface
    assert_respond_to(@object, :diameter)
end

このモジュールをDiamaterizableロールを担うすべてのオブジェクトのテストにincludeすることでロールのインタフェースが変わったときにテストが正しく壊れるようにすることができます。

# Diamaterizableロールを実装する具体的なクラスのテスト
# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6214-6218). 株式会社技術評論社. Kindle 版. 
class WheelTest < MiniTest::Unit::TestCase
  include DiameterizableInterfaceTest

  def setup 
    @wheel = @object = Wheel.new(26, 1.5)
  end

  def test_calculates_diameter
    # ...
  end
end


# テストダブルがDiamaterizableロールを実装することを示すテスト
# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6234-6237). 株式会社技術評論社. Kindle 版. 
class DiameterDoubleTest < MiniTest::Unit::TestCase
  include DiameterizableInterfaceTest

  def setup 
    @object = DiameterDouble.new
  end
end

これにより、Wheelクラスのdiameterメソッド名が変更されると、Gearクラスのテストは成功しますが、WheelTest、DiameterDoubleTestが失敗してインタフェースの変更を検知することができます。

プライベートメソッドをテストする

 下記の理由からプライベートメソッドはテストすべきではありません。

  • プライベートメソッドは不安定
  • プライベートメソッドのテストが失敗したならパブリックインタフェースのテストもまた失敗するので冗長
  • テストによりプライベートメソッドの使用方法が文書化され、他の人に利用される可能性を高めてしまう

送信メッセージをテストする

クエリメッセージは無視する

 クエリメッセージは、それを送ることによる影響がテスト対象内部に限定されるのであればテストする必要はありません。そのためselfに送られたメッセージもテストする必要はありません。

コマンドメッセージはテストする

 他のオブジェクトにメッセージを送ることで他のオブジェクトの状態に影響を与えるようなテスト対象のメソッドがある場合、そのメッセージを正しい引数・回数で呼び出すことをテストする必要があります。
 これにはモックを使用します。下記の例ではset_cogメソッド内でobserverのchangedメソッドが正しく呼び出されることをテストしています。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6015-6020). 株式会社技術評論社. Kindle 版. 
class GearTest < MiniTest::Unit::TestCase
  def setup
    @observer = MiniTest::Mock.new
    @gear = Gear.new(chainring: 52,
                     cog: 11,
                     observer: @observer)
  end

  def test_notifies_observers_when_cogs_change 
    @observer.expect(:changed, true, [52, 27])
    @gear.set_cog(27)
    @observer.verify
  end
end

継承されたコードをテストする

継承階層内のすべてのオブジェクトがリスコフの置換原則に従うことを証明するには、その共通の契約に共有されるテストを書き、すべてのオブジェクトにそのテストをincludeします。共通の契約に共有されるテストは、スーパークラスのインタフェースのテストと、スーパークラスによって課されるサブクラスが満たすべき要件のテストを指します。

スーパークラスのインタフェースのテスト

スーパークラスが満たすべきインタフェースを定義します。このテストを成功するサブクラスはどれでもスーパークラスのように振る舞うことができると判断できます。Bicycle階層構造内のすべてのクラスはこのテストをinclude する必要があります。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6320-6322). 株式会社技術評論社. Kindle 版. 
module BicycleInterfaceTest
  def test_responds_to_default_tire_size
    assert_respond_to(@object, :default_tire_size)
  end
  # ...インタフェースのテストが続く
end

サブクラスに課される要件のテスト

 サブクラスが満たすべき要件も共通のテストに記述し、すべてのサブクラスのテストでincludeします。下記のコード内で本当にサブクラス内で実装する必要があるのは、スーパークラスで例外を返すようにしているlocal_sparesメソッドだけで、他のメソッドの実装は任意です。local_spares以外のテストは、サブクラスがおかしなことをしてインタフェースを壊していないか確認する程度のものになります。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6364-6367). 株式会社技術評論社. Kindle 版. 
module BicycleSubclassTest
  def test_responds_to_post_initialize
    assert_respond_to(@object, :post_initialize)
  end

  def test_responds_to_local_spares
    assert_respond_to(@object, :local_spares)
  end
  
  def test_responds_to_default_tire_size
    assert_respond_to(@object, :default_tire_size)
  end
end

新たに追加されるサブクラスに対するテスト

上記のスーパークラスのインターフェースのテスト、およびサブクラスに課される要件のテストをincludeすることで、新たに追加されるサブクラスが標準的なBicycleのサブクラスであることを確信することができます。そのためまだ知識の浅いメンバーでも安全にサブクラスを作成することでできるようになります。

具象サブクラス固有のテスト

 上記でサブクラス共通のテストはできているので、あとはサブクラスごとに固有の性質をテストします。上記の例だとdefault_tire_sizeはサブクラスでの実装が必須なので、サブクラスごとにdefault_tire_sizeがどのように振る舞うかテストする必要があります。

抽象スーパークラス固有のテスト

 抽象スーパークラスは動作するのに必要なメソッドを実装していないこともあるので、インスタンス化が難しい場合があります。そのときは下記のようにスタブしたサブクラスを作成してスーパークラスのテストに利用することができます。

# Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Kindle の位置No.6500-6503). 株式会社技術評論社. Kindle 版. 
classStubbedBike < Bicycle
  def default_tire_size
    0
  end

  def local_spares
    {saddle:'painful'}
  end
end

 この方法にはテストダブルを作成したときと同様に、サブクラスに課される要件が変わっているのにStubbedBikeを利用したテストが成功してしまう可能性があることに注意する必要があります。この問題への対策もテストダブルのときと同様で、StubbedBikeTestを作り、BicycleSubclassTestをincludeすることでサブクラスの要件の変更による影響を検知できます。

おわりに

 テストを書くときの方針となるように自分なりにまとめてみましたが、私はまだ実際にこの方針通りにテストを書いたことがありません。実際にやってみるとこの通りにやるのがうまくいかない場面も出てくるかもしれないので、そのときはこの記事に追記するなり新しい記事なりにしようと思います。