取りあえず何か創る

ソフトウェアエンジニアだけど仕事以外でも何か作りたくなったので、主にその記録のためにブログを開設しました。

読書記録 GPUを支える技術

0. この投稿の概要

  • GPUを支える技術」を読んで学んだことをまとめました。2021年に出た増補改訂版の内容です。進化の激しい分野なので、読まれる場合は改訂版を手に取ることをおすすめします。

  • 自分はディープラーニングの学習/推論のためにGPUを使うので、主にそこに焦点を当ててまとめています。グラフィックスについては省略しています。

1. 学んだこと

ハードウェア

  • 並列処理の方式: 同じ命令を多数のスレッドで並列実行するような動きをするSIMT方式が主流。データ並列度は使っていないため、依存関係のない命令を集めたり、データのサイズを演算器の数に合わせたりしなくても効率を高めやすい。
  • 精度の問題: 科学技術計算では64bitの倍精度浮動小数点も必要とされているが、ディープラーニングの推論ではそれほどの精度は必要なく、16bitの半精度浮動小数点や場合によっては8bitの固定小数点で計算しても結果に影響はない。参考: How to Quantize Neural Networks with TensorFlow « Pete Warden's blog
    • INT8の演算器はFP32よりも5~10倍小さい面積で実装ができるため、Googleは8bit整数で推論を行うTPUというカスタムLSIを開発した。
    • IEEEで規定されているFP16は指数部が5bit、仮数部が10bitだがディープラーニングの場合はオーバーフローしないように指数部が大きい方が扱いやすい。そのため指数部8bit, 仮数部7bitとしたBF16が多くのチップでサポートされている。
  • ベンチマーク: ディープラーニングを実行する場合の性能のベンチマークとしてMLPerfが開発されている。
    • 学習ベンチマークでは画像分類や物体検知のようなタスクに対して、目標性能に達した時間の学習時間を測定する。
    • 推論ベンチマークではFP32による推論の99%の精度を達成する必要があり、推論時間を測定する。

ソフトウェア

  • CUDA: NVIDIAが開発したGPUプログラミングに最低限必要な機能をC言語に追加した言語。
    • CPUとGPUはそれぞれメモリを持っているため、CUDAではデータの格納場所を修飾子で指定する。低速大容量のデバイスメモリやGPUに内蔵された高速低容量のシェアードメモリがある。
    • CUDAではベクトル型の変数タイプをサポートしている。ベクトル長は1~4まで。
    • 大まかな処理の流れ: ホスト/デバイス双方にデータの受け渡しようのメモリ領域を確保→ホストからデバイスへデータのコピー→カーネルを起動して計算を実行→結果をデバイスからホストにコピー→使い終わったメモリを解放。
      • ユニファイドメモリを使う場合はデータのコピー処理を明示する必要はなくなる。
  • OpenCL: 業界標準GPUプログラミング環境で、CUDAと似ているところが多い。
    • OpenCLは各社のGPUをサポートするため、構成を問い合わせる関数で情報を得て、それに対応した操作を行えるように作られている。
    • OpenCL 2.2ではC++14ベースのカーネル記述がサポートされている。
    • NVIDIAはCUDAのサポートを優先しており、AMDはHIPというCUDAのコードをAMD GPU用にコンパイルするシステムを開発している。
  • OpenMP/OpenACC: C言語のコードに「この部分はGPUで並列実行」といった指示行を加えるだけでGPUプログラミングを実現する。
    • OpenMPの方がサポート範囲は広いが、NVIDIAはOpenACCを優先している。NVIDIA GPUを使う場合はOpenACC、そうでなければOpenMPという使い分け。
    • CUDA/OpenCLで最適化したプログラムに性能は及ばないが、開発工数は少ないのでまず試してみる価値はある。

2. 感想

Can Spatiotemporal 3D CNNs Retrace the History of 2D CNNs and ImageNet?を読んだまとめ

0. この投稿の概要

  • 動画認識の機械学習モデル、3DCNNに関する論文"Can Spatiotemporal 3D CNNs Retrace the History of 2D CNNs and ImageNet?"を読んだので、その内容をまとめます。論文は以下のページから読めます。

