JavaScript Standard Style を試す

2017年2月17日 0 開発 , ,

話題の JavaScript Standard Style を試してみた。

背景

以前、以下の記事とはてブで JavaScript Standard Style を知った。

はてブではセミコロンの省略に抵抗感のある人が多く、私もそうだった。しかし同はてブで id:mysticatea さんが指摘されているように ESLint の no-unexpected-multiline でセミコロン省略時に問題のおきるコードを検出できる。また、

  • 昨年末に Swift 入門してセミコロンのないコードに慣れた
  • Electron の JavaScript コードがセミコロンなしルールで読みやすかった

という理由もあり、セミコロンなしも案外よいものじゃないかと考えるようになった。

私のコーディング スタイルは世間の標準からみると独特で、これは過去に在籍していたプロジェクトのルールを踏襲している。主な特徴としては

  • 括弧の内側にスペースを入れる
  • ifwhile などのキーワードと関数名の後にはスペースを入れない

というもの。C 言語や JavaScript でよく見られる K&R 系だとこんな感じのコードも

function isArray (arg) {
  if (Array.isArray) {
    return Array.isArray(arg);
  }

  return Object.prototype.toString.call(arg) === '[object Array]';
}

私のスタイルだとこうなる。

function isArray( arg ) {
  if( Array.isArray ) {
    return Array.isArray( arg );
  }

  return Object.prototype.toString.call( arg ) === '[object Array]';
}

これはこれで気に入っていたのだが GitHub で OSS を運用にするようになり、第三者からの PR は一般的な方のスタイルでくるため扱いに困っていた。スタイルの違いを理由に断るとか直すのも面倒なので今はそのまま merge しているけれど、そもそも自分の好みより世に迎合するほうがよいんじゃないか?と思い始めた。

あと Xcode の Editor におけるコーディング スタイル設定が貧弱というのもある。他の IDE だと私のスタイルを再現するのに十分な設定があるためそうしてきた。しかし Xcode の Text Editing は驚くほど設定がない。まともにいじれるのは Indentation ぐらいである。

これまでは仕方なく根性で手動整形してきたが、Swift 入門を機にあきらめた。iOS で SQLite – FMDB の使い方 2017のサンプルでは Xcode の提示するスニペットそのままに書いている。

この経験を経て、自分のスタイルへ固執することをやめることにした。プラットフォーム標準があればそれに従い、IDE や Editor、Linter の補助を最大限に享受する方針へ転換する。

というわけで、まずは公私ともに書く機会の多い JavaScript のコーディング スタイルから変更してみる。

JavaScript Standard Style

JavaScript のコーディング スタイルとしては

あたりが有名どころらしい。どれを選ぶか迷ったが Electron のようなセミコロンなしスタイルを採用している JavaScript Standard Style にしてみた。Standard と銘打つ度胸と GitHub の star 数も判断材料である。セミコロン以外はよく見るスタイルなので、ここを受け入れられるかが重要。

JavaScript Standard Style への準拠にあたり、それを保証する仕組みがほしいので ESLint を利用。今回は akabekobeko/npm-wpxml2md プロジェクトで試す。

feross/eslint-config-standard を参考にプロジェクトのローカルに必要な npm をインストール。

$ npm i -D eslint-config-standard eslint-plugin-standard eslint-plugin-promise

次にプロジェクトのルートで .eslintrc を定義。

{
  "extends": "standard",
  "env": {
    "mocha": true
  },
  "rules": {
    "no-multi-spaces": 0,
    "yoda": 0
  }
}

JavaScript Standard Style を使用するだけなら "extends": "standard" だけでよい。しかし mocha で書いたユニット テストも対象にしたいのと、

  • 連続した複数行の変数宣言などで縦位置をスペースで揃えたい
  • if 文で不等号による範囲チェックを if (0 <= value && value < max) のように書きたい

のでそれらの設定を追加した。JavaScript Standard Style はスタイルに準拠していることを示す証として

Standard - JavaScript Style Guide

というバッヂを提供している。ルール緩和した場合でもこれをつけてよいものか迷ったけれど緩和は極小なので README へ掲示することにした。第三者が README をながめたとき、基本となるコーディング スタイルを視認できるのはよいことだ。

私は JavaScript のコーディングに Atom を使用しており、ESLint によるリアル タイムなチェックのため

を採用している。これまで linter-eslint はグローバルにインストールした ESLint とプラグインを使用して設定も ~/.eslintrc を参照するようにしていたが、このプラグインはプロジェクトのローカルに ESLint と .eslintrc を検出するとそちらを優先してくれる。

そのため既存プロジェクトは現行のスタイルを維持しつつ、個別に JavaScript Standard Style を採用する運用が可能である。いきなりグローバルを書き換えてもよいけど、少しずつ移行するほうが安全だろう。

