生産性から考えるコード1行の価値

佐藤太一氏:みなさんこんばんは。「生産性の壁を突破しろ! コード自動生成」ということでお話しします。

まずは自己紹介から。電通国際情報サービス(ISID)の佐藤太一です。長いのでISIDの太一と呼んでください。私はふだん、GitHubやJiraといった、現代的な構成管理ツールの利用推進や、部門横断的な技術支援、会社の制度改善などを仕事にしています。

本題に入る前に、まずは軽く生産性の話をしておきましょう。物的生産性は計測しやすいので、よく生産性の議論をする上で利用されます。ソフトウェアの業界における生産量として計測しやすいのは、コードの行数です。各コードの1行あたりの価値が均質だとみなせるなら、非常に便利です。

しかし、実際にはコードの1行あたりの価値はまったく均質ではありません。コードの自動生成ツールを例にとって説明してみましょう。

今のプロジェクトでは、ほぼ私1人で書いた7,000行程度のツールから、現時点で18万行程度のコードを自動生成しています。このツールで、最終的には25万行以上のコードを自動生成する予定です。つまり、現時点でも25倍程度のレバレッジがあり、最終的には35倍以上のレバレッジを見込んでいるわけです。

単にコードの生産量だけを見るタイプの生産性については、ソースコードの自動生成技術があれば、その議論をほぼ無意味なものにできます。もし自動生成の対象になり得る領域をうまく見つけられるなら、桁違いの生産性を達成できるでしょう。

自動生成の対象になるものの特徴

では、どんなものが自動生成の対象になるのでしょうか? それらには、いくつかの特徴があります。

まず、仕様の中に条件分岐が少なく、なんらかの表形式でデータを作りさえすればコードに落とし込めるもの。さらに、そこで使われるデータ構造やオブジェクトが、繰り返し同じ構造が現れるならよりよいでしょう。また、同じ処理構造が安定的に繰り返されるものは、自動生成しやすいです。

自動生成ツールを作ること自体も高度な生産活動ですし、直接的にアプリケーションを作るよりは難しいコードを書く必要があるので、少なくとも10倍程度のレバレッジが効く領域で自動生成したいですね。

とはいえ、採用しているプログラミング言語次第では、ある種のメタプログラミングを実施したり、ジェネリクスのような機能を使うことで、アプリケーションランタイムやコンパイラに自動生成を任せられることもあります。ファイルを出力するだけが、コードの自動生成ではありません。

本日の話題はGoにおける自動生成なので、込み入った言語機能は、現時点では実装されていません。ただし、ジェネリクスについては実装が進んでおり、その内容次第では、コードの自動生成で対象となる領域は狭くなるでしょう。

では、具体的にどんなものが自動生成の対象になるのでしょうか? ここでは説明しやすくするために、ドメインドリブンデザインの用語で説明します。

ValueObjectは、言語組み込みの型を使わずにドメイン特化の型を定義するために使うものです。私たちの実装では、メンバ変数は基本的に公開せず、Factory関数やEqualsメソッドを自動生成しています。

EntityはValueObjectとよく似ていますが、メンバ変数としてValueObjectが列挙されるところが違います。私たちの実装では、EntityにFactory関数は定義せず、構造体のメンバはすべて公開しています。

ここでいうRepositoryは、データベースアクセスするコードをカプセル化するためのものです。INSERTやPRIMARY KEYによるSELECT、DELETEはいつも同じコードになるので、自動生成するのが望ましいと考えています。

GORMのようなSQLを積極的に隠蔽するタイプのORM(Object-Relational Mapping)は、パフォーマンス障害の原因になりやすいので、私たちは利用しません。SQLはRDB(Relational Database)にアクセスするためのDSL(domain-specific language)として完成しているので、それを隠蔽したら問題は抽象化されないからです。

自動生成ツールで利用する入力ファイル

どんなものを自動生成の対象にするか、具体的にイメージしてもらえる状態になったところで、ここからは自動生成ツールをどのように設計するのかを説明していきます。

設計に先立って、自動生成ツールで利用する入力ファイルを紹介しましょう。(スライドを示して)今見せているのは、ドメイン用語辞書です。このファイルを使ってプロジェクト内で利用される用語を一元管理しています。

