開発人生25年で学んだ7つのソフトウェア原則(翻訳)
本記事は、私が2024年9月にEuRuKoカンファレンスで行ったキーノートスピーチを大まかに記事化したものです(スピーチの動画はこちらです)。残念ながら録画という形での登壇でしたが、それでも大変光栄なことでした。このテーマは私にとってとても重要なので、テキストで読みたい方のために、本記事で少々手を加えた形で公開することにいたしました。
私はかれこれ25年にもわたってソフトウェア開発に携わってきました。
そのうち20年間はメインの言語としてRubyを用いてきました。
私のRuby言語への貢献や、その他オープンソースへの貢献については以下をどうぞ。
参考: Contributions to Ruby
参考: github.com/zverok (Victor Shepelev )
また、Rubyに関する多くのブログ記事も執筆しており、Ruby Changesというプロジェクトのメンテナーでもありますので、この中のどれかについてお聞き及びの方もいらっしゃるかと思います。
私は、米国企業であるHubstaffの首席エンジニア「のような存在」ですが、以下のような経緯↓があったため、あくまで「のような」という立場です。
今の私にとって最も大事なのは、自分が軍に所属しているウクライナ人であるということです。現在は戦闘任務には就いていませんが、自分の得意な他の活動に取り組んでいます(任務とは別にそれ以外の活動も行っています)。
私が軍に入ったのは、軍国主義者だからでもなければ戦闘が好きだからでもなく、祖国を守る義務があるからです。
私だけではなく、多くのウクライナの同胞たちもそうです。
現在のウクライナ軍には実に多様な人々が所属しています。私も、ウクライナ軍に勤務している戦友たちの詩を英語に翻訳して公開するというささやかな個人プロジェクトを運営しています。
そういうわけで、キーノートスピーチの本当のタイトルは以下のとおりです。
10年の戦争を経て学んだ7つのソフトウェア原則
開発とRubyの話題である点は変わりませんが、私のウクライナ人としての体験についてもある程度サイドストーリーとして盛り込ませていただくことにしました。これによってキーノートスピーチがより充実したものになれば幸いです。なお、生々しい内容やつらい話などは一切いたしませんので、どうかご安心ください。
それでは本題に入りましょう。私が学んだ7つの法則をここに披露したいと思います。
🔗 1: どんなフレームワークもいつか身の丈に合わなくなる
テック業界に限らず、私たちは普段から、「何をしてよいか」「何を期待できるか」を理解するために、さまざまな制度や枠組み(フレームワーク)を当てにして生きているものです。
国際関係を例に取ると、私たちウクライナ人が長年当てにしていたのは、世界の大国によって安全が保証された主権国家であるという事実です。しかしこの枠組みは、私たちが思っていたほど盤石ではありませんでした。
最初はいつだってシンプル
私たちプログラマーも、多くの場合フレームワークに依存して生きています。Rubyistであれば、例のフレームワークに依存することになります。ただし私は特定のフレームワークを批判するつもりはありませんのでお間違いのなきよう。
ここで申し上げたいのは、フレームワークは「コードはここに配置すればよい」という場所を提供することで、後はビジネスロジックの実装に集中すればよいという安心感と自信を与えてくれるということです。
最初はいつだってシンプルなのに
しかし皆さんもご存知のように、コードが成長するに連れて、さまざまな概念も増えてきます。フレームワークによって追加される概念もあれば、開発チームによって導入される概念もあります。
そして、どのような形でコードを拡張しようとも、きっとフレームワークに収まりきらずにはみ出す部分が生じてしまうのです。
あと少しで、よく整理されたコードになりそう
あたかもフラクタルのように、最初はどれほど小さな概念に過ぎないものであろうと、いつしかそれ自身が独自のフレームワークにまで成長してしまうものなのです。上のコードは今ではありふれたcallable objectですが、これと同様に、アイデアとしてはいたってシンプルなのです。「こういうものを導入すれば、段階的にメンタルモデルが改善されて最終的に完全なものになるかもしれない」という気にさせられるわけです。
そこに新しい要件や新しいユースケースがいくつかやってくると、最終的には以下のようにDSLを定義する方法がたくさんありすぎて迷ってしまうことになります。
あと少しで、よく整理されたコードになりそうなのに
ここでどの定義方法を選んだとしても、既存の定義から何らかの形ではみ出してしまいます。しかしこれを適切に処理しておかなければ、概念がパンパンに膨れ上がって、lib/
フォルダやapp/services/
フォルダでよく見かける瓦礫の山になってしまい、ひと目見ただけでは一体何をするコードなのかがさっぱりわからなくなってしまいます。
これを適切に整理するには、依存しているフレームワークの種類にかかわらず、アーキテクチャ上の決定を独自に下せるようにしておく必要があります。しかし、そこには問題があります。
🔗 2: デザインパターンや方法論は、やがて通用しなくなる
私が確信している原則その2は、いかなるデザインパターンや方法論であろうと、いつかつまずきとなり、時代遅れとなるということです。
ウクライナでの経験からもおわかりのように、国際的な安全保障体制全体がある日突然にお笑い草となり、国際的な人道組織の無力さが明らかになり、人権擁護活動家たちは人権以外のものばかり擁護している、といった事態がありうるのです。
開発者も、何らかの技術的決定を下すときは、以下のような方法論、信頼できそうな手法、書籍、デザインパターンを表すさまざまな略語たちを頼りにしています。
いつだって何らかの指針はある
しかしコードが成長してプロジェクトが成熟し、業界で新しい意見が醸成され、技術的課題が変化してくるに連れて、さまざまな疑問やツッコミが噴出してきます。
いつだって何らかの指針はあるんだけど
「継承よりコンポジション」では?
データとアルゴリズムは分離すべきでは?
そもそも単一責任の「単一」とは?
FactoryパターンやBuilderパターンは本当に必要なのか?
ミュータブルはそんなに悪いのか?
DRYはドライすぎるし、SOLIDはソリッドじゃないし、本物の鳥は存在しない1
「継承は教科書でしか通用しない」とか「オブジェクト指向プログラミングは失敗だった」という学びを得た人もいるかもしれません。「Active Recordの方法論は本当に使い物になるのか」「パッシブエンティティやリポジトリの方がいいのでは」といった疑問を抱くようになるかもしれません。
SOLID原則は、1人の人物がマーケティングのためにこしらえた略語であり、そこにまとめられている原則の中には有用なものだけでなく、あいまいな定義や矛盾もあることをご存知でしょうか。一般に、どんな汎用的な方法論であっても、それに対する有用論や有害論(さらにそれらへの反論や再反論)はいくらでも存在するのです。
課題が増えるたびに方法論も増える
こうして私たちは、1段階上のレベルの指針を発見することになります(例: ドメイン概念とコーディング表現の構造的な一致というきわめてまっとうなアイデアに沿ったDDD)。しかし、それでもカバーできない設計上の空白は残ります。「ドメイン概念は小さいのに実装で巨大なアルゴリズムを必要とする場合」や「ドメイン概念が重要であるにもかかわらず、対応する実装が小さい場合」などです。
そこで導入される大量の新しい用語は、多くの開発者がクラスにしたがるので、最終的に通常のMVCクラスの他にエンティティ(entities)やら集約(aggregates)やら境界付きコンテキスト(bounded contexts)やらも必要になってきます。
また、かつてある賢人が、いみじくもこう述べています。
Avoid doing DDD Driven Design when you should be doing Domain-Driven Design. #dddesign #protip
— Robert Smallshire (@robsmallshire) October 24, 2018
ドメイン駆動設計を行う必要があるなら、DDD駆動設計2を行ってはならない。
ヘキサゴナルアーキテクチャについても似たような話があるとは思いませんか?
ここで特筆すべきは、「なぜ六角形なのか」という理由がまったく説明されていないということと、「コア」アーキテクチャ全体に対する設計上の決定を自分自身に委ねていることです(ただし、ここに有用な概念がないといった話をしているのではありません)。
デザインパターンや方法論が役に立つこともありますが、新しいものを設計するのは、最終的には自分自身なのです。なぜそうなるのでしょうか?
🔗 3: 規模は時間とともに常に拡大する
理由は、規模は時間とともに拡大する一方だからです(トートロジー)。世の中の多くのものがこれに当てはまります。
当たり前のことを言っているようですが、これがもたらす結果は見落とされがちです。
あらゆるプロジェクトは肥大化し続ける
自分たちがこれまでメンテナンスしてきたRailsアプリを思い浮かべてみてください。おそらく、そのアプリは成長することはあっても縮小することはなかったでしょう。
資本主義の世界で生きている私たちは、生き残るために機敏に行動しなければなりません。
「SPAのような新技術に乗り換える」「市場シェアを従来よりも規模の大きいor小さい顧客に拡大する」「既存顧客を失わずに市場に適合させる」「ひたすら直線的に成長させる」など、さまざまな行動が考えられます。
どんな行動を取るにせよ、コード量もテーブル数もエンドポイント数もテスト数も、とにかくあらゆるものが結果として増えることになります。
例として、GitLabオープンソース版のコードベースを見てみるとしましょう。一見したところ、普通のRailsアプリのように見えます。
初期のGitLabが何だったというと、Gitで共同作業を行うUIに過ぎませんでした。
メインのフォルダを開いたときに見えるものは、新規Railsアプリより少し多い程度です。app/
フォルダを開いてみると、さまざまな独自概念に名前が与えられている様子が見えます。フォルダの1つを開いてみると、さらにいろんなものが見え、その先にもさらにさらにいろんなものがあります(スクショでは"c"の部分までしか見えていないことにご注意ください)。
世の中のプロジェクトはGitLabほどの規模に達しているものばかりではありませんが、商用プロジェクトであれば、いつかは「レポート機能」「通知機能」「より良いロール管理」「支払いシステム」「新しいAPI」「一括処理」「ビジネス分析」といった、もろもろの機能が必要になるものです。
私がここでお伝えしたいのは、単純だが見落とされがちな結果です。すなわち、問題に対処する従来の方法が後になって通用しなくなるのは、まったく自然の摂理なのです。
10個のコントローラがあり、それぞれに標準的なメソッドが4つずつある場合の考え方は、重要なエンドポイントが数百個に増えたときには対応できないのです。
「じゃどうすればいいの?」そうですね、逆説的な答えならあります。
🔗 4:「ストーリー」に注目するべし
私からの答えは、「(パターンや方法論よりも)個別のストーリーにこそ着目すべき」というものです。
ソフトウェアのいかなる機能についても(新機能の実装、古い機能の調整、正しく動作しない理由を理解するなど)、その機能が出現するに至るまでのナラティブ(narrative: 物語)というものが必ずあるはずです。
そこには「原因と結果」「入力と出力」「変換と結果」があり、それらは一本のストーリー上に配置できます。
しかし、階層や規約を超えた「全体像」にばかり着目していると、個別のスレを見失い、その結果、全体像まで失ってしまいがちです。
ちょっとした例を紹介します。
以前の私は、RuboCopに新機能の追加(#8071)を試みたことがありました(別の理由で実現しませんでしたが)。
そのとき、2〜3行程度のいくつかのメソッドに分割されたアルゴリズムを理解するために丸一日かけた末に、最終的に1つに書き直したものが画面の半分程度に収まってくれた瞬間、「AHA」体験を得ました(ただし「複雑さのメトリクス」には合格していなかったのですが)
比較のために、変更前のアルゴリズムのスクショ(当時)も以下に示しておきます。ただし長すぎて1枚に収まりきれていません。
間違えないでいただきたいのですが、私はRuboCopのコードの書き方がよくないなどと言っているのではありません(RuboCopは素晴らしいソフトウェアプロジェクトであり、メンテナーも極めて優秀です)。そうではなく、RuboCopチームの方法論と私の方法論が、根本的に食い違っていることを指摘しておきたいのです。
「コードを小さなメソッドに分割する」「あらゆる条件判断を述語メソッドに切り出す」という方法論は、実際にRubyコミュニティで広く定着しています(この方法論は、デフォルトのlinterであるRuboCopの「Complexity(コードの複雑さ)」というしきい値によって強化されています)。
しかし弊社Hubstaffの「小さなコーディング規則集」には、それと対照的な意見が述べられています。
「最初にすべてを1つにまとめよ、次にそこにアーキテクチャを適用せよ」というものです。
「ストーリー第一」とは:
- 最初にメソッドを1つ作る。
- メソッド内の処理の流れが読む人にちゃんと伝わるまで改修する
- 別のメソッドやクラスに切り出すのは、2.を終えてからにすること(かつ必要な場合に限ること)。
ここで大事なのは、議論の対象は「1個のメソッド」だけではないということです!
本当に重要なのは、この規則集の主要な目的が「コードをひと目見ただけで、そこで何が行われているかを明らかにする」ことに注力する視点を養うことです。
私たちが守っている規則を簡単なリストにまとめると以下のようになります。
- 無駄を削ぎ落とし、本質をついたコードを書くこと
- ひと目で意味がわかる、表現力の高いコードを書くこと
- 状況に適した書き方や命名が他にもないか検討すること
- コードは小さくこまめに書き直すこと
- 1ページ(1画面)内に収まる範囲のコードで考えること
こうすることで、そのコードに関連する概念を的確に表す良い名前と構造を発見できるようになります。
この方が、アーキテクチャの計画段階でトップダウン的に命名や構造をひねりだして、それを後から「全体像」に適合させるよりもずっと信頼性が高まります。
しかし、シンプルな「全体像」が都合よくプロジェクトに当てはまることはほとんどありません(長年運用されているプロジェクトでクラス図やフロー図を書いたことがある人なら誰でも同意するでしょう)。
私たちが手がけているのは、一度建てたら変えられないコンクリートの高層建築ではなく、互いに絡まりあったストーリーの集合体であり、多くの小道が行き交う花園なのです。それにしても、ストーリーがそこまで重要な理由とは何でしょうか?
🔗 5: 目指すべきは「真実」と「明快さ」である
開発者として、そして人としての私たちの最終目標は、「真実」と「明快さ」です。そしてその第一歩は、ストーリーに耳を傾けることから始まります。
もっとも、人によってはソフトウェアアーキテクチャを語るときに「真実(truth)」という言葉を持ち出すのをいぶかるかもしれませんね。むしろ哲学(あるいは法律)の領域ではないかと。
テック業界における無意識的な真実の追求
OOP: 対象をあるがままにモデリングする
TDD/BDD: どんな振る舞いが期待されているかを記述する
DRY: 真実の情報源を1つに絞り込む
KISS: 背後の抽象化を隠蔽しないこと
DDD: ビジネスドメインと同じ用語で説明する
しかし私は、どんな原理原則や方法論も、真実を求めるための方法として導入されるものであると直感レベルでは確信しています。
たとえばオブジェクト指向プログラミング(OOP)は、今よりもシンプルな時代に、世界全体をモデリング可能にするという希望のもとで発明されました。BDD(振る舞い駆動開発)は、ソフトウェアのコード片を実装する前に、そのコード片がどう振る舞うかを率直に記述するための方法です。
私たちのどこが問題かというと、正確さは不十分だが優れたアイデアを片っ端から固苦しい規律に形式化してしまうことと、問題を正しく反映することについてはそっちのけで、最終的に何が何でも規律に厳格に従わせることにすり替わってしまうことです。
小さな例として、かつてRSpecという信じられないほど強力なテスティングDSLがRubyコミュニティで生み出されましたが、最終的に「テストコードの表現力は強くすべきではない」「テストコードは可能な限りベタ書きにすべきである」という見解に落ち着きました。
これで本当にありのままの振る舞いが伝わりますか?
その結果、多くのRubyistたちが、テスト対象のコードよりも冗長なベタ書きテストを(時には非常に冗長なベタ書きテストを)書くよう教えられ、当然ながらそれを嫌がる人たちもいますが、他の書き方が可能かどうかを夢想することすら禁じられていたりします。
実はそういう書き方を作ったんですけどね。
振る舞いをありのままに伝えるとは、こういうこと
上のサンプルコードについては好みが分かれるかとは思いますが、それよりも、このサンプルコードが期待される振る舞いを「ストーリーとして示している」ことを少しでも感じていただければ幸いです。
この方法論を成功させるための試みには、「ストーリーテリング」という方法論がコードの書き方の語彙にいかに影響するかも示されています。この方法論が普及するためには、一度定義すれば今後多くのspec作者が助かるほどの新しい高レベルの抽象化が必要でしょう。
しかしご存知の通り、こういう方法論はしばしば激しい反発に遭うものです。私の第6の原則は、これを踏まえたものです。
🔗 6: 孤立を味わう可能性がある
真実のストーリーにこだわる者は、大変な孤独を味わうことになるかもしれません。何よりも、「全体像」は人それぞれ異なっているものですし、そこに何らかのストーリーが提示されたら、そのストーリーが自分たちの思い描く全体像と調和するかどうかを真っ先に気にするでしょう。
どうしてこうなってしまうのか?
- ベテランエンジニアは「良いコードかどうか」は重要ではないと割り切っている
- 良いコードとは、SOLIDのことではない
- 良いコードであろうとなかろうと、大した違いがあるとは思えない
- 凝りすぎたクイズのようなコードを書いてはいけない
- そういうコードは見苦しいし
- 癒着がひどすぎるし
- 従来の「XXX方式」に反している
- 言語の高度な機能はむしろ知らない方がいい
- (いいから早いとこマージしちゃおうよ!)
そういうわけで、コードレビューやペアプログラミングやスタイルガイドの議論の場で、「そこで何が起きているのか」に着目するストーリー中心の方法論を売り込もうとしたら、完全なスルーから積極的な反発までさまざまな反応に遭遇することになるかもしれません。
真実を追い求める作業は、自らのメンタルモデルを常に調整し続け、自らの経験を問い直す作業でもあります。特に経験豊富な専門家やソフトウェア開発者は、自分たちが築き上げたメンタルモデルや経験を解体して作り直すことに抵抗するのが一般的です。ツールを毎日のように取っ替え引っ替えするのは平気でも、心の内面で確立されている信念や無意識レベルにまで身につけた習慣を後から組み替えるのは、無理というものです。
最近の業界では、「スマートすぎるコードや表現力が高すぎるコード(賢すぎてクイズのようになったコード)」に対する強い反発が目につくようです。より広義には、特定のコードの書き方を軽んじ、その結果コードレビューを実践することに対する強い反発が生まれることもあるようです。
コードレビューとは...
- 関所の見張り番
- 信頼の欠如
- 通行を邪魔するでこぼこ
- ネチネチあげつらう場
- 自主性を脅かされる場
はい、承知しております。
一般に、開発者は自分でコードを書く形で問題を解決したがるものなので、他人の書いたコードを読んだり編集したりすることをつい避けがちです。しかしこのままでは、コードレビューの場がレビューする側にとってもされる側にとっても、義務を果たすためだけの形ばかりの作業になってしまい(手順通りに進める以外に何をしたらいいのかわからない状態)、しまいにはレビュアーが言葉でぶん殴ることで支配を確立する以外に何の成果も上がらなくなり、無意味で悲しい結果に終わってしまいます。
そうではなく、コードレビューとは...
- 知識を共有する場
- 命名や書き方を確立する場
- コードの意味が正しく伝わることを確認する場
- 共通目標に貢献する場
- 互いに支え合い、助け合う場
しかし私の考えでは、コードに語らせるストーリーが他の開発者に正しく伝わるようにし、他のストーリーと矛盾していないようにし、共通の語彙が誤用されて意図しない内容が伝わることのないようにするには、お互いのコードを読んで議論する以外にないと思います。
これは非常に人間らしい活動であり、それ以外の方法では説得できないでしょう。それでは、少々センチメンタルではありますが、最後の原則に進みましょう。
🔗 7: それでも真実の追求を諦めないこと
どんなにつらくても、真実の追求を諦めてはいけません。
知識を追い求め続けましょう。
自分の世界観を修正し続けましょう。
問いを発し続けましょう。
古いコードは書き直し続けましょう。
誤った仮説や信頼できない根拠を棄却し続けましょう。
プロフェッショナルの美徳とは
- 相手の話に耳を傾け、コードやドキュメントをしっかり読み取って反映すること
- 自分の知識や経験が陳腐化したことを自覚し、認められること
- 「凄い」と言わせるのではなく、理解してもらうことを心がける
- 相手をねじ伏せて従わせるのではなく、理解してもらうことを心がける
- ストーリーを飾らずに率直に伝えること
ソフトウェアの作者は、何よりも作家です。
「今の私にわかるのはここまでですが、説明には最善を尽くしました」と胸を張って言い切れる人のことです。
難解な用語や概念や既存の権威を盾にして責任逃れをせず、この姿勢を保つことを他のどんなことよりも優先することは、プロジェクトが長期的な成功を収めるうえで、あるいは、基本的に人間のあらゆる長期的な活動において、貴重な資質となるでしょう。
これが、私にとって最も大きな学びです。私の長いキャリアにおいては些細なことかもしれませんが、私個人においても私の職業においても重要です。
ありがとうございました。いつか皆さんに直接お目にかかれればと願っています。
そして、たとえこのスピーチのことは忘れても、ウクライナへの支援をどうかお忘れなきよう。
お読みいただきありがとうございます。ウクライナへの軍事および人道支援のための寄付およびロビー活動による支援をお願いいたします。このリンクから、総合的な情報源および寄付を受け付けている国や民間基金への多数のリンクを参照いただけます。
すべてに参加するお時間が取れない場合は、Come Back Aliveへの寄付が常に良い選択となります。
関連記事
Ruby: "uselessシンタックスシュガー"シリーズ: numbered block parameters(翻訳)
- 訳注: "the birds aren't real"は陰謀論を皮肉った有名なジョークで、「すべての鳥は米国政府が米国民を監視するためのドローンにすり替えられている」というものです。 ↩
- 訳注: 「DDD駆動設計」は、ドメイン駆動設計(DDD)そのものによって駆動される設計、つまり設計手段であるはずのDDDが目的にすりかわってしまったことを指します。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。