コマンドの実装に使うCobraの使い方

佐藤太一氏:自動生成ツールにおける設計の方向性を説明したので、次はいよいよ実装の話をしましょう。まずは、コマンドの実装に使うCobraの使い方を説明します。(スライドを示して)Cobraで最低限意識してほしいのは、これらのAPIです。

根幹部分となる構造体は、Commandです。この構造体をAPIリファレンスで確認するとたくさんの公開メンバが定義されていますが、基本的にUse、Short、RunEだけ設定すればいいです。あとは必要になった時に調べましょう。

Cobraは広く使われているライブラリなので、普通に思いつくような機能は、だいたいなんでも実装されています。

Useにはコマンド名を設定します。Shortにはコマンドのヘルプを設定します。わかりやすい端的な説明を書きましょう。RunEには、このコマンドが実行されたら動作する関数を定義します。ある意味ここが本体だと言ってもいいでしょう。

第2引数には、解釈されなかったコマンドラインオプションがすべて入っています。コマンドにひもづく関数としては、Execute、AddCommand、PersistentFlagsを把握すれば十分です。

Execute関数は、すべての準備が終わったらmain関数から呼び出します。エラーが戻ってきたらとりあえずエラー出力をして、プロセスを異常終了しましょう。AddCommand関数は、コマンドを入れ子にするためのAPIです。子どもになるコマンドでは、Useメンバーにわかりやすい名前を設定しましょう。

PersistentFlags関数から設定したコマンドラインオプションは、実行するコマンドが何になるとも解釈されるオプションを設定するためのFlagSetが得られます。FlagSetは、メンバ変数については特に気にせず使いましょう。

ここで紹介するのは、ポインタを渡してコマンドラインオプションを設定するStringVarとBoolVarです。私の場合、StringVarとして設定ファイルのパスを受け取るように実装しています。

コマンドラインオプションをたくさん増やすぐらいなら、設定ファイルにその内容を永続化しましょう。そのほうが動作の再現性が高まります。

BoolVarとして設定するのは、「--execute」のような実行フラグです。例えば、これが付いている時は、処理結果としてファイルが出力されるようにしています。オプションの短縮名を合わせて登録できる、StringVarとBoolVarもあります。

コマンド群の基本的なパターン

Cobraを使って実装するコマンド群の基本的なパターンを説明しましょう。まず、パッケージレベルの変数として、configPathとexecuteを定義しています。これらは、PersistentFlagsとして読み込まれたものがセットされます。

main関数では、サブコマンド方式のCLIアプリケーションにおける根本部分となるコマンドを生成しています。これを便宜上、“ルートコマンド”と呼びましょう。ここでUseとShortだけを設定しているのは、このアプリケーションでサブコマンドなしの動作は想定していないからです。

その次の2行では、先ほど定義したconfigPathとexecuteのポインタをFlagSetの関数に渡すことで、コマンドラインオプションをparseした際に値が設定されるようにしています。

GenerateEntity関数は、そうした上でルートコマンドに対してAddCommand関数を呼び出して、サブコマンドを渡しています。このサブコマンドでは、RunEに関数をしっかりと設定しているので、Entityサブコマンドが実行されると、GenerateEntity関数が呼び出されます。GenerateEntity関数はどこか外側に定義されていて、すばらしい動作をするものとして想像してください。

main関数の最後は、ルートコマンドに対するExecute関数の呼び出しです。ここでのエラー処理は、コードを単純化するためにpanicにしてしまいました。適切に実装するならロギングライブラリを使ってエラー出力したほうがいいでしょう。

このように実装すると、最下部のようなかたちで実行できるコマンドラインアプリケーションになります。

最初のgenが実行バイナリです。2番目のentityは、サブコマンドで指定したUseと一致するように引数を渡しています。オプションの--executeが渡されたので、モジュール変数のexecuteにはtrueが設定されます。

Cobraが内部的に使っているフラグのparserがPOSIX/GNUスタイルで動作するので、オプションの先頭にハイフンが2つ付いています。

configオプションを渡していないので、デフォルト値である「./gen.json」がモジュール変数に設定されます。残りのコマンドラインオプションは、GenerateEntityの第2引数として渡されます。ここでは、path/to/inputfilesが渡されるということです。

モデルの実装に使うExcelize

次は、モデルの実装に使うExcelizeの説明と、具体的なモデルのコード例を説明します。自動生成ツールが入力ファイルを処理するためにExcelizeを使っていく上で覚えるAPIは、これだけです。逆に言うと、これ以外のAPIを使わないと読めないようなファイルフォーマットのExcelにはしないほうがいいでしょう。

OpenFileの第2引数はパスワードを設定するために使うので、基本的には不要です。GetRows関数は、シート名をパラメーターに渡すと、当該シートを全部読んでstringの配列に格納したものを返してくれます。

