blog.syfm

徒然なるままに考えていることなどを書いていくブログ

オブジェクト指向入門 第2版 原則・コンセプト 読書メモ 1

これはなに

オブジェクト指向入門 第 2 版 原則・コンセプト」の読書メモ。

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

この本は大きく Part A, B, C と構成されているので、それに従って分割していて、今回は Part A (第 1 章 - 第 2 章)。
他の part は WIP です。(継続的に読んでるけどボリュームが多すぎてなかなか進まない!)

Part B あたりまで読んだ感想としては、やっぱり扱っている例が古いなと感じる部分が多い。ただ、オブジェクト指向の理論や背景をはじめとした内容の大部分が今でも非常にためになる。
よくオブジェクト指向で議論になることは、すでにこの本で扱われていることが多い。そのためオブジェクト指向技術を扱っているソフトウェアエンジニアは必ず読むべき本だと感じた。
もっと具体的な感想や全体のまとめはすべて読み終わったあとに書くことにしたいと思う。

第 1 章 ソフトウェアの品質

ソフトウェア工学の目的は、品質の高いソフトウェアを構築することであり、指標として幾つかの要因を組み合わせると表現しやすい。 品質要因は大きく分けて 外的品質要因内的品質要因 の 2 種類に区別できる。

外的品質要因

レイテンシなどの、ソフトウェアのスピードや使いやすさといった、ユーザが認識できる性質。

内的品質要因

モジュール性や、ソースコードの可読性などの、開発者寄りの人が認識できる性質。 内的品質要因は、ユーザが認識できない性質なので、最終的に重要となるのは外的品質要因であるが、それを実現するために開発者は内的品質要因を保証する必要がある。

外的品質要因の種類

正確さ (correctness)

正確さとは仕様によって定義されているとおりに、仕事を実行するソフトウェア製品の能力である。

ソフトウェア開発を、個々の層と捉え、より下位の層に依存すると言った考え方 (前提条件) が必要となる。

以下の層があるとき、アプリケーションプログラムは、コンパイラが正しく動作するという前提の上で正しさをチェックする

例) アプリケーションプログラム > コンパイラ > OS > ハードウェア

前提条件により、アプリケーションプログラムとコンパイラの問題への関心を分離することができる。 下位の層を盲目的に信頼するということではなく、各層の関心を独立させるべきということ。

頑丈さ (rubustness)

頑丈さとは異常な条件に対して適切に対応するソフトウェアシステムの能力である。

ソフトウェアの仕様が決まっているとして、それ以外の条件で発生する状態に対してシステムが破壊的なイベントを起こさないようにすること。 例えば、適切なエラーメッセージを出力し、 graceful shutdown を行うといったハンドリングが行われる場合などが該当する。 異常系であっても、それが仕様に含まれているのであれば正確さの範疇に含まれる。

拡張性 (extendibility)

拡張性とは仕様の変更に対するソフトウェア製品の適応のしやすさである。

拡張性の問題は、規模の問題であり、小規模なソフトウェアであれば適応は容易だが、大規模になるほど困難になる。 ソフトウェアは、本質的にドメインモデリングしたものであり、常にドメインが変化するので、そのモデルを捉え続けるには拡張性が重要となる。

拡張性を向上させるには、設計の単純さ非集中化 といった二つの原則が重要になる。 アーキテクチャが単純であればあるほど常に複雑なアーキテクチャよりも変更に対して適応しやすい。 また、モジュールにより関心が分離されているほど影響を受けるモジュールが少なくなり、変更する箇所が少なくなる。

オブジェクト指向は、単純かつ非集中的な構造のシステムの設計を助けるためのアーキテクチャ手法である。

再利用性 (reusability)

再利用性とは多数多様なアプリケーションの構築に使うことのできる、ソフトウェア要素の能力である。

ソフトウェアは同じようなパターンが繰り返し現れるため、その共通性を利用することで新しく解決方法を考え直さずに済み、再利用可能な要素を他の問題へ適用できる。

互換性 (compatibility)

互換性とは、ソフトウェアの要素の、ほかのソフトウェア要素との組み合わせやすさである。

ソフトウェアは相互に作用して動作するが、他のドメインでは前提条件が異なるため問題が生じる場合が非常に多い。 互換性の鍵として、設計が同質であることや、プロトコルが一致しているかなどが挙げられる。

効率性 (efficiency)

効率性とは、処理時間、内部記憶及び外部記憶上の空間、通信装置で使用する帯域幅などのハードウェア資源を出来る限り必要としないソフトウェアシステムの能力である。

