Dependency Injection (DI) is a design pattern that enables loose coupling by injecting dependencies into objects rather than having them create their own. The IMDb Scraper uses a DependencyContainer to centralize dependency creation and lifecycle management.
The DependencyContainer acts as the Composition Root - the single place where all application dependencies are wired together.
from domain.interfaces.use_case_interface import UseCaseInterfacefrom domain.interfaces.scraper_interface import ScraperInterfacefrom domain.interfaces.proxy_interface import ProxyProviderInterfacefrom domain.interfaces.tor_interface import TorInterfacefrom application.use_cases.save_movie_with_actors_csv_use_case import SaveMovieWithActorsCsvUseCasefrom application.use_cases.save_movie_with_actors_postgres_use_case import SaveMovieWithActorsPostgresUseCasefrom application.use_cases.composite_save_movie_with_actors_use_case import CompositeSaveMovieWithActorsUseCasefrom infrastructure.persistence.csv.repositories.movie_csv_repository import MovieCsvRepositoryfrom infrastructure.persistence.csv.repositories.actor_csv_repository import ActorCsvRepositoryfrom infrastructure.persistence.csv.repositories.movie_actor_csv_repository import MovieActorCsvRepositoryfrom infrastructure.persistence.postgres.repositories.movie_postgres_repository import MoviePostgresRepositoryfrom infrastructure.persistence.postgres.repositories.actor_postgres_repository import ActorPostgresRepositoryfrom infrastructure.persistence.postgres.repositories.movie_actor_postgres_repository import MovieActorPostgresRepositoryfrom infrastructure.scraper.imdb_scraper import ImdbScraperfrom infrastructure.persistence.postgres.postgres_connection import connection_poolfrom infrastructure.network.proxy_provider import ProxyProviderfrom infrastructure.network.tor_rotator import TorRotatorclass DependencyContainer: """ Un contenedor centralizado para la inyección de dependencias. Gestiona la creación y el ciclo de vida de los servicios de la aplicación. """ def __init__(self, config): self.config = config self._db_connection = None self.proxy_provider = ProxyProvider() self.tor_rotator = TorRotator() def get_db_connection(self): """Gestiona la conexión a la BD para que se cree una sola vez.""" if self._db_connection is None and connection_pool: self._db_connection = connection_pool.getconn() return self._db_connection def close_db_connection(self): """Cierra la conexión y la devuelve al pool.""" if self._db_connection and connection_pool: connection_pool.putconn(self._db_connection) self._db_connection = None print("Conexión a la base de datos cerrada y devuelta al pool.") def get_csv_use_case(self) -> UseCaseInterface: """Construye y devuelve el caso de uso para CSV.""" return SaveMovieWithActorsCsvUseCase( movie_repository=MovieCsvRepository(), actor_repository=ActorCsvRepository(), movie_actor_repository=MovieActorCsvRepository() ) def get_postgres_use_case(self) -> UseCaseInterface: """Construye y devuelve el caso de uso para PostgreSQL.""" conn = self.get_db_connection() return SaveMovieWithActorsPostgresUseCase( movie_repository=MoviePostgresRepository(conn), actor_repository=ActorPostgresRepository(conn), movie_actor_repository=MovieActorPostgresRepository(conn) ) def get_composite_use_case(self) -> UseCaseInterface: """Construye el caso de uso compuesto.""" use_cases = [self.get_csv_use_case(), self.get_postgres_use_case()] return CompositeSaveMovieWithActorsUseCase(use_cases) def get_proxy_provider(self) -> ProxyProviderInterface: """Fábrica para el proveedor de proxy.""" return ProxyProvider() def get_tor_rotator(self) -> TorInterface: """Fábrica para el rotador de TOR.""" return TorRotator() def get_scraper(self) -> ScraperInterface: """ Construye y devuelve el scraper principal inyectando TODAS sus dependencias. """ use_case = self.get_composite_use_case() proxy_provider = self.get_proxy_provider() tor_rotator = self.get_tor_rotator() engine = self.config.SCRAPER_ENGINE.lower() if engine == "requests": return ImdbScraper( use_case=use_case, proxy_provider=proxy_provider, tor_rotator=tor_rotator, engine=engine ) elif engine == "playwright": raise NotImplementedError("El motor 'playwright' aún no está implementado.") else: raise ValueError(f"Motor de scraping '{engine}' no soportado.")
The container manages different object lifecycles:
Singleton
Transient
Configured
Database Connection - Created once and reused
def get_db_connection(self): """Singleton pattern - creates connection only once.""" if self._db_connection is None and connection_pool: self._db_connection = connection_pool.getconn() return self._db_connection
The database connection is expensive to create, so it’s reused across all repositories.
Repositories - Created fresh each time
def get_csv_use_case(self) -> UseCaseInterface: """Transient - creates new instances.""" return SaveMovieWithActorsCsvUseCase( movie_repository=MovieCsvRepository(), # New instance actor_repository=ActorCsvRepository(), # New instance movie_actor_repository=MovieActorCsvRepository() # New instance )
CSV repositories are stateless and lightweight, so creating new instances is acceptable.
Scraper - Created with configuration
def get_scraper(self) -> ScraperInterface: """Creates scraper based on configuration.""" engine = self.config.SCRAPER_ENGINE.lower() if engine == "requests": return ImdbScraper(...) elif engine == "playwright": return ImdbScraperPlaywright(...)
The scraper implementation is chosen based on environment configuration.
Each get_*() method is a factory method that encapsulates object creation:
get_csv_use_case()
Creates the CSV persistence use case with all its repository dependencies.
def get_csv_use_case(self) -> UseCaseInterface: """Construye y devuelve el caso de uso para CSV.""" return SaveMovieWithActorsCsvUseCase( movie_repository=MovieCsvRepository(), actor_repository=ActorCsvRepository(), movie_actor_repository=MovieActorCsvRepository() )
Dependencies created:
MovieCsvRepository
ActorCsvRepository
MovieActorCsvRepository
get_postgres_use_case()
Creates the PostgreSQL persistence use case with database connection.
def get_postgres_use_case(self) -> UseCaseInterface: """Construye y devuelve el caso de uso para PostgreSQL.""" conn = self.get_db_connection() # Reuse singleton connection return SaveMovieWithActorsPostgresUseCase( movie_repository=MoviePostgresRepository(conn), actor_repository=ActorPostgresRepository(conn), movie_actor_repository=MovieActorPostgresRepository(conn) )
Dependencies created:
Database connection (singleton)
MoviePostgresRepository
ActorPostgresRepository
MovieActorPostgresRepository
get_composite_use_case()
Creates a composite use case that executes multiple persistence strategies.
def get_composite_use_case(self) -> UseCaseInterface: """Construye el caso de uso compuesto.""" use_cases = [ self.get_csv_use_case(), self.get_postgres_use_case() ] return CompositeSaveMovieWithActorsUseCase(use_cases)
Dependencies created:
CSV use case (and all its dependencies)
PostgreSQL use case (and all its dependencies)
Composite wrapper
get_scraper()
Creates the scraper with all network and persistence dependencies.
container = DependencyContainer(config)try: scraper = container.get_scraper() scraper.scrape()finally: container.close_db_connection() # Ensures connection is returned to pool
The container’s only job is dependency creation and wiring:
# ✅ Container creates and wiresclass DependencyContainer: def get_scraper(self): return ImdbScraper( use_case=self.get_composite_use_case(), proxy_provider=self.get_proxy_provider(), tor_rotator=self.get_tor_rotator() )# ✅ Scraper focuses on scraping logicclass ImdbScraper: def scrape(self): # Business logic only, no dependency creation movies = self._fetch_movies() for movie in movies: self.use_case.execute(movie)