arxiv.org

  • ディープラーニング、特に2D CNNの威力を世に示したのはImageNetという大規模画像データの認識コンテストかと思います。この論文では3D CNNがそのポテンシャルを発揮するにはやはり大規模な動画のデータセットが必要であり、Kineticsがその役割を果たすポテンシャルを持つことが示されています。

1. 論文の内容

目的

  • ImageNetにより層の深い(2D)ResNetが成功した歴史をKineticsと3D ResNetが同じ道を辿るか実験する。そのために行動認識タスクで3D ResNetの層を深くすればするほど性能が高まることを示す。
  • 動画のデータセットではHMDB-51とUCF-101が使われてきたが、より大きなActivityNetやKineticsが近年発表された。本論文ではこれらのデータセットを実験の対象にする。
  • 行動認識タスクのモデルとして、RGBとoptical flowをそれぞれ2D CNNの入力するTwo-Stream CNNがある。2つのStreamがそれぞれ空間、時間方向の特徴を抽出する。
    • より直接的にspatio-temporal特徴量を抽出するために3D CNNが提案された。VGGを3D化させたようなC3D、Inceptionの3D版であるI3D、更にResNetの3D版も提案されているが層は比較的浅いものに限られている。

実験

  • 実験にはResNetとその派生のモデルを使用した。各モデルのブロックは以下の図のような構成で、これを複数つなげることでResNetを構成する。

実験1
  • 各データセットで比較的浅いResNet-18を最初から学習する。Kineticsでのみ良い精度が得られるという過去の結果を再現する。
  • [結果]Kinetics以外のデータセットではvalidation lossとtraining lossで大きな乖離があり、オーバーフィットしていることがわかる。逆にKineticsではオーバーフィットは起きておらず、より深いResNetでも学習が可能なことが示唆されている。

実験2
  • KineticsでResNet-18~200を学習し、どこまで深いネットワークを学習できるか検証する。
  • [結果]層が深くなるほど精度は上がり、152層で飽和している。この傾向は2D CNNのよるImageNetの分類と同様。

  • 今回の最も良い結果はResNeXt-101だが、Two-stream I3Dの方が高い精度が出ている。
実験3
  • Kineticsで学習したモデルをHMDB-51やUCF-101でfine-tuningをする。これにより3D CNNでも転移学習が有効であることを示す。*最後のCNNブロックと全結合層のみ再学習している。
  • [結果]scratchで学習した場合に比べて明らかにfine-tuningした方が高い精度を持つ。Kineticsと同様に、深い層程精度がよくなり、ResNeXt-101が最も良い結果となった。


2. 感想

  • 自分も仕事で動画を入力とした機械学習の応用を検討しています。そこでネックになるのがデータの数です。本論文で示された転移学習が色々なタスクでも有効であれば是非とも使ってみたいです。
  • この論文の著者は日本人で、その方が以下の雑誌で動画認識技術の解説を書いています。動画認識技術の歴史やtransformerを使った最新のモデルまで説明されているのでおすすめです。

  • ありがたいことに著者のgithubにコードやpretrainモデルが公開されていますので、今度自分でも試してみようと思います。

github.com

読書記録 テスト駆動開発

0. この投稿の概要

  • 今更ながら「テスト駆動開発」を読んだので、今後の自分の開発にどうやって活かそうか考えたことをまとめます。

  • 本書は3章立てで、1章でJavaで多国通貨を足し合わせるプログラムをTDDで実装していき、2章はPythonでテストツール(xUnit)をTDDで実装、3章はTDDのパターンの解説です。自分は2章を写経しました。

1. 学んだこと, 考えたこと

