Blog Entry  (March 14, 2017, 3:20 a.m.)

Tilo Mitra's avatar

「後工程はお客様」なコーティング

トヨタには「前工程は神様、後工程はお客様」という言葉があるそうです。この後半部分の「後工程はお客様」という言葉を拝借してコーディングで意識すべきだと思うことについて説明します。なお、トヨタが使っている本来の意味とはかけ離れていると思われますのでその点はご容赦ください。

呼び出し側がお客様という考え

まずはこの記事で使われる「後工程」という言葉について明らかにしておきます。この記事で使う「後工程」というのはウォーターフォールで使われるような工程の話だけではなく、APIの設計や関数の実装のようなものも含んでいます。APIや関数の呼び出される実装部分が前工程、呼び出し部分が後工程という考えです。実装される順番ではなく処理される順番ですので、後工程が前工程よりも先に実装されている場合もあります。

例えば、私達が業務効率化のために何かのコマンドを実装して実行する時、まず私達自身がお客様です。そのコマンドの main() 関数からは a() という関数が呼ばれるとします。その場合 a() の実装部分が前工程、呼び出している部分が後工程(=お客様)です。

なぜ「後工程はお客様」なコーディングをするのか

世の中には良いとされる実装やそれらを紹介する書籍がたくさんあります。そういったノウハウを学んで実践するのは良いことですが、同時に自分で良いコードとはどんなものかを考えることも大切だと思っています。自分で考えて良いコードを実装するときの心構えとして、そのコードを利用する人(自分・他人問わず)の側に立つことで、本に書いていないようなことでも自分で意識することができます。「お客様」という言葉はわかりやすく意識しやすい言葉です。

後工程をお客様とする実装

具体的な方法については「リーダブルコード」などを参考にするといいと思いますが、自分の意識している点を幾つか上げてみたいと思います。

適度な関数の分離

初心者にありがちな実装は一つの関数にあらゆる処理が上から下まで書かれているというものです。これは前工程も後工程もない状態です。コードが長くなってしまってとても見にくいです。

例えば、ある配列から奇数だけを抜き出しその合計を算出するプログラムを実装してみましょう。なお、Pythonを知っている人から見ると無駄な実装をしていますが、説明のためあえて非効率に書いてある部分があります。

def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    # 奇数を抜き出す
    odd_numbers = []
    for i in numbers:
        if i % 2 != 0:
            odd_numbers.append(i)

    # 合計を計算
    sum_number = 0
    for i in odd_numbers:
        sum_number += i

    print(sum_number)

この機能を実装する場合、お客様である main() からは「配列から奇数を抜き出す手続き」と「配列を合計する手続き」を実行すればいいということが理解できます。それが分かっていればまずは骨組みだけ実装できます。

def get_odd_numbers(numbers):
    # TODO: 正しく実装
    raise


def get_sum_number(numbers):
    # TODO: 正しく実装
    raise


def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    odd_numbers = get_odd_numbers(numbers)
    sum_number = get_sum_number(odd_numbers)
    print(sum_number)

最後に肉付けをします。繰り返しになりますがあえて長ったらしく書いています。 1

def get_odd_numbers(numbers):
    odd_numbers = []
    for i in numbers:
        if i % 2 != 0:
            odd_numbers.append(i)
    return odd_numbers


def get_sum_number(numbers):
    sum_number = 0
    for i in numbers:
        sum_number += i
    return sum_number


def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    odd_numbers = get_odd_numbers(numbers)
    sum_number = get_sum_number(odd_numbers)
    print(sum_number)

コード全体の量は最初のコードよりも多くなっています。しかし、呼び出し側の main がシンプルになり、それぞれの処理も名前がつけられたため何をしているのかがわかりやすくなります。わざわざコメントを買う必要もありません。この程度の実装であればメリットは小さいかもしれませんが、プログラムが大きくなっていくにつれメリットも感じられるようになります。

一つのことだけを行う関数