このファイルでは、アプリケーションコードにおける論理的な名称と実装型の名前、そして、内包する組み込み型の名前、データベースに永続化する際の型やバリデーションが1つのファイルで一覧できます。

今見せているのが、テーブル定義書です。これも、ごくごく単純な表になっています。少しこのフォーマットについて説明します。

論理名は、テーブル定義について議論する際に使う和名です。物理名は、DDL(Data Definition Language)を作成する際にカラム名として使います。そして、論理データ型に書かれた名前は、ドメイン用語辞書を参照しています。つまり、データベースの型定義はここにはありません。

複数のテーブルで論理的に同じ型だと見なせるカラムがあれば、それはすべて同じになることが保証できます。私たちが作るようなシステムでは、それほどカツカツに正規化してテーブル設計しないので、同じ論理データ型が複数のテーブルに表れることはよくあります。

(スライドを示して)ここまでで見せた2つのファイルから生成するコードの対応関係は、このようになっています。私たちがドメイン用語辞書から生成しているのは、ValueObjectです。

そして、ドメイン用語辞書を参照しながらテーブル定義から生成しているのが、RepositoryとEntity。また、それらのデータソースとなるテーブルのDDL、つまりCREATE TABLE文です。

本日は見せられませんが、私たちのプロジェクトでは、顧客の業務に強くひもづいていながら、ドメイン用語辞書を参照する設計ドキュメントはほかにもいくつかあり、それらからもコードの自動生成をしています。

自動生成ツールの設計

入力ファイルを見せたので、ツールの設計について説明しましょう。まずコマンドです。今回作っているのは、Gitのようなサブコマンド方式のCUIアプリケーションで、1つのバイナリファイルからさまざまなファイルを出力できます。

コマンドはユーザー入力からモデルを構築する処理を呼び出し、得られた構造体をテンプレートに渡す役割を持ちます。コマンドを設計する際は、デフォルトの動作をDry runモードにします。Dry runモードとは、入力されたファイルを読み取ってモデルを構築するものの、テンプレートの呼び出しは行わないモードです。

例えば、オプションとして「--execute」を付けた時だけファイル出力を行うようにします。Dry runモードを付けておくと、CIサーバー上で設計ドキュメントをバリデーションできるようになったりします。標準出力や標準エラーを使ったログ出力のセットアップや、設定ファイルの読み込みもコマンドの役割です。

今回は、サブコマンド方式のCUIアプリケーションを難なく実装できるライブラリとしてCobraを採用しています。

モデルの実装で、普通の構造体に対して、しっかりと型の付いたメンバーを定義します。テンプレートで使うような型名や変数名を作るには、モデルにひもづく関数として定義すると便利です。

Excelから呼んだデータ構造をそのままテンプレートに渡すのではなく、いったんモデルに変換するのがコツです。これによって入力ファイルが持つデータ構造と、テンプレートを切り離せます。

例えば、セルの順序を入れ替えたくなったり、途中で新しい項目を足したくなることはいくらでもあります。最初に作った入力フォーマットが完璧だということはありません。こういうものを何度も作っている私でもそうです。

モデルを作り込む理由としてもう1つ伝えておきたいのは、「モデルのバリデーションはしっかりと作り込みましょう」ということです。

例えば、入力必須項目のチェックやテーブル定義におけるカラム名の重複、ドメイン用語辞書に登録されていない用語の利用などは、バリデーションで検知します。Excelファイルを読み込むためのライブラリとしては、Excelizeを利用しています。

テンプレートは、モデルを受け取ってファイルを出力する部分です。テンプレートの実装方式にはさまざまなものがありますが、今回は内部DSL方式のライブラリであるJenniferを採用しました。

内部DSL方式では、ソースコードの自動生成をやりやすいかたちで再定義したプログラミング言語の構文を使います。

DSL型のAPIでは、コードを書いてコンパイルが通った時点で、生成されるコードのコンパイルが通ることがある程度保証されています。つまり、ある程度の大きなコードを一気に生成しても、生成物の状態が予測可能なのです。

(次回に続く)