blog

主にJavaScriptを書いています

ドキュメントを書く技術

READMEを始め、ソフトウェアのドキュメント全般を書く技術というものをもっと洗練させていきたい。要件定義書のようなものだけでなく、開発方針や設計方針、API定義などなど。

これらのドキュメントをしっかりと整備するだけで、レビューの質も上がり新しい人が入ったときもスムーズに意識のズレなく開発ができる。はずだが、なかなかドキュメントの上手い書き方や管理の仕方というものは、コーディングのそれとは違い議論が活発ではない。

最近試してみたこと

そういったドキュメントの中でも、"開発方針"や"設計思想"をどう残していくかということを考えている。それらを残しておくことで、コーディングのときも立ち戻る場所ができ、大きく道を踏み外さなくなる。

例えば、レイヤードアーキテクチャのようなものの"境界"をドキュメントにしていく。MVCでもクリーンアーキテクチャでも何でも良いけど、それらのアーキテクチャではそれぞれのレイヤの役割は定義されているものの、必ずしも理想通りにいくとは限らないし、理想を追求することだけが正ではない。そういったアプリケーション・組織独自の"境界"をドキュメントに残していく。

個人的に各レイヤ(controllersやmodelsといったディレクトリ)にひとつずつREADMEを置いていくのが好きなのだけど、最近はそこに「MUST(MUST NOT)」、「SHOULD(SHOULD NOT)」、「MAY」などを書くといいのでは?と思って書き始めている。

# Controllers

## MUST

- ファイル名はXXXControllersとすること
- メソッドはGETならfindXXX、POSTなら....
- このレイヤでリクエストパラメータをxx型に変換する

## SHOULD

- ビジネスロジックは書くべきでないが、xxxなケースは例外とする

MUST/SHOULD/MAYの表記などはRFCでも使われていてエンジニアには比較的馴染みがある(?)はず。READMEに書いてあればレビューも通せるし、新しくプロジェクトに入った人がドキュメントの場所がわからない、ということもない。

ドキュメントの再現性

設計指針というものは、人によって判断がわかれる"迷い"から生まれると思っている。だからこそ、その迷いが出た時に「これはControllerのSHOULDじゃないの?」みたいに一種のフレームワークに当てはめると考えやすくなる。

ドキュメントを書く技術というのは、同時にドキュメントを"書かせる"技術でもある。コーディングと全く同じで、自分だけが書けても意味がなく、誰が書いても一定のレベルにすることが重要。そのためにはフォーマットを作ったりドキュメントの目的を共有したりして、適切な"枠"から無理なくドキュメントが生まれる環境を考えていきたい。

ドキュメントはコードから自動生成させていきたい

ドキュメントは腐りやすい

ドキュメントはどうやっても更新されなくなってしまう。Wikiに書こうか、Confluenceのほうがいいのか、色々置き場所を考えてはみるものの、大きく改善することはあまりない。

その理由のひとつが、コードとドキュメントのライフサイクルが違うこと。元来ドキュメントは「コーディングの前に書くもの」であったし、その意識はなくてもPull Requestに「あのドキュメントも更新しておきます」と書いている時点で、それはライフサイクルが合っていないものを、信頼性の低いプロトコルで同期を取ろうとしているようなもの。

それを解決する手段のひとつが、コードとドキュメントを同じリポジトリに置くということだけれど、READMEが大量にできるというのもそれはそれで見通しもメンテナンス性もあまり良くない。

必要とされるドキュメントの形はさまざま

それなら、とコードからドキュメントを自動生成させたいと思う。動いているコードは常に現状を正しく表しているので、そこから生成されるドキュメントも常に正しい(バグがないとは言っていない)。

JSDoc(やその源流のJavaDocなど)がそれに近いアプローチをとっているが、あまりうまく活用できたことがない。

コードは変えたけどコメントを直し忘れた、というケースがあるように、やはり「コードがコメントの通りに動いていること」は理想だけどそうならないケースがある。もちろんちゃんと運用したら良いツールなんだけど、そこが少しもったいない。

