前章でも言いましたが、提供機能をインターフェースとして実装することで、メール送信を担うコードと送信対象ユーザーのデータを取得するコードの「より良い分離」を実現することができます。システムを、担当するタスク (機能) によって異なるユニットに明確に分離できれば、そしてそれぞれのユニットの他のユニットからの独立度合いが高ければ、システムの維持と拡張はその度合いに比例して容易になります。それぞれのユニットに対するインターフェースが固定されている限り、あるユニットの入れ替えや変更はシステムの他の部分には一切影響を及ぼさないからです。
現実世界の話を引き合いに出せば、COBOL 言語で記述され磁気テープ利用を前提として身動きできなくなっているシステムと、時代による変化を受け入れて更新され続けているシステムの違い、ということもできるでしょう。また、早い段階で行った1つ以上の設計上の決定が最適ではなかった場合に、コース変更の実施がより容易になる、という追加のボーナスもあります。
このデザイン原則は Separation of Concern (SoC: 関心の分離) と呼ばれているもので、この原則に基づいて設計したことに遅かれ早かれ感謝することはあれ後悔することはないはずです。ちゃんと覚えておいてくださいね。
ではここまでの話を我々自身のプログラムに活かしましょう。メール送信機能とユーザー情報取得機能を分離して実装します:
import csv
class UserFetcher:
def __init__(self, source):
self.source = source
def fetch_user(self):
with open(self.source, 'r', encoding='utf-8') as csv_file:
reader = csv.DictReader(csv_file)
users = [user for user in reader]
return users
import csv
import smtplib
from email.mime.text import MIMEText
from user_fetcher import UserFetcher
class Mailer:
def send(self, sender, recipients: list[str], subject, message):
msg = MIMEText(message, _charset='utf-8')
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ','.join(recipients)
with smtplib.SMTP(host='server_name', port=123) as mail_sender:
mail_sender.login('user_name', 'password')
errors = mail_sender.send_message(msg)
if isinstance(errors, dict) and len(errors) > 0:
print(f'Error: {errors}')
if __name__ == '__main__':
user_fetcher = UserFetcher('users.csv')
mailer = Mailer()
for x in user_fetcher.fetch_user():
mailer.send(
'email@example.com',
[x['email']],
'ご挨拶とお知らせ',
f'{x["surname"]} {x["name"]} 様\n良い一日をお過ごしください'
)
このように「正しく分離」された形に実装するにはちょっとした手間がかかるのは事実ですが、メール送信機能本体を一切変更することなく、ユーザーデータの取得源を PostgreSQL や Redis であったり、また、まるっきり外部の API 等に如何に簡単に変更できるかお分かりになると思います。
さて、ユーザーに電子メールを送信するための「素晴らしい」コードが記述できました。これを見たクライアントから、「Facebook や Twitter といったソーシャルメディアプラットフォームに新規投稿 (status updates) を送信できるようにもしてくれないか」と依頼があるかもしれません。
あなたは開発中のメール送信コードに変更を加えるのは気が進みません。そんな時に、投稿機能をサポートしてくれるパッケージ、もしくは、プログラムを見つけたとします。送信機能本体を変更することなくそれらを利用して多くの異なるプラットフォームへの投稿に対応できるようにするにはどのような投稿インターフェース (messaging interface) を構築すればいいのでしょうか?それとも、自分のニーズに合うようにサポートパッケージ等を利用せず一から作成すべきなのでしょうか?
この問題をより一般的なものとして表現してみれば次のようになるでしょうか: インターフェースはあるんだよ、でも私の要件を満たしてくれるものではないんだよ! (The interface I have is not the interface I want!)
ソフトウェア開発における他の頻出課題と同様、この問題に対してもデザインパターンが提唱されています。そのパターンに対するより直感的な理解を得るために、まず最初にちょっとしたサンプル問題を考えてみましょう。その後で、我々が直面している問題にそのパターンを適用してみましょう。
まずは極端に単純な例で考えます。外国に旅行するときに普段使用しているラップトップパソコンを持っていきます。その時には、行先の国のソケットに合うプラグアダプターを持っていかないと充電できませんね。こういったことは日常的に発生します。三股のプラグを二股ソケットに差すためのアダプター、パソコンの HDMI 出力を VGA モニタに接続するためのアダプター、その他諸々色々あるでしょう。こういった各種アダプターが「インターフェースはあるんだよ、でも私の要件を満たしてくれるものではないんだよ!」問題の「物理的」な解決例です。
もしシステム全体を一から作り上げるのであればこういった問題には直面しないでしょう。なぜなら、システムで使用する抽象化レベルも、システムのそれぞれのパートの関連性も、それを決定するのは自分自身だからです。
しかし現実世界では、全てのものを一から自分だけで作り上げる、ということはほとんど起こり得ません。サードパーティー製のパッケージやツール類を使用する必要があるでしょう。1年前に自分が記述したコードを、思ってもいなかった方法で拡張しなければならなくなるでしょう。その際、一からすべてをやり直す、などという贅沢は許されません。
数週間、または、数ヶ月単位で、あなたは自分自身が以前の自分よりもより良いプログラマーになっていることに気付くでしょう。しかしそれでもなお、自分が以前関わったプロジェクト (もしくは、そこで仕出かしているミス) をサポートするために駆り出される可能性は十分にあります。
手元にはないインターフェースが必要です。そうです、アダプターが必要なのです。