テスト駆動開発をどう取り入れるか

  • テスト駆動開発によるプログラミングの作業は以下の3フェーズからなる。
    • レッド: 動作しない、コンパイルすら通らないかもしれないテストを1つ書く。
    • グリーン: そのテストを迅速に動作させる。この時、コードに期待値をベタ書きしたり類似する関数をコピペするような罪を犯すことを厭わない。
    • リファクタリング: テストを通すために発生した重複をすべて除去する。一般化する。
  • 一方で普段の自分の作業順は以下の通り。個人的に一番楽しいのは最後に修正とテストを繰り返しているフェーズ*1。TDDはこのフェーズを繰り返すことで開発を進めるようなものなので、うまく出来たら楽しい時間で埋め尽くせるか?
    • まず実装する。この時点で必要な一般化も考慮している。
    • テストを作る。大体最初から複数のテストケースを作成している。
    • テストの実行、実装の修正をテストケースが全て通るまで繰り返す。
  • プログラミングに限らず、多くの知的な活動の基本は複雑なものを分割して解いていくこと。TDDはそれを実行する手段で、本書に掲載されている例もこれでもかという程細かいステップに分割されている。
    • 問題はどれくらい細かく分割すべきかという点。テストの粒度、グリーンフェーズでどのような実装をするか。これは自分のスキルと取り組む問題に依存するため、客観的な指標でルール化するのは難しそう。
    • ステップの幅を決める手がかりは自分の不安や退屈さになると思う。十分に簡単な実装なら最初から一般化すれば良いし、思ったより時間がかかる場合は途中でも方針を変えてステップを刻む。
  • 方針: 何はともあれ最初にテストを作ることのデメリットは少ないので、そこは取り入れる。ステップの細かさは自分が心地よく作業ができるように随時調整をしていく。

その他実践すること

  • テストケースには数式を直接書く。
    • 何故か今までテストケースには計算した結果を記述し、コメントに計算式を書いていた。最初から数式をテストケースに入れておけば手計算をする必要もないし可読性も上がる。
  • テストファーストにするにはテストを書くハードルを下げる。テストコードも最初は最低限に。
    • テストを作る時は最初から一般性を意識して複数のテストケースを作ることが多い。それが面倒でテストを作るのを後回しにするくらいなら、最初は最も簡単なケースだけにする。
    • テストコード自体もリファクタリングされることを前提にする。コミットとコミットの間で消えるコードがあるのと同じで、一時的なテストだってあり得る。
  • 頭の中の悩みをテストに表現する。
    • 本書では2つの異なるクラスから生成されるインスタンスの不一致性を確認していた。普段は期待値との一致パターンしかしていなかったが、もっと柔軟にテストを考えてみよう。

2. 感想

  • テクニカルな内容も多く書かれていましたが、私としては自分の感情を頼りにステップを細かくすることがTDDの本質だと解釈しました。無理なく取り入れられるところから始めていこうと思います。
  • 訳者の和田さんによる近年のテスト駆動開発の解説がとても興味深かったです。特に以下の一節にはプログラミングを学んでいく上でとても勇気づけられます。

優れたプログラミングテクニックパラダイムは「使いすぎてみて」少し戻ってくるくらいが良い塩梅です。(中略)すべて手を出してみて、夢中になり、正気に戻り、良いと思ったエッセンスを自分の中に残してください。

*1:フロー状態の要素として「直接的で即座のフィードバック」があることが関係しているんだろう。

ゼロから作るDeep Learning3 フレームワーク編を写経した

0. この投稿の概要

  • フレームワークということでただ動くだけではなく、ユーザビリティやメモリ管理にも気を配っています。そこで本記事はコアとなる自動微分を実現する仕組み、ユーザビリティを向上させる仕組み、メモリ効率を改善する仕組みという3つの軸で学んだことをまとめました。
  • なお本書の最初のセクションはwebで公開されていますので、まずはこちらを確認して購入を検討されると良いかと思います。最初のセクションに一番コアなことが書かれていますので太っ腹ですね。

koki0702.github.io

1. 学んだこと

自動微分を実現する仕組み

  • 変数を格納するVariableクラスと各種関数のベースとなるFunctionクラスを作成、自動微分は基本的にこの2クラスだけで成立している。
    • 特にFunctionクラスは順伝播と逆伝播を行うメソッドを持ち、具体的な計算は継承先のクラスでそれぞれ実装される。
  • 誤差逆伝播を自動化させるために関数と変数のつながりを保持する必要がある。このつながりは順伝播でデータを流すときに作り、この特徴がDefine-by-Runと呼ばれる。
  • 分岐を含む複雑な計算グラフで逆伝播を正しく実現するために、順伝播をした際に変数や関数の世代を記録し、後ろの世代を優先的に逆伝播時に処理する。
  • 勾配もVariableクラスとして保持することで、逆伝播時も計算グラフを作れるようになる。勾配をさらに逆伝播することで高階微分を求めることができる。