もうひとつは自由度の低さというのか、スコープの違いというのか。普段アプリケーションを作っている身としては、関数一覧が並んでいたらそんなに嬉しい?という気持ちになるし、生成されたドキュメントを見るよりコードを直接見たほうが早い。チーム外の人のためにわざわざドキュメントを生成してもほとんど見られないだろうし、なによりも別に知りたいことは関数一覧じゃなかったりする。

だからコードから生成しつつ、自由度が高い方法が欲しかった。

babylonでコードからドキュメントを生成する

とは言ってもJSDocのアプローチは捨てがたく、より面白いことができないかとbabylonに手を付けてみたら思いの外簡単だった。

babylonはあのbabelが使っているAST(Abstract Syntax Tree)を作るparser、平たく言うと「ソースコードを読み込んでその構造をオブジェクトにしますよ」というやつ。コードをbabylonにかけると、「クラス名はこれですよ」「メソッドはこんなのがありますよ」というのが簡単に取れるというもの。babelはこれを使って、コードを構造化したあと、いい感じに変換していい感じに出力している。

どれくらい簡単かというと、こういうコードがあれば

class Hoge {
  getMsg1() {
    return "hello"
  }

  getMsg2() {
    return "world"
  }
}

module.exports = Hoge

こういうコードを書くだけでメソッド一覧が取得できる。

const { parse } = require('babylon')
const traverse = require('babel-traverse').default
const fs = require('fs')

const code = fs.readFileSync('./index.js', 'utf-8')

const ast = parse(code)

let className
let methods = []
traverse(ast, {
  Class: path => {
    className = path.node.id.name
  },
  ClassMethod: path => {
    methods.push(path.node.key.name)
  }
})

console.log(`class: ${className}, methods: ${methods}`)
// => class: Hoge, methods: getMsg1,getMsg2

もちろんbabelのpluginでflowやJSXも読めるし、コード上のほぼすべての情報が取れるので、例えば「Componentを継承しているクラスの一覧がほしい」とかもすぐにできる。もちろんクラスやメソッド一覧だけじゃなくて、定数一覧でも何でも。

// pluginを使う
const ast = parse(code,{ plugins: ["flow"]})

これを使って例えば「外部サービスに接続している場所一覧」みたいなのをMarkdown形式でファイルに出力しておけば、自由度が高く必要な情報だけに絞ったドキュメントが自動生成される環境が作れる。

コメントも価値あるものとなる

そしてそれにdoctrineというJSDocパーサーを合わせるのも結構面白かった。結局JSDocに戻ってるじゃん、という気持ちにもなるが、例えば別ドキュメントへのリンクとか、あまり更新されないコメントであれば保守にも耐えられるだろう。たぶん。

先ほどの例でいくと、

/**
 * @see http://example.com
 */
class Hoge {
  getMsg1() {
    return "hello"
  }

  getMsg2() {
    return "world"
  }
}

module.exports = Hoge

というものをdoctrineで読み込むとこうなる。

const { parse } = require('babylon')
const traverse = require('babel-traverse').default
const fs = require('fs')
const doctrine = require("doctrine")

const code = fs.readFileSync('./index.js', 'utf-8')

const ast = parse(code)

let className
let link
let methods = []
traverse(ast, {
  Class: path => {
    className = path.node.id.name
    const comments = path.node.leadingComments.map(comment => comment.value)
    if (comments.length > 0) {
      const tags = doctrine.parse(comments[0], { unwrap: true }).tags
      link = tags.find(tag => tag.title === "see").description
    }
  },
  ClassMethod: path => {
    methods.push(path.node.key.name)
  }
})

console.log(`class: [${className}](${link})`)
// => class: [Hoge](http://example.com/juuyouna/reference)

どういうドキュメントがあると嬉しいのか、それはどういうアーキテクチャなら簡単に生成できるのか、アイデアがあれば何でも出来そうな気持ちに久しぶりに楽しくなった。