定期的に繰り返し実行する簡単ではないお仕事


いやー、この問題は本当に難しい。難しすぎて、どうやって解決すればいいかいまだによくわからない。わからないので、ここに書いてみる。


最初、とあるお客さんのために「ひよこの餌やりプログラム(仮)」を作っていたんだ。開始ボタンを押すとひよこの餌が出てくる。たったそれだけのプログラム。


今回は、これを「定期的に実行する機能が欲しい」と言われた。
この要望を実現するのがすこぶる難しかったんだ。


「やねうらおってそんなプログラムすら書けないの?老害なの?」


とか言わないで欲しい。この問題、本当に難しいんだよ!


■ 1度目のひよこの全滅


まず、この要望に沿って、私の会社のプログラマが当初、次のようなダイアログをつけたわけだ。



繰り返し実行のところにチェックを入れた場合、ここで指定された時間後にも繰り返し実行する。単位は分で指定する。1日ならば60×24 = 1440を指定する。そうすると、ひよこの餌やりのタスクが終わったあとに1440分待機するような動作になる。


ところが、このプログラムを使ったところ、ある日、ひよこの餌が出てこなくて、ひよこが全滅した。これにはお客さん、カンカンで…って、以前似たような話を書いたが*1、今回のケースは少し状況が違うので順番を追って書いていく。



あらかじめ予告しておくけど、今回はこのあとひよこは何度も全滅するんだぜ…。



さて、なぜ、最初ひよこが全滅したのかその原因を書いておこう。

次図をご覧の通り、お客さんのイメージでは24時間ごとに餌やりをして欲しかったわけだ。お客さんの言う「定期的に」と言うのは、「(餌やりのタイミングが)一定間隔になっていて、その間隔が調整できること」が本来の要件だった。



ところがこの要件が正しくヒアリング出来ていなくて弊社プログラマが次図のように実装してしまった。



こういう実装にしてしまうと「ひよこの餌やり」タスクが長引くと次の餌やりのタイミングが遅延してくる。間違った実装と言えるかも知れないが、後者のほうがお手軽な実装だし、タスクの内容によってはこれで事足りる場合もあるから、これがありえないような実装というわけでもないとは思う。しかし、今回のタスクはこの実装ではまずく結果としてひよこが全滅したということだ。


ここまではヒアリング不足、要求仕様の理解不足と言えると思う。


■ 2度目のひよこの全滅


そこで、上の前者の図(お客様の要望)のようになるようにプログラムを修正した。UIはさきほどと同一である次のUIのままである。



しかし、ここでお客さんから待ったがかかった。


前回のことで煮え湯を飲まされたお客さんは「あと何分後にひよこの餌やりタスクが実行される予定なのか画面に表示させて欲しい」という追加仕様を出してきたのである。


これは「さもありなん」という感じで、「(次のタスクの実行が)あと何分後」という表示があれば、次のタスクの実行がきちんと予定されていることが目で見てわかるし、また、その表示があれば上の後者の図ではなく前者の図のように実装されているかのチェックにもなる。なるほど、我々は最初からこのような表示をするべきだった。


ということで次図のようなダイアログに変更した。



それで気分よく運用していたお客さんであったが、ある日、Windows UpdateがあってOSが再起動した。これでひよこはまたもや全滅した。


今回製作しているアプリ自体はOS起動時に自動的に起動するようになっている。アプリが起動していなかったので餌やりタスクが実行されなかったとかではない。


またアプリは定期的に状態を保存して(シリアライズして)、起動時に状態を戻す(デシリアライズする)。ゆえに、UI上の数値などはアプリを終了〜再起動した場合も前回の状態のままである。アプリが設定されたUI上の数値を保存していなかったなどでもない。


それでは原因は何かと言うと、「次回の実行は XXXX分後です」の数字はインターバルタイマーで定期的に(1分ごとに)1を引く実装になっており、ここが0になった時点で再度タスクが実行されるという実装にしてあった。


ところが、この実装だとアプリを終了させ、次回起動するまでの時間はノーカウントになってしまう。ゆえに、そのノーカウントである時間の分だけ餌やりのタイミングが遅れてしまうわけだ。


