プログラム書くときにファイル分割するのめんどくさいのに、どうしてわざわざファイル分割するのさ

と、思って居た時期が私にもありますし何なら今でも「まーーー小さいプログラムだからいっか☆」って言って一枚で書いちゃおうとする悪癖があります。よくないですね。

ファイル分割して書くの、めんどくさいよねぇ

例えば

def main(input_price: int):
    price: Price = Price(input_price)
    return calc_taxin(price)

def calc_taxin(price: Price):
    return price.get_taxin(1.1)

class Price:
    def __init__(self, price: int):
        self.price = price
    
    def get_taxin(self, tax_rate: int):
        return self.price * tax_rate

みたいな構成の、小さなプログラムがあったとします。

メインと、税率を設定するところと、金額を取り扱う為のクラス。

いやー、こんなもんね、なんならわざわざcalc_taxin()部分切り出す必要もなくて、どころかPriceクラスなんか使わないで

def main(input_price: int):
    return price * 1.1

で、十分じゃないねえ、って、思うじゃない。思うよ。思うわ。

これをちゃんとファイル分割してプログラムにすると、

# main.py
from service.calc import calc_taxin
from model.price import Price

def main(input_price: int):
    price: Price = Price(input_price)
    return calc_taxin(price)
# service.calc.py
from model.price import Price

def calc_taxin(price: Price):
    return price.get_taxin(1.1)
# model.price.py
class Price:
    def __init__(self, price: int):
        self.price = price
    
    def get_taxin(self, tax_rate: int):
        return self.price * tax_rate

で、フォルダ構成は

main.py
service
    └ __init__.py  # import出来るようにするための空ファイル
    └ calc.py
model
    └ __init__.pyprice.py

……と、ファイルやフォルダはモリモリ増える、依存関係の都合で、テストのためにcalc.pyを直接

$ python calc.py

で実行しようとするとPriceがインポートエラーになり、かといってcalc.pyを直に実行出来るように

from ..domain.price import Price

と相対インポートするとmainから呼びだした時にインポートエラーになって、「うがぁ」ってなるしで めんどくさい。だって2行だよ、2行で済む話になんでこんなに沢山あれこれ書かないといけないのさ…………って、思うじゃん。思うよ。

んじゃ、ちょっと変更があったので修正をお願いしますね

食品の取り扱いも始めるんで、税率が8%の商品も入ってきます。なんとかして下さい。

って言われたとします。二行で書いた方は……

def main(input_price: int, tax_rate: int):     # 引数にtax_rateを追加
    return price * tax_rate                    # *1.1を*tax_rateに変更

とでも直しましょうか。税率貰えるなら、なんてことはない。

ファイル分割した方は

# main.py
def main(input_price: int, tax_rate: int):     # 引数にtax_rateを追加
    price: Price = Price(input_price, tax_rate) # Priceのコンストラクタにtax_rateも渡す
    return calc_taxin(price)

# service.calc.py
def calc_taxin(price: Price):
    return price.get_taxin()

# model.price.py
class Price:
    def __init__(self, price: int, tax_rate: int):  # コンストラクタ引数にtax_rateを追加
        self.price = price
        self.tax_rate = tax_rate
    
    def get_taxin(self):    # tax_rateはselfに入っているので不要になる
        return self.price * self.tax_rate

って感じで直しましょうか。(そもそも、calc_taxin()の中に1.1がべた書きしてあったのが良くなかったですね)

これだと、修正箇所多いなぁ、めんどくさいなぁ…… ってなりますねぇ。

もっと修正が入ってきました

新人が数字を全角で入れたらシステム落ちたんだけど

という報告が来ました。「まあ半角で入ってくるだろう」という見込みは外れました。修正が必要です。

def main(input_price: int, tax_rate: int):
    if not isinstance(price, int):    # priceが半角数字じゃなかったら早期リターン
        raise TypeError("価格が半角数字ではありません")
        
    return price * tax_rate

うん、まあ、大丈夫大丈夫。これくらいならオッケーオッケー。

一方ファイル分割した方は……

# main.py
def main(input_price: int, tax_rate: int):
    price: Price = Price(input_price, tax_rate)
    return calc_taxin(price)

# service.calc.py
def calc_taxin(price: Price):
    return price.get_taxin()

# model.price.py
class Price:
    def __init__(self, price: int, tax_rate: int):
        self.price = __varidate_price(price) # バリデーション掛けてから入れるようにする
        self.tax_rate = tax_rate  
    
    def __price(self, price: int):    # バリデーションを追加
        if not isinstance(price, int):
            raise ValueError("価格が半角数字ではありません")
        return price
    
    def get_taxin(self):
        return self.price * self.tax_rate

if文をPriceクラスの中に押し込んでおけています。ifはバグの元なので、できるだけ下位層に押し込んでおいて、影響範囲を広げたくないんですね。上位層にifがあると、条件指定ミスで処理一つ丸っと飛ばされちゃうとか発生しますし。