効率性は、限られた資源を有効に使うために重要な要因ではあるが、一般に、効率性を考えるときはそれ単体ではなく、相反する拡張性や再利用性、正確さとのバランスを考え無くてはならない。 場合によって、効率性が正確さへ悪影響をおよぼす場合もあるが、効率性やその他の要因により正確さを犠牲にする理由は絶対に有り得ない

可搬性 (portability)

可搬性とは多様なハードウェア及びソフトウェア環境へのソフトウェア製品の移植のしやすさである。

使いやすさ (ease of use)

使いやすさとは、経験も資格も異なる人々がいかに容易にソフトウェア製品の利用法を学習し、問題解決に応用できるかである。これには、インストールや、操作、監視の容易さも含まれる。

適切に構造化された設計のシステムは、複雑なそれより学習しやすく使いやすい傾向にあるため、使いやすさの鍵の一つとなる。

「経験や資格」は、前提条件として見ることができ、これらの前提条件を出来る限り作らないようにする方針を取り入れることで良いインターフェースの設計を行えるようになる。

機能性 (functionality)

機能性とは、そのシステムが提供できるサービスの範囲である。

より多くの機能を提供しようとすると、2 つの問題が発生する。 一つは、新機能の追加により使いやすさが損なわれ、一貫性が失われることで、これを解決するには、製品全体の一貫性についての作業を何度も繰り返し、全体としてある共通の性質を持つ一つのかたまりになるようにする。 もう一つの問題は、機能性に注目しすぎて他の品質要因を忘れがちになるところで、その解決策の一つとして、一定の品質レベルをプロジェクトを通して維持するためにオブジェクト指向を取り入れた技法を取り入れることもできる。

適時性 (timeliness)

適時性とはユーザが必要としているとき、または、必要とする前にソフトウェアシステムをリリースできることである。

ドキュメント

ドキュメントは、品質要因を考慮した結果であり、3 種類に分類できる。

外部向けドキュメント

ユーザがシステムを快適に利用できるようにするためのドキュメントで、使いやすさの結果となる。

内部向けドキュメント

開発者がシステムの構造と実装方法を理解するためのドキュメントで、拡張性の結果となる。

モジュールインターフェースドキュメント

開発者が実装法右方を理解せずにモジュールの機能を理解するためのドキュメントで、再利用性の結果となる。 特定の変更によって、特定のモジュールに影響が及ぶことを判定できるので、拡張性の結果でもある。

ソフトウェアの保守

保守の主な作業は、新しいモデルへ適応するための拡張と修正、そしてバグの修正やドキュメントの追加等が多くを占める。 前者は必要な作業だが、後者のコストは、他の品質要因のレベルを上げたり、データの物理的構造への依存を小さくすることによって十分に下げることが可能である。

第 2 章 オブジェクト指向の基準

オブジェクト指向の概念を大まかに掴むための章で、今後の章で取り上げる内容のプレビュー的な位置にある。

基準について

この章で取り上げる基準を対象となる環境に適用したとき、すべての基準を満たす必要があるとは限らない。
オブジェクト指向はブール演算ではなく、環境 A が 環境 B よりもオブジェクト指向的であるという判断もできる。
とはいえ、オブジェクト指向環境の選択には、すべての基準を知っている必要はある。

カテゴリ

これから説明する基準は以下の 3 つに分類できる。

  • 方法論と言語: ソフトウェアを生産するときの思考過程や、その際に使用される表記法。「言語」という言葉は、プログラミング言語だけでなくテキストなども含まれる。
  • 実装と環境: オブジェクト指向的概念の適用を可能にするツールの性質
  • ライブラリ: 基本ライブラリの入手しやすさと、ライブラリの作成に必要なメカニズム

方法論と言語

継ぎ目のない連続性 (seamlessness)

オブジェクト指向による問題解決は、ソフトウェアのライフサイクル全体を守備範囲とする。
方法論と言語が分析・設計・実装・保守といった各段階に適用可能であるかをチェックするべきである。
ライフサイクル全体をカバーすることで、開発過程における「継ぎ目」がなくなる。同じ概念と表記法を利用していることで各段階への移行が用意になる。

クラス (class)

オブジェクト指向はクラスという概念に基づく。
クラスは抽象データ型とその部分的、または全体的実装を表現するソフトウェア要素である。
また、抽象データ型はオブジェクトの集合で、その集合に適用可能な特性 (feature) のリストと属性 (propaties) によって定義される。

表明

言語はクラスと特性に対して表明 (事前条件、事後条件、不変表明) を与えることができなければならない。 またツールを使って表明からドキュメントを生成でき、選択的に、実行時に表面を監視できなければならない。