ついでに言えばWindows Updateのあと、お客さんとしてはまだ実行を開始する時間じゃないからしばらくパソコンの電源を切ったままにしていたらしいのだが…。


そこで正しい実装としては次のようになっているべきだったのだ。


(1) タスクを開始した時点で、
次回予定実行時刻 = タスク開始時刻 + 何分後に実行するのかのテキストボックスで設定された時間
とやって次回予定実行時刻というのを算出する。


(2) アプリ終了時にはこの次回実行時刻を保存しておき、アプリ起動時にはこの次回実行時刻を復元する。


(3) 「次回の実行は XXXX分後です」という表示は
XXXX = 次回予定実行時刻 − 現在時刻
とやって算出して表示するべきだし、XXXXが0を下回って、かつ、「繰り返し実行する」のチェックボックスにチェックが入っていれば、そのタイミングで再度タスクが実行される。


これで万事うまく行くように思えた。ところが…。


■ 3度目のひよこの全滅


このプログラムを長時間動作させているとまたもやひよこが全滅した。


お客さんは、昼の12:00から24時間ごとにタスクを実行するつもりで1440分と設定していた。


なのに次第にタスク開始時刻が日に日に数秒ずつずれて来て、長期間運用すると何時間も開始時刻が昼の12:00からずれてしまっていた。


これは、
(4) (3)でタスクが再実行されたとき、
タスク開始時刻 = 現在時刻
としていたためだ。ここは、
タスク開始時刻 = 次回実行予定時刻
としなければタスク再実行〜現在時刻の取得までの分の誤差が累積してしまうというのが原因だった。


3度目のひよこの全滅を目の当たりにしたお客さんは「毎日昼の12時にひよこの餌をやって欲しいだけなのに、なんでこんな簡単なこと出来ないの?馬鹿なの?死ぬの?」と怒り心頭に発した。


さて、(4)の修正をして、さすがに三度目の正直、これでうまく行くだろうと安易に考えていたのだが…。


■ 4度目のひよこの全滅


お客さんは、タスクの開始時刻が12:00からずいぶんずれてしまったので、明日はまた12:00から開始したいとおっしゃるわけである。しかし12:00にパソコンの前に正座して開始ボタンを押したくはないので、明日の12:00に開始されるように設定できないのかと言われたわけだ。


ああ、わかりました。設定できるようにしましょう。
ということで、「次回の実行は XXXX分後です」と表示されていたXXXXの部分をテキストボックスにしてここの値を変更できるようにした。



ここの値は自由に変更できるので残り時間が明日の12:00までの時間になるように設定してもらえればいいですよ、と。


そこでプログラムには次の処理を追加した。


(5) 「次回の実行は XXXX分後です」のXXXXの部分がユーザーによって変更された場合は、
次回実行予定時刻 = 現在時刻 + XXXX
と再計算する。


ところが、このようにアプリの仕様を変更したところ、ひよこがまた全滅した。お客さんが残り時間の計算を間違えて設定してしまったからだ。


お客さんいわく「昼の12:00に実行したいだけであってそのために残り時間とか電卓で計算したくないんだよ!全滅したひよこは弁償してもらいますからね!」だそうで、確かに使いにくいUIかも知れないが、そのUIでいいと言ったのもあんただし、計算を間違えたのはあんただろと思いながらも、やはりこのUIには問題があることは認めざるを得なかった。


すなわち、「次回実行は XXXX分後です」ではなく、最初から「次回の実行予定時刻は 2012/06/06 12:00:00です」のようにDateTimeが表示されているべきだったのだ。


そして、次回実行予定時刻を変更したいという要望に対しては、この値をDateTimePickerで選択できるようになっているべきだったのだ。


■ 5度目のひよこの全滅



以上の試行錯誤のすえ、我々が最終的に行き着いたUIは次のUIだった。



なるほど、これは素晴らしい。


お客さんがある日このアプリの起動を忘れていて12:10に起動した場合も、次回実行予定時刻はその日の12:00になっているので、即座にタスクは開始されたし、(4)の処理により、次回実行予定時刻は翌日の12:00になった。翌日も12:00になればきちんと処理が開始される。


