『データ指向プログラミング』を読んだ

2023年10月29日 engineering

こんにちは、 @kz_morita です。

今回は, 『データ指向プログラミング』 という本を読んだのでその感想などを書いていきます.

本について

データ指向プログラミング
【目次】
Part1 柔軟性
  第1章 オブジェクト指向プログラミングの複雑さ― 気まぐれな起業家
  第2章 コードとデータの分離― まったく新しい世界
  第3章 基本的なデータ操作― 瞑想とプログラミング
  第4章 状態管理― タイムトラベル
  第5章 基本的な並行性制御― 家庭内での対立
  第6章 単体テスト― コーヒーショップでプログラミング
Part2 スケーラビリティ
  第7章 基本的なデータ検証― おごそかな贈り物
  第8章 高度な並行性制御― さようなら、デッドロック
  第9章 永続的なデータ構造― 巨人の肩の上に立つ
  第10章 データベースの操作― 雲は雲
  第11章 Webサービス― 忠実な配達人
Part3 保守性
  第12章 高度なデータ検証― 手作りの贈り物
  第13章 ポリモーフィズム― 田舎で動物とたわむれる
  第14章 高度なデータ操作― 考え抜かれたものは明確に表現される
  第15章 デバッグ― 博物館でイノベーション
付録A データ指向プログラミングの原則
付録B 静的型付け言語での汎用的なデータアクセス
付録C データ指向プログラミング:プログラミングパラダイムの一環をなす
付録D Lodash リファレンス

内容と 4 つの原則について

本書は,DOP (Data Oriented Programming) について語られている本でした.

DOP の基本的な概念などは,この本の著者のブログでも書かれています.

4 つの原則として,以下が挙げられています.

  • コード (振る舞い) をデータから切り離す
  • データを汎用的なデータ構造で表す
  • データをイミュータブルとして扱う
  • データスキーマをデータ表現から切り離す

それぞれに焦点をあてて感想を書いていきます.

コード (振る舞い) をデータから切り離す

こちらはデータとふるまいを分けるという原則です. OOP ではデータをカプセル化して外部に公開せずに,振る舞いとセットでクラスとしてまとめるということを行うと思いますが,DOP ではデータを第一と考え,データとそれに対する振る舞いに分離します.

そのため,書籍では振る舞いを表す関数はすべて static メソッドで表すといった具合に書かれています.

書籍で書かれているようなコード例だと以下のような感じです.

OOP like

val person = new Person(lastName = "Tanaka", firstName = "Taro")
person.fullName() // => Tanaka Taro

DOP like

val personData = Map("lastName" -> "Tanaka", "firstName" -> "Taro")
fullName(personData) // => Tanaka Taro

DOP のメリットとしては,fullName 関数は,Person データじゃなくても,firstName と lastName を持つデータであれば再利用できるという点が挙げられていました.

また,OOP like なコードで本の著者 Author みたいなクラスを作成するとし,その Author の fullName を計算したい時処理が重複しますがその解決策としては,以下の2つになるかなと思います.

  • User のような抽象的なクラスを設計し,Author が継承する
  • フルネームを計算するようなコンポーネントを作成し,委譲する

最近の OOP だと,継承による共通化はあまり推奨されていない認識です.後者の委譲する方に関しては振る舞いがデータ (Person や Author) から切り離されていると捉えることができます.

そのため後者は,DOP の原則の 1 つめのエッセンスを兼ねているといえそうです.

余談ですが以下の YouTube 動画がデータと振る舞いを分離するという点について,データと振る舞いの分離をそれぞれの抽象度の階層がことなることで設計が破綻するという例をだしながら説明されていて,とても良かったです.

データを汎用的なデータ構造で表す

こちらは,データを Person クラスのように定義するのではなく,Map 型のような汎用的なデータ型で表すという原則です.

こちらはデータをモデリングする際に,それぞれ定義しないとコード上の表現力が下がる気がしてあまり理解できなかったのですが,原則 4 とセットだと理解が捗りました.(データの表現力,モデリングについては原則 4 で話します)

汎用的なデータ構造であらわすということを,スキーマオンリードとしてデータを扱うということだと捉えるとスッキリ理解できました.

スキーマオンリードとスキーマオンライトの概念は,データベースの文脈で語られることが多く,スキーマオンライト書き込むときにデータスキーマが規定されているもので RDB などはこちらにあたります.スキーマオンリードは読むときにデータスキーマが決まるようなもので NoSQL などの Document 型の DB などになります.

