6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

いつの間にかPythonが軟派な言語ではなくなっていた~ロバストPython:拡張性~

6
Posted at

ごあいさつ

こんにちはNSS江口です。
少し前にロバストPythonの記事を書かせていただきましたが、今回はその中から拡張性について記事を書かせていただきます。
いつの間にかPythonが軟派な言語ではなくなっていた~ロバストPython~

拡張性

image.png

そもそも拡張性とは何かというと「システムの既存部分を変更せずに新機能を追加できるという性質」となります。皆さんも色々な現場で、

このシステムは拡張性が本当に低い。よくもこんなコードを書けるものだ。
エンジニアとして恥ずかしくないのか。

といった意見を多かれ少なかれ聞いたことがあるでしょうし、皆さんもこういった感想を抱いたことはあるかと思います。
私も毎回思っている気がします。ただ逆に

このソースコードの拡張性は本当に素晴らしい。このコードを書いた人とは仲良く出来そうだ。

という意見は不思議と聞いたことがないです。これは以下の特性がそうさせているものだと考えます。

  • プロダクトの悪い点を指摘するのは容易だが、良いものを生み出すのは困難
  • プロダクトの開発初期時点では、実際に拡張される箇所を予想することが難しい
  • 拡張性を重視するソースコードを作成したとして、拡張性の評価をできるほどその担当者が在籍していることが少ない

なので、私が満を持して拡張性の高いソースコードを作成したとして、全くもって的外れなこだわりかもしれないですし、想定していなかった箇所に修正が頻発するかもしれないですし、引き継ぎが上手くされず拡張性を生かすことができなくなってしまうかもしれません。
この記事においては以下のようなユースケースで拡張性を考慮した修正を行っていきます。(サンプルは少し変更します)

  1. お客さんからの注文をトリガとしてアプリが各担当者にその情報を通知する
  2. 1のついでに特定の連絡先(メールアドレス)にもメールを送る
  3. 別の連絡先(メールアドレス)にも通知を送れるようにする
  4. メールの送信だけではなく、情報を連携するAPIの呼び出しも行う
  5. メール・API以外にもテキストメッセージもサポートする

こういったユースケースの場合、複数の通知手段(ショートメール、Eメール、API)をUnionで統合し、複数の通知情報(特別料理、在庫切れ、賞味期限切れ、新メニュー)を同じくUnionに統合して、通知周りの設定を通知をつかさどるモジュールへ集約させることで拡張性を高めていました。

# 通知情報としてまとめる
NotificationType = Union[
    NewSpecial, IngredientsOutOfStock, IngredientsExpired, NewMenuItem
]

# 通知手段としてまとめる
NotificationMethod = Union[Text, Email, SupplierAPI]

# 通知を行う関数
def notify(
        method: NotificationMethod,
        notification: NotificationType):
    # 通知手段によって切り替える
    if (isinstance(method, Text)):
        __send_text(method, notification)
    elif (isinstance(method, Email)):
        __send_email(method, notification)
    elif (isinstance(method, SupplierAPI)):
        __send_to_suppier(method, notification)
    else:
        raise ValueError(f"サポートされていない通知手段です。{method}")

この手法であれば、確かにUnionへのクラスの追加と通知モジュールの改修でおおよその改修を最小限で行えるように思われます。
このコーディングの保守性が高いのは理解できるのですが、では自身でこのコーディングに辿り着けるかというととっかかりが少ない気がしましたので、自分ならどうやって高い保守性に辿りつくだろうかと考えました。
私はこういう時、AWSとかSpringとかのフレームワークを参考にするアプローチをとります。
ソースコードとAWSって結構かけ離れているように感じるかもしれないのですが、やはりシェアの高いサービスはこの辺もきちんと考慮してくれているので非常に参考になります。
AWSの場合例えば、処理主体がLambdaだったとしたら、以下のような構成になるかと思います。

image.png

AWSにおける構成の場合ですと、通知という内容をTopic、通知先をSubscriptionとして抽象化します。
これによりロジックと通知の種類、通知先を分離することができ、疎結合で拡張性の高い設計にできます。
この構成をPythonに落とし込んだ場合の登場人物は

  • ビジネスロジックを実行する人
  • 通知をとにかく行ってくれる人(SNS相当でNotifier)
  • 通知という内容を表す種別(Topic
  • 通知先を管理するオブジェクト(Subscription
  • 通知先を表すオブジェクト(Notification

AWSの場合サービスなので、JSONのような汎用的なコンテンツでやり取りする必要がありますが、プログラミング言語の良いところはを扱える所なので、この構成を型セーフにした上で落とし込むのが良いでしょうか。

# 通知内容
TopicContent = Union[
    NewSpecial, IngredientsOutOfStock, IngredientsExpired, NewMenuItem
]

# 通知のプロトタイプ
class Notification(Protocol):
    def notify(self, topic: TopicContent):
        '''実装不要'''

# テキストメッセージ
class TextNotification:
    phone_number: str
    def notify(self, topic: TopicContent):
        pass
# Eメール
class EmailNotification:
    email_address: str
    def notify(self, topic: TopicContent):
        pass
# API
class SupplierAPINotification:
    def notify(self, topic: TopicContent):
        pass

# 特別な処理
def declare_notification(
        dish: Dish, start_date: datetime, end_date: datetime):
    # 何らかの処理

    # 通知する
    notify(
        NewSpecial(dish, start_date, end_date))

def notify(topic: TopicContent):
    # 通知先を取得
    subscriptions: list[Notification] = provide(topic)

    # 通知を実行
    for subscription in subscriptions:
        subscription.notify(topic)

def provide(topic: TopicContent) -> list[Notification]:
    if (isinstance(topic, NewSpecial)):
        return __get_NewSpecial(topic)
    elif (isinstance(topic, IngredientsOutOfStock)):
        return __get_IngredientsOutOfStock(topic)
    elif (isinstance(topic, IngredientsExpired)):
        return __get_IngredientsExpired(topic)
    elif (isinstance(topic, NewMenuItem)):
        return __get_NewMenuItem(topic)
    else:
        raise ValueError(f"サポートされていない通知手段です。{topic}")

ロバストPythonのコーディングと何が異なったのかと言うと以下の点です。

  • Text,Email,SupplierAPIのUnionをやめ、Protocolを用いてインタフェース化
  • notifyの中で直接実行せず、ProviderによってTopic毎の通知先を取得するように変更

どうでしょうか?先述した「自分で保守性の高いコードに辿り着けそうにない」といった課題を解消できるアプローチとなるのではないでしょうか?
ロバストPythonで示されていた構成とは異なりますが、複数送信先の設定とそれに伴う通知ロジックの再利用を促すことができるのではないかと思います。
AWSの発想でいうと、TopicはEnumになるかと思うのですが、プログラムではせっかく型を使えるので、型のみにしました。これにより引数が一つで済むようになりました。
他にもNotificationをインタフェース化することにより、Topicによらず直接通知することもできることが良くなった点かと思います。
一見これで拡張性が高くなったかと思うのですが、こだわり過ぎかもしれないですし、予期していない修正により、すぐに使い物にならなくなるかもしれません。
ただここで言いたいのは世の中でシェアをとっている製品というのは多くのユースケースに出くわしている訳なので、個人が一から考える最適化よりよほど参考になるということです。

まとめ

Pythonのロバストネスを高めるための拡張性について勉強することが出来ました。
拡張性はプログラミングを行う際の設計力の見せ所なので、こだわりつつ得られた教訓というのはどんどん次に生かしてまいりたいと思います。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?