Excelizeで使うAPIは極めて簡単なので、ここでは配列から生成するモデルのコードを紹介します。全体として特別なことはなにもしていない構造体を、ここでは“モデル”と呼んでいます。テーブルのモデルに難しい部分はないと思いますが、ポイントはColumnsというメンバ変数で型付けしたColumnModelを持っていることです。

また、テンプレートで使う型の名前は、テーブルのモデルにひもづく関数として定義しています。ここではパッケージ名、エンティティ名、リポジトリ名、検索結果のカーソルを表すRowsの型名の生成処理を関数として定義しています。

カラムのモデルにおけるポイントは、LogicalTypeです。この構造体は、ドメイン用語辞書を読み取って生成するモデルです。これはColumnModelのメンバ変数として抱えることで、型情報を扱えるようにしているのです。

JenniferのAPI構造

最後は、テンプレートの実装に使うJenniferの実装例を説明します。Jenniferは、Code、Statement、Group、Fileの関係性さえ理解すれば、あとは想像したとおりのものがAPIとして定義されています。

まず、インターフェイスとしてCodeがあります。これはJenniferで最小限のコード断片を表すものです。例えば変数名1つとか、if文のif部分とか、そういうやつです。プライベートなrender関数から類推できるとおり、コード断片を文字列として出力できるなら、それはCodeです。

Statementは、そのCodeが複数集まったものです。JenniferでCodeを生成する際は、基本的にStatementの関数を呼び出します。Statement自体も、Codeのインターフェイスを満たしています。

Groupは、Statementと同様に、Codeが複数集まったものです。StatementとGroupの違いは、開始、終了のある・なしです。つまり、if文に付随するブロック、構造体の宣言、関数本体のように、開始記号と終了記号のあるものはGroupになります。

FileはGroupを内部に抱え込んでいるので、Groupに定義されている関数はすべて呼び出せます。Fileには、それに加えてSave関数によるファイル出力や、ImportAlias関数によるインポート宣言の管理機能が付いています。要は、ファイル出力とインポート宣言の便利機能を除けば、FileはGroupだとみなせます。

次は、コードを生成するための関数について説明しましょう。まずは右半分に並べた関数をざっと眺めてください。関数3つが1セットになっていますね。

Jenniferでは、基本的にfluentなAPIでコードを生成していくので、StatementやGroupにひもづいた関数を呼び出していくことが多いです。コードの開始点になる変数がないために、時々セットの最初で定義されているような独立した関数を呼ぶことになります。

ここでは使っていく上で、間違いなく頻出するであろうものを5セット選んでみました。特に意識して覚えておいてほしいのは、1番上のQual関数と、1番下のStructFunc関数です。これらの詳細については、ぜひ自身で調べてみてください。

Jenniferを使ったサンプルコード

JenniferのAPI構造がざっくりとわかったところで、サンプルコードを見ましょう。(スライドを示して)Jenniferを使ったコード例が左側で、そこから出力されるコードのイメージが右側です。

ProcessGenerate関数の中では、まずJenniferのNewFile関数でFile構造体を生成しています。この時に渡した名前が、パッケージ宣言で使われます。

次に、ImportAlias関数を使って、モジュールのパスにエイリアスを付けています。実はエイリアスは一切付けなくても、Jenniferはインポート文を作ってくれます。しかし、そうすると可読性の悪いコードができやすくなるので、私はできるだけエイリアスを付けるようにしています。

type関数の呼び出しから始まる行では構造体を宣言しています。構造体の名前は、Id関数の引数に渡した文字列が使われます。ここではTableModelのEntityType関数を呼び出して、構造体に名前を付与しています。その結果、“MyEntity”という名前が出力されています。

次は、StructFunc関数の呼び出しに際して、コールバック関数を渡しています。そうしてTableModelのメンバとして読み込んでおいたColumnModelをループしながら、その情報を使って構造体のメンバを定義しています。

このような構造にすることで、Jenniferで書いたソースコード側と、実際に出力されるソースコードの処理構造が似通ったかたちになるので、コードの可読性が少し改善します。

最後はSave関数を呼び出して、所定の場所にファイルを出力しています。私たちは自動生成コードをGitリポジトリにコミットして管理しているので、自動生成されたことが一目でわかるよう、ファイル名の末尾に.generatedを追加しています。

ここまで説明してきたコマンドとモデルの実装に、このテンプレートの実装を組み合わせれば、自動生成ツールは容易に実装できるでしょう。

コードの自動生成は高いレバレッジを達成できる手段

それではまとめに入ります。コードの自動生成は、プログラミングにおいて極めて高いレバレッジを達成できる手段です。適用対象になる領域を見つけたら、ぜひ試してみてください。

コード自動生成ツールの設計の基本は、コマンド、モデル、テンプレートです。それぞれの役割と責任範囲については、みなさんの状況に合わせて調整をお願いします。

最後になりますが、電通国際情報サービスでは、中途採用者を絶賛募集中です。QRコードからサイトにアクセスして、ご自身のキャリアに合う募集がないか調べてみてください。

以上です。ご清聴ありがとうございました。