ユーザビリティを向上させる仕組み

  • 複数の入力がある関数に対応するため、Functionクラスは可変長引数を使っている。
  • Variable同士、あるいはVariableとndarrayとの計算を+や-のような演算子を使ってできるように演算子オーバーロードをしている。
    • 他にもVariableクラスに格納されている中身(ndarray)を見やすくするため、ndarrayと同様のインスタンス変数(shape, ndim)などをpropertyとして実装。また__repr__メソッドを実装してprintにも対応した。
  • Parameterクラス(Variableと同一)の集約としてLayerクラスを実装。さらにパラメータ更新作業を一括して行うOptimizerクラスを実装。これが無いとパラメータごとに更新する退屈なコードをユーザが書かなければならない。

メモリ効率を改善する仕組み

  • pythonのメモリ管理は参照カウント方式が基本、もし循環参照がある場合は参照カウント方式では削除されないため、循環参照を作らないように心がける。
    • Functionが入出力のVariableを参照し、Variableはそれを生み出したFunctionを参照するため循環参照がある。本書ではFunctionから出力Variableへの参照を弱参照に置き換えた。
  • MeanSquaredErrorのような複数の関数の組み合わせで求められるものは、それ単体としてFunctionとして定義をした。既存の関数の組み合わせで実装すると、途中の変数に対しても計算グラフが生成されてメモリ効率が良くないため。
  • 本書の範囲では順伝播の結果を全てFunctionクラスが保持しているが、関数の中には逆伝播の計算に順伝播の情報が不要のものがある。関数ごとに保持するデータを決めれば不要な情報を保持する必要がなくなる。

2. 感想

  • 実装してみて、普段利用しているフレームワークのコアは自動微分の仕組みにあることに改めて気付かされました。逆に言えば既存のフレームワークの理解を深めるならまずはそこを抑えておいた方が良さそうです。
  • 本書は500ページ近くのボリュームがある本ですが、特に面白い自動微分に関わる実装は前半で終わるので、まずはそこまで写経をしてみると良いかと思います。
  • 次のステップとしてはモチベーションが高いうちにPyTorchのコードリーディングに少しでも挑戦したいです。また、今回とは違うdefine-and-run方式、特にどのようなネットワークの最適化が行われるかも調べたいと思います。

読書記録 ドメイン駆動設計入門

0. この投稿の概要

  • ドメイン駆動設計入門を読んで学んだことをまとめます。ドメイン駆動設計とはプログラムの適用対象の領域の知識に焦点をあてた設計手法です。

  • ドメインの知識はドメインオブジェクトとしてコードで表現され、それらを組み立てることでアプリケーションが実現されます。本書ではドメインオブジェクトとアプリケーションの実装パターンが主に紹介されています。

1. 知識を表現するパターン

値オブジェクト
  • システム固有の値を表したオブジェクトであり「値」がもつ3つの性質を持つ。その性質は不変である、交換が可能である、等価性によって比較されること。
    • ここでいう値は1や"hello"のようなリテラルのようなものか。これらは当然変更できないし、あくまでこれらの値をもつ変数の中身を交換することができる。また、==のような演算子で比較ができる。
  • どの値を値オブジェクトとして実装するかの判断基準は「ルールが存在しているか」(ルールをオブジェクトが担保するため)と「それ単体で取り扱いたいか」。
  • 値オブジェクトのメリットはその値に関するルールの実装を集約できること。プリミティブ型を使う場合、その値を利用する場所全てでルールチェックをする必要がある。
