『オブジェクト指向でなぜつくるのか』第二版の感想

はいどーもこんばんは、病気でダウンしていた無職です。
本日は『オブジェクト指向でなぜつくるのか』第二版の感想記事を書いていきたいと思います。

初版は読んだことがある

『オブジェクト指向でなぜつくるのか』ですが、実は初版を一年ほど前に読んでいます
今回改めて第二版を読み直したのは、「次に読むときは、お金を稼いで、ちゃんと新しい版のものを買いたい」と思っていたことを覚えていたからです。
初版を読んだときも内容のわかりやすさに感動したものですが、やはり一年も経つと忘れている部分も多く、復習にちょうどよかったです。

前半はOOPについて

前半は「プログラミング言語としてのオブジェクト指向」について書かれています。
僕もこの一年は主にこの「プログラミング言語としてのオブジェクト指向」について勉強し、実践してきました。
ポリモーフィズムは今やこれなしでのプログラミングは考えられませんし、継承やカプセル化についても利用すべき場面、注意する点など一通りは把握しているつもりです。
特に継承については各所で「継承の多用はコードが複雑になる」なんて言われていますよね。
僕は浅く広く継承するなら問題ないと思っていますが、深く狭い継承はやらないように気をつけています。
カプセル化には「隠蔽」の側面と「情報をまとめる」側面の2つがあって、これをゴッチャにして解説されると初心者はとまどうのかなあ、なんてのも感じました(これは本書で指摘されていることではなく、僕が勝手に思っていることです)。

メモリの管理について説明が秀逸

