UECジャーナル

ユニケージエンジニアの作法その六 「1プログラム1役」を徹底せよ

「1人1役」ならぬ「1プログラム1役」

コンピューターは人より遥かに正確かつ素早く仕事をこなす。しかしそれは、1つプログラムに何役もの仕事をさせるべき理由にはならない。1台のPCで多くのプログラムを走らせるのはよいかもしれないが、1つのプログラムに与える役目は1つにすべきだ。理由は、プログラムとはPCだけでなく、人間も読むからである。

1プログラム1役には、次のような利点がある。

一度に把握すべきプログラムの規模が小さくなる
関数を100行以内に収めよと言われる理由と同じ。人が一時的に把握していられる物事の量は少ない。プログラムが長過ぎると、終盤の処理を把握しようとしている間に序盤で把握した処理を忘れてしまう。こうした人の限界を重視するなら、実社会で人1人が与えられる作業量以上の役割を1つのプログラムに与えるべきではない。
データの確認が容易になる
作法その五に添っていれば、プロセス間通信にはファイルが使われる。するとデータはファイルという容器によって一旦固定化される。これは途中に流れるデータの確認が容易になることを意味する。
処理が滞った際、途中から再開できる
データをファイルとして固定化しておけば、内容確認のみならず、部分的に実行するということも行いやすくなる。もし、作業1から作業3までを1つのプログラムでやっていて、作業2に問題があったとする。単一のプログラムだと、修正後に作業1からプログラムを実行する必要がある。これに対し、プログラムを分割してあるなら、作業2からの実行で済む。

このように、プログラムを分けることにはさまざまな利点がある。理想的な役割分担を実施すべきだ。

1度にすべきことが1役ではない

1プログラム1役の利点は理解できても、気付かぬうちに2役も3役もこなすようなプログラムを書いてしまっているということは多い。たとえば、次の例を考えよう。

Webブラウザから利用するWeb投票アプリケーションの改良をする。分けることの利点を知ったため、一般ユーザー向けCGIスクリプトを閲覧用、投票用の2つに分けることにした。

ここでは2つに分けることが提案されているが、それは不十分だ。投票用CGIスクリプトについて考えてみよう。投票という処理は、ブラウザから投票先の項目名を受け取って、これを得票データベースに書き込むという処理から構成される。「受け取る」という処理と「書き込む」という処理は、それぞれ別プログラムに任せてもよい。実社会での「選挙」を手本にするなら、投票所の受付者と開票作業者は別々の作業者とみなせる。

つまり次のように分ければよい。投票プログラムはCGIスクリプトとして動作させ、投票受付しかしない。ブラウザから受け取ったCGI文字列データを一意なファイルに書き出す処理だけをする。新しく開票プログラムを追加し、cron(8)で定期的に実行させ、ファイルからデータを読み取り、得票データベースに書き込んだのち、ファイルを削除する。

このような設計にすれば、投票処理部分・開票処理部分に不具合があった場合に個別に対処できる。不正投票対策を講じるとして改良を加えるなどの場合も、片方を動かしたまま作業をすることが容易になる。

プログラムの分割を実施したつもりが、実はうまく分割できていないという事はよくあることだ。物事の理解と分割というのは経験もスキルも必要になる部分なので仕方のないところはある。少なくとも、役割ごとに分けることがとても重要であるということを覚えておこう。

実戦における例

ユニケージエンジニアが実戦で書いた例を紹介する。リスト2~4がそれだ。すべて1つにまとめて書くとリスト1となる。

#!/bin/sh
#======================================================================
#リスト1.URE_TOTAL_RATIO_MASKER.sh (本日の実店舗・通販売上から、累積売上実店舗/通販比率 の作成)
#======================================================================

# 変数定義
indir=/INPUT; outdir=/OUTPUT
today=$(date +%Y%m%d)
yday=$(date +%Y%m%d -d"yesterday")

# ================================================
# 本日の実店舗売上が届いており、かつ本日までの
# 累計売上が作成されてない(完了印がまだ無い)場合
# ------------------------------------------------
if [   -e $indir/REAL_URE.$today.sem        -a \
     ! -e $outdir/REAL_URE_TOTAL.$today.sem    ]