お客様目線では、何もかもをやってくれる関数があると便利に見えるかもしれませんが、実際にはそうとも限りません。前項で説明した、ある配列から奇数だけを抜き出しその合計を算出するプログラムの main() 関数は実はもっとシンプルにすることはできます。

def sum_odd_numbers(numbers):
    # 奇数を抜き出す
    odd_numbers = []
    for i in numbers:
        if i % 2 != 0:
            odd_numbers.append(i)

    # 合計を計算
    sum_number = 0
    for i in odd_numbers:
        sum_number += i

    print(sum_number)


def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    sum_odd_numbers(numbers)

main() 関数をシンプルにするという目的は達成されましたが、ほとんど最初に書いた複雑な main() 関数の名前が sum_odd_numbers() に変わっただけです。

ここで、計算の途中に奇数だけのリストを print するという要件が出てきた場合どうすればいいでしょうか。解決策の一つとして次のようなコードが書けます。

def sum_odd_numbers(numbers):
    # 奇数を抜き出す
    odd_numbers = []
    for i in numbers:
        if i % 2 != 0:
            odd_numbers.append(i)

    # 計算途中でprint
    print(odd_numbers)

    # 合計を計算
    sum_number = 0
    for i in odd_numbers:
        sum_number += i

    print(sum_number)


def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    sum_odd_numbers(numbers)

これはお客様である main からは本来変更できない箇所で、明らかに悪手です。仕様が増えるたびにメンテナンスしにくくなり、汎用性も低くなります。途中経過を print する関数としない関数を両方実装しなければならなくなったときにどうしようもなくなり、 sum_odd_numbers2() のように print を除いただけの関数を作るなど、どんどんドツボにハマっていきます。

2つの処理がしく関数が分離されている場合は、仕様の変更にも強くなります。

def main():
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    odd_numbers = get_odd_numbers(numbers)

    # 計算途中でprint
    print(odd_numbers)

    sum_number = get_sum_number(odd_numbers)
    print(sum_number)

どうしても main() をシンプルにしたい場合は、あくまで get_odd_numbers get_sum_number を利用した便利な関数として sum_odd_numbers を実装するのはありだと思います。

def sum_odd_numbers(numbers):
    odd_numbers = get_odd_numbers(numbers)
    sum_number = get_sum_number(odd_numbers)
    return sum_number

一連の処理をまとめて実行する実装

これまでは関数を分離することについて書きましたが、機能によっては分離すべきでないものもあります。RDBにデータを書き込む際にKVSにも書き込むという機能を考えてみましょう。

class RDB(object):
    def write(self, data):
        _write_to_rdb(data)


class KVS(object):
    def write(self, data):
        _write_to_kvs(data)


def main():
    data = 'spam egg'
    RDB().write(data)
    KVS().write(data)

お客様は神様ではありません。お客様はお客様です。つまり人間です。必ずミスを犯します。このデータを書き込む際は必ずKVSに書き込むという仕様を見逃しているかもしれませんし、何度も同じような実装をする中でたまたま実装し忘れてしまうかもしれません。解決方法としては色々あると思いますが、ここではFacadeパターンを取り上げます。


class RDB(object):
    def write(self, data):
        _write_to_rdb(data)


class KVS(object):
    def write(self, data):
        _write_to_kvs(data)


class DataFacade(object):
    def __init__(self):
        self.rdb = RDB()
        self.kvs = KVS()

    def write(self, data):
        self.rdb.write(data)
        self.kvs.write(data)


def main():
    data = 'spam egg'
    DataFacade().write(data)

このように、各システムにはFacadeを通してアクセスすることでお客様はRDBとKVSに必ず書き込むという処理を実装し忘れることがありません。

わかりやすい命名

お客様目線では、自分が知っていることは相手も知っているという考えではなく、初めてコードを見た人にも何をやっているかわかりやすくなければいけません。また、お客様はすべてのコードに目を通さず大まかにしか見ないかもしれません。そこで命名が重要になってきます。

例外によるフールプルーフ