クラスとインスタンスの関係を説明するとき、メモリを絡めて説明している点は秀逸だなと思いました。
僕が普段触れている言語(JavaScriptとC#)にはGCが実装されていて、メモリについて考えることはほぼないのですが、それでもやはり「どのようにメモリが使われているのか」を知ることはプログラマとして必要なことでしょう。
それまでの僕の理解ですと「クラスで動かない情報(メソッドやstaticフィールド)は一度だけ生成されて、インスタンスごとに固有の情報はnewのつどメモリが確保される」といった程度のレベルでした。
実際これは当たらずといえども遠からずだったようなのですが、本書では「スタック領域」「静的領域(メソッドエリア)」「ヒープ領域」と厳密に区別して説明してくれています。
上記の僕の理解で言えば静的領域が「クラスで動かない情報」に相当し、ヒープ領域が「インスタンスごとに固有の情報」で、スタック領域はメソッド呼び出しのスタックを確保したり、呼び出す際のもろもろの情報(ローカル変数や引数の情報など)を保存していく領域のようです。
このスタック領域こそがマルチスレッドで使われる領域のようで、逆に言うとヒープ領域はアプリケーションで一つだけのようです(もちろん静的領域も)。
マルチスレッドであるスタック領域から、一つしかないヒープ領域に同時にアクセスしたりするとデッドロックが発生するのでしょう。
あとヒープ領域を圧迫しまくるとGCが働くみたいです。

中間コードについて

ついでと言ってはなんですが、.NETなどでよく言われている「中間コード」「共通言語ランタイム(CLR)」について今更ながらに理解できましたw
前々から「CLRってよく聞くけどなんやねん」と悩んでいたので、これは思わぬ収穫でした。
本書に書かれていることをまとめると、以下のような感じです。
まず、僕たちプログラマが書いた「ソースコード」があります。
そのコードをコンパイラが中間コードに変換します。
この生成された中間コードは文字通り「中間」のコードであり、さらに各プラットフォーム向けに用意されたインタプリタによって機械語に変換・実行されます。
で、以上の仕組みはJavaの場合だとJava VM(Java仮想マシン)と呼ばれているそうなのですが、.NETでは「共通言語ランタイム」と呼んでいるのですね。
なぜこんな名前がついているのかと言うと、.NETで扱える言語は全て共通の仮想マシンになっているから(共通の中間コードを生成する?)だと言うのです。
なるほどなあと思いましたよ。

注*ちょっと疑問に思ったのは、一般的にC++は高速だと言われていますが、中間言語に変換したら別にC#と速度が変わらないのでは? ということです。なぜといって、C#で書いてもC++で書いても結局は中間コードに変換され、それが各プラットフォームで用意されている仮想マシンによって実行されるからです。この理屈でいけば実行速度は変わらないと思うのですが、そうではないっぽい? ので、ちょっとよくわかりません。ただC++も色々と種類があるようなので、C++自体に詳しくならないと多分理解できないんでしょうね……。

デザインパターンについて

デザインパターンについても少しだけ触れられていました。
ただ本当に「触れているだけ」といった感じなので、デザインパターンについては専門の書籍を読むのがよいかなと思います。
Observerパターンとかよくわからない、という方は本書でも紹介されている本をパラパラと眺めてみてはいかがでしょーか。

余談ですが、ツクールMVでフォルダとファイルの構造に似せた「アイテム管理プラグイン」を作ってみようかなあ……なんて考えているのですが、妄想だけで終わりそうですw

後半は汎用の整理術としてのオブジェクト指向

後半は「汎用の整理術としてのオブジェクト指向」について書かれています。
「汎用の整理術」ってなんやねん、ということですが、本書で主に取り扱われているのはUMLについてです。
初版を読んだときはUMLについての理解が不十分だったので、ぶっちゃけこの辺の章で書かれていることは理解できていなかったと思いますw
ですが今は実際の業務でUMLを使ったり、自分の趣味のプログラミングでも情報を整理する目的でUMLを書いたりしているため、わりとすんなり説明を飲み込むことができました。
これは一年前からの成長ですね(成長と言う点で言うとデザインパターンのところもそうかも)。

プログラミングの前段階

何かを作る際、いきなりプログラミングを始めることはないでしょう。
本書の例で言えば「業務分析」「要求定義」「設計」の3つをしてからプログラミングに取り掛かります。
僕もプラグインの依頼については依頼者とやり取りし(要求定義)、設計を考え、それから実際のプログラミングに取り掛かります。
本書でも触れられているように、業務分析については「やらない分野、やっても役に立たない分野もある」かと思います。ゲームもそれに近いんじゃないかなあと思いました。
それはそれとして、こういった一連のプロセスについて書かれていることも、実務経験の薄いプログラマである僕にはありがたいことでした。
誰かから依頼があったり、チームで開発したりする際、本書の流れに沿って説明していけばだいたいうまく行くんじゃないかなーなんて妄想しています。
ちなみに設計は

1.実行環境の定義
2.ソフトウェア全体の構造定義
3.個々のソフトウェア部品の定義

の3段階に分けられていました。
実行環境の定義はわりと大切で、ゲーム制作ならスマホを対象にするのか、そのスマホは新しいものなのかどうか、あるいはPCだけを対象とするのか、でだいぶん作成方法が変わってくるんじゃないでしょうか(PCだけを対象にする方がラクチンだと思ってます)。
ソフトウェア全体の構造定義はいわゆるMVCパターンとかですかね。
個々のソフトウェア部品の定義は恐らく多くの方が思う「プログラマのお仕事」に相当するやつではないでしょーか(クラスおよびメソッドの仕様やインターフェイスを決める)。
経験上、この設計を疎かにしていると、変更に弱いプログラム(一つを変更するとアチラコチラも一緒に変更しないといけない)ができあがるんじゃないかなと感じています。
もちろんプログラミング自体に慣れていないときは設計なんて考える余裕がないので、「とにかく動くものを」という気持ちもわからないでもないですが。

設計の目標

「で、どんな風に設計していけばええねん」ということですが、本書では大まかな目標が3つ挙げられています。
すなわち

・重複を排除する
・部品の独立性を高める
・依存関係を循環させない

これらを見て「そうだよな、そうだよな」と思わない方は要注意じゃないでしょーか(ただ過度な重複な排除によってコードが複雑化するなら、僕は重複を選ぶ場合もあります。この辺はみなさんどうしてるんでしょうね)。
「部品の独立性を高める」ための指針として、本書では凝集度と結合度という考え方も紹介されています。
有名なのでご存知の方も多いかと思いますが、自分の備忘録も兼ねてあらためて紹介しておきます。

凝集度:個々の部品の機能のまとまり度合いを評価する尺度
結合度:部品間の結びつき度合いを評価する尺度

一般的に「凝集度は強いほどよい設計」「結合度は弱いほどよい設計」と言われています。
僕の経験に照らし合わせてみても、実際その通りだなと思います。
「上位のモジュールを下位のモジュールに依存させるべきではない。どちらも抽象化に依存すべきである」とか「抽象化は詳細に依存すべきではない」とかその辺の話ですね。
例えばクラスAがクラスBに依存していたとして、クラスBの一部を変更したいだけなのに、つられてクラスAも変更しなければ正常に動作しないとすれば、それはとても面倒なことですよね。また、人間ですからクラスのAの変更をうっかり忘れることもあるでしょう。
これはよくない設計です。なので「結合度は弱いほどよい」のですね。

「部品の独立性を高めるコツ」として紹介されている指針の一つに「ひと言で表現する名前をつける」というものがあって、これは本当にその通りだと思います。
よく「うまい名前付けに悩む」というシチュエーションに遭遇すると思うのですが、大抵の場合は(英語の知識が足りないとかは別にして)その名前をつけたい機能が巨大なんですよね。
「あっ、これとこれ分割できるじゃん」「そしたら名前バシッと決まるじゃん」ってことは僕も何回か経験がありましたよ。

開発手法の復習

本書は開発手法についても一章分割かれています。
アジャイル開発手法について聞いたことのある方も多いと思いますが、それらについての復習としてよくまとまっているのではないでしょーか。
またテスト駆動開発やリファクタリング、継続的インテグレーションについても触れられています。
どれも一度も聞いたことがない、または一度もしたことがない、という方は一読の価値があるかなと思います。
ただ説明はどれもあっさりとしているので、予備知識が全くない状態で読むと「ん?」と思うことがあるかもしれません(例えばテスト駆動開発のことを全く知らない人は「テストを実行して、失敗することを確認する」という一文で面食らうかも)。

初版との違いについて

はじめに「初版は読んだことがある」と書きました。
で、第二版と初版って一体何が違うんだ? ということなのですが、ぶっちゃけ記憶が曖昧で相違点がよくわかりませんでしたw
「プログラミング言語としてのオブジェクト指向」について話している部分は「あ、これ前に読んだことがある」とおぼろげながら思い出せたのですが、「汎用の整理術としてのオブジェクト指向」については新鮮味しか感じませんでしたw
たぶん当時の僕の知識量が足りず、理解できないまま読み進めたのだと思います。
とはいえ各章は時代に合わせて少しずつ修正はされているのでしょう。一番の大きな違いは「関数型」についての章が増えたことです。
関数型については僕もまだまだ勉強不足なので、この章は大変助かりましたよ。

関数型言語について

オブジェクト指向についての説明が一通り終わったあと、関数型言語についての説明がありました。これは初版には書かれていなかったものです。
いわく関数型言語の7つの特徴は以下の通りです。

・関数でプログラムを組み上げる
・すべての式が値を返す
・関数を値として扱える
・関数と引数を柔軟に組み合わせることができる
・副作用を起こさない
・場合分けと再帰でループ処理を記述する
・コンパイラが型を自動的に推測する

以下ではこの中で僕が気になったものをちょっとずつ見ていきます。

関数でプログラムを組み上げる

C#に慣れていると、クラスを基本単位としてプログラムを組み上げることが普通となるかなと思います。これに対して関数型言語では関数でプログラムを組み上げると言うのです。
「これってCとかの時代に逆戻りじゃ?」と思うのですが(実際「おや?」と思う方がいるのではと本書でも指摘されていました)、関数型言語とCなどの言語での「関数」はずいぶんと定義が違うようです。
すなわち従来のプログラミング言語で言う関数は「ひとまとまりの手続き」を意味するのに対し、関数型言語における関数は「数学の関数とほぼ同じ」だと言うのです。
そのため「関数型言語の関数は引数と戻り値を必ず持つ」必要があるようです。
ここまで読んで僕は「あれ、これってRxに似ているな」と思いました。
Rxも「引数を戻り値に変換する関数」をチェーン状につなげていって、最終的に何をするか(何を出力するか)を決める側面があります。関数型言語もこれと同じ感じなんですかね。

関数を値として扱える

「関数を値として扱える」と聞いて、ツクラーのみなさんなら「おや、JavaScriptでも同じことができるぞ」と思うかもしれません。
実際、JavaScriptの関数は関数型言語の関数と同じように値として扱えますから、引数として渡したり、戻り値として返したりすることができます。
C#の場合だとデリゲートを渡したり、デリゲートを返したり、といったところでしょうか。
UniRxやLINQを使う場合だとラムダ式を渡す部分がこれと似ているかもしれません。

関数の部分的用

関数型でおもしろいなと思ったのが、「関数の部分適用」と「関数の合成」です。
これはオブジェクト指向型の言語では珍しい機能でしょう。
関数の部分適用というのは、本書によれば以下の仕組みのことを指すそうです。
「2つ以上の引数を持つ関数に一部の引数を与えると、残りの引数を持つ別の関数を作ることができる」
C#に慣れていると関数から関数を生成する、ということはあまりやらないかと思います(多分)。
特に「2つ以上の引数を持つ関数に一部の引数を与えると、残りの引数を持つ別の関数を作る」なんて組み方はしないと思います。
この関数の部分適用を順次適用していくことをカリー化なんて呼ぶそうですよ。用語自体はチラッと耳にしたことはありましたが、意味はこの本を読んで初めて理解しました

関数の合成

「既存の関数をまとめて新しい関数を作る仕組み」のことを関数の合成と呼ぶそうです。
これもC#やJavaScriptにはない機能ではないでしょうか。
例えばHaskellでは以下のようなコードで合成を実行するそうです。

このコードを最初に読んだとき、僕は「squareしてからincrementするのかな」と思ったのですが実際は逆で、incrementしてからsquareするそうです。
便利そうではありますが、メソッドチェーンに慣れているので、ちょっと直感的ではないなと感じてしまいました。

先ほどの部分適用と組み合わせることもできるようなので、確かにこれはプログラミングがちょっと楽しくなりそうだなと思いましたよ。
ただデザインパターン的なのを参照しないと(あるかどうか知りませんが)、無闇に複雑になりそうで少し怖いですが……w

副作用を起こさない

この項目はC#やJavaScriptにも共通する部分が多い気がしました。「なるべく副作用は避けてプログラムを組もう」というのは今や常識と化していることですね(C#もメソッド内でconst的なの使えるようになりませんかね……)。
本書でも書かれていますが、副作用がなければテストはしやすいですし、バグが発生した際の調査も楽です。また、依存するものが少ないので容易に再利用も可能です。
LINQやUniRxは副作用のないメソッドをチェーン状にして実行していくので、これも似ていますね。
ちなみに関数型言語では「変数に値や式を設定することを代入と呼ばず、一度設定したら二度と変更できないニュアンスを込めて束縛と表現」するらしいですよ。

遅延評価

LINQを使って初めに躓くのが遅延評価ですよね。
これって関数型言語から来てるんでしょうか。本書にも遅延評価の説明がありました。
注釈として「遅延評価方式を採用しているHaskellでは、この性質を利用して「1から∞(無限大)」までのリストを定義することが可能です」と書かれてあります。
LINQでも似たようなことができます(ただしリスト全てをチェックするメソッドを実行したらいつまで経っても終わらない処理になってしまうので注意が必要ですが……)。
読んでいて、関数型言語を理解すれば、LINQやUniRxで用意されている機能ももっと理解できるんじゃ? となんだか関数型言語にだんだん興味がわいてきましたw

型推論

型推論はC#にもある機能です。
いわゆる「var」で宣言する変数ですね。
LINQで便利なのでよく使われるアレです。
いちいち型を意識するのが面倒なときにも使われるでしょうが(僕も特別な意図がないとき以外はvarで済ませます。短いですしw)、匿名型を使う場合が主な用途ではないでしょうか。
Haskellにおける型推論もこれと似ていて、「他の部分からデータや関数の型を推測できる場合、ソースコード上で明示的に型を宣言する必要がない」らしいです。代わりに、コンパイラが自動的に推測してくれるのですね。
非常に便利ですね。

終わりに

本書ではオブジェクト指向を以下のように表現しています。
「難しいソフトウェア開発を楽に行うための総合技術」
本書を読み終えたあと、この意味がわかるようになりました。
これまで「プログラミング言語としてのオブジェクト指向」についてチマチマ勉強してきた僕ですが、まだまだ奥が深いのだなあと改めて思いましたよ。
また、関数型言語にも非常に興味がわきました(今までは得体の知れないよくわからないモノという印象が強くて敬遠していた)。
そろそろTDDとリファクタリング関連の本でも読もうか、それとも関数型言語を何か一つ勉強しようかどうか悩んでいますw

ほなそんな感じでまた。

フォローする