then

  # 本日までの実店舗累計売上データを作る
  # (入力:実店舗の売上データ)
  # 1:日付 2:店舗コード 3:品番 4:販売数 5:販売額
  delf 1 2 $indir/REAL_URE.$today              |
  sort                                         |
  up3 key=1 $outdir/REAL_URE_TOTAL.$yday       |
  sm2 1 1 2 3                                  \
  > $outdir/REAL_URE_TOTAL.$today
  # (出力:実店舗の累計売上帳票)
  # 1:品番 2:販売数 3:販売額

  # 作業完了印をつける
  touch $outdir/REAL_URE_TOTAL.$today.sem

fi

# ================================================
# 本日の通販売上が届いており、かつ本日までの
# 累計売上が作成されてない(完了印がまだ無い)場合
# ------------------------------------------------
if [   -e $indir/EC_URE.$today.sem          -a \
     ! -e $outdir/EC_URE_TOTAL.$today.sem      ]
then

  # 本日までの通販累計売上データを作る
  # (入力:通販の売上データ)
  # 1:日付 2:品番 3:販売数 4:販売額
  delf 1 $indir/EC_URE.$today                  |
  sort                                         |
  up3 key=1 $outdir/EC_URE_TOTAL.$yday         |
  sm2 1 1 2 3                                  \
  > $outdir/EC_URE_TOTAL.$today
  # (出力:通販の累計売上帳票)
  # 1:品番 2:販売数 3:販売額

  # 作業完了印をつける
  touch $outdir/EC_URE_TOTAL.$today.sem

fi

# ================================================
# 2つの累計売上データ(作業完了印)があった場合
# ------------------------------------------------
if [ -e $outdir/REAL_URE_TOTAL.$today.sem   -a \
     -e $outdir/EC_URE_TOTAL.$today.sem        ]
then

  # 通販比率を計算する
  loopj num=1 $outdir/REAL_URE_TOTAL.$today    \
              $outdir/EC_URE_TOTAL.$today      |
  # (中間データ)                               #
  # 1:品番       2:実店舗販売数 3:実店舗販売額 #
  # 4:通販販売数 5:通販販売額                  #
  awk '{print $1,$2,$4,($4==0?0:$2/$4)}'       |
  marume 4.2                                   \
  > $outdir/URE_RATIO.$today
  # (出力:実店舗/通販比率)
  # 1:品番 2:実店舗販売数 3:通販販売数 4:実/通比率

  # 作業完了印をつける
  touch $outdir/URE_RATIO.$today.sem

fi
#!/bin/sh
#======================================================================
#リスト2.REAL_URE_TOTAL_MASKER.sh
#(本日実店舗売上から、①累積実店舗売上 の作成)
#======================================================================

# 変数定義
indir=/INPUT; outdir=/OUTPUT
today=$(date +%Y%m%d)
yday=$(date +%Y%m%d -d"yesterday")

# 本日の実店舗売上データの到着を待つ
while sleep 60; do
  [ $today -lt $(date +%Y%m%d)    ] && exit 1
  [ -e $indir/REAL_URE.$today.sem ] && break
done

# 本日までの実店舗累計売上データを作る
# (入力:実店舗の売上データ)
# 1:日付 2:店舗コード 3:品番 4:販売数 5:販売額
delf 1 2 $indir/REAL_URE.$today        |
sort                                   |
up3 key=1 $outdir/REAL_URE_TOTAL.$yday |
sm2 1 1 2 3                            \
> $outdir/REAL_URE_TOTAL.$today
# (出力:実店舗の累計売上帳票)
# 1:品番 2:販売数 3:販売額

# 作業完了印をつける
touch $outdir/REAL_URE_TOTAL.$today.sem
#!/bin/sh
#======================================================================
#リスト3.EC_URE_TOTAL_MASKER.sh
#(本日通販売上から、②累積通販売上 の作成)
#======================================================================

# 変数定義
indir=/INPUT; outdir=/OUTPUT
today=$(date +%Y%m%d)
yday=$(date +%Y%m%d -d"yesterday")