商品を手に取る時、親切すぎる注意書きを目にすることがあります(「飴玉は喉に詰まらせないように注意してください」など)。供給する側が思いもよらないようなおかしな行動をする人は必ずいます。そのためにシステムの機能設設ではよくフールプルーフな設計がされますが、コーディングにおいてもそれは必要な概念だと思います。

フールプルーフとは、機器の設計などについての考え方の一つで、利用者が操作や取り扱い方を誤っても危険が生じない、あるいは、そもそも誤った操作や危険な使い方ができないような構造や仕掛けを設計段階で組み込むこと。また、そのような仕組みや構造。
2

例外処理という概念のある言語では、想定内のエラーは try catch などで例外処理するべきでしょう。例えば下のコードはデータベースに何度も同じデータを入れようとした場合に冪等性を保証しつつも、例外は発生させない例です。

def insert(data):
    try:
        _insert(data)
    except AlreadyExistsError as e:
        print(e)

しかし、この関数を使う人(=お客様)の実装ミスによる想定外のエラーは適切に例外を発生させるべきです。

def insert(data):
    if type(data) != dict:
        raise TypeError

    try:
        _insert(data)
    except AlreadyExistsError as e:
        print(e)

関数内のコメントで「ここにはdictを入れること!!!」などと書いてもそのコメントは読まれないと考えたほうがいいです。 assert を使うのもいいかもしれません。

使い方が明確な関数

Pythonでの話になりますが、Pythonは関数の引数を可変長にできます。これは便利なのですが場合によっては非常に見にくくなります。

def spam(*args, **kwargs):
    egg1(*args)
    egg2(kwargs.get('x'))
    egg3(kwargs.get('y'))

この例では引数をなんでも受け入れ、それをそのまま別の関数に渡しています。このような実装をしてしまうと、この関数を使いたい人(=お客様)は関数で使っている関数まで遡って複雑なコードを読み込まなければ何が必要な引数なのかがわかりません。可変長にする必要が無い場合は何を指定するかを明確にした方が良いです。

def spam(a, b, c, x=None, y=None):
    egg1(a, b, c)
    egg2(x)
    egg3(y)

(ちなみに引数名はあくまで例のためですので、 a b c などは好ましくありません。)

別ファイルによる個別設定

一つのコードが複数の環境で動くことは珍しくありません。環境ごとに設定を変える必要がある場合、コード内にバラバラに散らばった固定値をいじるのではなく、一箇所にまとまった設定ファイルで更新できると便利な場合があります。お客様にはなるべく面倒なことをさせずに、簡単なフォーマットの設定ファイルを提供すると便利に思ってもらえるかもしれません。

おわりに

さて、ここまでは具体的に実装について書きました。この他にも直交性・DRY・スコープ…など書きたいことはたくさんありますが、余白が足りなくなるのでこれ以上は「達人プログラマー」や「リーダブルコード」を読んでいただきたいと思います。

ここに書いてきたことに反対する意見や足りないという意見があるかもしれませんが、始めの方に書いたように、ここでお伝えしたかったのはこういった実装例そのものではなく、自分で良い実装はどんなものかを考えることです。

小さな関数の実装からAPIの設計、プロジェクト全体の開発、実際の取引先との取引など、それぞれのフェーズにおいていろいろなノウハウがあり、書籍等にもまとめられています。どのフェーズにおいても自分なりの考えを持つことで、それらの様々なノウハウを参考にしつつもそれをただ鵜呑みにするだけではなく、反対する場合は反対することができると思います。そうやって意見交換やレビューすることで良いものが出来上がっていくのではないでしょうか。


  1. こんな感じで書ける print(sum([i for i in range(1, 11) if i % 2]))

  2. IT用語辞典 フールプルーフ http://e-words.jp/w/%E3%83%95%E3%83%BC%E3%83%AB%E3%83%97%E3%83%AB%E3%83%BC%E3%83%95.html (2017年3月3日参照)

元の記事へ