スタイルのチェックは基本的に Atom 上で確認 & 修正するのだがファイル単位で個別に作業していると抜けも出やすいため、一括チェック可能な仕組みも用意する。私は npm-scripts に

{
  "scripts": {
    "eslint": "eslint ./src"
  }
}

を定義して

$ npm run eslint

を実行している。これは AltJS/AltCSS の transpile のようにバックグラウンドでファイル変更の検出と自動チェックさせるほうがよいのかもしれない。

セミコロンなき世界

旧スタイルから JavaScript Standard Style へこのように書き換えてみた。これらの中で比較的、短めのコードを引用する。

#!/usr/bin/env node

'use strict'

const CLI = require('./cli.js').CLI
const WpXml2Md = require('../lib/index.js')

/**
 * Entry point of the CLI.
 *
 * @param {Array.<String>} argv   Arguments of the command line.
 * @param {WritableStream} stdout Standard output.
 *
 * @return {Promise} Promise object.
 */
function main (argv, stdout) {
  return new Promise((resolve, reject) => {
    const options = CLI.parseArgv(argv)
    if (options.help) {
      CLI.printHelp(stdout)
      return resolve()
    }

    if (options.version) {
      CLI.printVersion(stdout)
      return resolve()
    }

    if (!(options.input)) {
      return reject(new Error('"-i" or "--input" has not been specified. This parameter is required.'))
    }

    if (!(options.output)) {
      return reject(new Error('"-o" or "--output" has not been specified. This parameter is required.'))
    }

    return WpXml2Md(options.input, options.output, {
      noGFM: options.noGFM,
      noMELink: options.noMELink,
      report: options.report
    })
  })
}

main(process.argv.slice(2), process.stdout)
.then()
.catch((err) => {
  console.error(err)
})

実にスッキリ。見慣れるまでは JavaScript に見えないかもしれない。

これまで C 言語系の構文をもつプログラミング言語に慣れ親しんできたためセミコロン入力は手癖になっていたけど、いざ不要になるとこれがどれだけ負担だったかを認識させられる。

はじめは、ほんの 1 文字だしプログラミングでは書くより考える時間のほうが長いのだから気にするほどのことか?と考えていた。しかし ; + EnterEnter に置き換わることは、実際に体験してみると実に大きい。正確に構文の末尾へセミコロンを置くことと、単に改行するだけというのはかなり違う。セミコロンなし派が一定数いる意味を身をもって知った。

なおセミコロン省略により起き得る問題は前述のように no-unexpected-multiline が検出してくれる。実際の eslint-config-standard/eslintrc.json でも "no-unexpected-multiline": "error" と設定されているため安心だ。

所感

JavaScript Standard Style 導入の所感をまとめる。

  • 一般的な JavaScript と自身のコードを交互にながめても違和感をおぼえにくくなった
  • Atom のスニペットをそのまま利用できるようになった
  • 括弧のスペースを詰めてもそれなりに読める
  • まともな Editor なら構文強調のおかげで括弧とそれ以外を区別しやすいので困らない
  • セミコロンなしはスッキリしてかなり読みやすい
  • セミコロンを入力するのがどれだけ手間だったか実感できる

結論。JavaScript Standard Style は素晴らしかった。今後、他のプロジェクトでも採用する予定。

npm 開発で脱 Babel してみる

2017年1月25日 0 開発 , , ,

自作 npm の開発で脱 Babel したときの対応と問題点まとめ。

  • 2017/1/31 訂正
    power-assert の作者、t_wada さんより power-assert は babel-register を通すなどしないと assert 置換が働かず素の assert になってしまうという指摘があったのでユニットテスト関連の記述を訂正

脱 Babel を決めた背景

私はいくつか自作 npm を公開している。これらは ES2015 以降の機能と構文を利用して npm publish の際に Babel で ES5 相当へ transpile している。この運用で特に問題も起きていない。またプリセットに babel-preset-latest を採用することで ES 関係の規格追従を Babel 任せにできる安心感もあり、ずっとこのままでいいと思っていた。

ある日、職場で Node アプリを開発している人から「Babel 依存は怖くないですか?」という質問があった。Babel にバグがあったら調査や修正は困難だし、使わなくてよいならばそうするに越したことはないのでは?と。

これまで C++、C#、Java などコンパイル前提の言語で開発した経験から、よほどのことがない限りコンパイル結果は信頼に足ると判断していた。また、コンパイルされたマシン語や中間言語を人間が直に記述するのは非常にキツイ。そのため脱コンパイラーという選択肢は現実的ではないと認識している。

一方、JavaScript における transpile は高級言語どうしの変換となる。その気になれば人間が書けるのだ。

transpiler への慣れから、この事実をすっかり忘れていた。Node であれば Web フロントエンドと異なり動作環境の分岐は少なく、package.json の engines にて対象環境を限定することも可能である。ならば新しい Node を前提として脱 Babel を検討してみるのもよさそうと考えはじめた。