エンティティ
  • 値オブジェクトとは異なり、属性ではなく同一性によって識別されるオブジェクト。値は可変で、識別のためにIDを持ち、等価性の比較はIDだけを比べる。例: ユーザ情報
  • エンティティを採用する判断基準は「ライフサイクルが存在し、そこに連続性が存在するかどうか」。同じものでもシステムの関心によって値オブジェクトとエンティティどちらが適切か変わる。
  • エンティティ、値オブジェクトのようなドメインオブジェクトを使うメリットは、ドメインの変化に対応するために変更する場所が明確になること。
ドメインサービス
  • ドメインオブジェクトに実装すると不自然になるようなふるまいを提供する。例: ユーザ名の重複確認
  • 手続き型の実装を助長してしまいので、可能な限り使わない。

2. アプリケーションを表現するためのパターン

リポジトリ
  • オブジェクトのインスタンスをデータストアに保存するときは、直接的にデータストアに書き込みを実行せずに、リポジトリに依頼をする。逆にインスタンスを再構築するときもリポジトリに依頼をする。
  • リポジトリはインターフェースとして抽象化して、具体的なデータストアの操作はその具象クラスとして実装する。
アプリケーションサービス
  • ドメインオブジェクトを組み合わせてユースケースを実現するオブジェクト。例: ユーザ情報のCRUD処理
  • ユーザ情報取得処理ではドメインオブジェクトをそのまま返すとアプリケーションサービス以外のオブジェクトがドメインオブジェクトのふるまいを呼び出してしまう可能性がある。それを防ぐためにはデータ転送用のオブジェクトを代わりに返す。
  • ドメインのルールはアプリケーションサービスに記述しないで、ドメインオブジェクトやドメインサービスに記述する。
ファクトリ
  • 複雑なオブジェクトの生成を処理するオブジェクト。「コンストラクタ内で他のオブジェクトを生成するかどうか」はファクトリを作る際の動機づけになる。

3. 知識を表現する、より発展的なパターン

集約
  • 集約は複数のオブジェクトをまとめてひとつの意味を持ったオブジェクト。集約の外部から内部のオブジェクトを操作してはいけない。必ず集約オブジェクト自身が内部のオブジェクトを操作する。
  • そもそもインスタンスを持たせなければ、内部のオブジェクトを外部から呼び出しようがなくなる。インスタンスの代わりにそのIDを保持すればメモリの節約にもなる。
仕様
  • 仕様はあるオブジェクトが評価基準に達しているか判定するオブジェクト。
  • 複雑な評価はドメインオブジェクトの一つなのでアプリケーションに記述すべきではないが、エンティティに置くと煩雑になる。その場合は仕様として独立しドメインオブジェクトとして切り出す。

4. その他

依存関係逆転の原則
  • データストアの操作などハードに近い処理を行うオブジェクトは下位レベルで、クライアントに近い処理は上位レベル。上位が下位に依存してはならず、抽象オブジェクトを作成し、どちらのオブジェクトも抽象に依存するようにする。
ユビキタス言語
  • プロジェクトにおける共通言語(語彙)、最終成果物であるコードでもこの表現が現れるべき。

5. 感想

  • ドメイン駆動設計というと謎の用語が大量にあってハードルが高く感じるが、本書を読むとそこまで身構える必要がないような気分になる。ちゃんと理解するには原典を読むべきなんだろうが、その下準備として読んで損はない一冊だと思う。

  • 私はボトムアップな設計*1とはどのようなものか知りたくて本書に手を出したが、本書で取り上げるアプリケーションが小さかったので、ドメインからどのようなドメインオブジェクトを取り出せばよいかという設計の話がほとんど無く少し残念だった。
  • ドメイン駆動設計」を採用せずとも、知識やルールを部品として独立したオブジェクトにして、それらを組み合わせてアプリケーションを作るというのはオブジェクト指向の基本だと思うので、拝借できるテクニックや考え方は使わせてもらいたいと思う。

*1:副題にボトムアップという言葉が入っているが、あくまでドメイン駆動設計をボトムアップ的に説明するという意味。著者もドメイン駆動設計はボトムアップなアプローチだと書いているが、その詳しい説明はなかったと思う。

pytestでテスト入力をテスト関数間で共有する