そしてさらに、

ねえ、なんか税率に5とか入ってきてるんだけど?

まさかの税率も手入力だったようで。入力値チェックを追加しましょう…………

def main(input_price: int, tax_rate: int):
    if not isinstance(price, int):
        raise TypeError("価格が半角数字ではありません")
        
    if tax_rate != 0.8 and tax_rate != 1.1:    # tax_rateが0.8でも1.1でも無かったら早期リターン
        raise ValueError("税率が間違っています")
        
    return price * tax_rate

べた書きの方はとりあえず、こんな感じ……?うーん、ifが増えてきて、きな臭くなってきた。

# main.py
def main(input_price: int, tax_rate: int):
    price: Price = Price(input_price, tax_rate)
    return calc_taxin(price)

# service.calc.py
def calc_taxin(price: Price):
    return price.get_taxin()

# model.price.py
class Price:
    def __init__(self, price: int, tax_rate: int):
        self.price = __varidate_price(price)
        
        # こちらもバリデーション掛けてから入れるようにする
        self.tax_rate = __varidate_tax_rate(tax_rate)
    
    def __price(self, price: int):
        if not isinstance(self, price, int):
            raise ValueError("価格が半角数字ではありません")
        return price
    
    def __varidate_tax_rate(self, tax_rate: int):    # バリデーションを追加
        if tax_rate != 0.8 and tax_rate != 1.1:
            raise ValueError("税率が間違っています")
        return tax_rate
    
    def get_taxin(self):
        return self.price * self.tax_rate

ファイル分割した方は、if文が増えてもPriceクラスの中に押し込んでおけて、きな臭さが表に出てきません。良い感じです。

さらに修正が加わります

ねえ、やっぱり表示用に¥マークとカンマ区切り付けた状態のデータちょうだいよ

と言われました。現実世界だったら「バーロー、それはこっちの業務範囲じゃねえよ、てめえのプログラムで表示整えろ」と言いたいところですが、ここはサンプルワールドなので残念ながらその理不尽を受け入れます。

def main(input_price: int, tax_rate: int):
    if not isinstance(price, int):
        raise TypeError("価格が半角数字ではありません")

    if tax_rate != 0.8 and tax_rate != 1.1raise ValueError("税率が間違っています")
    
    taxin_price = price * tax_rate    

    return f'{price:,}'

わーい、これはいよいよ本格的にきな臭くなってきました。でもファイル分割めんどくさいからこのまま行きましょう。

# main.py
def main(input_price: int, tax_rate: int):
    price: Price = Price(input_price, tax_rate)
    return calc_taxin(price)

# service.calc.py
def calc_taxin(price: Price):
    return price.get_formatted_taxin()    # get_formatted_taxinを使うように変更
    

# model.price.py
class Price:
    def __init__(self, price: int, tax_rate: int):
        self.price = __varidate_price(price)
        self.tax_rate = __varidate_tax_rate(tax_rate)
    
    def __price(self, price: int):
        if not isinstance(price, int):
            raise ValueError("価格が半角数字ではありません")
        return price
    
    def __varidate_tax_rate(self, tax_rate: int):
        if tax_rate != 0.8 and tax_rate != 1.1:
            raise ValueError("税率が間違っています")
        return tax_rate
    
    def get_taxin(self):
        return self.price * self.tax_rate
    
    def get_formatted_taxin(self):    # フォーマットするメソッドを追加
        return f"{self.get_taxin():,}"

ファイル分割をした方は、Class内に新たな関数を追加することで対応します。そして、サービス層のcalc_taxinで返すものを、get_taxinの結果からget_formatted_taxinの結果に変更しました。

怒られました

なんかいつの間にか数値じゃなくて¥とかカンマとかついてるデータが出てくるようになったんだけど?!こっちのプログラムがバグ出すから戻して!!

と、別の人から言われました。そんな理不尽な。先にその辺確認してから改修依頼出してくれよ、と思いますが世の中そう上手くは行かないものです。

でも、カンマ区切りのデータが必要な人は居るわけです。つまり、入口を二つにして、こっちの入口から入ってきた人にはカンマ区切りを、そっちの入口から入ってきた人にはプレーンテキストをお渡しするように大改修が必要になりました。殺生な。

一枚で書いていた方は、もうどうしようもありません。

def main_plain(input_price: int, tax_rate: int):
    if not isinstance(price, int):
        raise TypeError("価格が半角数字ではありません")

    if tax_rate != 0.8 and tax_rate != 1.1raise ValueError("税率が間違っています")

    return price * tax_rate 
 
 def main_formatted(input_price: int, tax_rate: int):
    if not isinstance(price, int):
        raise TypeError("価格が半角数字ではありません")

    if tax_rate != 0.8 and tax_rate != 1.1raise ValueError("税率が間違っています")
    
    taxin_price = price * tax_rate    

    return f'{price:,}'

