こんにちは、開発チームのアーキテクトをやっている竹野(@Altech)です。先日、新人研修でソフトウェアの設計について話す機会がありました。
ソフトウェアの設計というのは関連する領域が広いため、どうしても断片的な理解になりがちです。そこで、早い段階で全体像を感じてもらうために、ソフトウェア設計の Why と How と What を1時間でまとめて話すというちょっと意欲的なコンセプトで研修を行いました。今回は、その内容を記事にしました。
この研修のねらい
はじめに
ソフトウェアの設計について書かれた情報は世の中に多いですが、その情報の多くは How であり、それだけを読んで適切に使うことが難しいと感じています。その直接的な理由は、How に対しての What、How / What に対しての Why が語られることが少ないからです。
ただ、How だけを知っていると、それは本当に問題を解決しているのかを考えることができず、場合によってはどこにも行きつかないということにもなります。How に閉じずに、目的志向で考えることは、相応の思考を要求されることではありますが、やはり必要なのです。
そこで、この研修では、Why から始めたいと思います。
スコープ
とはいえ、1時間の研修で話せることには限りがあります。
そこで、この研修の中では、重要だが意外と見落とされがちなことにフォーカスします。特に、Why と What と How をつなげること、及びその中で、世の中的によく出てくるキーワードを使用すること、それらのキーワードが実際にはどのように接続しているかということ、つまり各種の関連を重視します。
そのようなキーワードを持ち帰って、今後のプログラミングの中で、自分で考える材料にしてほしいと思います。
Why
なぜソフトウェアに設計が必要になるか
ではソフトウェアの設計はなぜ必要で、なんのために行うのでしょうか。ここでは、二つの異なる見方をまず紹介します。
- 保守的な見方:ソフトウェアの設計は複雑性をコントロールしながら開発し続けるための必要経費である
- 進歩的な見方:ソフトウェアを設計することはソフトウェアが何であるかを定義づけることそのものである
ここでの保守的と言う言葉は、そのことで困らない、それがボトルネックにならなければ良いというイメージです。複雑になることで開発が継続できなくなる、ということにならないように設計していく、っていうことですね。
逆に進歩的という言葉は、そのことが新しい認識を作ったり、新しい価値を作るというイメージで使っています。流行りの言葉で言えばいわゆる「デザイン思考的」な考え方に近いと思います。ここでは、「設計」は英語で “design” であるという事実を思い出せば十分でしょう。
いきなり二つの見方がある、という話をしてしまいましたが、あえて二つ紹介したのはどちらの見方も有用だからです。
たとえば僕自身は、普段のアプローチとしては、ほとんどの小さな問題には保守的に取り組みます。ただ、たまに出くわす大きな問題には両方の視点から考えます。これは、ソフトウェア・デザインのレベルで大きなアイデアを持ち込まないと解決できない問題がしばしばあると感じるためです。
今回の講義では、頻繁に出くわす問題にうまく対処できることが第一歩であるという考えで、複雑性をベースに話を進めていきます。
ただ、実際には、困らないためにやった設計が、しばしば新しい価値を産むデザインであったりもするので、ここではこのふたつを対立的なものとして扱う必要はないですし、むしろ同じものの別の側面だと思って欲しいと思います。覚えておいて欲しいのは、定義の問題が顔を出す場合には定義それ自体に向き合う必要がある、ということです。
「複雑性」(Complexity)
それでは複雑性に話を移しましょう。とっかかりとして、1986年にフレデリック・ブルックスという人が書いた論文の一節を引用してみます。
“すべてのソフトウェア構築には、本質的作業として抽象的なソフトウェア実体を構成する複雑な概念構造体を作り上げること、および、偶有的作業としてそうした抽象的実在をプログラミング言語で表現し、それをメモリスペースとスピードの制約内で機械言語に写像することが含まれている”
このブルックスという人は IBM で OS/360 という OS を作ったりしていた人です。OS というのは高度なソフトウェア・システムの代表例ですから、この文章は複雑性の高いソフトウェアを開発したら何がボトルネックになるのか、という視点で読むと良いと考えます。
ちょっと読み上げてみますね。・・。
難しい言葉が多いですね。ちょっと一個一個みていきましょう。「複雑な概念構造体」なるものをコードで表現して、実行することが全てのソフトウェアを構築するという作業には含まれてる、ということらしいです。次のような質問をしてみましょう。
「複雑性が高いってどういうこと?新しい概念ってどんなもの?」
「複雑性が高いままだとどう困るの?」
「ソフトウェア以外の構築物とソフトウェアってなんか違うの?」
順を追って見ていきます。
複雑性につながる新しい概念の例
「複雑性が高いってどういうこと?新しい概念ってどんなもの?」
Web アプリで実際にものをみていきたいところですが、実は、単純なコンテンツ・サービスであれば Web の前提に乗れるので、Web アプリケーションのフレームワークを使っていればプログラムはそこまで複雑にはなりません。しかし、思わぬところで新しい概念が登場することがあります。
例えば、ミニマルなものとしてはこれです。
これ(左)は Wantedly の募集に載るユーザー情報です。ところで、これは何でしょうか?プロフィール(右)にも同じようなラベルがあるように見えるのですが、それと同じなのでしょうか?
ひとまず、その人の所属・役職を一言で表したものな気もします。だとしたらどのように計算されるのが良いでしょうか。職歴のリストから計算されるのが妥当でしょうか。学生の場合はどうでしょうか。学歴は計算対象でしょうか(ビジネスSNSにおいてユーザーを名前とラベルで表現するとしたら、それは何であるべきか、という定義の問題に行きつきそうです)。
そして、おそらく、募集ページに載る情報はそれと同じではないかもしれません。共有している部分はありますが、募集を出している会社における役職、という方が正しそうです。
こういった質問をせずに、ロジックを適切に共有したり関数として分離したりすることをしないまま開発を続けていると、後述するようにいろいろと問題が起きます。実は、ここで出した例はこのように定義できていなかったために、退職した後に別会社の役職になってしまう、という問題が長らくありました。
Web のコンテンツサービスを作る場合、Web や Rails がレールを敷いてくれています。しかし、自分たちのサービスに固有の複雑性は自分たちで対処する必要があります。実際、Rails の作者である DHH も次のように言っています。
“しかし、何事もそうですが、慣習の力には危険がないわけではありません。Railsがこれだけ多くのことをあまりにも取るに足らないようにみせていると、アプリケーションのあらゆる側面は既成のテンプレートで作れるのではないかと考えがちです。しかし、構築する価値のあるほとんどのアプリケーションには、何らかの方法でユニークな要素があります。それは5%や1%に過ぎないかもしれませんが、そこに存在しています。” - The Rails Doctrine - DHH(訳:高橋征義
)
これは個人的な意見ですが、DX やら IoT やらでソフトウェアのフロンティアが拡張され続けている現在、実世界とつながるサービスが増えてきて、過去よりも単純なウェブ上のコンテンツサービスでは済まなくなってきている傾向があるように感じています。そのため、ソフトウェアの設計が活きる部分というのもまた増えていく、つまり設計が重要性を増しているのではないか、と考えています。
身近なところで言えば、この「5%や1%」の割合は Wantedly においては上がっていると感じます。たとえば、従業員のモチベーションを管理するサービスを作るのであれば従業員のマネジメントとはどのようなものとして定義するのが妥当か、というようなことを考える必要が生まれますよね。これはコンテンツサービスよりも複雑性があります。
ここで挙げた話はバックエンドのいわゆるドメイン設計と呼ばれるものですが、フロントエンドで言えばリッチな GUI アプリケーションの複雑性は高くなりがちなので、似たような話は別の領域でもあると思っています。たとえばプロフィール機能を作っていても、それなりに設計をしないと開発が難しくなる複雑なアプリケーションだなと思います。
複雑性に対処しないことで表出する問題
「複雑性が高いままだとどう困るの?」
少し抽象的な話が続きましたが、複雑性によって表出する問題の典型は次のようなものです。
- Change amplification:変更箇所がめっちゃ増える、くらいの意味
- Cognitive load:直訳すると「認知負荷」。要するに「コードが読めん,わからん」という状態。
- Unknown unknowns:あるコードを修正する際に,どの条件を満たしたらそのコードを修正しきったかが把握できない。
これは身に覚えがある人がほとんどなのではないでしょうか。
保守的に言えば、これらが開発のボトルネックにならないように対処していくことが大事です。「ならないように」と言っているのは、多くの場合は「なってから」対処するよりもならないようにする方が安いからです。特に Wantedly Visit のような継続性が見えているサービスの場合やよりそうです。このあたりの塩梅や考え方は、How のパートでも話します。
ちなみに、この問題の分類の出典は “A Philosophy of Software Design” という本です。2018年に出た本ですが、複雑性という概念を軸にソフトウェア設計の勘所についてまとめた良書なので、英語ですが興味のある人は読んでみることをお勧めします。
建築物との対比
「ソフトウェア以外の構築物とソフトウェアって違うの?」
ソフトウェアの特性を理解する上で、ソフトウェアでないものと比べることは有用だと考えます。ここでは建築物と比較してましょう。単純に考えて、たとえば100階建てのビルという建築物で、25階と99階に特有の依存関係、例えば階段があることはないですよね。あとは建て増しとかするときにも、空中で横方向に無限に建て増しが行われるとかないです。
余談ですが、「制約」というキーワードは複雑性を考える上で実はとても重要です。制約があると、複雑性が減ります。Web というシステムもリソースは一意な識別子で表現され GET や POST など5つのメソッドでしか変更できない、という制約によって複雑性が減り、発展していきました。
What
複雑性に対処しなければならないことはわかったけど、じゃあそれってどういうことなの?というのがこのパートのお話です。つまり複雑性に対処する行為を「設計」と呼ぶことにしたとして、それはどういった行為でしょうか。これもまた一度立ち戻って考えましょう。
そのためには、ソフトウェア以外のところからヒントを得ることにします。
人間の営みを考えてみると、この現実の世界は複雑なわけですが、それを有限の思考能力でそれなりに理解して、複雑な道具を使いこなしたりして生きているわけですよね。
ソフトウェアも(少なくとも当面の間は)人間が読み書きするものなわけですから、同じような形で複雑性に対処することになります。
ここでは、「時計」と「アプリ」と「スタック」を例にして、いくつかの異なるキーワードを使ってそのことを見ていきます。
「抽象」(Abstraction)
よく、設計について語る際、「抽象化」という言葉が出てきます。では、その結果生まれる「抽象」ってなんなんでしょうか。なんで、抽象化をすると、設計ができたことになり、ソフトウェアが開発しやすくなったりするのでしょうか。
実は、「時計」「アプリ」「スタック」はどれも抽象です。はい、よくわかりませんね。しかし、今僕たちが「時計」だと思っているものがやっぱり抽象なのです。ひとまずのところ、これらのもので僕たちが便利に生活できていることは、まあ直感的に疑問を挟む余地はないのではないでしょうか。
抽象って誰が生み出すのでしょうか。「時計」は人類ですね。「アプリ」は、考えた人は知らないけど、広めたのは Apple っていう会社でしょう。「スタック」はコンピュータ・サイエンティストです、きっと。
補足:Abstraction は概念化する過程とその成果物の両方の意味があり、冠詞で区別できる。ここでは区別のために "Abstraction" に「抽象化」、"An abstraction" に「抽象」という語を当てている。
抽象いろいろ
いろいろな領域(ドメイン)に対して、色々な抽象があります。GUI オペレーティングシステムの領域には「アプリ」以外にもさまざまな抽象が含まれていますし、ソフトウェア一般の領域には「スタック」以外のデータ構造やデータのやり取りの仕方、などの抽象があります。
サービスを作る際に、そういう「スタック」のようなすでにある抽象は便利に使えば良いわけですが、僕たちサービスの開発者が設計・デザインすべきものは何だと思いますか?
アプリケーションのプログラマーであれば、アプリケーションそれ自体の領域(ドメイン)に含まれるものをデザインすべき、と言えるかもしれません。これは「ドメイン駆動設計」と言う考え方で知られています。
まあこれ自体は考え方の派閥の一つなのであまりそのことにはこだわらないでおきましょう。フロントエンドのプログラマーであれば、UI に特有の振る舞いを抽象化する、ということも必要でしょうから、そういうことも含めて言っています。あ、この意味で、「UIデザイナーの仕事に興味を持つ」みたいなのはめっちゃ重要です。対象に興味を持つのは良い設計をするための必要条件です。
あとうちの dx チーム(注:Developer Experience = 開発者の体験を最大化することをミッションとしたチーム)が作っている “kube fork” なども抽象化の良い例ですね。
(ちなみに、「抽象」と似たような言葉に「モデル」と言う言葉があります)
「インターフェイス」(Interface)
抽象化を行う際、「インターフェイス」という言葉もよく出てきます。インターフェイスについて説明するためには、一つ前提となる考えに戻らなければいけません。
それは、仕様は実装より容易に理解できる、という見立てです。
抽象化を施したソフトウェア部品を「コンポーネント」と仮にここでは呼ぶことにしましょう。コンポーネントには仕様と実装があります。つまり、外から見て期待する振る舞いと、それを内でどのように実現するかという二つの観点です。そして、うまく抽象化されている場合、実装がコンポーネントの内側に隠蔽されるため、仕様は実装よりもずっと理解がしやすいものになるはずだ、という見込みがあります。この考えの元では、仕様と実装をきちんと区別することが重要になります。
仕様と実装を区別しようと思うと、ほとんど必然的に仕様と実装の間にある「境界」に着目することになります。それを「インターフェイス」と呼びます。
“Interface: a point where two systems, subjects, organizations, etc. meet and interact.” - 英英辞典
インターフェイスいろいろ
例に戻りましょう。「時計」は時刻が円状に分割され長針と短針で時刻を読み取れるようなインターフェイスを持っています。このインターフェイスがあることで、時計がどのように動作しているのかを理解することなく時計から時刻を読み取ることができます。しかも、色々な時計があっても、それを共通で時刻を理解するものとして理解し、利用することができます。すごいですね。
では「アプリ」のインターフェイスってなんだと思いますか?では、当てますね。・・・。
正解はありませんが、僕は「アプリ」は、アプリストアから入手できて、ホーム画面に並べられる視覚的な記号と結びついていて、タップしたら使い始められるインターフェイスを持っているもの、と考えました。これによって、たくさんの人がスマートフォン・デバイス上でアプリケーションを使うことができています。ユーザー・インターフェイスは、要するにこのようなものです。
では「スタック」のインターフェイスってなんでしょう?これはもしかしたらわかりやすいかもしれません。当てますね・・・。
そうですね。〈なにか〉をプッシュでき、ポップすることでプッシュした順番とは逆順に〈なにか〉が出てくるインターフェイスを持っています。この定義も完全ではありませんが、全てを言わずに要点だけをいうことにインターフェイスの利便性があります(余談ですが、この〈なにか〉は多くの型システムにおいて「ジェネリクス」ないし「型パラメータ」として表現されますね)。
プログラミング・システムにおいて、抽象を定義する手段であるインターフェイスは非常に重要なものなので、発展がみられます。
ちょっと昔のプログラミングの本を読むと、関数のシグネチャについてちょっと重視している雰囲気が伺えます。それからオブジェクト(クラス)のインターフェイスを定義するメカニズムが導入され、今では API のスキーマを定義するインターフェイス定義言語である Protocol Buffers を僕らは使っています。
そのくらい、インターフェイスは重要なものです。
(休憩タイム)
ここから少し次の話に進むので、ちょっと休憩しましょう。少し質問、コメントを見てみます。
考えてみたら一個一個の具体的なものじゃなくて「インターフェイス」という言葉があるの、不思議な気がする。
これはそうですよね。僕も資料を作っていて、インターフェイスという言葉があるの偉いなと思いました。「インターネット」という言葉があるように、"inter" というプレフィックスが存在するので、英語にはもともとそういう概念が存在するような気がします。日本語でそういう概念があまり思いつかなかったので、知っている人がいたら教えて欲しいです。
抽象を組み合わせること
ここまででも結構いろいろなことを話してきたのですが、実際のシステムと繋げて考えるために、もう少し続けます。
これまで話してきた抽象やインターフェイスというのは主として個々のコンポーネントの説明でした。実際には、こういったコンポーネントを組み合わせることでより複雑なシステムを作ることになります。
コンポーネントは、単に組み合わせてもいいし、層(レイヤー)のようなコンポーネント依存先を制限する構造的制約(アーキテクチャ)を設けることもあります。
ちなみに、依存というのひとつのキーワードで、これが多ければ多いほど、同時に理解しなければいけないものの数が増えます。ただし、依存しているものがよく抽象化されていれば、一個一個の理解が容易になるという関係はあります。
コンポーネントを分けたりするなどして複数のコンポーネントが存在する場合、問題が起きたりします。たとえば、
「このロジック(ないしデータ)はどのコンポーネントに存在するんだろう?」
「どのコンポーネントを変更すれば良いんだろう?Aを変更しても変更できるし、Bを変更することでも実現できるんだけど」
という経験は誰しもあると多います。How に近い、テクニカルな話になりますが、これに関連してよく出てくるキーワードがあるため、紹介していきます。
「責務」(Responsibility)
単純な話、登場人物が複数いると誰がどこまでやるかという役割分担が必要になるわけですね。その役割分担に使われる概念が責務です。
責務は、だいたいの場合、そのコンポーネントが行うべき仕事の範囲を文で定義します。外からの期待について述べているという点で、ちょっとインターフェイスに似ていますよね。これらは、厳密な関係を追求するよりは、物事の見方や記述の仕方の違いだと考えておくと良いかなと思います。
責務は説明的である分、ファジーですが、いろいろなところで使えるという利点はあります。例えば、特定のコンポーネントではなく、コンポーネント群が含まれる「レイヤーの責務」というものを考えることができます。
たとえば MVC とかで「これはコントローラーの責務じゃないですね」というような会話ができます。これは、コントローラーというレイヤーに含まれる個々のコンポーネントに一般的に期待される責務から逸れている、ということを言っているわけですね。
「関心」(Concern)
似たような言葉として、「関心」というキーワードもときどき出てくるので、一応紹介しておきます。
これはソフトウェアの変更が行われる観点のようなものです。例えば、React の入門のページ(react.org)を見ると、JSX の導入のところで次のような説明が出てきます。
“マークアップとロジックを別々のファイルに書いて人為的に技術を分離するのではなく、React はマークアップとロジックを両方含む疎結合の「コンポーネント」という単位を用いて関心を分離します。”
これは JSX というものが見た目が少し突飛なので必要性を説明するために書かれているのだと思いますが、関心に基づいて抽象化を行って成功した例です。気せずして、ここでもコンポーネントという言葉が用いられていますね。
ポイントとしては、分割の仕方について考えることは抽象について考えることと同じくらい大事だということです。表裏一体と言っていいと思います。
How
最後のパートでは How について。ここでは自力を上げるための「備え」、プログラムを書くときの「スタンス」、良い設計を実現してくための「プロセス、の三つお話しします。
備え:概念を知り、技法を知る
良い設計をするための自力を上げる方法を、二つのアプローチで紹介します(どちらが良い、ということはありません)。
- 教科書的アプローチ:関数型プログラミング、オブジェクト指向プログラミング、マルチパラダイム、・・
- 獣道的アプローチ:異なるパラダイムの言語・フレームワークに触れる。良いコードを読む。
一つ目は教科書的アプローチの方で、これはプログラミング自体について扱った本を読むことです。特定のパラダイムに特化した本もありますし、それらを並べて扱った本もいくつかあります。もちろん、ここで挙げているものはソフトウェア一般の領域のことなので、開発しているプロダクトや事業のことはそれぞれ理解する必要があります。
二つ目は獣道的アプローチで、実際に触れてみる、ということですね。これは日々の業務の中でもできることだと思います。
ここで一点だけ、学習をする上での注意喚起。世の中にないろいろな設計論・アーキテクチャ論がありますが全て目安であって、ルールとして捉えるべきではないです。ルールをそのまま適用すれば良い設計のソフトウェアができる、というようなことはありえないです。
というか、ここまでの話の中で、アーキテクチャなどというものは単なる構造的制約であって設計のごくごく一部であるということは伝わっているのではないでしょうか。知識を摂取するとき、この設計論はどういう目的で何を解決するのか?というような目的を考える視点を常に持つようにしましょう。
スタンス:完璧な設計ができれば良いのか?
さて、この研修は設計についての研修でしたが、それを実行する上でどのようなスタンスでいれば良いでしょうか。
ここでは、「完璧な設計ができれば良いのか?」という問いを敢えて立てました。良いエンジニアリング v.s. 良いプロダクト、とも言い換えてもいいかもしれません。これについて、僕の尊敬するプログラマーの中島聡さんの言葉を紹介します。
“あのね、私が昔から言っていることがあるんですけど、コードの1行1行というのは経営判断なんですよ。例えば、「このプログラムを使ったら完璧に近いんだけれども、どうしても時間がかかってしまう」という選択肢と、「このプログラミングだったら手っ取り早く動かすことは可能だけれど、いろいろ問題点もある」という選択肢の二択を迫られる場面というのが、エンジニアには常にある。
結局、どっちのコードを使うかで、サービスの質が変わったり、事業の収益に影響が出たりするわけだから、プログラマーの営みというのは、いつも経営判断の繰り返しなんです。科学者とエンジニアの最大の違いがここにあると言ってもいい。その自覚を持ってやっているエンジニアになるのかならないのか。それで大きく違ってくる。”
経営判断、っていうとちょっと大仰に聞こえるかもしれませんが、「このプログラムを使ったら完璧に近いんだけれども、どうしても時間がかかってしまう」という選択肢と、「このプログラミングだったら手っ取り早く動かすことは可能だけれど、いろいろ問題点もある」という選択肢がある、というのはわかると思います。
エンジニアは真実を追求する科学者ではないので、この二つを常に天秤にかけながらプログラミングをしていくことが必要で、対立軸ではないということですね。
じゃあどうすればいいの?
とはいえ、じゃあどうすれば良いの?という話はあると思います。
「今すぐ機能をリリースしたい」「まだアプリケーションのコアドメインが固まりきっていない」
あると思います。
この解法としては、問題を静的なプログラムそのものではなく、時間軸を伴うプロセスとして捉えましょう。ある時点で完璧なことを目指すのではなく、やっていく中で徐々に良いものに近づけていく、ということです。良いデザインは良いプロセスから生まれる、と弊社のCDOも言っています。
リファクタリング
だんだんとソフトウェアの設計を改善していく行為がリファクタリングです。
そしてそのリファクタリングを行いやすくするのが、テストです。テストも、ただ書けば良いというわけではなく、リファクタリングを支えるように書かなければ意味がないので、そう単純ではないですが・・。
リファクタリングを行う中で、語彙が足りなければ「言葉」(ユビキタス言語)を定義することにもなるでしょう。そして、いまいちだったら書き直せば良い。
設計・リファクタリングを楽にやるコツを三つ紹介します。
一つは、ちょっとしたことから始めるということです。冒頭、すごくよくできた抽象を紹介しましたが、関数の名前や引数などといった、小さなところから始めれば良いです。
二点目は、あとから変えられるものと変えにくいものを区別することです。極端な話、Java などのプログラミング言語の標準ライブラリの API は、一回決めたらほとんど変えられないです。それと比べれば、僕らの運営するサービスで後から変えられるものは多いです。ただ、モノリスの関数と違ってマイクロサービスのエンドポイントはずっと変えにくいので、そこは設計に少しコストをかけた方が良いでしょう。
三点目は、全てに言えることではありますが、重要なものにフォーカスするということです。例えば誰も読みに行かないし、事業上もそこまで重要ではないコードをリファクタリングすることの価値は低いです。
全体的に、この辺りのコツは「リーンスタートアップ」にも通じるものがあります。弊社の推奨書籍の一つですね。
お話は以上となります。この研修を作るにあたって直接引用したもの、影響を受けたアイデアを含む書籍・記事を挙げておくので、もっと知りたい人は読んでみると良いかもしれません。
研修に使用したスライドはこちらとなります。
最後にちょっと宣伝。ウォンテッドリーでは現在、さまざまなポジションのソフトウェア・エンジニアの採用を拡大中です。よければ話を聞きにきてみてください :)