1
1

設計原則に基づくリファクタリング:Webスクレイパーを例に

Last updated at Posted at 2024-07-20

はじめに

ソフトウェア開発において、コードの品質向上は常に重要な課題です。本記事では、Webスクレイパーを例に、段階的なリファクタリングを通じて設計原則やベストプラクティスの適用方法を具体的に解説します。

改善前のコード

最初に、すべての機能が1つのクラスに統合された非常にシンプルなWebスクレイパーから始めます。

project_root/
├── web_scraper.py
├── main.py
└── requirements.txt

web_scraper.py

import requests
import json
import sqlite3

class WebScraper:
    def __init__(self, url):
        self.url = url

    def fetch_data(self):
        response = requests.get(self.url)
        data = response.text
        parsed_data = self.parse_data(data)
        enriched_data = self.enrich_data(parsed_data)
        self.save_data(enriched_data)
        return enriched_data

    def parse_data(self, data):
        return json.loads(data)

    def enrich_data(self, data):
        # ここでビジネスロジックを適用
        # 例:特定のキーワードを含むデータのみを抽出
        return {k: v for k, v in data.items() if 'important' in v.lower()}

    def save_data(self, data):
        conn = sqlite3.connect('test.db')
        cursor = conn.cursor()
        cursor.execute('INSERT INTO data (json_data) VALUES (?)', (json.dumps(data),))
        conn.commit()
        conn.close()

main.py

from web_scraper import WebScraper

def main():
    scraper = WebScraper('https://example.com/api/data')
    data = scraper.fetch_data()
    print(data)

if __name__ == "__main__":
    main()

改善すべき点

  1. 単一責任の原則に違反:データの取得、解析、エンリッチメント、保存の全てを一つのクラスが担当
  2. ビジネスロジックの不明確さ:enrich_dataメソッドにビジネスロジックが埋め込まれているが、他の処理と混在
  3. 再利用性の欠如:機能が密結合しており、個別の再利用が困難
  4. テストの困難さ:個々の機能を独立してテストすることが難しい
  5. 設定の硬直性:データベースパスなどの設定が直接コードに埋め込まれている

リファクタリングの段階

1. 責任の分離:データ取得、解析、保存の分離

  • 主な変更: データ取得、解析、保存の責務を別々のクラスに分離
  • 目的: 単一責任の原則の適用による再利用性の向上

ディレクトリ構造

project_root/
├── data_fetcher.py
├── data_parser.py
├── data_saver.py
├── data_enricher.py
├── web_scraper.py
├── main.py
└── requirements.txt

data_enricher.py

class DataEnricher:
    def enrich(self, data):
        return {k: v for k, v in data.items() if 'important' in v.lower()}

web_scraper.py

from data_fetcher import DataFetcher
from data_parser import DataParser
from data_enricher import DataEnricher
from data_saver import DataSaver

class WebScraper:
    def __init__(self, url):
        self.url = url
        self.fetcher = DataFetcher()
        self.parser = DataParser()
        self.enricher = DataEnricher()
        self.saver = DataSaver()

    def fetch_data(self):
        raw_data = self.fetcher.fetch(self.url)
        parsed_data = self.parser.parse(raw_data)
        enriched_data = self.enricher.enrich(parsed_data)
        self.saver.save(enriched_data)
        return enriched_data

この変更により、各クラスの責任が明確になり、再利用性とテスト容易性が向上しました。ただし、ビジネスロジックはまだDataEnricherクラスに埋め込まれたままです。

2. インターフェースの導入と依存性注入

  • 主な変更: インターフェースを導入し、依存性注入を実装
  • 目的: 柔軟性と拡張性の向上、環境変数の拡張、ビジネスロジックの抽象化

ディレクトリ構造

project_root/
├── interfaces/
│   ├── __init__.py
│   ├── data_fetcher_interface.py
│   ├── data_parser_interface.py
│   ├── data_enricher_interface.py
│   └── data_saver_interface.py
├── implementations/
│   ├── __init__.py
│   ├── http_data_fetcher.py
│   ├── json_data_parser.py
│   ├── keyword_data_enricher.py
│   └── sqlite_data_saver.py
├── web_scraper.py
├── main.py
└── requirements.txt

interfaces/data_fetcher_interface.py

from abc import ABC, abstractmethod

class DataFetcherInterface(ABC):
    @abstractmethod
    def fetch(self, url: str) -> str:
        pass

interfaces/data_parser_interface.py

from abc import ABC, abstractmethod
from typing import Dict, Any