0. この投稿の概要

  • クラスメソッドをテストする時に、同じ入力を異なるメソッドのテストで使いまわしたいと思うことがあったので、pytestでの実現方法を調べてみました。
  • 今回はfixtureとparametrizeを組み合わせた実装を紹介します。多分色々方法はあるかと思いますので、一例として参考になればと思います。

1. 実装例

テスト対象のクラス

  • 初期化時に変数を2つ受け取り、メソッドで足し算や掛け算結果を返すという単純なクラスです。
import pytest

class Calculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add(self):
        return self.a + self.b

    def mul(self):
        return self.a * self.b

    def add_c(self, c):
        return self.a + self.b + c

パターン1: 1つのテスト関数で複数の関数をテストする。

  • parametrizeに複数の関数の期待値を記載する方法です。
  • 最も単純なやり方ではありますが、柔軟性がないのでadd_cのように別途引数がある場合などでは他の関数に無駄なテストが追加されてしまいます。
  • 今回は引数も期待値も整数型なので問題にはなりませんが、これがリストや辞書など大きな入力になると視認性も悪くなりそうです。
@pytest.mark.parametrize(
    ("a", "b", "add_expected", "mul_expected"),
    [(1, 2, 3, 2), (3, 4, 7, 12)],
)
def test_all(a, b, add_expected, mul_expected):
    assert add_expected == Calculator(a, b).add()
    assert mul_expected == Calculator(a, b).mul()

パターン2: parametrizeのindirectを使う。

  • parametrizeのindirectオプションを使うと、指定したパラメータの値を受け取ったfixtureを使うことができます。
  • 下の例ではfixture calc_instanceに"test1"や"test2"といった値がrequest.param経由で渡されます。それを受けてパラメータを切り替えたインスタンスを異なるテスト関数に渡すことができます。
  • パターン1よりも柔軟にテストを作成できると思います。test_add_cではfixture以外の引数cも別途指定しています。
@pytest.fixture
def calc_instance(request):
    config_dict = {"test1": {"a": 1, "b": 2}, "test2": {"a": 3, "b": 4}}
    config = config_dict[request.param]
    return Calculator(config["a"], config["b"])


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test1", 3), ("test2", 7)],
    indirect=["calc_instance"],
)
def test_add(calc_instance, expected):
    assert expected == calc_instance.add()


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test1", 2), ("test2", 12)],
    indirect=["calc_instance"],
)
def test_mul(calc_instance, expected):
    assert expected == calc_instance.mul()


@pytest.mark.parametrize(
    ("calc_instance", "c", "expected"),
    [("test1", 1, 4), ("test2", -1, 6)],
    indirect=["calc_instance"],
)
def test_add_c(calc_instance, c, expected):
    assert expected == calc_instance.add_c(c)

参考:
docs.pytest.org

パターン3: getfixturevalueを使う。

  • getfixturevalueにfixture名を渡せばそのfixtureが返ってきます。
  • 下の例ではfixture名"test3"や"test4"をparametrizeで渡しています。
  • 今回の簡単な例ではパターン2とパターン3ではほぼ同じような使い勝手です。もっと複雑な例では違いが出てくるかもしれません。
@pytest.fixture
def test3():
    return Calculator(1, 3)


