はじめに
ソフトウェア開発において、コードの品質向上は常に重要な課題です。本記事では、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()
改善すべき点
- 単一責任の原則に違反:データの取得、解析、エンリッチメント、保存の全てを一つのクラスが担当
- ビジネスロジックの不明確さ:
enrich_data
メソッドにビジネスロジックが埋め込まれているが、他の処理と混在 - 再利用性の欠如:機能が密結合しており、個別の再利用が困難
- テストの困難さ:個々の機能を独立してテストすることが難しい
- 設定の硬直性:データベースパスなどの設定が直接コードに埋め込まれている
リファクタリングの段階
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
この段階での主な変更点は以下です。
- インターフェースの導入により、異なる実装への切り替えが容易になりました。
- 依存性注入により、
WebScraper
クラスの柔軟性が向上しました。 -
fetch_data
メソッドがurl
を引数に取るように変更され、URLの指定がより柔軟になりました。 - ビジネスロジックが
DataEnricherInterface
として抽象化され、KeywordDataEnricher
として実装されました。 - 環境変数を使用してキーワードを設定可能にし、ビジネスロジックの柔軟性を向上させました。
これらの変更により、システムの柔軟性と拡張性が大幅に向上しました。しかし、ビジネスロジックは依然としてDataEnricherInterface
とその実装に埋め込まれたままです。次のステップでは、このビジネスロジックをさらに分離し、ドメイン層として明確に定義します。
3. ドメイン層の導入とビジネスロジックの分離
前のステップでは、インターフェースの導入により、システムの柔軟性が向上しました。しかし、ビジネスロジック(この場合、データの重要性の判断とフィルタリング)は依然としてデータ層の一部として扱われています。ドメイン駆動設計の考え方に基づき、このビジネスロジックをシステムの中心的な概念として扱い、独立したドメイン層として実装することで、以下の利点が得られます。
- ビジネスロジックの集中管理
- ドメインモデルを通じた、より表現力豊かなコード
- ビジネスルールの変更に対する柔軟性の向上
- テストの容易性
更新されたディレクトリ構造:
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
この段階で、DataEnricherInterface
とKeywordDataEnricher
の役割は、ドメイン層の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
この変更により、以下の点が改善されました。
-
ビジネスロジックがドメイン層に移動し、
DataEnricherInterface
が不要になりました。 -
KeywordDataEnricher
の機能はDataEnrichmentService
に統合され、ビジネスロジックが一箇所に集中しました。 -
ScrapedData
モデルにis_important
メソッドが追加されました。これにより、データの重要性の判断がドメインモデル自体の責任となり、ドメインの概念がより明確になりました。 -
DataEnrichmentService
はScrapedData
オブジェクトを直接扱うようになり、型の安全性が向上しました。
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
の導入は、単なるインターフェースの置き換えではなく、ビジネスロジックの扱い方に関する根本的な変更です。
まとめ
本記事では、データ収集クローラーシステムの段階的なリファクタリングプロセスを通じて、コード品質の向上と設計原則の適用方法を具体的に示しました。主な改善点は以下の通りです。
- 責任の分離: 単一責任の原則を適用し、データの取得、解析、エンリッチメント、保存を個別のクラスに分離しました。
- インターフェースと依存性注入の導入: システムの柔軟性と拡張性を大幅に向上させ、異なる実装への切り替えを容易にしました。
- ドメインモデルとサービスの導入: ビジネスロジックを明確に分離し、システムの中心となる概念を定義しました。
- レイヤードアーキテクチャの採用: ドメイン層、データ層、アプリケーション層を明確に分離し、各層の責任を明確にしました。
- インターフェースの維持: データ層の抽象化を保ち、実装の柔軟性を確保しました。
これらの改善により、システムのモジュール性、再利用性、テスト容易性、保守性、拡張性が大幅に向上しました。特に、ドメイン駆動設計の一部概念を適用することで、ビジネスロジックがより明確になり、将来の要件変更にも柔軟に対応できる構造が実現しました。同時に、インターフェースを維持することで、データ層の実装を容易に変更・拡張できる柔軟性も確保しました。
重要なのは、このリファクタリングプロセスが一回で完結するものではなく、継続的な改善の一部であるという点です。プロジェクトの規模や複雑さに応じて、適切なレベルで設計原則やDDDのコンセプトを採用し、段階的に改善を進めていくことが重要です。
最後に、本記事で示したアプローチは、データ収集クローラーに限らず、さまざまなソフトウェアプロジェクトに応用可能です。コードの品質向上と設計の改善に取り組む際の参考として、ぜひ活用してください。