数週間ほど前、コミットをみていたら「ひっかかる」APIがありました。それについてコメントし、わかりやすいAPIについてやりとりしました。そのやりとりの中で、設定ファイルの項目名をわかりやすくするために、いままであまり意識せず工夫していたことに気付きました。それは「設定ファイルの項目名を否定形で修飾しない」というものです。「否定形」という言い回しではなく、「ネガティブな単語を使う」というような言い回しの方が近いような気がしますが、あまりしっくりくる言い回しを思いつかないので、ここでは「否定形」ということにします。
それでは、そのときのやりとりと明文化された工夫を紹介します。
きっかけとなったのは次のようなdiffです。
1 2 3 |
bool config_is_disable_preview (Config *config);
+bool config_is_hide_preview (Config *config);
bool config_is_enable_rss_feed (Config *config);
|
disableとhideはどう使い分けているのか?is_disable_XXXは英語としてみたときに不自然じゃないか?などいくつか気になる点が複数の人から挙がりました。その中で、「disable」と「enable」という反対の意味の単語を使ったAPIが並んでいることにひっかかりました。そこで、まずはどうしてひっかかったかを整理しました。
disableとenableだけみると反対の意味と分類できます。そこにhideも加えるとdisable/hideが否定形で、enableが肯定形と分類できます。否定形の表現が混ざっていることがひっかかった理由でした。
否定形が混ざっていると、コードを読むときに次のように考えるため、ひっかかってしまいます。肯定形に揃っていたほうが読むときにスムーズに読みやすくなります。
disableなAPIでifを書くと次のようになります。
1 2 3 |
if (!config_is_disable_preview(config)) { /* 有効なときの処理 */ } |
これを読むときのことを考えます。
if (!config_is_...
このあたりまで見たときは、最初に「!
」があるので、「あぁ、否定がついているから無効なときだろう」と勝手に推測して、次のように考えています。
1 2 3 |
if (!config_is_...) { /* 無効なときの処理 */ } |
その後、
if (!config_is_disable_...
あたりまでくるとdisableが見えるので「disableの否定だからenableで有効なときか」と次のように考えます。
1 2 3 |
if (!config_is_disable_...) { /* 有効なときの処理 */ } |
実際はここまで意識していませんが、どう考えているだろうと振り返ってみるとこう考えていそうです。一度推測が外れていることがひっかかると感じるポイントなのでしょう。
ひっかかるポイントがわかったので解決策を考えます。
ひっかかるポイントは「否定のときに真を返す関数」があることです。これをなくせばひっかかるポイントをなくせます。つまり、真偽値を返す関数をすべて肯定のときに真を返すように統一すればよいのです。
is_disableはis_enableに、is_hideはis_showに、といった具合です*1。
このようなやりとりの中で、「そもそもどうして否定形のAPIにしたか」の理由を教えてもらいました。理由は「設定ファイルの項目名とあわせるため」ということでした。この理由はプロダクト全体での統一感があがるので妥当な理由といえます。
設定ファイルでは次のような設定項目となっていました。
disable_preview = true hide_preview = true enable_rss_feed = true
では、どうしてこのような項目名にしたかというと「デフォルト値を変える場合に設定を書くから」ということでした。
デフォルトでプレビュー機能は有効なので、プレビュー機能を使うために設定ファイルに何かを記述する必要はありません。設定ファイルに記述するときはプレビュー機能を無効にするときだけです。そのため、「disable_preview」という設定ファイルになっているということでした。これもそれっぽい理由に思えます。しかし、APIの統一感を損なっている(否定形と肯定形が混ざっている)のが実情です。
では、どうすればよいでしょうか。それを考えたとき、あまり意識せず設定項目の名前のつけ方を工夫していたことがわかりました。だいぶ長い前ふりでしたね。
否定形の設定項目名にならないように、次のように設定するように工夫していました。
preview = disable rss_feed = enable
項目名にenable/disableなどの情報を入れずに、項目名は設定対象だけにし、値を真偽値にします。場合によっては「disable/enable」だけではなく「yes/no」や「true/false」も受け付けるようにします。
preview = false rss_feed = yes
そして、デフォルト値をコメントとしてデフォルトの設定ファイルに書いておくようにします。
# preview = enable # <- デフォルトで有効な場合 # rss_feed = disable # <- デフォルトで無効な場合
こうすれば、設定を変更しようと設定ファイルを確認したとき、どのような値にしなければいけないか迷わずに済みます。現在の挙動が設定ファイルにコメントとして書かれているので、その項目のコメントを外し、反転すればよいことを推測できるでしょう。
milter managerの設定ファイルでは、次のようになっています。デフォルトでデーモン化しないので「false」を設定する行がコメントになっています。
# manager.daemon = false
今回のpreviewのようにdisable/hideと複数の設定項目がある場合はグループ化します。このとき、enabled/visibleというように肯定形を使います。
パターン1:
preview.enabled = true preview.visible = true
パターン2:
[preview] enabled = true visible = true
肯定形に統一できたので、APIを設定項目に合わせてもAPIを肯定形に統一できます。例えば、config_preview_is_enabled()
となります。
APIと同じように設定項目も否定形と肯定形がまざっているとひっかかってしまいます。そのため、設定項目がAPIと関連付いていない場合でも肯定形に統一することはわかりやすい設定項目を作ることにつながります。
diffを見て気になったAPIについてやりとりすることで、統一感のあるわかりやすい設定項目にするためにしていた工夫に気づくことができました。工夫のポイントは次の4点です。
設定ファイルを作るときは、すべて肯定形に統一しているかを気にしてみてください。設定の書きやすさ、理解のしやすさが変わります。
*1 is_enabledやis_visibleなどの方が英語として自然ですが、そこまで変えるとここで説明しようとしている内容が増えて視点が分散してしまうのでis_enableでいきます。
最近、fluent-plugin-droongaという分散データストリームエンジンを書いています。その中で、RubyでDSLを実現するときに工夫していることに気づきました。それは、値を設定するときは代入する字面にするということです。代入する字面にするために、グループ化用のオブジェクトを作っていました。
これだけだとどういうことかわからないので、具体例を示しながら説明します。
Rubyを使っているとRubyで実現されたDSLに触れることが多くあります。RubyのMake実装であるRakeの設定ファイルもそうですし、ライブラリー管理ツールのBundlerの設定ファイルもそうです。
Rakeの場合:
1 2 3 |
task :test do ruby("test/run-test.rb") end |
Bundlerの場合:
1 2 |
source "https://rubygems.org/" gem "rake" |
設定ファイルではなく、Rubyのコードの中で使われているDSLもあります。WebアプリケーションのSinatraはWebアプリケーション作成のためのDSLと自称しているくらいです。
Sinatraの場合:
1 2 3 4 5 6 |
require "sinatra" get "/" do cache_control :public, :must_revalidate, :max_age => 60 "Hello world!" end |
いろんなDSLを見るといくつかの種類に分類できることに気づきます。例えば次のように分類できます。
task :test
:タスクを定義get "/"
:「GET / HTTP/1.1」されたときの動作を定義attr_reader
とかの特化版)
source "https://rubygems.org/"
:RubyGemsの取得元を宣言gem "rake"
:使うRubyGemsを宣言ruby("test/run-test.rb")
:Rubyでスクリプトを実行cache_control
:Cache-Controlヘッダーの値を設定今回注目するのは設定系です。設定系のDSLを作るときに工夫していることです。
設定系は値を変えるのでまさに代入という動作です。そのため、「代入する字面」になるようにします。
Sinatraのcache_control
は、次のように宣言系の字面になっています。
1 |
cache_control :public, :must_revalidate, :max_age => 60 |
そうではなく、「代入する字面」にするということです。「代入する字面」とは、例えば次のようにするということです。
1 |
self.cache_control = [:public, :must_revalidate, {:max_age => 60}] |
ここで気になるのが「self.
」です。Pythonと違いRubyでは明示的に「self
」を書くことがほとんどありません。明示的に書くときは次のようなケースです。
class << self
)「代入する字面」にすると2番目の「自分の代入メソッドを呼び出すとき」というケースに当てはまるので、明示的に「self
」と書く必要があります。これは、ローカル変数への代入と代入メソッドの呼び出しを区別するためのRubyの制限です。
1 2 3 4 5 6 |
def xxx=(value) @xxx = value end xxx = "local" # <- ローカル変数への代入 self.xxx = "method" # <- xxx=メソッドの呼び出し |
self
はRubyでは不自然な字面*1です。self
を使わず、自然な字面にするためにオブジェクトを作ります。例えば次のようにします。
1 |
response.cache_control = [:public, :must_revalidate, {:max_age => 60}] |
「レスポンスのCache-Controlを設定する」と読めるコードで妙なところはありません。このようにself
ではなく何かオブジェクトを使うように工夫していました。
この例ではレシーバーとしてself
ではなくresponse
を導入しました。このレスポンスをどうやって見つけるか。それを見つけるために、「グループ化」して考えていました。
Cache-Controlの例で言うと、「Cache-Controlは何関連の設定だろう」と考えます。レスポンスのヘッダーの設定なのでresponse
を導入しました。「レスポンス関連の設定」と考えたということです。
レシーバーをつけると記述が長くなります。
1 2 |
cache_control :public, :must_revalidate, :max_age => 60 response.cache_control = [:public, :must_revalidate, {:max_age => 60}] |
これをデメリットと考えることもできますが、次のメリットはそのデメリットを上回ります。
このことから、設定系のDSLはレシーバー付き代入式にすることをオススメします。このとき、レシーバーには設定対象のグループを表す名前をつけます。
いくつか実例を示します。
まずは、この工夫をしていることを自覚したfluent-plugin-droongaのケースです。
fluent-plugin-droongaは入力メッセージや出力メッセージの処理をプラグインでカスタマイズできます。プラグインは特定のメッセージを処理します。どのメッセージを処理するかをメタデータとして設定するAPIになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module Droonga module Plugins module CRUD class Adapter < Droonga::Adapter message.input_pattern = ["type", :equal, "add"] message.output_pattern = ["body.success", :exist?] def adapt_input(input_message) end def adapt_output(output_message) end end end end end |
message.input_pattern=
とmessage.output_pattern=
が設定系のDSL*2です。メッセージ関連の設定なのでmessage
をレシーバーにしています。input_message.pattern=
とoutput_message.pattern=
にしてもよいでしょう。
これを次のようにすることもできます。
1 2 3 4 |
class Adapter < Droonga::Adapter input_pattern ["type", :equal, "add"] output_pattern ["body.success", :exist?] end |
宣言系のDSLのような字面です。DSLが好きな人はこちらを好むかもしれません。しかし、ここで設定した値を使うときにself
とは違った違和感があります。
設定した値を取得するときは引数なしで同じメソッドを呼びます。
1 2 |
Adapter.input_pattern # -> ["type", :equal, "add"] Adapter.output_pattern # -> ["body.success", :exist?] |
これらのメソッドの実装はこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Droonga::Adapter class << self PATTERN_NIL = Object.new def input_pattern(pattern=PATTERN_NIL) if PATTERN_NIL == pattern @input_pattern else @input_pattern = pattern end end end end |
1つのメソッドで2つのことをしているので理解するまでにワンクッション必要なコードになっています。
message.input_pattern=
の実装はこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Droonga::Adapter class MessageConfiguration attr_accessor :input_pattern attr_accessor :output_pattern def initialize @input_pattern = nil @output_pattern = nil end end class << self def message @message ||= MessageConfiguration.new end end end |
オブジェクトが1つ増えていますが、やっていることは明快なのですぐに理解できます。
別の例を紹介します。
設定ファイルで値を設定するケースです。milter managerというソフトウェアの設定ファイルはRubyスクリプトで次のようになっています。
1 2 3 4 5 6 7 |
security.privilege_mode = false security.effective_user = nil security.effective_group = nil manager.connection_spec = "inet:10025@[127.0.0.1]" manager.unix_socket_mode = 0660 manager.unix_socket_group = nil |
security
とmanager
をグループとしています。
milter managerもグループごとにオブジェクトを作る実装になっています。
Rubyで自然なDSLを作るコツとして、値を設定するときは、レシーバーに設定対象のグループを示す名前をつけ、代入式にするとよいということを紹介しました。実例と実装例もつけたので、よさそうだと思ったらマネしてみてください。
*1 Rubyist Magazine - Ruby コードの感想戦 【第 2 回】 WikiR - set_srcのこと参照。
*2 fluent-plugin-droongaに特化したシンタックス。DSLではなく、単なるAPIだよね、といってもいいくらい薄いDSL。
現在、Mozilla Japan主導でFirefoxとThunderbirdの法人利用者向け情報の整理が進められています。 クリアコードもMozilla Japanサポートパートナーとしてこの作業に協力しており、弊社内で蓄積されてきた技術情報を積極的にFAQ項目へフィードバックしております。
この作業はGitHub上の公開のリポジトリで行われており、前述のFAQも、現状の最新版をWeb上で参照できるようになっています。
現在は上記のGitHub上のリポジトリでしか参照することができませんが、近いうちに他の情報も併せて普通のWebサイトとして公開される見込みです。
また、せっかくのGitHubですので、「こんな項目もあった方がよい」といった意見や追加の情報がありましたら、随時プルリクエストの形でお送り頂ければ幸いです。
サポートパートナーとしてMozilla製品のサポート事業を展開している弊社では、FirefoxやThunderbirdの導入を検討されている法人のお客様からのお問い合わせを頂くことが度々あります。 その際、実際にお話を伺ってみると、いくつかのお客様で共通したご質問・ご要望を頂くことが多いという事実がありました。
ところが、調査してみると、Web上にはそれらの要望に応えられるような直接的な情報はなかなか見つかりませんでした。 (だからこそ弊社にお問い合わせ頂いたというわけなのですが……) これは、一般的な個人ユーザと法人ユーザではFirefoxやThunderbirdに求めるカスタマイズの性質が異なることが多く、Mozilla サポートのような個人のボランティアベースでのナレッジには、「法人のシステム管理担当者の方が社内にFirefoxやThunderbirdを展開する」という観点での有用な情報が集まりにくい傾向があるからだと考えられます。
一方で、サポート事業を継続的に行ってきた中で弊社内に蓄積されてきた技術情報について、一般向けに整理し公開するための適切な場が存在しないという問題もありました。 いくつかの情報はMozilla Developer Networkやこのククログで断片的に公開していますが、残念ながら、上記のような観点で参照しやすい状態にはなっていません。
現在編纂中のFAQでは、「法人のシステム管理担当者の方が社内にFirefoxやThunderbirdを展開する」という具体的なユースケースを前提として項目を検討し、記載しています。 これにより、今までは弊社のようなサポートパートナーでなければ応えられなかったような法人ユーザ特有のカスタマイズの要望を、ユーザ自身(法人内のシステム管理担当者の方)の手である程度までは自己解決できるようになるものと思われます。
現時点では、目に見えてニーズの多い物からということで、デスクトップ環境でFirefox(24ESR以降のバージョン)を利用する場合の情報を優先してFAQ項目を編纂しています。 将来的には、モバイル環境での利用やThunderbird向けのFAQ項目も拡充していく予定です。
弊社では省力化その他の観点から、FirefoxやThunderbirdのカスタマイズ内容について、ニーズが多い物は可能な限り設定ファイルやアドオンの組み合わせによってカスタマイズを完結できるようにする方針を持っています。 その結果として相当数のアドオンを開発・公開していますが、そのような方針に基づいて開発されているため、個々のアドオンは単体では利用し辛かったり、利用方法が分かりにくかったりする部分がありました。
上記のFAQでは、具体的な要望に対して「その場合はこのアドオンを使うとよい」「アドオンをこのように設定するとよい」という形で利用法を紹介しています。 また弊社開発のアドオン以外や、アドオンを使用しない場合のカスタマイズ方法についても同様に紹介しており、運用の都合に合わせた最適な方法を選択できるように記述する事を意識しています。
なお、FAQで紹介しているアドオンは以下のページから閲覧・入手することができます。
具体的には、今回のFAQに関連する以下のアドオンはすべて公開済みです。
また、これらのアドオンや設定ファイルを同梱した組織内向けカスタマイズ済みFirefox・Thunderbirdのインストーラを作成するための仕組みとして、弊社では「Fx Meta Installer」というソフトウェアも開発・公開しています。 こちらも併せてご参照下さい。
クリアコードでは、Mozilla製品の導入を検討中あるいは導入済みの法人ユーザさまからのお問い合わせを引き続きお待ちしております。
2/9にクリアコードをはじめ6社が集まりGroongaビジネスパートナーズ(略称グルンパ)が発足しました。グルンパはGroongaの普及とGroongaに関するビジネスの拡大を目的とした団体です。Groongaに関する有償サービスの紹介やGroongaの導入検討に関する資料を共同で作成するなどして、Groongaの導入を促進するような活動を行います。今回は、グルンパ発足の経緯や発足後の変化を紹介します。
クリアコードでは2009年からGroongaの開発に参加してきましたが、2012年頃からGroongaに関するサポートの問合せが増えてきました。そこでサポートサービスを手軽に利用していただけるよう未来検索ブラジル、スパイラルアーム、クリアコードの3社でGroongaインシデントサポートサービスを開始しました*1。このGroongaインシデントサポートサービスは検索システムを自社で開発する企業に対してGroongaに関するテクニカルサポートとプロダクトサポートを契約時間に応じて提供するものです。お客さまが開発することを前提にしているため、我々が開発を請け負うことは想定していませんでした。
しかし、Groongaに関する問合せには、検索システムのリプレース案件やデータ量増加に伴なうGroongaからMroongaへの移行案件など開発リソースが求められるケースがありました。都度知り合いの企業に相談して一緒に提案したりしていましたが、なかなかよい提案ができず失注するケースが多くありました。これはGroongaに関するノウハウは有しているものの、ECサイトなどWebアプリケーション開発のノウハウが不足していることが原因でした。Groongaの普及のためにはGroongaを使ったWebアプリケーションを開発する体制を整える必要がありました。
そこで、昨年11/29の全文検索エンジンGroongaを囲む夕べ4でGroongaでビジネスをしませんか?という発表を行い、Groongaに関するサービスを提供する企業の結集を呼びかけました。
11/29の発表後、興味をもっていただいた企業やこれまでにGroongaの案件を一緒に手がけた企業と意見交換しました。幸いなことに各社ともGroongaに関するサービスやGroongaを使った検索システムの開発に力を入れていきたいという意向があり、Groongaに関するサービスを共同でプロモーションし、Groongaの案件獲得につなげる取り組みを行なうことで意見が一致しました。結果、クリアコード含め次の6社でグルンパを発足することになりました。
実は、クリアコードは他の5社すべてと一緒に仕事をした経験があります。いずれの企業も確かな技術力があり、ビジネスのパートナーとして心強い存在です。またそれぞれの企業はインフラ構築からスマホ用アプリの開発まで様々な分野に強みをもっており、お互いに補完し合える関係にあります。
グルンパ発足にあたり、Groongaに関するサービスを提供する企業とグルンパの活動を紹介するサイトであるgroonga.bizを公開しました。サイトの制作はディジティ・ミニミさんが無償で担当しています。そして2/9にグルンパ発足のプレスリリースを行いました。PR TIMESというサービスを利用して120余りのメディアに配信し、39のサイトに掲載されました。またプレスリリースのページヘのアクセス数も1500を超えました。
グルンパの検討を始めてから、メンバー企業によるGroonga提案の機会が増えてきました。Groongaに関する新たなサービスの企画も始まっています。クリアコードでもいくつかGroongaに関するお問い合せをいただいており、従来のGroongaインシデントサポートサービスに加えて、メンバー企業と連携した検索システムの受託開発も提案しています。昨年まではサポート内容をGroongaインシデントサポートサービスで対応できる範囲に狭めようと交渉していましたが、今はメンバー企業と連携することを前提にお話ができるようになりました。
2/9にグルンパが発足し、Groongaに関するサービスを提供する様々な企業が存在することを紹介することができました。またメンバー企業においてはGroongaを提案する体制が整い、幅広い提案ができるようになりました。
クリアコードでは、引き続きGroongaインシデントサポートサービスを販売するとともに、メンバー企業と連携してGroongaや現在開発に力を入れているDroongaの導入案件を手がけていきます。クリアコードならびにメンバー企業各社では、Groongaの導入を検討中あるいは導入済みの法人ユーザさまからのお問い合わせを引き続きお待ちしています。各社への問い合わせ先はパートナー紹介ページをご確認ください。
*1 当初はクリアコード単独で提供することを検討していましたが、十分なサポート体制を確保するため3社で連携しサービスを提供することになりました。