@pytest.fixture
def test4():
    return Calculator(2, 4)


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test3", 4), ("test4", 6)],
)
def test_add2(calc_instance, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.add()


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test3", 3), ("test4", 8)],
)
def test_mul2(calc_instance, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.mul()


@pytest.mark.parametrize(
    ("calc_instance", "c", "expected"),
    [("test3", 1, 5), ("test4", -1, 5)],
)
def test_add_c2(calc_instance, c, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.add_c(c)

参考:
docs.pytest.org

読書記録 良いコード/悪いコードで学ぶ設計入門

0. この投稿の概要

  • 最近話題の「良いコード/悪いコードで学ぶ設計入門」を読んだので学んだことをまとめます。どれくらいのレベル感の内容が説明されているか知るための参考になれば幸いです。

1. 学んだこと

クラス設計の基本

  • 完全コンストラクタ: 初期化時点でインスタンス変数を指定し終え、その後はsetterなどで値を変更しない。値を更新する必要があれば新しくインスタンスを生成して返す。→思わぬ場所で値が変更されることを防ぎ、実行結果が毎回同じになるためテストもしやすくなる。
    • Pythonでも3.8からFinal型注釈を付けられるようになっています。 PEP 591 – Adding a final qualifier to typing | peps.python.org
    • ローカル変数や引数も基本的には不変にしておくことが推奨されています。可変にするのは大量のデータ処理などでパフォーマンスに影響があるケースなど。
    • getterやsetterを実装することはカプセル化ではない。
  • 値オブジェクト: 値をプリミティブ型ではなくクラスとして表現する。これにより不正な値を防ぐことができる。また、その値に関する計算も備えさせると、そのロジックが色々な場所に実装されることを防げる。
    • 基本的にはインスタンス変数を用いるメソッドをクラスに持たせ、ログ出力用のような汎用的なもの以外でstaticメソッドを避ける。
    • 目的別に異なる初期化をするためのファクトリメソッドもstaticメソッドとして実装される。
  • コレクション型についても同様にコレクションとそれを操作するメソッドをクラスにまとめる(ファーストクラスコレクション)。
    • Pythonではリストの要素の変更を禁止するメソッドは見当たりませんでした。代わりにcopyメソッドで別のオブジェクトとして返せばいいかと思います。
  • サブクラスはスーパークラスに大きく依存するため、継承は安易に使うべきではない。継承より委譲、コンポジションとして使いたいクラスをもたせると変更の影響を受けにくい。

ネストを浅くするテクニック

  • 早期リターンでif文のネストやif-else文をすっきりさせる。
    • C言語の一部の規約では関数末尾以外でのreturnを禁止されているため、自分は早期リターンに心理的な抵抗がありましたが、今後はもっと活用しようと思います。
  • 種類毎に処理を切り替えたい場合はswitch文の代わりにinterfaceを用いて実装する(ストラテジパターン)。mapから使いたいインスタンスを持ってきて、共通のメソッドをコールする。
  • 複数の条件をチェックしたい場合はポリシーパターンを使う。これは条件判定のinterfaceを作り、それを元に具体的な条件を1つ1つ実装する。ルールを集約するクラスに必要な条件をコレクションとして持たせてfor文で全て確認する。

名前設計

  • たとえ現実世界のオブジェクトが1つだとしても、関心事や目的に応じてより具体的なクラスへ分割する。例: Userを個人認証のためにPersonalAccount、特徴表現のためにProfileとして定義する。
  • 開発中の会話の中で頻発するキーワードや形容詞はそのままコード上のクラス名などに実装する。
  • Manager, Processor, Controllerという単語をclass名に入れると巨大なクラスになりがち。具体的にやることで名付けるべき。
  • 「動詞+目的語」のメソッドに注意。この場合目的語の概念を表現するクラスを作り、そのクラスに動詞1語のメソッドを追加するとよい。

2. 総評

  • オブジェクト指向プログラミングにまだ自信のない自分としては、参考になるテクニックを見つけることができました。ただ章と章の結びつきが弱く、ストーリー性が薄いためTips集のような印象を受けました。逆に言えばどの章からでも読み始められる利点はあるのですが…
  • interfaceの理解は特に重要だと思いますが、本書のように初心者向けの本でわかりやすい説明が書かれたものは少ないように思います。言語の本は機能だけの説明になりがちですし、OOPの本でさえポリモーフィズムの説明はありますがそれが実践の場でどのように役立つのかイメージしづらいです。
  • そういった意味でJavaエンジニアの初心者には文法を学んだ次の一冊としておすすめします。初心者にとって他言語で書かれた本はとっつきにくい所ではありますが、オブジェクト指向の言語を使うのであればJavaエンジニア以外も選択肢に入れて良いと思います。

3. その他の参考書

  • 現場で役立つシステム設計の原則: 業務システムを例としたシステム設計の入門書です。言語はJavaで、データベースやUIなどについても説明されているため本書よりも範囲が広いです。本書でも紹介されていますが、業務システムを開発するなら次に読む本として丁度いいかと思います。