こんばんは。マル太です!
前回は、リーダブルコードの第14章「テストと読みやすさ」についてまとめました。
今回は、第15章「「分/時間カウンタ」を設計・実装する」について、まとめていきたいと思います。
どんな「カウンタ」を作るの?
この章では、過去1分間、1時間のWebサーバーへのリクエスト数をカウントする「分/時間カウンタ」を作成します。
「分/時間カウンタ」とは、以下のようなものです。
・直近1分間と1時間の転送バイト数を把握したい
・高パフォーマンスなアプリケーションにも耐えうる設計であること
問題を整理する
まず、「分/時間カウンタ」を作成するにあたって、どんな問題があるかを整理します。
1.パフォーマンス
高頻度でリクエスト数をカウントする必要があるため、処理速度が重要になります。
2.正確性
過去1分間、1時間のカウント数を正確に集計する必要があります。
3.保守性
コードが複雑になると、後々の修正や機能追加が難しくなります。
解決策を検討する
問題を整理した上で、解決策を検討していきます。
試案1:素朴な解決策
class Counter:
def __init__(self):
self.counts = []
def add(self, value):
self.counts.append((time.time(), value))
def get_count(self, seconds):
now = time.time()
total = 0
for t, v in self.counts:
if now - t <= seconds:
total += v
return total
このコードは、リクエストが来るたびにエントリを追加し、get_count()
が呼ばれるたびに、エントリを先頭から走査してカウント数を計算します。
この方法だと、エントリ数が増えるほど計算量が増えてしまい、パフォーマンスが悪いです。
試案2:ベルトコンベヤー設計
試案1のパフォーマンス問題を解決するアイデアとして、「ベルトコンベヤー設計」があります。
ベルトコンベヤー設計では、リングバッファと呼ばれる特別なデータ構造を使用し、固定長の配列とインデックスにて、データを順番に格納していく仕組みです。
データが追加されるたびに、インデックスが更新され、古いデータが新しいデータで上書きされます。
class Counter:
def __init__(self, size):
self.counts = [0] * size
self.times = [0] * size
self.size = size
def add(self, value):
index = int(time.time()) % self.size
self.counts[index] = value
self.times[index] = time.time()
def get_count(self, seconds):
now = time.time()
total = 0
for i in range(self.size):
if now - self.times[i] <= seconds:
total += self.counts[i]
return total
この設計にすることで、get_count()
の計算量を大幅に削減できます。
しかし、この設計にも問題点があります。
それは1時間よりも長い間隔の集計に対応できないことです。
試案3:時間バケツの設計
試案2での課題を解決するために、「時間バケツ設計」が紹介されています。
これは、固定長の時間間隔(バケツ)で集計を行う設計です。
この設計では、1分間、1時間といった集計間隔をあとから追加できます。
class BucketCounter:
def __init__(self, bucket_size):
self.buckets = []
self.bucket_size = bucket_size
def add(self, value):
now = time.time()
self.buckets.append((now, value))
self._remove_old_buckets(now)
def get_count(self, seconds):
now = time.time()
total = 0
for t, v in self.buckets:
if now - t <= seconds:
total += v
return total
def _remove_old_buckets(self, now):
self.buckets = [(t, v) for t, v in self.buckets if now - t <= self.bucket_size]
※時間バケツ設計の解説
ここでは、バケツと呼ばれる固定長の時間間隔で区切られたデータ構造でカウントを保持します。
各バケツには、開始時刻とその時間帯のカウント数が格納されます。
上記時間バケツ設計の良い点
・柔軟性
時間バケツ設計では、集計間隔を後から自由に変更できます。
例えば、1分間、1時間だけでなく、30秒間や12時間といった間隔でも集計できます。
・パフォーマンス
試案1のように毎回全てのデータを走査するのではなく、バケツ単位で集計を行うため、データ量が増えてもパフォーマンス劣化が少ないです。
・保守性
バケツの管理や集計処理がクラスとしてまとまっているため、コードの見通しが良く、保守がしやすいです。
以上の点から、試案1から試案3までの内、最終的に
「時間バケツ設計」
が一番実装するのに相応しいという結論になりました。
まとめ
今回は、過去1分間、1時間のWebサーバーへのリクエスト数をカウントする「分/時間カウンタ」を作成しました。
本章では、最終的に「時間バケツ設計」を採用しましたが、
重要なことは、「問題を正しく認識し、様々な解決策を試行錯誤すること」です。
また、それぞれの解決策にはメリット・デメリットがあるため、「要件に合った解決策を選択すること」も重要です。
このプロセスを辿ることで、より良いコードを作成できることを学びました。
最後に
最後までお読みいただきありがとうございました。
「リーダブルコード」解説ブログは、今回で最終回となります。
このブログでのアウトプットを通して、「リーダブルコード」への理解を深めることができたかなと思います。
この経験を活かして、今後も機会があれば、様々な技術情報の発信をしていきたいと考えています。