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