そんな折、npm-run-all が v4.0.0 で Babel による transpile を廃止。Node は v4 時点で ES Modules を除く大半の ES2015 機能が実装されているため、この範囲で足りるなら transpile せずにそのままリリース可能だ。その前例として普段利用しているツールが脱 Babel したのはインパクトある。

これらを踏まえ、まずは自作 npm のうちダウンロード数の少なくニッチな wpxml2md から脱 Babel を試してみることにした。

脱 Babel への道のり

脱 Babel 対応で実施したことを書く。

Node 環境の明示的な指定

脱 Babel における前提条件として動作環境とする Node のバージョンを決める。npm-run-all は Node v4 を下限としているようだが、wpxml2md では v6 としておく。

開発と検証コストを考慮して自作 npm の動作環境は「最新 + 最新 LTS」としている。2017/1 時点の最新 Node は v7 系、LTS は v6 と v4 があるため対象は「v7 + v6」となる。これまでは Babel による transpile で v4 以下でも動作していたのだが、これを廃止することで明示的な下限の指定が必要となった。これは package.json の engines プロパティに記述する。

{
  "engines": {
    "node": ">= 6"
  }
}

ちなみに

node v6, v7

というバッヂを用意していて前から README へ掲載していた。今回の対応により、ようやくこれが本来の意味をあらわすようになった。

Babel の transpile を廃止

これまでは Babel の transpile を前提として以下のような Babel 設定と npm-scripts を利用していた。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js",
    "watch": "babel src --out-dir ./ --watch",
    "start": "npm run watch",
    "build": "babel src --out-dir ./",
    "prepublish": "npm run build"
  }
}
script 内容
test 予約された npm run のタスク。 mocha と power-assert によるユニット テスト。
watch 開発用。ファイル監視による自動 transpile を実行。
start 予約された npm run のタスク。watch を呼び出すだけ。
build リリース用。現時点のソース コードで transpile を実行。
prepublish 予約された npm publish 時に呼び出されるタスク。build を呼び出しているため npm として公開されるイメージは transpile されたものになる。なお prepublish は現在 deprecated になっていて prepare へ修正すべきなのだが直し忘れていた。

これが脱 Babel によりこうなる。

{
  "babel": {
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js"
  }
}

transpile 不要となるため関連するタスクが消え、ユニット テストだけが残った。ただし power-assert を利用しているなら標準 assert を置換するための transpile が必要なので、このための設定は維持する。

env.development.presetspower-assert だけ指定することで、Babel 依存がユニット テストに限定されていることが明示されるだろう。必要な Babel 関連の npm も babel-registerbabel-preset-power-assert だけになる。

ES Moduels を CommonJS 化する

Node の ES Modules 対応については以下が詳しい。

なお現時点の最新 Node である v7 においても ES Modules には対応していないため、export/import は CommonJS の exports/require へ修正する必要がある。例えば

export const Options = {
};

export default class CLI {
}

const Options = {
};

class CLI {
}

module.exports = {
  Options: Options,
  CLI: CLI
};

とする。読み込む側は

import CLI from `cli.js`;
import { Options } from `cli.js`;

const CLI = require( `cli.js` ).CLI;
const Options = require( `cli.js` ).Options;

とする。ひとつのモジュールから exportdefault export で複数のインターフェースを公開していると面倒である。しかし ES Modules で書いていたなら import はソース コード冒頭に集約されているから悩む余地なく機会的な作業になる。なんなら正規表現でまとめて変換できる。

ユニット テストについても同様に対応すること。

余談。

CommonJS にした後でも将来の ES Modules 移行を容易にするため require はソース コード冒頭へ書く習慣をつけたほうがよいかもしれない。その場合、読み込み先を camelCase で命名しているとローカル変数と競合する可能性が高くなるため PascalCase にしたくなり、私はそうしている。例えば fsFs と命名している。

Node と CommonJS だとスコープを意識して require を使い分け、なるべく関数ローカルで宣言する派が多数な感じなので ES Modules 対応されたときが気になる。私のようにするか、それともよりよい慣習となるのか?実に楽しみだ。

ESDoc 対応

npm のコード ドキュメント生成に ESDoc を採用している場合、そのままでは CommonJS を解釈できないので対応が必要になる。CommonJS は

上記 issue で紹介されている esdoc-node により対応できる。ESDoc にプラグイン機能があることを初めて知った。これを指定することでコードが ESDoc に解釈される前処理を実行できるらしい。例えば esdoc-node は CommonJS を ES Modules に変換して ESDoc に渡す。

さっそく使ってみよう。まず esdoc-node をプロジェクトに追加。

$ npm install -D esdoc-node

次にこれを ESDoc 設定へ追加する。私は ESDoc の設定を package.json に定義しているので

{
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./test"
    },
    "plugins": [
      { "name": "esdoc-node" }
    ]
  }
}

