チャット送信の待機処理のコードを統一した(ChatSpooler開発記)

はじめに

macOS用のAIチャット自動送信ツールを開発していて、コード内に点在する待機処理が保守の妨げになる問題に直面しました。送信前の遅延、キュー間の待機時間、テスト送信のカウントダウンなど、似たような処理が複数箇所に散らばっていたのです。

この記事では、これらの待機処理を統一するアプローチを実践した経験を共有します。結果として、コードの保守性と拡張性が大幅に改善されました。

待機処理統一化でコード保守性が劇的改善 問題 送信遅延 キュー間隔 テスト送信 3箇所に散在 解決 WaitManager 統一インターフェース コールバック対応 効果 保守性: 1箇所修正で全体反映 一貫性: 統一操作方法 拡張性: 新機能追加が簡単 実装のポイント 統一前: 個別実装 × 3箇所 → 修正コスト大 統一後: WaitManager × 1箇所 → 修正コスト小 コールバック関数で柔軟性確保 ・ステータス更新 ・停止/スキップ制御

問題の発見

開発を進める中で、待機に関する処理が3つの異なる場所に実装されていることに気づきました。

まず、送信前の遅延処理です。ユーザーが設定した秒数だけカウントダウンしてから送信を開始する機能でした。次に、キューの各アイテム間の待機時間。これは送信完了後に次のアイテムを送信するまでの間隔を制御します。そして、テスト送信時のカウントダウン処理もありました。

それぞれの処理は独立して実装されていたため、修正が必要になると複数の場所を変更する必要がありました。これは典型的なコードの重複(DRY原則違反)でした。

最初の改善:テスト送信の簡素化

まず手をつけたのは、テスト送信のカウントダウン処理でした。実際に使ってみると、テスト送信時に3秒待つのは不便だと感じたからです。

テストは素早く実行できる方が開発効率が良いものです。カウントダウンを削除して即座実行に変更しました。

def test_send(self):
    """テスト送信(即座実行)"""
    text = self.text_area.get("1.0", tk.END).strip()
    if not text:
        messagebox.showwarning("警告", "送信する文章を入力してください")
        return
    
    method = self.method_var.get()
    target_app = self.target_app_name if self.target_app_name else self.selected_app_name
    
    # 即座実行(カウントダウンなし)
    self.update_status("テスト送信中...", "normal")
    success = self.core_manager.sending_controller.test_send(method, target_app, text)
    
    if success:
        log_message("テスト送信完了")
        self.update_status("テスト送信完了", "success")
    else:
        log_message("テスト送信失敗")
        self.update_status("テスト送信失敗", "error")
    
    # 2秒後に通常状態に戻る
    self.root.after(2000, lambda: self.update_status("待機中", "normal"))
Code language: PHP (php)

この変更により、テスト送信が快適になりました。しかし、他の待機処理はまだ統一されていません。

WaitManagerクラスの設計

残る待機処理を統一するため、WaitManagerクラスを設計しました。このクラスは待機処理の共通インターフェースを提供します。設計時に考慮したポイントは3つです。

  • まず、送信前の遅延とキュー間の待機では、求められる動作が微妙に異なることです。送信前の遅延はカウントダウン表示が適切ですが、キュー間の待機は残り時間と残りアイテム数の表示が有用です。
  • 次に、停止とスキップの処理です。ユーザーが途中で停止やスキップを選択できるようにする必要がありました。これらの操作は待機の種類によらず共通の動作です。
  • 最後に、ログ出力の統一です。どの待機処理でも一貫したログが出力されるようにしました。

WaitManagerの実装

WaitManagerクラスは2つの主要メソッドを持ちます。

class WaitManager:
    """待機処理の統一管理クラス"""
    
    @staticmethod
    def execute_countdown(seconds: int, update_status_callback: Callable[[str, str], None],
                         stop_check_callback: Callable[[], bool] = None,
                         skip_check_callback: Callable[[], bool] = None,
                         message_prefix: str = "実行まで",
                         logger: Optional[Logger] = None) -> bool:
        """カウントダウン実行(送信遅延用)"""
        if seconds <= 0:
            return True
            
        for i in range(seconds, 0, -1):
            # 停止チェック
            if stop_check_callback and stop_check_callback():
                if logger:
                    logger.log("カウントダウンが停止されました")
                return False
            
            # スキップチェック
            if skip_check_callback and skip_check_callback():
                if logger:
                    logger.log("カウントダウンがスキップされました")
                return True
            
            # ステータス更新
            status_message = f"{message_prefix} {i} 秒..."
            update_status_callback(status_message, "warning")
            
            if logger:
                logger.log(f"{message_prefix} {i} 秒...")
            
            time.sleep(1)
        
        return True