特性に基づく計算

例えば、ある従業員を表すオブジェクト e に対し、d 日付けで n ドル昇給させるには、e に対して e の特性 raise (昇給) をパラメータ d と n とともに呼び出す。つまり、特性呼び出しが第一の計算メカニズムである。
あるクラス C の特性の呼び出しを含むクラスは C のクライアントであると言う。
特性の呼び出しはメッセージパッシングとも呼ばれている。この呼び方の流儀では上のような呼び出しは『d と n を引数として、「あなたを昇給させなさい」というメッセージを e へ渡す』という表現になる。

情報隠蔽

クラスが内部で使用するためだけに使われる特性は、実装の一部ではあるがインターフェースには含まれない。
このクラスのクライアントがインターフェースに含まれない特性を利用できないようにするための機構である、情報隠蔽が必要となる。
現実的には、クラスの設計者が、一部のクライアントに対してのみ選択的に特性をエクスポートできなければならない。

例外処理

システムの実行時には、媒体障害やゼロ除算、オーバフロー、ソフトウェアのバグの結果として適切に実行できない呼び出しが発生する可能性がある。
これらから回復する手段として、例外処理が必要となる。

静的型付け

よく定義された型システムであれば、静的型チェックプログラムを書くことができる。
定義された適合規則を矯正することによって、受け入れたシステムの実行時の型安全性を保証しなければならない。

総称性

型付けを実用的にするには、型をパラメータとして与えることのできる総称クラスが定義できなければならない。
このような形式の型のパラメータ化を制約なし総称性と呼ぶ。

単一継承

ソフトウェア開発には、多数のクラスが関係するが、中には他のクラスの変形であるものがある。
その結果、複雑になるのを防ぐために継承と呼ばれる分類の仕組みが必要となる。

多重継承

いくつかの抽象概念を組み合わせる必要が生じる場合がある。
多重継承は、一つのクラスではなく概念的に正しい限り、任意の数のクラスから継承することを保証する仕組みを提供する。
多重継承ではしばしば名前空間の衝突が生じるため、多重継承を提供する表記法はその適切な解決法を敬供しなければならない。

反復継承

多重継承によっては、同じクラスを複数の経路で継承するケース (反復継承) が生じる。
このような場合、その祖先クラスから反復継承された特性それぞれに対し、共有 (ただ一つのみ定義する場合) 、複製するかを選択できなければならない。

制約付き総称性

制約なし総称性とは異なり、パラメータとして受け入れる型を制限する。
例えば、総称クラス SORTABLE_LIST が特定の順序関係に従い、自身をソートする sort 特性を備えているリストの場合、この総称パラメータとして順序関係を定義できない型を使うことは明らかに不可能である。
順序関係を備えたオブジェクトを表すクラス COMPARABLE があるとして、これの子孫でなければならないことを記述するためのメカニズムが必要になる。

再定義

継承した特性の実装や属性、シグネチャを変更する必要が生じることがあるため、それらは再定義可能でなければならない。

多相性

型システムに多相性が提供されていない場合、あるクラス C を継承するクラス D と E があるとき、C の型として宣言されたエンティティ (変数などの概念を一般化したもの) を使って D や E 型のオブジェクトを参照できない。
多相性は許されている様々な型のオブジェクトに結びつき得るエンティティの能力である。
静的型付けの場合、多相性は継承関係によって管理される。

動的束縛

多相的なエンティティへの特性の呼び出しの場合、その特性は エンティティの型を継承するクラスによって再定義されている可能性がある。
特性の実装が常にエンティティに紐づく実際のオブジェクトの型から導かれるものであることを保証するメカニズムを動的束縛という。

実行時の型検査

型情報が失われている場合など、前もってオブジェクトの型を予期することができない場合がある。
その場合、安全にオブジェクトにアクセスするメカニズムが必要になる。
試行代入はその一つで、これはオブジェクトをエンティティへ結びつけようとする際に、オブジェクトの型がそのエンティティの型と適合する場合には通常の代入となり、それ以外の場合は無効な値となる操作である。

暫定特性と暫定クラス

使用は記述されているが、完全に実装は行われていない特性やクラスを暫定特性、暫定クラスと呼ぶ。
あるクラス C が一般的すぎるため、完全に実装はできないが、最終的にその子孫クラスにより実装され、エンティティに結びつくことが保証されている場合は C 型のエンティティとして利用できることを期待する。

実装と環境

ドキュメント

クラスやシステムの開発者は、自分以外の人々に対して明確で高レベルのドキュメントを提供しなければならない。
ドキュメントは出来る限りソフトウェアのテキストから自動的に生成すべきである。