このようにした。設定後、ESDoc を実行して CommonJS なコードも正しく解析されることを確認。

ただしローカル実行ではなく ESDoc Hosting Service は CommonJS やプラグインに対応していないようだ。試しに wpxml2md の API Document を生成 してみたのだが、2017/1/25 時点では esdoc-node 未指定の状態と同様に CommonJS の絡むものが解釈できていない。

この点について要望 issue を登録してみたのだけど、もしプラグイン対応する場合

  • Hosting Service 側でプラグインを網羅しておき選択実行する
    • プラグインを中央管理する仕組みがないと網羅できない
    • プラグインのバージョンはどうするのか?常に最新?
  • プロジェクトの ESDoc 設定と package.json から動的にプラグインを決定する
    • プラグインのインストールはどうする?
    • ドキュメント生成とごにプラグインをインストールすると Hosting Service の負荷が大きい
    • CI 系サービスのように VM や Docker コンテナで対応するとしても負荷は大きい

といった問題が予想されるため難しそうだ。しかし要望があることだけは記録しておきたかったので issue 登録することにした。

別の方法として ESDoc 本体が esdoc-node 処理を取り込むという選択肢もある。ただ、いずれ Node が ES Modules へ正式に対応するとしたら CommonJS は過渡期の存在である。

  • そのために対応コストを割くのか?
  • ESDoc と名乗るツールとして ECMAScript とは直に関係しない CommonJS へ対応することは設計思想として望ましくないのでは?

という考えもあるだろう。どのような対応、または非対応のままになったとしても ESDoc 作者の意向を尊重したい。

ESDoc Hosting Service を利用しない場合、対象プロジェクトが GitHub で管理されているならそのリポジトリに対して GitHub Pages を使う手もある。ローカルの ESDoc + esdoc-node で出力したものを自前でアップロードするか、そういうタスクを npm-scripts に定義して CI サービス経由で生成から公開まで自動化するなどの方法が考えられる。

本記事を書いた直後に ESDoc 作者の @h13i32maru さんから見解が。

ESDoc Hosting Service 的にはセキュリティや負荷を考慮し、ESDoc 公式プラグインのみサポートしているとのこと。ただ Node の ES Modules 対応は相当に先となりそうなので、この長い過渡期に Node かつ脱 Babel したい開発者としてどうか?という悩ましい課題がある。

まとめ

一部、問題もあったが transpile 不要となったことで Node のバージョンだけを意識して開発すればよい。記述したコードはそのまま実行されることが保証される。Babel 由来の潜在的なトラブルを考えなくてよいため精神的に楽。

あと npm をユニット テストではなく普通のプログラムとして走らせて検証してみたい場合、いちいち transpile しなくて済むのも助かる。

検証はテストに定義すべきでは?という意見もあるだろうけどリポジトリの examples フォルダに npm として参照したときのサンプルを配置しているとき、その動作検証で npm publish する前の現行コードを試したくなったりする。その場合、transpile なしだとビルド系タスクを実行せず動かせてよい。

以上を踏まえ、他の npm についても気が向いたら脱 Babel してゆく予定。

iOS で SQLite – FMDB の使い方 2017

2017年1月22日 0 開発 , , ,

2011 年に書いた iOS で SQLite – FMDB の使い方という記事へ現在も結構なアクセスがある。

しかし当時は ARC すらなくサンプルとして古すぎる。なにしろ 5 年も前だし。また最近 iOS アプリ開発に戻ってきたこともあり、2017 年度の開発環境を学ぶ題材としてサンプルを再実装してみた。プロジェクトは GitHub に公開してある。

サンプル再実装における考察などは以下にまとめる。

開発方針

再実装にあたり開発方針をまとめる。

  • サンプル プロジェクトの機能と UI は元記事の内容を踏襲
  • Objective-C と Swift の両方を実装
  • FMDB は CocoaPods で管理
  • ユニット テストを実装する

なるべく最新の Objective-C と Storyboard を使用するのは当然として、Swift 版も実装する。Swift は 2014 年に発表されてから破壊的な変更を繰り返してきたが、Swift 3 で一段落ついたと認識している。

Swift 3の開発の振り返りとSwift 4の計画が記されたメールの紹介 – Qiita と冒頭で紹介されているメーリング リストを読むに、Swift 4 では互換性が重視され Swift 1 〜 3 のような構文レベルの大変更は抑止されるだろう。というわけで今こそ Swift 入門のチャンスと判断した。

サンプルは Objective-C、Swift 共に同等の内容とする。馴染みある Objective-C から先に実装してそれを Swift へ移植。なるべく Swift 的に好ましい機能や記法を採用するがオブジェクトやメソッド定義の変更をともなうレベルの差分は控える。

FMDB のインストールは CocoaPods を採用。元記事では FMDB のソースをプロジェクトにコピーしていたが、この方法だとバージョン管理に難がある。パッケージ管理が利用可能ならそちらへ任せるほうがよい。