Code language: CSS (css)

execute_countdownメソッドは送信前の遅延処理に使用します。カウントダウン形式で残り時間を表示し、ユーザーの停止・スキップ操作に対応します。

    @staticmethod
    def execute_interruptible_wait(seconds: int, update_status_callback: Callable[[str, str], None],
                                  stop_check_callback: Callable[[], bool],
                                  skip_check_callback: Callable[[], bool],
                                  message_prefix: str = "待機",
                                  remaining_items: Optional[int] = None,
                                  logger: Optional[Logger] = None) -> bool:
        """中断・スキップ可能な待機実行(キュー間隔・最終待機用)"""
        if seconds <= 0:
            return True
            
        for i in range(seconds):
            # 停止チェック
            if stop_check_callback and stop_check_callback():
                if logger:
                    logger.log("待機が停止されました")
                return False
            
            # スキップチェック
            if skip_check_callback and skip_check_callback():
                if logger:
                    logger.log("待機がスキップされました")
                return False
            
            # ステータス更新
            remaining_seconds = seconds - i
            if remaining_items is not None:
                status_message = f"{message_prefix} {remaining_seconds} 秒... (残り{remaining_items}件)"
            else:
                status_message = f"{message_prefix} {remaining_seconds} 秒..."
            
            update_status_callback(status_message, "normal")
            time.sleep(1)
        
        return True
Code language: CSS (css)

execute_interruptible_waitメソッドはキュー間の待機や最終待機に使用します。残りアイテム数の表示機能があり、より詳細な情報をユーザーに提供できます。

コールバック関数による柔軟性の確保

WaitManagerの特徴は、コールバック関数を多用していることです。これにより、呼び出し側の状況に応じて柔軟に動作を変更できます。

update_status_callbackはUIのステータス表示を更新します。stop_check_callbackとskip_check_callbackは、ユーザーの操作状態をチェックします。これらのコールバックにより、WaitManagerは特定のUIフレームワークに依存しない汎用的な設計になりました。

コールバック関数の仕組みは、電話の留守番電話に似ています。留守番電話サービスは、メッセージが入ったときに何をするかを事前に設定できます。同様に、WaitManagerは待機中に何かが起きたときの動作を事前に設定できるのです。

既存コードの統合

WaitManagerを既存のコードに統合する際、インポート文の整理が必要でした。core.pyでWaitManagerを使用するため、utils.pyからのインポートを追加しました。

from utils import (
    Config, Logger, QueueItemData, HistoryItemData, 
    TextFormatter, ErrorHandler, log_message, WaitManager
)
Code language: JavaScript (javascript)

また、開発過程で一度作成したWaitTimeManagerクラスは、WaitManagerと機能が重複するため削除しました。このような整理作業は、コードベースを清潔に保つために重要です。

送信処理への適用

実際の送信処理では、WaitManagerを以下のように使用します。

送信前の遅延処理では、execute_countdownメソッドを呼び出します。ユーザーが設定した遅延時間分だけカウントダウンし、完了後に遅延時間を0にリセットします。これにより、遅延は一度だけ適用される仕組みになっています。

キューの各アイテム間では、execute_interruptible_waitメソッドを使用します。残りアイテム数も表示されるため、ユーザーは進捗状況を把握できます。

最終待機処理でも同じメソッドを使用しますが、残りアイテム数は表示しません。送信完了後の待機であることが明確になるよう、メッセージを調整しています。

まとめ

待機処理の統一化により、コードの品質が大幅に改善されました。WaitManagerクラスによる統一インターフェース、コールバック関数による柔軟性の確保、そして一貫したエラー処理の実装が主な成果です。この経験から、似たような処理が複数箇所に散らばっている場合は、早期に統一化を検討することが重要だと感じました。

  1. Python tkinter — Python 3.13.5 documentation – Python標準ライブラリのtkinterに関する公式ドキュメント
  2. Mac Automation Scripting Guide: About Mac Scripting – Apple公式のmacOS自動化・AppleScriptガイド
  3. Callback (computer programming) – Wikipedia – コールバック関数の概念と実装に関する包括的な解説
  4. Don’t repeat yourself – Wikipedia – DRY原則の定義と適用方法に関する詳細な説明
  5. Python GUI Programming With Tkinter – Real Python – tkinterを使用したGUIプログラミングの実践的チュートリアル
  6. Python Design Patterns Tutorial – GeeksforGeeks – Pythonにおけるデザインパターンの実装方法
  7. How to Use AppleScript to Automate macOS Tasks – UMA Technology – AppleScriptによるmacOS自動化の実践的な活用方法