Web + DB vol.132
オブジェクト指向神話からの脱却
オブジェクト指向を見つめ直す
- オブジェクト指向とは
- オブジェクト指向 as プログラミング技術/言語
- 具体的なプログラミングの実装方法
- クラス、インスタンス、メソッド、プロパティ、継承、多態など
- C++のものが代表的
- 抽象データ型のオブジェクト指向とも呼ばれる
- この Doc で扱うのはコレ
- 具体的なプログラミングの実装方法
- オブジェクト指向 as ソフトウエア技術
- システムの設計や構造のための哲学や考え方
- リアルワールドの問題をモデリングするためのフレームワーク
- DDD とか?
- オブジェクト指向 as プログラミング技術/言語
- ふわっとしたオブジェクト指向は忘れましょう
- 👎 ソフトウェアをうまく作る工夫をすべてオブジェクト指向と捉えている
- それはオブジェクト指向というよりも、ソフトウェア工学の話
- 👎
data.method()
という形式がオブジェクト指向だと思っている- これはオブジェクト指向というよりも、単なる構文の話
- 👎 ソフトウェアをうまく作る工夫をすべてオブジェクト指向と捉えている
オブジェクト指向機能の基本文法
オブジェクト指向の最も重要なポイント
- 抽象データ型による情報隠蔽とカプセル化
- ポリモーフィズムによるコードの柔軟な共通化
- 名前空間によるモジュール機能 (これは本質ではない)
クラスの基本
- 抽象データ型 / ADT / Abstract Data Type
- データとデータに関して行える操作を規定してまとめたもの
- 利用者はデータの詳細に依存したり気にかけたりする必要がなく、抽象的な操作だけを知っておけばよい、という状態を実現するためのもの
- 情報隠蔽・カプセル化を実現するための手法
- リスコフが考案したもの
- ✂️✂️✂️ 以下は ADT を実現するための機能 ✂️✂️✂️
- メンバ変数
- 言語によってプロパティ、フィールドなどとも呼ばれる
- メソッド
- 値に対して行える操作
- クラスと構造体の違いは、クラスはメソッドを持つことである
- 名前空間
- 変数や関数の名前を独立して管理する機能
- クラスは名前空間を作る
- モジュール
- 関連性の高い要素でのグルーピングを行う機能
- クラスは名前空間の仕組みにより、モジュールの性質を持つことができる
- インスタンス化
- クラスをひな形として使い、オブジェクト/インスタンスを作ること
- インスタンス変数と static 変数
- インスタンス変数はインスタンス化した時に初めて割り当てられる
- static 変数はクラスに定義した時点で変数の領域が割り当てられ利用可能になる
- アクセス指定子
- public / private / protected
- データは private でメソッドは public にするのがよくあるパターン
- protected は派生クラスからのみアクセス可能にするものだが、あまり意味がないので最近の言語では不採用になっている
- オブジェクトと呼ぶかインスタンスと呼ぶか
- インスタンスと呼ぶのは:
- 実体であることを強調したい時
- メモリ領域を割り当てることを強調したい時
- オブジェクトと呼ぶのは:
- クラスとインスタンスをあわせた話をしたい時
- インスタンスと呼ぶのは:
- コンストラクタ
- カプセル化を行って外部にデータを公開しないようにすると、値の初期設定を行うための特別な手法が必要となる
- その手法がコンストラクタ
- クラスはオブジェクトか?
- Java
- オブジェクトではない
- メタクラスはある
- 「メタ Hoge」は「Hoge の Hoge」という意味
- Ruby
- オブジェクトである
- JavaScript
- クラス自体がない
- 元ネタ(prototype)をコピーして別のオブジェクトを作っているだけなので、すべてがオブジェクトである
- プロトタイプベースのオブジェクト指向と呼ばれている
- Java
継承
- 継承とは
- あるクラスの機能を引き継いだ別のクラスを定義する仕組み
- 基底クラス/スーパークラスを拡張し、派生クラス/サブクラスを作る
- スーパークラスという名前はおかしいという説もある
- 「スーパー」と言いながら子の要素をすべてんでいないので
- 構造体の拡張
- 基底クラスは上位型であり、派生クラスはその部分型である
- 部分型 is a 上位型
- 「その一部分が上位型である」と考えると覚えやすいかも?
- リスコフの置換原則 / 「上位型が求められる時に代わりに部分型を渡しても問題ない」という原則
- オーバーライド
- 派生クラスにおいて基底クラスのメソッドを上書きすること
- 派生クラスから基底クラスを呼ぶこともできるが分かりづらいからやめるべき
super.open()
など- オーバーライドしてほしいところだけを別関数に切り分けるとよい
- テンプレートメソッドパターンという。ただし今となってはオワコン。
// 基底クラスにおいて
void close() {
if(!opened) error;
closeImpl();
opened = false;
}
void closeImpl() {}
// 複数の派生クラスにおいて
@Override
void closeImpl() {
// 特徴のある処理がここに入る
}
- 継承による多態 / ポリモーフィズム
- コンパイル時の型ではなく、実行時に扱っているオブジェクトの型によって呼び出されるメソッドが変わること
- 単なる関数定義では不可能だったコードの共通化が行えるようになるのが 最大の特徴
- e.g. 前述の
close()
メソッドは、多態がないと実現できない- 単なる関数定義(
super.close()
を使ったコード)では、closeImpl
の前後のコードを共通化できないですよね
- 単なる関数定義(
- e.g. 前述の
- 抽象クラス
- 抽象メソッド / 実装を省略してオーバーライドを強制できる仕組み
- 抽象クラス / インスタンス化できないクラス / 抽象メソッドを持つクラス
- 具象クラス / インスタンス化できるクラス / 抽象メソッドを持たないクラス
- e.g.
- 哺乳類クラス(abstract class)
- 犬クラス(concrete class)
- 猫クラス(concrete class)
- これはあくまで抽象クラスの説明にすぎないので注意。実際に動物を分類したいときは単に「分類」や「種別」のフィールドを作るほうが適切。
- 哺乳類クラス(abstract class)
オブジェクト指向の周辺技術
オブジェクト指向に関する技術とその趨勢
詳細実装ではなくドメインに注目する
- 色々な方法論とその具体的手法(クラス図、状態遷移図、UML など)があったが、いずれも以下の問題を抱えていた
- 実装に近すぎる
- 設計段階を独立して進められない
- ⭐️ 実装ではなくユースケースに注目すべき
- データ構造に注目している
- ⭐️ まずはドメインレベルでのデータの流れを考えるべき
- すべてを同質なオブジェクトとして扱っている
- システム上のすべてを「データと振る舞いが一体になったオブジェクトの集まり」として組むことは非合理的
- ⭐️ 永続データとデータ処理手続きは分けて扱うべき
- 実装に近すぎる
- その反省から、ドメインに注目が集まった
- コードを書く前にまとめておくべきことは、システムの具体的な構造ではなく、むしろシステムの外側の話
- 用語
- ドメイン / システムが使われる問題領域
- 解決領域 / ドメインの問題を解決するための手段や方法、またはアプローチを定義する領域
- 代表的なのは Domain Driven Development / DDD
- 前述の ⭐️ を実現するための手法
分散オブジェクトからマイクロサービスへ
- 分散オブジェクトとは
- 物理的に異なる位置に存在するが、ネットワーク経由で通信することにより、あたかも同 じメモリ空間に存在するかのように扱えるオブジェクト
- 平たく言うと Remote Procedure Call (RPC)
- 代表的な RPC の仕組み
- CORBA
- ORB, IIOP, IDL / 1990 年 / 超複雑
- RPC 以外の要素を多く含んでいたため、壮大過ぎて離陸できず。
- SOAP
- XML / 1998 年 / 複雑
- REST
- JSON / 2006 年 / シンプル
- gRPC や Thrift
- 独自のシリアライズの仕組みを持つ
- 共通インターフェース定義からクライアント・サーバのコードを生成できる
- CORBA と似た仕組みだが RPC に専念しているので使いやすい
- マイクロサービスで使われる
- CORBA
オブジェクト指向データベースから NoSQL へ
- オブジェクト指向データベースとは
- オブジェクトをそのまま保存できる DB だが、流行らなかった
- O/R マッパ
- オブジェクトをあたかもそのまま DB に保存できるように見せかける仕組み
- RDB とオブジェクトの間の差(インピーダンスミスマッチ)の解消のための技術
- NoSQL データベース
- オブジェクトを JSON にして保存できる
オブジェクト指向 機能の現在の使い方
オブジェクトの分類
- ソフトウェアには部分によって性質の違いがあり、オブジェクトですべてが解消される訳では無い
- エンティティ
- システムで扱うデータを保持するオブジェクト
- 抽象データ型として扱う
- データの分類のために継承や部分型を利用する
- コントロール
- ビジネスロジックを記述するオブジェクト
- 状態を持たない
- シングルトンである
- レイヤーアーキテクチャを構成する
- 無限ループを防ぐために、呼び出しは層の深い方への一方通行にする
- 昨今のアプリではコントロールを中心として星型になることが多い
- バウンダリ
- UI や通信のインターフェースを記述するオブジェクト
- 入力を受け取ったり結果を出力する
- 状態を持つステートマシンである
- 入力状態、接続状態、出力状態など
- ステートマシン / State が Event によって Transition していく Machine / ≒ redux 的なもの
- 接続先等によって異なってくる処理を抽象化するために継承を使う
- オブジェクト指向的な考え方 を最も適用しやすいオブジェクトではあるが、通常はライブラリやフレームワークがその責任を負うので、自前で書くことは少ない
- エンティティ
モジュールと型の分離
- クラスをモジュールとして扱う場合は、型の機能は使われないことが多い
- e.g. DI コンテナ
- 関数を集約した名前空間としてのみ利用されている
- データのひな形としての機能は全く使われていない
- e.g. DI コンテナ
構造体の拡張と部分型の分離
- 継承とは以下の 2 つを同時に行う仕組み
- 構造体の拡張
- メンバ変数やメソッドを追加する
- 部分型の定義
- 上位型の代わりに部分型を使えるようにすること (リスコフの置換原則)
- 構造体の拡張
- 継承は菱形継承の問題があり使うべきでないとされている
- これを回避するために Protocol / Interface / Trait といったものが生まれた
- クラスには単一継承を許し、インターフェース等には多重継承を許すことにした
- いまは継承に変わる方法がある
- 構造体の拡張
- Composition で行う
- 部分型の定義
- Interface などで行う
- TypeScript は Structural Typing なのでデータの名前構造さえ同じなら同じものとして扱える
- Golang なら振る舞い(Interface)が共通なら同じものとして扱える
- Rust ではトレイトにより同じものとして扱える
- Enum を使って構造体をグループ化した型を作成できたりもする
- Interface などで行う
- 構造体の拡張
オブジェクト指向言語の変化
デザインパターン
- GoF パターンなどは当時のワークアラウンド的なものも多く、既にオワコンとなっているものもある。たとえば:
- テンプレートメソッドパターン
- 前述の
closeImpl()
のような書き方のこと - 現代では関数を値として扱えるので、関数を一つ引数として渡すだけで済む。格段にシンプル。
- 前述の
- シングルトンパターン
- 遅延初期化や DI コンテナなど、言語機能でよりシンプルに解決できるようになっている
継承によらない部分型
- 公称型 / Nominal Type
- 継承関係を明示する必要がある
- e.g. Java のクラス
- 構造的部分型 / Structural Subtyping
- 型の構造さえ一致していれば OK
- e.g. TypeScript クラス
- 継承関係を明示できるので公称型に見えるけど、実際の判定は構造により行われている
オブジェクト指向と関数の融合
- オブジェクト思考と関数を組み合わせて使える
- 以下の例では、
Object::toString
のメソッド参照が、オブジェクト指向の機能を使っている部分 - 変数に関数を代入して利用しているのが、関数型の機能を使っている部分
- 以下の例では、
Function<Object, String> toStr = Object::toString;
toStr.apply(123) // 123
toStr.apply(new Object()) // 'java.lang.Object@1b6d3586'
Object o = 123
toStr.apply(o) // 123 (=> 与えられた変数の型ではなく、変数に割り当てらた値の型で処理が変わっている)
- 似たような機能にメソッドのオーバーロードがあるが以下の点で異なる
- オーバーロードではコンパイル時に処理が確定する
- 前述の例では実行時に処理が確定する
オブジェクト指向言語という分類は適切か
- パラダイム / オブジェクト指向言語・関数型言語・手続き型言語という分類のこと
- 現代の多様な機能が載っ た言語の分類としてはもはや使えない
まとめ
- オブジェクト指向の古い定義は今となってはそのまま使えないし、かと言って共通の指針となり得るような新しい定義もない
- 言語横断的に共通なプログラミングの指針はもはや成り立たないから、以下に集中する方が良い
- オブジェクト指向や言語のパラダイムにはこだわらず、利用する言語の言語機能をいかに活用するか
- ソフトウェア開発の様々な特性をそれぞれどう解決するか
コンテナ化実践ガイド
コンテナ化の必要性の判断
- モノリスにありがちな問題
- 開発速度の低下
- ドメイン間の相互依存の増加により発生する
- トイルの増加
- トイルとは
- プロダクションサービスを動作させることに関連する作業
- 自動化可能でそれ自体に価値がない作業
- 作業量がサービス成長と比例する作業
- サーバ の増加・多様化により発生する
- トイルとは
- 開発速度の低下
- コンテナ技術のメリット
- イメージ作成が容易
- Immutable Infrastructure によるサーバ多様化の防止
- コンテナオーケストレーションのメリット
- 迅速なリソース割り当て
- 自動化機能
- ヘルスチェックと自動復旧
- スケールイン・アウト
- ローリングアップデート
- デメリット
- 性能・コストへの影響
- デプロイ・運用の見直しが手間
- マイクロサービス
- ドメインで DB を区切り、相互の連絡は API を介して行うこと
- (ドメイン相互依存を減らしたいだけならモノリスでも十分可能な気はする。本質はそれ以外のところにありそう。性能とか独自デプロイとか)
- コンテナ化が必要なとき
- (よくわかんなかった。マイクロサービスとコンテナ化は切り離して考えた方が良いのでは。)
(2 章以降は必要になったら読む)
テストダブル
- テストダブルとは
- 自動テストに使用する偽物・代用品のこと
- ダブル = 影武者、身代わりの意
- Stub, Spy, Mock, Fake などの種類がある
- 忠実性を下げる代わりに、テストの速度や決定性を向上させる技術
- 忠実性 / Fidelity / 本物を模した度合い
- 決定性 / Determinism / ある出来事が、その出来事に先行する 出来事のみによって決定する性質
- 決定性が高いテストとは
- テスト対象コードの実装によってのみテストの結果が左右される
- 外部のランダムな要因によってテストの結果が変わらない
- 決定性が高いテストとは
- 自動テストに使用する偽物・代用品のこと
- メリット
- テストしにくいものをテスト可能にする
- テストの速度と決定性を向上させる
- デメリット
- テストが脆くなり、変更を妨げる
- もしそもそもの設計がダメなのなら、テストダブルでごまかすな
- テストの偽陰性をまねく
- 偽陰性 / 失敗してほしいテストが失敗してくれないこと
- テストダブルが本物と同じ動きをする保証はない
- モックドリフト / テストダブルの実装が本物とズレてしまった状態
- 偽陰性 / 失敗してほしいテストが失敗してくれないこと
- テストが脆くなり、変更を妨げる
- オススメの手法
- テストダブルは、決定性の向上のために使う。単なる速度向上のためには使わない。
現場でコードを書き続ける
- 勉強だけしてると痛い人になりがち
- 覚えたテクニックを全部使いたがったり、とにかく流行りの技術を使いたがって、結果的に現場を混乱させるとか
- 現場の課題を解決する経験を積むべし
- ピアノはピアノを弾くことでしかうまくならないし、サッカーもサッカーをすることでしかうまくならない
- オープンソースへの貢献もいい
sync/atomic による変数操作
- Golang の排他制御は通常
sync.(RW)Mutex
により行う- コードの一定の範囲をデータ競合から守る
- これとは別に
sync/atomic
という手法もある- 効率が良い一方で、とても分かりにくいバグを生む可能性もある両刃の剣
- 変数が単体であり、値の操作が 1 つで済む場合に使うと良い、かも
常に論理的に正しくあれ
- 論理的な意思決定は重要
- コードがシンプルになるから
- 論理的なコードなら大量のコメントや補助資料は不要
- 読み手の期待を裏切らない
- 論理的な根拠群に基づいた決定であれば、意思決定に自身が持てるから
- コードがシンプルになるから
- 自分の頭で論理的に考えて道を切り開け
- 既存の情報を参考にしているだけでは先行者利益は一生得られない
- 漫然と既存技術の星取表を作ってるだけじゃ世話ないぜ
構造化ログ
- 問題発生時には以下のような情報が必要
- 処理が正常に開始・終了しているか
- エラーの内容
- エラーが発生した:
- 時間
- ユーザー
- ジョブ
- 場所
- 構造化ログにより上記を取得できるようにしておく
- 構造化ログ / キーと値のペアで記録されたログ
- 多くの場合、構造化ログ用のライブラリが用意されているので調べてみるとよい
- バッファされていない標準出力にログを記録し、他のツールに処理を任せるのがシンプルで強力なアプローチである
- by The Twelve-Factor App
- 他のツールとは例えば Amazon CloudWatch Logs
- Amazon CloudWatch Logs では:
- JSON 出力された値を自動検出して検索可能にしてくれる、やばい
- もし構造化されていなくても glob や正規表現での検索が可能