あわせて Xcode の提供するユニット テスト機能も試す。

FMDB とは?

iOS アプリ開発において、SQLite を扱いやすくするためのライブラリ。Apple 的には Core Data 推しであり Xcode の GUI からテーブル編集可能などの優遇措置がある。

だが Core Data は SQLite を基本的に隠蔽している。そのため他のプラットフォームで培った SQL 知見を活かすには素で操作したくなる。特に iOS/Android 両対応のアプリを開発する場合、DB 設計と SQL 文を共用したい場面もあるだろう。

しかし SQLite は C 言語で実装されているため、Objective-C や Swift から利用しようとすると API や接続状態の管理が実に厄介だ。FMDB はこの辺をわかりやすく面倒みてくれる。

要は JDBC とか Android でいう SQLiteDatabase、.NET の System.Data.SQLite みたいなものだ。クライアント言語によりそった簡易なデータベース接続と操作 API を提供してくれる。

CocoaPods と FMDB

CocoaPods による FMDB インストールの詳細は CocoaPods を試すにまとめたので、そちらを参照のこと。サンプル プロジェクトのリポジトリには Podfile を定義してあるので、これを clone してから

$ pod install

すれば FMDB をインストールできる。その後に *.xcworkspace ファイルを Xcode で開けば FMDB への参照がプロジェクトに設定された状態となっている。

Xcode で iOS アプリを開発する場合、現在は以下のパッケージ管理を利用できる。これらから CocoaPods を選んだ理由について。

CocoaPods は Objective-C と Swift に両対応している。

Carthage は CocoaPods よりも先に Swift 対応したことで注目を集めたシステムで、機能も簡素である。CocoaPods の不満点を解消するために生まれたらしい。

Swift Package Manager は Swift 用である。Swift は OSS 化されているため iOS アプリ以外の開発でも採用される可能性がある。そのため iOS や macOS とは独立したパッケージ管理となっているようだ。

これらのうち今回は Objective-C から利用可能で知見も多い CocoaPods を選んだ。CocoaPods は 2016/5 に v1.0 がリリースされ、Podfile まわりで互換問題もあったようだが現在は落ち着いている。つまり当面は安定するだろう。これも採用理由である。

CocoaPods でインストール可能な FMDB には複数の仕向けがある。ccgus/fmdbe の CocoaPods 欄から引用。

pod 'FMDB'
# pod 'FMDB/FTS'   # FMDB with FTS
# pod 'FMDB/standalone'   # FMDB with latest SQLite amalgamation source
# pod 'FMDB/standalone/FTS'   # FMDB with latest SQLite amalgamation source and FTS
# pod 'FMDB/SQLCipher'   # FMDB with SQLCipher

今回は FMDB/FTS を採用する。FTS というのは全文検索モジュールのこと。SQLite FTS3 and FTS4 Extensions に詳しい。特別なテーブル内の TEXTMATCH で高速に検索可能となる。

この機能はオプションなので不要ならば使わなくてもよい。おなじみの SQLite へ便利機能が追加された程度であり、普通に使う分には FTS を意識することはないだろう。

DAO と RAII

FMDB はデータベース接続と SQL 文を実行する API を提供するのだが、その知識をカプセル化するため DAO ( Data Access Object ) パターンを採用。FMDB API を利用する DAO クラスと、DAO インスタンスの生成を担当する Factory クラスを用意する。

FMDB におけるデータベースは FMDatabase - databaseWithPath などのメソッドから得られた FMDatabase インスタンスとなる。データベース接続する場合はこのインスタンスに対して open、接続を閉じるなら close メソッドを呼ぶ。

NSString   *path = @"データベース ファイルのパス";
FMDatabase *db = [FMDatabase databaseWithPath:path];
if ([db open]) {

  // ...データベース操作

  [db close];
}

データベース接続を安全に管理するため open/close を対応させる必要がある。簡単なのは対応をメソッドでカプセル化することだ。なにかデータベースを操作したくなったらメソッドを追加し、その中で open/close を完結させる。

しかしこの方法だと複数の操作を連続実行したい場合、その単位でカプセル化するか毎回 open/close することを覚悟してメソッドを複数実行することになる。元記事のサンプルではそうしていた。

今回はこれを避けるため、DAO クラスのインスタンス生成と破棄を open/close に対応させる。Objective-C は init/dealloc、Swift なら init/deinit で open/close を処理。いわゆる RAII ( Resource Acquisition Is Initialization ) 的な管理である。

Objective-C と Swift はインスタンス生成と破棄を明確にハンドリングできるため RAII と相性もよい。ARC ( Automatic Reference Counting ) を利用していても参照カウンター管理を自動化するだけである。インスタンス生成したスコープで参照が完結するなら、そこを抜けたときにカウンターがゼロとなり「インスタンス破棄 = データベース接続を閉じる」ことになる。