class DataParserInterface(ABC):
    @abstractmethod
    def parse(self, raw_data: str) -> Dict[str, Any]:
        pass

interfaces/data_enricher_interface.py

from abc import ABC, abstractmethod
from typing import Dict, Any

class DataEnricherInterface(ABC):
    @abstractmethod
    def enrich(self, data: Dict[str, Any]) -> Dict[str, Any]:
        pass

interfaces/data_saver_interface.py

from abc import ABC, abstractmethod
from typing import Dict, Any

class DataSaverInterface(ABC):
    @abstractmethod
    def save(self, data: Dict[str, Any]) -> None:
        pass

implementations/keyword_data_enricher.py

import os
from interfaces.data_enricher_interface import DataEnricherInterface

class KeywordDataEnricher(DataEnricherInterface):
    def __init__(self):
        self.keyword = os.getenv('IMPORTANT_KEYWORD', 'important')

    def enrich(self, data):
        return {k: v for k, v in data.items() if self.keyword in str(v).lower()}

web_scraper.py

from interfaces.data_fetcher_interface import DataFetcherInterface
from interfaces.data_parser_interface import DataParserInterface
from interfaces.data_enricher_interface import DataEnricherInterface
from interfaces.data_saver_interface import DataSaverInterface

class WebScraper:
    def __init__(self, fetcher: DataFetcherInterface, parser: DataParserInterface, 
                 enricher: DataEnricherInterface, saver: DataSaverInterface):
        self.fetcher = fetcher
        self.parser = parser
        self.enricher = enricher
        self.saver = saver

    def fetch_data(self, url):
        raw_data = self.fetcher.fetch(url)
        parsed_data = self.parser.parse(raw_data)
        enriched_data = self.enricher.enrich(parsed_data)
        self.saver.save(enriched_data)
        return enriched_data

この段階での主な変更点は以下です。

  1. インターフェースの導入により、異なる実装への切り替えが容易になりました。
  2. 依存性注入により、WebScraperクラスの柔軟性が向上しました。
  3. fetch_dataメソッドがurlを引数に取るように変更され、URLの指定がより柔軟になりました。
  4. ビジネスロジックがDataEnricherInterfaceとして抽象化され、KeywordDataEnricherとして実装されました。
  5. 環境変数を使用してキーワードを設定可能にし、ビジネスロジックの柔軟性を向上させました。

これらの変更により、システムの柔軟性と拡張性が大幅に向上しました。しかし、ビジネスロジックは依然としてDataEnricherInterfaceとその実装に埋め込まれたままです。次のステップでは、このビジネスロジックをさらに分離し、ドメイン層として明確に定義します。

3. ドメイン層の導入とビジネスロジックの分離

前のステップでは、インターフェースの導入により、システムの柔軟性が向上しました。しかし、ビジネスロジック(この場合、データの重要性の判断とフィルタリング)は依然としてデータ層の一部として扱われています。ドメイン駆動設計の考え方に基づき、このビジネスロジックをシステムの中心的な概念として扱い、独立したドメイン層として実装することで、以下の利点が得られます。

  1. ビジネスロジックの集中管理
  2. ドメインモデルを通じた、より表現力豊かなコード
  3. ビジネスルールの変更に対する柔軟性の向上
  4. テストの容易性

更新されたディレクトリ構造:

project_root/
├── domain/
│   ├── __init__.py
│   ├── scraped_data.py
│   └── data_enrichment_service.py
├── data/
│   ├── __init__.py
│   ├── interfaces/
│   │   ├── __init__.py
│   │   ├── data_fetcher_interface.py
│   │   ├── data_parser_interface.py
│   │   └── data_saver_interface.py
│   ├── implementations/
│   │   ├── __init__.py
│   │   ├── http_data_fetcher.py
│   │   ├── json_data_parser.py
│   │   └── sqlite_data_saver.py
├── application/
│   ├── __init__.py
│   └── web_scraper.py
├── main.py
└── requirements.txt

この段階で、DataEnricherInterfaceKeywordDataEnricherの役割は、ドメイン層のScrapedDataモデルとDataEnrichmentServiceに移行します。以下に、この変更の詳細を示します。

変更前(セクション2)

class DataEnricherInterface(ABC):
    @abstractmethod
    def enrich(self, data: Dict[str, Any]) -> Dict[str, Any]:
        pass
class KeywordDataEnricher(DataEnricherInterface):
    def __init__(self):
        self.keyword = os.getenv('IMPORTANT_KEYWORD', 'important')

    def enrich(self, data):
        return {k: v for k, v in data.items() if self.keyword in str(v).lower()}