一枚で書いていたのでもうどうしようもありません。mainをコピーして増やして返す値だけ変えました。

いや、さすがに、全く同じ処理が二回出てくるのは良くない。せめて関数の切り出しくらいはしてあげましょう……

def main_plain(input_price: int, tax_rate: int):
    return calc_taxin(price, tax_rate)
 
def main_formatted(input_price: int, tax_rate: int):
    taxin_price = calc_taxin(price, tax_rate)
    return f'{taxin_price:,}'

def calc_taxin(price: int, tax_rate: int):
     price= varidate_price(price)
     tax_rate = varidate_tax_rate(tax_rate)
     return price * tax_rate
         
def varidate_price(price: int):
     if not isinstance(price, int):
        raise TypeError("価格が半角数字ではありません")
     return price
 
def varidate_tax_rate(tax_rate: int):
     if tax_rate != 0.8 and tax_rate != 1.1raise ValueError("税率が間違っています")
     return tax_rate
 

うん……うん……うん、まあ。なんとか……?同じ処理を繰り返さないようにすると、バリデーション処理が奥の方に入って行ってしまって、ちょっとよろしくないなぁ、とは思いますが……(入力値に問題がある場合、処理には掛けずにさっさとリターンしてしまいたいですよね)

一方、最初からファイル分割していた方はというと……

# main_plain.py
def main_plain(input_price: int, tax_rate: int):
    price: Price = Price(input_price, tax_rate)
    return calc_taxin(price)

# main_formatted.py    # フォーマット済みの価格を取るための入口を増やす
def main_formatted(input_price: int, tax_rate: int):
    price: Price = Price(input_price, tax_rate)
    return format_taxin(price)

# service.calc.py    # calcは純粋に計算するだけに戻す
def calc_taxin(price: Price):
    return price.get_taxin()

# service.format.py    # フォーマット済みの価格を取得するサービスを増やす
def format_taxin(price: Price):
    return price.get_formatted_taxin()

# model.price.py
class Price:
    def __init__(self, price: int, tax_rate: int):
        self.price = __varidate_price(price)
        self.tax_rate = __varidate_tax_rate(tax_rate)
    
    def __price(self, price: int):
        if not isinstance(price, int):
            raise ValueError("価格が半角数字ではありません")
        return price
    
    def __varidate_tax_rate(self, tax_rate: int):
        if tax_rate != 0.8 and tax_rate != 1.1:
            raise ValueError("税率が間違っています")
        return tax_rate
    
    def get_taxin(self):
        return self.price * self.tax_rate
    
    def get_formatted_taxin(self):
        return f"{self.get_taxin():,}"

主要な処理はPriceに押し込めてあるので、残りの部分を増やしてあげればOKです。この辺になってくると圧倒的に分割してある方が話が早くなってきます。

税率が変わったとしたら

税率が変わりました。今まで1.1で計算していたところを1.2にしてください。

ここはサンプルワールドなので消費税が上がります。現実世界で上がったら私は暴動を起こしますが、サンプルワールドなので粛々と上がります。

さて……プロジェクト内のどのファイルのどこを直したら良いでしょう

ファイル分割してある方は、価格にまつわることなのでPriceクラスが置いてあるPrice.pyを見れば良さそうです。仮に自分が作ったプログラムじゃなかったとしても、「まあ大体この辺見ればその辺の処理が書いてあるだろ」って当たりが付けられます。

しかも、ファイルが分割してあるので、1つのファイルの記述は短く、「ここだ、ここじゃない」を判断するための見通しも良くなります。

さらに可能なら、この改修のときにTaxクラスなんか作っちゃってもいいかもしれませんね。この後さらに消費税が撤廃されたりしても、Taxクラスを変えれば良いとすぐ分かります。

しかし分割していない方は、まず価格計算ロジックがどこに書いてあるのか、上から順番に見ていかないとわかりません。さらに、一箇所直しても、本当にそれで全部なのかもわかりません。最後まで全部読まないといけません。めんどっくさ。

ファイルを分割しておくと保守がすごく楽

フォルダ名・ファイル名を見ただけで「どこにどの処理が書いてあるか」が分かるのは、保守・改修をする上で非常に便利です。

逆に言うと、一度作ったが最後二度と改修することなく、数ヶ月で使い捨てる様なプログラムだとしたら、そこまで神経質にファイル分割する必要はないということです。

ただ、世の中往々にして「こんなはずじゃなかったのに」になりがちです。「ちょっとしたプログラム」のつもりで作り始めたものが、気がついたらどんどん大きくなって行くということはありがちです。

ですので、まあ、本当に1~2回で使い捨てる様なプログラムでもなければ、ある程度は分割して作っておいた方が将来の自分の為になります。

って、3年前の自分に言いたいプロジェクトがいっぱいあるんだよねぇ………………

この記事が気に入ったらサポートをしてみませんか?