# 本日の通販売上データの到着を待つ
while sleep 60; do
  [ $today -lt $(date +%Y%m%d)  ] && exit 1
  [ -e $indir/EC_URE.$today.sem ] && break
done

# 本日までの通販累計売上データを作る
# (入力:通販の売上データ)
# 1:日付 2:品番 3:販売数 4:販売額
delf 1 $indir/EC_URE.$today          |
sort                                 |
up3 key=1 $outdir/EC_URE_TOTAL.$yday |
sm2 1 1 2 3                          \
> $outdir/EC_URE_TOTAL.$today
# (出力:通販の累計売上帳票)
# 1:品番 2:販売数 3:販売額

# 作業完了印をつける
touch $outdir/EC_URE_TOTAL.$today.sem
#!/bin/sh
#======================================================================
#リスト4.REAL_EC_RATIO_MASKER.sh
#(①実店舗累積②通販累積から、③実/通累積売上比率 の作成)
#======================================================================

# 変数定義
indir=/INPUT; outdir=/OUTPUT
today=$(date +%Y%m%d)
yday=$(date +%Y%m%d -d"yesterday")

# 本日までの実店舗・通販累積データの完成を待つ
while sleep 60; do
  [ $today -lt $(date +%Y%m%d)     ] && exit 1
  [ ! -e REAL_URE_TOTAL.$today.sem ] && continue
  [ ! -e EC_URE_TOTAL.$today.sem   ] && continue
  break
done

# 通販比率を計算する
loopj num=1 $outdir/REAL_URE_TOTAL.$today    \
            $outdir/EC_URE_TOTAL.$today      |
# (中間データ)                               #
# 1:品番       2:実店舗販売数 3:実店舗販売額 #
# 4:通販販売数 5:通販販売額                  #
awk '{print $1,$2,$4,($4==0?0:$2/$4)}'       |
marume 4.2                                   \
> $outdir/URE_RATIO.$today
# (出力:実店舗/通販比率)
# 1:品番 2:実店舗販売数 3:通販販売数 4:実/通比率

# 作業完了印をつける
touch $outdir/URE_RATIO.$today.sem

リスト2、3は起動した当日のそれぞれ実店舗・通販の売上データファイルにもとづき、その日までの累計売上データファイルを生成するプログラム。リスト4は2、3が生成したファイルに基づいて実店舗と通販の最新累積売上比率データファイルを生成するプログラムとなっている。

リスト1は、売上比率は各々の累積データの更新なしには作られないということで、1つにまとめる形で作ったもの。リスト1は入力データが到来する度に起動する必要があるが、リスト2~4は朝に一度起動するだけでよい。

実店舗・通販共に累計売上データは正しいのに売上比率がおかしいという障害が起こり、これを修正するケースを考える。分割している場合はリスト4だけ見ればよい。しかしリスト1では、すべての行を見ていって最後の方に問題があることを見つけなければならない。修正して動作確認を行う場合でも、分けているケースではリスト2、3を実運用と同様に稼働させたまま対応することができる。

分けることは、別けることに非ず

ユニケージエンジニアの作法には「分ける」作法が多い。「関数化して処理をまとめるより、1つ1つ同じ内容を書いて分けよ」という作法があったがこれもその1つだ。

しかし、まとまっていて意味を成すものを破壊的に分ける、すなわち「別ける」ことまでも是とするわけではない。データなど値のみならず他の値といかに関係しているかも含めて重要なものまで無闇に分けてはならない。関数を非とする理由も、それが上の行から下の行へという時系列による記述のまとまりを別ける存在だからと捉えれば、「別ける」はやはり非である。別けずに上手に分けることを常に心がけてほしい。

ユニケージユニバーサル・シェル・プログラミング研究所の登録商標。

usp Tukubaiユニバーサル・シェル・プログラミング研究所の登録商標。

※ 本ページで公開されているプログラムとそれに付随するデータの著作権およびライセンスは、特に断りがない限りOpen usp Tukubai本体と同じMITライセンスに準拠するものとする。

USP MAGAZINE 2013 winter「第五回 ユニケージエンジニアの作法」より加筆修正後転載。

Last modified: 2019-02-26 00:00:00