GraphQL と Data Transfer Object は似ている
技術エッセイです。
EoPAA の Data Transfer Object
「技術はレイヤーだ。今の技術は、過去の技術の積み重ねの上にある」という koriym さん(PHP フレームワーク BEAR.Sunday の作者で、前職の技術顧問)の言葉を今も覚えています。
この2ヶ月間、業務でサーバーサイドエンジニアとペア・モブプログラミングをしながら PHP を書いているため、ちゃんとオブジェクト指向プログラミング(OOP)と向き合おうと「エンタープライズアプリケーションアーキテクチャパターン(通称 PoEAA)」を読んでいます。
PoEAA の邦訳の出版は2005年です。記述が古くなっているところももちろんありますが、それでも今でも開発の現場で使われている OOP の語彙を理解、説明するために有用な記述もたくさんあります。
EoPAA を通して、自分が過去に目にしてもその内容を理解しきれていなかった用語の内容を把握するため、また人に意図を説明するときに適切な語彙を選択できるようにするため総ざらいのような気持ちで読んでいます。
さて、このような古典に対する自分の読み方はまた別の記事に譲るとして、今回は一見関係のないような事柄に対する面白い共通事項を発見したので、それを書き留めて起きます。
それは Data Transfer Object と RESTful ではない(いわゆる Restishな) API のレスポンス、そして GraphQL についてです。
Data Transfer Object
Data Transfer Object は、メモリとは異なり、ネットワークや DB を経由するため取得にコストがかかる値たちをまとめるオブジェクトです。EoPAA では分散パターンの章で紹介されています。
(なお、原文にはネットワークへの言及しかないですが、Google 検索してみると主に DB からの値の取得に使うのだという記述が散見されたので、両方書いてます。)
DTO には getter と setter しかなく、ドメインロジックは記述しません。プロパティにはプリミティブな値もしくは他の DTO を保持すると書籍には書かれています。
雑誌を例に挙げると、DB の Magazine テーブル、Article テーブル、Author テーブルのそれぞれから一度ずつデータを読み込むより、一度読み込んだデータを DTO に保持しておく方が良いというところでしょうか。DB のデータにアクセスするオブジェクトを Data Access Object(DAO)として疑似コードを書いてみます。
class Publisher {
pub func main() {
magazine = this.getMagazine(1)
article = this.getArticle(2)
author = this.getAuthor(3)
// ...
}
pub func sub() {
magazine = this.getMagazine(1)
article = this.getArticle(2)
author = this.getAuthor(3)
// ...
}
priv func getMagazine(id) {
return dao.magazine.find(id)
}
priv func getArticle(id) {
return dao.article.find(id)
}
priv func getAuthor(id) {
return dao.author.find(id)
}
}
このようなコードでは、Magazine、 Article、Author の値を求めるたびに、DB へのアクセス数が嵩みます。そこで、DAO に Magazine、Article、Author をまとめてしまうと DB へのアクセス効率が良くなります。
RDB であれば、それぞれのテーブルに対して Select 文を合計3回発行していたところ、リレーションを使ってテーブルを join すれば getMagazineDto 内のクエリ実行は一度で済みます。
class Publisher {
pub func main() {
magazineDto = dao.getMagazineDto(1, 2, 3)
magazineTitle = magazineDto.magazineTitle
articleTitle = magazineDto.articleTitle
authorName = magazineDto.authorName
// ...
}
pub func sub() {
magazineDto = dao.getMagazineDto(1, 2, 3)
magazineTitle = magazineDto.magazineTitle
// ...
}
}
本書では DTO はシリアライズ可能であるとされているので、以下のようなオブジェクトになっていると想像できます。
class MagazineDto {
public magazineId: number
public magazineTitle: string
public articleId: number
public articleTitle: string
public authorId: number
public authorName: string
}
DTO 自体は昔からあるパターンです。しかし、これが RESTful ではない API のレスポンスの形に似ていると思いました。
RESTful ではない API レスポンス
ここでいう「RESTful ではない API レスポンス」とは、API を境界として、フロントエンドエンジニアとバックエンドエンジニアが分業をして開発する現場でよくある、フロントエンドの画面が欲しいデータが全部入りで入っている API レスポンスのことです。
RESTful API では、エンドポイントをリソース単位で用意します。この考え方では、v1、v2で区切ってバージョニングするなんてもっての他です。上記の雑誌の例を引き続き使うと、雑誌のデータは /api/magazines/:id から、記事は /api/articles/:id から、著者は /api/authors/:id から、GET メソッドの HTTP リクエストを使って取得します(なお、便宜的に /api をパスにつけていますが、これは RESTful API の考え方とは無関係で、わかりやすくしているだけです)。
しかし、雑誌のレスポンスに含まれている記事 ID を取得して記事のエンドポイントにリクエストを投げ、それを著者でも繰り返すと API リクエストの数は増えてしまいます。これは Round Trip(原義は往復旅行)と呼ばれています [^1]。
そこで「現場の」エンジニアが考え出した解決策は、画面が欲しいデータをレスポンスに可能な限り突っ込むことです。これはよくみられます。特に、元々 MVC のフレームワークを使っていて、Controller から View(テンプレートエンジン)に値を返していたのを、API で JSON を返すように変えたというケースでは当たり前にあることではないでしょうか。
イメージとしては以下のような JSON です。/api/magazine/1 にリクエストを送ったと仮定すると、レスポンスは以下のようになるでしょう。
{
"id": 1,
"title": "雑誌のタイトル",
"articles": [
{
"id": 1,
"title": "記事タイトル1",
"author": "著者名1"
},
{
"id": 2,
"title": "記事タイトル2",
"author": "著者名2"
}
]
}
その良し悪しはここでは置くとして、これが Data Transfer Object に似ているなと思い、以下のようなツイートをしました。
パターンの再発見です。他の誰かも過去に発見していたでしょうし、別の誰かが未来に発見するでしょう。パターンとは現実から見出されるものだからです。
GraphQL のレスポンスにも似ている
この DTO と上記の JSON が似ているなと思い至ったところで、GraphQL にも似ているなと思いました。同僚も同様の趣旨のツイートをリプライでくれています。
そこで思い返したのが、「初めてのGraphQL」 という書籍にあった記述です。
関連記事: 「初めてのGraphQL」を読んでGraphQLの概要を学んだ
元々 GraphQL は「マシンパワーの弱いスマートフォンで、アプリの動作効率を上げるために通信を最適化する」という目的で考えられたとこの書籍で紹介されています。
通信を最適化するとは、画面の表示に必要十分なデータを API のレスポンスで取得するということです。多くても少なくてもいけません。データが含まれている場合はオーバーフェッチ、データが足りない場合はアンダーフェッチと呼ばれています。
記事のタイトルに立ち返ると、これは DTO の目的そのものです。DTO の定義は「メモリとは異なり、ネットワークや DB を経由するため取得にコストがかかる値たちをまとめるオブジェクト」でした。GraphQL から返ってくる JSON は以下のような見た目です。
{
"data": {
"magazine": {
"name": "雑誌タイトル",
"articles": [
{
"name": "記事タイトル1"
},
{
"name": "記事タイトル2"
}
]
}
}
}
DTO はオブジェクトであり、GraphQL のレスポンスは JSON ですが、考え方としては同じといって差し支えないと思います。実際、PoEAA の著者であるマーティン・ファウラーは 別のブログ記事で「DTOのようなものを使うとよいのは、プレゼンテーション層のモデルとドメインモデルとの間に大きなミスマッチがある場合です」と書いています。
異なるものの間に共通項を見出して抽象化する。これはパターンの力と言えるでしょう。
古典は温故知新に繋がる
いわゆる Connecting The Dots やアハ体験と呼ばれる、記憶の別の引き出しに放り込んでおいた知識が互いに繋がる発見はなかなか得難いものです。
まさか EoPAA という古典を読んでこのような思いつきに至るとは、書籍を手に取る前には想像もしていませんでした。まさに温故知新(古きを訪ねて新しきを知る)だなと思った「発見」でした。
追記
早速友人が「XMLへのシリアライズの言及が書籍内にある」と教えてくれました。
確かに該当箇所を読んでみると、「データ変換オブジェクトで直面する一般的な問題の一つは、シリアライズ形式をテキストまたはバイナリのどちらにするかと言うことである。テキストのシリアライズの場合、読み込みが容易なので何が通信されているかもすぐにわかる」と書いてあります。そして、この文と同じ段落に XML への言及がありました。
なお、書籍ではシリアライズという単語が直列化と訳されており、最初に読んだ時なにを指してるかわからず該当箇所を読み飛ばしていました(本記事を書く直前に直列化がシリアライズを指すとあるブログで知りました)。
XML を JSON と読み替えて考えると、やはりマーティン・ファウラーの慧眼もしかり、ソフトウェア界隈で今も昔も変わらないことはあるのだなあと感慨深いです。流行に振り回されず、このような基礎を追っていきたいものです。
[^1]: さらに、クライアント側では取得したレスポンスのデータをもとに、次に欲しい情報を取得するためのコードを記述しなければなりません。ただし、これは HATEOAS という設計思想を適用すればその煩雑さを避けられるそうです。ただ、自分が HATEOAS を理解しきれてないです。ハイパーメディアに詳しいサーバーサイドエンジニアで友人の Tsuchiya 君がこの辺りのことを指摘していました。
Happy Coding 🎉