変更後(セクション3)

@dataclass
class ScrapedData:
    content: Dict[str, Any]
    source_url: str

    def is_important(self) -> bool:
        important_keyword = os.getenv('IMPORTANT_KEYWORD', 'important')
        return any(important_keyword in str(v).lower() for v in self.content.values())
class DataEnrichmentService:
    def __init__(self):
        self.important_keyword = os.getenv('IMPORTANT_KEYWORD', 'important')

    def enrich(self, data: ScrapedData) -> ScrapedData:
        if data.is_important():
            enriched_content = {k: v for k, v in data.content.items() if self.important_keyword in str(v).lower()}
            return ScrapedData(content=enriched_content, source_url=data.source_url)
        return data

この変更により、以下の点が改善されました。

  1. ビジネスロジックがドメイン層に移動し、DataEnricherInterfaceが不要になりました。

  2. KeywordDataEnricherの機能はDataEnrichmentServiceに統合され、ビジネスロジックが一箇所に集中しました。

  3. ScrapedDataモデルにis_importantメソッドが追加されました。これにより、データの重要性の判断がドメインモデル自体の責任となり、ドメインの概念がより明確になりました。

  4. DataEnrichmentServiceScrapedDataオブジェクトを直接扱うようになり、型の安全性が向上しました。

WebScraperクラスも、この変更に合わせて更新します。

from data.interfaces.data_fetcher_interface import DataFetcherInterface
from data.interfaces.data_parser_interface import DataParserInterface
from data.interfaces.data_saver_interface import DataSaverInterface
from domain.scraped_data import ScrapedData
from domain.data_enrichment_service import DataEnrichmentService

class WebScraper:
    def __init__(self, fetcher: DataFetcherInterface, parser: DataParserInterface, 
                 saver: DataSaverInterface, enrichment_service: DataEnrichmentService):
        self.fetcher = fetcher
        self.parser = parser
        self.saver = saver
        self.enrichment_service = enrichment_service

    def fetch_data(self, url: str) -> ScrapedData:
        raw_data = self.fetcher.fetch(url)
        parsed_data = self.parser.parse(raw_data)
        scraped_data = ScrapedData(content=parsed_data, source_url=url)
        enriched_data = self.enrichment_service.enrich(scraped_data)
        self.saver.save(enriched_data)
        return enriched_data

この変更により、ビジネスロジックがデータ層からドメイン層に完全に移行し、システムの構造がより明確になりました。DataEnricherInterfaceの削除とDataEnrichmentServiceの導入は、単なるインターフェースの置き換えではなく、ビジネスロジックの扱い方に関する根本的な変更です。

まとめ

本記事では、データ収集クローラーシステムの段階的なリファクタリングプロセスを通じて、コード品質の向上と設計原則の適用方法を具体的に示しました。主な改善点は以下の通りです。

  1. 責任の分離: 単一責任の原則を適用し、データの取得、解析、エンリッチメント、保存を個別のクラスに分離しました。
  2. インターフェースと依存性注入の導入: システムの柔軟性と拡張性を大幅に向上させ、異なる実装への切り替えを容易にしました。
  3. ドメインモデルとサービスの導入: ビジネスロジックを明確に分離し、システムの中心となる概念を定義しました。
  4. レイヤードアーキテクチャの採用: ドメイン層、データ層、アプリケーション層を明確に分離し、各層の責任を明確にしました。
  5. インターフェースの維持: データ層の抽象化を保ち、実装の柔軟性を確保しました。

これらの改善により、システムのモジュール性、再利用性、テスト容易性、保守性、拡張性が大幅に向上しました。特に、ドメイン駆動設計の一部概念を適用することで、ビジネスロジックがより明確になり、将来の要件変更にも柔軟に対応できる構造が実現しました。同時に、インターフェースを維持することで、データ層の実装を容易に変更・拡張できる柔軟性も確保しました。

重要なのは、このリファクタリングプロセスが一回で完結するものではなく、継続的な改善の一部であるという点です。プロジェクトの規模や複雑さに応じて、適切なレベルで設計原則やDDDのコンセプトを採用し、段階的に改善を進めていくことが重要です。

最後に、本記事で示したアプローチは、データ収集クローラーに限らず、さまざまなソフトウェアプロジェクトに応用可能です。コードの品質向上と設計の改善に取り組む際の参考として、ぜひ活用してください。

1
1
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
1
1