ブラウジング

クラスを見るときに、そのクラスが依存しているクラスなどの情報が必要になる。
他のクラステキストへの迅速な切り替えを行うためのツールを提供する責任が環境にはある。

ライブラリ

オブジェクト指向での開発の特徴の一つとして、ライブラリに依存できるという点がある。
オブジェクト指向環境は良いライブラリを書くためのメカニズムを提供しなければならない。

また、環境は基本的な再利用可能なライブラリを提供しなければならない。

イケている技術、イケていない技術

たまに Twitter で「イケている技術」とか、「イケている会社」といったフレーズが流れてくることがある。そしてそのたびに違和感を覚えている。
Twitter で流れてくることにはほとんど価値はないので、それに対してなにか意見するのは不毛かもしれないけど思っていることを書いておきたい。

(ここでは面倒なので「イケている技術」で固定します)
イケている技術に挙げられているのは、例えば今、人気が徐々に出てきているプログラミング言語だったり、有名な企業が公開したライブラリやフレームワークなどの新しい OSS だったりが多い。
人はハロー効果に弱くて、例えば GitHub のスター数が 1000 を超えていたり、GoogleFacebook が公開しているというだけでいとも簡単にそのソフトウェアを信用してしまう。

しかし、実際に気になった OSS を読んでみたりすると、あまりにもひどいコードでうんざりすることがよくある。
言語特有のイディオムに則っていなかったり、エラーを握りつぶしていたり、テストがなかったり、OSS にもかかわらずドキュメントが整備されていなかったり、とにかく一度読んで見ればすぐにコードの臭いがすると思う。

「イケている」という言葉は、その対象以外はイケていないのか?と思ってしまうので、自分はかけられたくない言葉だし、言いたくない言葉だと思っている。 ただ、あえて自分の思うソフトウェア開発における、イケている技術を定義してみると、「ソフトウェアの品質を担保できる技術」だと思う。
プログラミング言語や、フレームワークは、ただの手段の一つであって、それ自体が品質を左右するわけではない。(左右する場合もあるけど、あくまで一つの要因でしかない)
適切な設計、ドキュメント、コードとコードレビュー、十分なテストと環境など、そういった一見地味に見えるところの方が大切だと思う。適切な設計がなされていれば、新しい技術を取り込むといった拡張も容易だし、バグの修正も容易となる。
一部の人には当たり前に見えると思うけど、実際は当たり前ではない。ひどいコードはインターネットのありとあらゆるところに落ちている。

yshibata.blog.so-net.ne.jp

自戒を込めて、初心を忘れずに日々学習していくために今思っていることをこうして残しておきたいと思う。

自作 CLI ツールのワークフローとそれを支える技術

去年の秋頃から作っていた gRPC クライアントツールである Evans が今年に入り、徐々にいろんな方々に使われ始めました。

github.com

Issue や Pull Request を頂けてとてもありがたい反面、それらを反映したバグフィックスや機能追加がツールに取り込まれても、ユーザは能動的に GitHub Releases をチェックしなければそのアップデートを知ることができません。
これは開発者的にもユーザ的にも損失です。そもそもユーザがアップデートを利用できるかどうか確認する作業や、アップデート方法に関する知識 (どういった方法でインストールしたかどうかなど) を知る必要も本来は必要ないはずです。 この問題をどうにかしたいと思い、最近は CLI ツールの配布、自動アップデート機構に関するワークフローを整備していました。
そのために使用したツールや開発したライブラリ、その経緯を書いていきたいと思います。

5月29日 追記

この記事の内容で LT を行いました。以下はそのスライドです。

speakerdeck.com

バージョニング

Go は、以下のように標準でリモートリポジトリからバイナリを取得 / 更新することができます。

$ go get -u github.com/ktr0731/evans

しかし、go get ではデフォルトブランチの HEAD、もしくは go1.x.x ブランチ (ローカルの Go のバージョンに依存) のソースが利用されるため、実質バージョンを元にした取得ができないといった問題があります。
ツールの継続的なリリース、自動アップデート機構のためには、これは大きな問題であるため、まずはセマンティックバージョニングによるリリースを前提としました。

最近 Russ Cox が提案した vgo ではライブラリのバージョニングとして、セマンティックバージョニング (Semantic Import Versioning)を前提としているため、今後はセマンティックバージョニングが Go プロジェクトにおけるバージョニングのデファクトスタンダードになるのではないかと思っています。

ktr0731/go-semver