例えば以下のように DAO クラスを定義して

- (void)init:(FMDatabase *)db {
    if (!(db)) { return nil; }

    self = [super init];
    if (self) {
        self.db = db;
    }

    return self;
}

- (void)dealloc {
    [self.db close];
}

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
}

- (NSArray *)read {
}

DAO Factory クラスでは

- (BookDAO *)bookDAO {
    return [[BookDAO allo] init:[self connection]];
}

- (FMDatabase *)connection {
    FMDatabase* db = [FMDatabase databaseWithPath:self.dbFilePath];
    return ([db open] ? db : nil);
}

というようにデータベース接続を済ませた FMDatabase を渡してインスタンス生成する。このメソッドから返された DAO インスタンスはスコープを抜けるまで add や read を繰り返しても同じデータベース接続が使いまわされる。

- (void)sample {
    // データベースが open される
    BookDAO *dao = [self.daoFactory bookDAO];

    // DAO を使用したデータベース操作

    // DAO インスタンス破棄 & データベース接続を閉じる
}

イメージとしては上記のような感じ。

Objective-C から FMDB を利用する

FMDB 関連の API を参照する場合は

#import <FMDatabase.h>
#import <FMResultSet.h>

のように import する。CocoaPods でインストールした場合はフレームワーク扱いとなる。面倒なのでフレームワーク名を省略しているが、名前衝突を心配するなら <FMDB/FMDatabase.h> のように記述してもよい。

SQL 文の定義

SQL 文は NSString リテラルとして定義する。FMDB の API 呼び出しで直に書くよりも、定数にしておいたほうが管理しやすいと思われる。この辺の話は NSString 連結を利用して heredoc 風に定数を記述するにまとめた。

定数の位置は DAO クラスの *.m ファイル冒頭にしておく。SQL 文が長く大量にある場合は別ファイルに括りだすのものよいだろう。

CRUD

FMDB API の代表的なものとして FMDatabase はデータベース接続と SQL 文の実行を担当して FMResultSet が処理結果となる。基本、これらだけ覚えれば利用できる。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase - executeUpdate メソッドを使用。これは第一引数に SQL 文となる NSString、それ以降は可変長引数になっていて SQL 文の Placeholder へ対応する。INSERT だとこんな感じ。

static NSString * const kSQLInsert = @""
"INSERT INTO "
  "books (author, title, release_date) "
"VALUES "
  "(?, ?, ?);";

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
    Book *book = nil;
    if ([self.db executeUpdate:kSQLInsert, author, title, releaseDate]) {
        NSInteger bookId = [self.db lastInsertRowId];
        book = [Book bookWithId:bookId author:author title:title releaseDate:releaseDate];
    }

    return book;
}

SELECT では FMDatabase - executeQuery メソッドを利用する。引数の仕様は executeUpdate と一緒。WHERE 句で条件指定する場合は第二引数以降も使用することになるだろう。以下は単純な全行取得の例。

static NSString * const kSQLSelect = @""
"SELECT "
  "id, author, title, release_date "
"FROM "
  "books;"
"ORDER BY "
  "author, title;";

- (NSArray *)read {
    NSMutableArray *books = [NSMutableArray arrayWithCapacity:0];
    FMResultSet    *results = [self.db executeQuery:kSQLSelect];

    while ([results next]) {
        [books addObject:[Book bookWithId:[results intForColumnIndex:0]
                                   author:[results stringForColumnIndex:1]
                                    title:[results stringForColumnIndex:2]
                              releaseDate:[results dateForColumnIndex:3]]];
    }

    return books;
}

戻り値は FMResultSet になる。これはカーソル型のオブジェクトで next メソッドを呼ぶことで取得された行カーソルが進む。next は成否を BOOL で返すため行の列挙は while で処理するとよい。

行カーソルが示す先のデータを取得するのも FMResultSet のメソッドになる。XXXXForColumnIndex 系が SELECT 文に指定された列のインデックス、XXXXForColumn 系は列名を指定して値を取得する。XXXX には返される値の型を示す。

インデックスと名前のどちらで取得するかはお好みで。インデックスは単純だが順番変更に弱い。名前の場合は管理が面倒だが、名前さえ維持できれば順番を意識せずに取得できる。

Swift から FMDB を利用する

Swift でフレームワーク内のクラスを利用する場合は名前空間を import するだけでよい。

import FMDB

非常に楽ちんだ。

SQL 文の定義

Swift でも Objective-C のようにクラスの外周に定数を宣言できるのだが、SQL 文と DAO の関係は密なのでクラス単位の static 定数としておく。

class BookDAO: NSObject {
    private static let SQLCreate = "" +
    "CREATE TABLE IF NOT EXISTS books (" +
      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
      "author TEXT, " +
      "title TEXT, " +
      "release_date INTEGER" +
    ");"