データ検証を,データを使うタイミング (Read するタイミング) で行うため,データ自体は汎用的な Map などのデータ構造で取り扱うというのがこちらの原則という認識です.

データをイミュータブルとして扱う

こちらの原則はデータをイミュータブルとして扱うという原則で,最近のプログラミングの流れとしては自然なものに見えます. たとえば,Rust が変数をデフォルトで immutable なものとし,変更したいときに mut キーワードを付けるような設計などが例となります.

データが immutable なので,データは基本的に複製されるような挙動になります.

ここで心配なのが,データのコピーが非効率的じゃないかという点になります.

本書では,永続データ構造を用いてそのコピーを最小限にするように提案されています.

永続データ構造とは,データが immutable なので.データをシェアできるという考え方です.

データが更新されたときに,不変な共通データはシェアし

以下のような例がわかりやすいかも知れません.

// データを定義
val data = { "user": {"firstName": "Taro", "lastName": "Tanaka" }, "books":{ /* ... */ } }
// 内部状態例
// { "user#1": { "firstName": "Taro", "lastName": "Tanaka" }}
// { "books#1": [ /* ... */ ] }

// user を取得
// user#1 の参照を得る
val user = data.user


// user の firstName を "Bob" に更新する疑似コード
val data2 = update(data, "user.firstName", "Bob")
// 内部状態例 (books は変更してないので同じデータをシェア)
// { "user#2": { "fistname": "Bob". "lastName": "Tanaka" } }
// { "books#1": [ /* ... */] }


// 以下は user#1 を参照しているので普遍
user.fistName // => Taro
data.user.firstName // => Taro

// user#2 を参照しているので新しいデータ
data2.user.firstName // => Bob

永続データ構造は,各言語によってライブラリなどが提供されていることがあるため基本的にはそちらを使うことになりそうです.

JavaScript でいうと Immutable.js など.

データスキーマをデータ表現から切り離す

こちらは,実データとデータのスキーマを切り離すという原則です.

具体的に JSONSchema のようなものを利用してデータモデリングはそちらで行うように提案されていました.

原則 2 の際にデータの表現力が落ちるのではという話がありましたが,モデリングは JSONSchema のようなもので行うというものでした. データを使用する際に,データがスキーマに沿っているかというバリデーションも JSONSchema を使用するといった内容が書かれています.

たとえば API サーバーの開発をしているとして,リクエストされた JSON が仕様を満たしているかどうかというチェックをするというユースケースを考えます.

既存の静的型付け言語では JSON => データ型 へのシリアライズに失敗することによってスキーマのバリデーションを行うと思いますが,DOP では JSONShema のバリデーションを記述してバリデーションするようなイメージです.

DOP ではバリデーションを実施するのがプログラマーの責任に委ねられることになります.型による自動的なチェックの恩恵は受けられないのが,データを汎用的なデータ型で扱いスキーマを切り離す際のデメリットです.

DOP とはなんなのか?

書籍を読み終え DOP について考えたときな率直な感想としては,Unix コマンドのような設計スタイルかなと思いました.

データを汎用データ型で扱い関数によってデータを変換していくさまは,文字列とファイルという共通インターフェースを持ち振る舞いをパイプでつなげていく Unix コマンドのように感じました.

小さく,シンプルな関数をつなげてデータを加工していくようなコーディングスタイルが DOP には合うのかなと思ってます.

また,スキーマオンリードというデータベースの概念を取り入れているようにも感じました.プログラムがデータ型を強く規定するのではなく,データがまずありそれに対してバリデーションと振る舞いを attach していくさまは NoSQL データベースを扱っているときのような思考パターンだな感じます.

そのため,動的型を持つ言語のほうが扱いやすいケースが多いかもしれません. 静的型付け言語でデータを汎用的なデータ構造で表すということは,Map<String, Any> のような型でデータを表現するということで,それはつまり取得時にキャストが必要になるということになります.

とはいえ,静的型付け言語の JSON parse ライブラリでよくあるような,getAsString のようなインターフェースを用意することで静的型付け言語でも DOP は可能であると記述はありました.

さいごに

今回は,DOP (Data Oriented Programming) についての書籍を読みました.

最近自分が書いていたプログラミングパラダイムとは異なる考え方でとても新鮮でした.まだコードを実際にかけていないため DOP の原則に従って実際のコードを書いてみてどのような感じなのか試してみたいと思います.

この記事をシェア