ツールの配布を自動化する上で、バージョニングも自動化したいと思うのが普通です。
そのため、セマンティックバージョニング用のライブラリ・ツールをつくりました。

github.com

go-semver を使い、ツールのソース内に以下のような変数を用意します。

var currentVersion = semver.MustParse("0.1.0")

currentVersion は MajorMinorPatch といった各値を表すフィールドや、Equal といったバージョンの比較のためのメソッドなどが定義されています。

fmt.Printf("current: %s\n", currentVersion) // current: 0.1.0
fmt.Println(currentVersion.LessThan(semver.MustParse("0.1.1"))) // true

詳しくは GoDoc をご覧ください。

go-semver が用意しているツール、cmd/bump により、対象のソースを AST へ変換して解析することで安全にバージョニングを行うことができます。

# main.go から semver.Parse or semver.MustParse を探し、そのバージョンのパッチを上げる
$ bump -w patch main.go

AST を用いたバージョニングは motemen/gobump を参考にしています。

github.com

各プラットフォームへのビルド、GitHub Releases へのリリース

せっかく Go で書いているので、色々なプラットフォーム向けにバイナリを配布したいものです。
Go は簡単に元々、各プラットフォーム向けにビルドすることができますが、mitchellh/gox を使うとより簡単に、並列にビルドすることができます。

github.com

作成したバイナリ群は、GitHub Releases へ並行にアップロードしています。
これには tcnksm/ghr を利用しています。

github.com

Evans では、これら二つの作業は、CI 上で実行しています。
ブランチが master へマージされたときに、ツールのバージョンが更新されているかどうかチェックし、更新されていればそのバージョンで Git のタグを切り、ビルド・リリースを行います。

自動アップデート

自動アップデートは今回、最も重要な機構です。

github.com

ライブラリを設計するにあたって、以下のことを重視しました。

  • アップデートに関する最低限の手段のみを提供し、どう使うかはライブラリの使用者に任せる
  • 複数の手段でアップデートできるようにする

go-updater には Means という概念があります。
これは、例えば Homebrew でのインストール、GitHub Releases からバイナリを取得してインストールといったそれぞれのインストール手段のことです。

go-updater を利用する際に使用する Means を決定します。もっともシンプルな例は以下のようになります。
標準で Homebrew、GitHub Releases の Means パッケージを用意しています。

means, _ := updater.NewMeans(github.GitHubReleaseMeans("ktr0731", "evans", github.TarGZIPDecompresser))

これを利用し、アップデートを行うことができます。

u := updater.New(currentVersion, means)
updatable, newVersion, _ := u.Updatable(context.Background())
if updatable {
    _ = u.Update(context.Background())
    fmt.Printf("update completed: %s → %s\n", currentVersion, newVersion)
}

updater の UpdateIf の値を変更することでアップデートが利用可能かどうかのレベルを変更することができます。

// minor 以上のアップデートが見つかった場合、Updatable は true を返す
u.UpdateIf = updater.FoundMinor

updater.SelectAvailableMeansFrom を使うことで、複数の Means を元に、どの Means によりインストールされたかを判別することができます。
Evans であれば GitHub Releases、Homebrew でのインストールをサポートしているので以下のように記述します。

means, err = updater.SelectAvailableMeansFrom(
    context.Background(),
    brew.HomebrewMeans("ktr0731/evans", "evans"),
    github.GitHubReleaseMeans("ktr0731", "evans"),
)

対象の Means によってインストールされているかどうかのロジックは、各 Means の実装に依存します。
例えば、GitHub Releases であれば ldflags により isGitHubReleasedBinarytrue となっているかどうか、Homebrew であれば brew list <name> コマンドを実行してパスが見つかるかどうかでチェックしています。

注意すべきなのは、Means によっては、インストールされているかどうかのチェックに時間がかかる可能性があることです。
実際、 Homebrew コマンドの実行に 0.3 ~ 0.5 秒ほどかかるので、それが許容できない場合はツール側で工夫する必要があります。
Evans では起動時に非同期でアップデートがあるかをチェックし、見つかった場合はその情報をキャッシュし、次回起動時にプロンプトでアップデートをするかをチェックしています。

まとめ

ktr0731/evans では、ktr0731/go-semver によるセマンティックバージョニングの導入、michellh/gox と tcnksm/ghr によるツールの高速なビルド・リリース、ktr0731/go-updater によるツールの自動アップデート機構の導入を行いました。
これらのツールやライブラリを利用することで、配布から CLI ツールのアップデートまでをほぼ自動的に行えるようになりました。
特に、自動アップデートはユーザの手間がほとんどかからないため、非常に便利です。