    func create() {
        self.db.executeUpdate(BookDAO.SQLCreate, withArgumentsIn: nil)
    }
}

Swift の String は + で連結されるため、これを利用してインデントをつけている。

CRUD

FMDB API は Objective-C とほぼ共通。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase.executeUpdate(sql:, withArgumentsIn:) メソッドなどを使用する。第一引数に SQL 文となる String、第二引数の Array が SQL 文の Placeholder に対応づけられる。INSERT だとこんな感じ。

class BookDAO: NSObject {
    private static let SQLInsert = "" +
    "INSERT INTO " +
      "books (author, title, release_date) " +
    "VALUES " +
      "(?, ?, ?);"

    func add(author: String, title: String, releaseDate: Date) -> Book? {
        var book: Book? = nil
        if self.db.executeUpdate(BookDAO.SQLInsert, withArgumentsIn: [author, title, releaseDate]) {
            let bookId = db.lastInsertRowId()
            book = Book(bookId: Int(bookId), author: author, title: title, releaseDate: releaseDate)
        }

        return book
    }
}

SELECT は FMDatabase.executeQuery(sql:, withArgumentsIn:) メソッドとなる。引数の仕様は executeUpdate と一緒。以下は単純な全取得の例。

class BookDAO: NSObject {
    private static let SQLSelect = "" +
    "SELECT " +
      "id, author, title, release_date " +
    "FROM " +
      "books;" +
    "ORDER BY " +
      "author, title;"

    func read() -> Array<Book> {
        var books = Array<Book>()
        if let results = self.db.executeQuery(BookDAO.SQLSelect, withArgumentsIn: nil) {
            while results.next() {
                let book = Book(bookId: results.long(forColumnIndex: 0),
                                author: results.string(forColumnIndex: 1),
                                title: results.string(forColumnIndex: 2),
                                releaseDate: results.date(forColumnIndex: 3))
                books.append(book)
            }
        }

        return books
    }
}

Objective-C に対する特徴的な違いとして Optional 型の扱いがある。

Objective-C の場合、nil なオブジェクトに対し [obj message] 形式でメソッドやプロパティを呼び出すと処理が空振りする。これを利用して nil チェックを避けるテクニックがあるのだが、Swift なら if let で Optional な変数を受ければ配下のスコープで nil 済みの安全なオブジェクトを使用できる。

Objective-C の処理をそのまま Swift へ書き換えただけだと大量のエラーに見舞われて面食らうだろう。しかし慣れてくるとそれらが nil という曖昧な状態を避けるための設計ギプスとして有効であることを理解できる。unwrap まわりの面倒くささは確実に nil への抑止力となるはず。

こうした null/nil 安全については以下に詳しい。

Swift に触れ、私も null/nil 安全について以前より強く意識するようになった。

ユニット テスト

Xcode 標準のユニット テストを試す。

まず FMDB 関連は直に使用しない。DAO クラスのインスタンスは DAO Factory から得られるため、これらに関するものだけに依存する。

Xcode プロジェクトを作成する際に Include Unit Tests と UI Tests をチェックするとテスト用プロジェクトも追加される。前者がユニット テスト、後者は UI の動作をテストするものである。今回はユニット テストだけ使用した。

SQLite のデータベースはファイル単位として管理される。そのためテストを実行する都度、ファイルの存在をチェックして前回実行の結果に影響されぬよう注意が必要。もっと厳密にやるならテスト メソッド単位でファイルを消去するほうが安全だけど、そこまではしない。

Xcode のテストは Objective-C、Swift 共に XCTestCase を継承したテスト クラスを実装する。テスト全体の開始に setUp、終了時は tearDown が呼び出されるため、データベースの生成と破棄や DAO Factory 生成などはこれらに絡めておこなう。

以下は Swift の例。

import XCTest

class BookDAOTests: XCTestCase {
    private let filePath = BookDAOTests.databaseFilePath()
    private var daoFactory: DAOFactory!

    override func setUp() {
        super.setUp()

        self.clean()

        self.daoFactory = DAOFactory(filePath: self.filePath)
        if let dao = self.daoFactory.bookDAO() {
            dao.create()
        }
    }

    override func tearDown() {
        self.clean()
        super.tearDown()
    }

    func clean() {
        let manager = FileManager.default
        if manager.fileExists(atPath: self.filePath) {
            do {
                try manager.removeItem(atPath: self.filePath)
            } catch {
                print("Error: faild to remove database file.")
            }
        }
    }

    private static func databaseFilePath() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let dir   = paths[0] as NSString
        return dir.appendingPathComponent("test.db")
    }
}

テスト対象にしたいクラスは Xcode 右にあるファイルのプロパティから Target Membership で Tests 系プロジェクトもチェックに含める必要あり。Swift は *.swift、Objective-C なら *.m ファイルをチェックすればよい。