これにはお客さんも「大変素晴らしいアプリを作ってくれた!」と掌(てのひら)を返したように大喜び。


ところが、喜びも束の間。数日後にまたもやひよこが全滅したのだ。


そうだ。お客さんが、ある日、ひよこ部屋の掃除のためにその日は手動で餌をやったのだ。


アプリの起動を2日後に起動させられたこのアプリは、二日分のタスクを連続して実行した。つまり通常の二倍の餌が1日で与えられたわけである。


我々は、アプリが起動時に2つ以上のタスクが実行される予定になっているときは警告ダイアログを出すようにした。つまり次の条件である。


(6) 警告ダイアログを出す条件
(次回実行予定時刻 <= 現在時刻 + XXXX * 2)
※ この条件を満たすとき、連続して2つ以上のタスクが実行されることになるので。


■ 6度目のひよこの全滅


ある日、このお客さんの会社の重役の人がこのアプリを別PCにもインストールして別の施設のひよこの餌やりにも使うことにした。その人は、繰り返し設定として1440分後に実行するように設定した。


しかし「次回の実行予定時刻」を設定するのを忘れた。この項目、初回起動時には1900/01/01 00:00:00になっている。


この状態で実行するとタスクが2つ以上連続実行されるという警告ダイアログが出る。


そこで次回の実行予定時刻のところを次回に実行して欲しい明日の12:00にセットした。


その結果…またもやひよこが全滅した。タスクが実行されなかったのだ。


そうだ。「次回の実行予定時刻」のところはタスクの実行を開始した瞬間にXXXXだけ加算される。つまり、この表示は「次回の実行予定時刻」だが、開始ボタンを押す前に設定すべき値は「(今回の)実行予定時刻」なのだ。いますぐ実行を開始したいなら現在時刻になっていなければならない。


この重役の人はカンカンで、「ここの値はアプリの初回起動時は現在時刻になっているべきではないのか」と言ってきた。なるほど、それもそうだ。


あと「次回の実行予定時刻」という表記も紛らわしい。
「2012/06/25 12:00:00 にひよこに餌をやるのを開始」というように変更した。


■ そして別のお客さんからのクレームの嵐


これらの処理を入れたのちは長期間この仕様で運用し、実績も出来、我々のUIもプログラムは完璧だと自負していた。


他のお客さんからも「定期的に実行したいんだけど」と言われれば、必ずこのUIを提供することにした。


ところが、他のお客さんにとっては、これが非常にわかりづらいらしく「もっとシンプルにならないの?」「何分後に実行するかだけ設定できればそれでいいんだけど?馬鹿なの?死ぬの?」など歴史は繰り返すわけである。


それらのお客さんもここに書いてきたような試行錯誤を経て、やがて理解はしてくださるわけだが、毎回毎回、「定期的に実行したいんだけど」と言われるごとにこれらの話を説明するのが面倒なのでこのように記事にまとめておく次第である。


■ まとめ


「定期的にタスクを実行する」ためにはどういうUIを提供し、どういう設計にすればいいのか、私が直面した課題を一通り書いた。これにより、簡単に出来そうな内容なのに一筋縄ではいかないことがはっきりわかったと思う。


私として、ここで書いたものが決定打だとは思っていないが、「タスクを定期実行したい」というありがちな要求仕様を満たす、一番シンプルなUIでありロジックだと思っている。ただし、お客さんからの受けはあまりよろしくない。


これよりいいUI/設計があるならコメント欄で教えていただきたい。


■ 2012/6/25 21:00 追記


twitterのほうでmsmhrtさんから「ひよこの餌やりプログラム(仮)」のUI、こんな感じでいかがでしょうか? → http://goo.gl/m4Pxu  」と提案いただきました。*2


これ、わかりやすいですね!こうやって皆さんの力で、「定期的に実行する」ためのスタンダードなUIが確立していくといいのですが。(他力本願)


■ 2012/6/25 21:30追記


はてブでお勧めされた。面白そうなのであとで読む。

誰のためのデザイン?―認知科学者のデザイン原論 (新曜社認知科学選書)

誰のためのデザイン?―認知科学者のデザイン原論 (新曜社認知科学選書)