Objective-C の場合、更に XCTestCase 派生クラス側のコードでテスト対象クラスのヘッダーを import しなければならない。ヘッダー管理の必要な言語って面倒だ。

これらの設定をせずにテストを実行しようとすると Tests プロジェクトのビルドでリンク エラーになる。Target Membership はよく忘れるので注意すること。

XCTestCase 派生クラスに testXXXX というメソッドを実装するとそれがテスト対象となる。テストを実行した時の処理順は以下。

  1. setUp
  2. testXXXX
  3. tearDown

実際に DAO クラスのデータ更新処理をテストしてみる。

class BookDAOTests: XCTestCase {
    func testUpdate() {
        if let dao = self.daoFactory.bookDAO() {
            let book = dao.add(author: "author", title: "title", releaseDate: Date())
            XCTAssertNotNil(book)

            // Before
            var books = dao.read()
            XCTAssertEqual(books[0].title, "title")

            // After
            let book2 = Book(bookId: (book?.bookId)!, author: (book?.author)!, title: "title2", releaseDate: (book?.releaseDate)!)
            XCTAssert(dao.update(book: book2))
            books = dao.read()
            XCTAssertEqual(books[0].title, "title2")

            XCTAssert(dao.remove(bookId: (book?.bookId)!))

        } else {
            XCTAssert(false)
        }
    }
}

テストにおける値の妥当性チェックは XCTAssert 系の関数で実施する。値の性質ごとに関数が提供されているので、適切なものを選ぼう。よく使うものとしては以下がある。

関数 機能
XCTAssert 一つの値に対して真であることを判定。偽ならば失敗する。
XCTAssertEqual 二つの値を比較。不一致ならば失敗し、それぞれの値の内容を表示する。Objective-C の場合、NSObject 系の比較には利用できないので注意する。Swift は問題なし。
XCTAssertEqualObjects 二つの値を比較。不一致なら失敗し、それぞれの値の内容を表示する。Objective-C で NSObject 系を比較する場合はこの関数を使用する。Swift は不要なので提供されていないようだ。
XCTAssertNil 一つの値に対して nil であることを判定。nil でなければ失敗する。
Not 系 各種 XCTAssert 関数名に Not のついたもの。例えば XCTAssertNotEqual など。元と判定が逆転する。

NSString を判定する場合、厳密さを意識する必要があるかもしれない。詳しくは以下を参照のこと。

Xcode 8.1 でもこの挙動はそのままである。例にある文字列をテストすると XCTAssertEqualObjects でも不一致となった。ここまで判定したいなら NSString - compare を使用する。

これが問題になるとしたら macOS の HFS+ みたいに Unicode 正規化で「が」を「か」と濁点にわけて管理しているものと、そうでない環境で得られたファイル名やパスの同一性などが考えられる。

データを扱う環境がひとつの系で完結しているとか文字エンコーディングと正規化の系が統一されていればよく、iOS アプリの場合はあまり意識しなくてよさそう。そのため今回のサンプルにおけるテストでは XCTAssertEqualObjects を採用した。

この問題が気になるなら以下の記事も参照のこと。

まとめ

Objective-C と Swift について。

Objective-C については 2 年前まで使用していたのでそれほど変化を感じない。元記事のサンプルと比較したら差分は大きいのだけど、想定どおりである。

一方、Swift は印象的だった。Swift 1 〜 3 までの変遷と混乱をチラ見して大変そうな印象しかなかったのだけど、実際にプログラミングしてみると nil 安全まわりが実によい。

基本的に nil を抑止する方針であること、使わざるを得ないときは Optional 型として明示される点が気に入っている。これを経験したことにより、null/nil 安全のない言語で書くときの設計も影響を受けるだろう。

ユニット テストについて。

他のプラットフォームにおける Power Assert 系のように XCTAssert も普通の比較でも失敗時に値の詳細を表示してほしい。用途ごとに関数を使い分けるのは面倒だ。いちおう Swift には keygx/PAssert があるのだけど、Power Assert 的なものは標準にしてもよいのではないか。

あと Cocoa Touch を使用するから仕方ないことだけど、テストに iOS シミュレーターや実機を要するため実行が遅い。スクリプト言語の気軽なテストに慣れていると重く感じる。

FMDB について。

細かな改善はたくさん反映されているのだろうけど、使用感は 5 年前と変わらず。これはよいことだ。普通に設計したらこうなるよね、という期待に答える直感的な API である。

以前よりサンプルの古さが気になっていたので、書き直せてスッキリした。

以下、余談。

この記事とサンプルは 2016 年末に書き終えていたのだけど Swift コードを構文強調する手段に WP Code Highlight.js を使うためブログ全体の Markdown 移行をしているうちに 2017 年へずれ込んでしまった。

結局 WP Code Highlight.js は行間へ余計な br タグを自動挿入するのを防げなくて導入は見送った。仕方ないので SyntaxHighlighter Evolved: Swift Brush を導入して構文強調している。