Namespaces & Inheritance: A Deep Dive into Dependency Injection
Namespaces & Inheritance: A Deep Dive into Dependency Injection
Let's connect the dots between namespaces and Dependency Injection (DI), especially through the less common lens of inheritance. Understanding this relationship reveals how names like UserRepository
map to concrete code and how dependencies flow through an application.
Crash Course: What is a Namespace?
At its core, a namespace is a system to ensure that all the names in a program are unique and can be used without conflict. Think of it as a dictionary where the keys are names (like variables or functions) and the values are the objects they refer to.
Why are they important?
- Avoiding Conflicts: They prevent ambiguity. If two libraries define a function called
process_data()
, namespaces keep them separate. - Organization: They group related items, making code more readable and maintainable.
- Encapsulation: They create boundaries. Names in one namespace aren't accessible in another unless explicitly imported.
Python uses namespaces everywhere. Every module (a .py
file), function, and class creates its own namespace.
How Dependency Injection Bridges Namespaces
Dependency Injection is fundamentally about bridging these namespaces. In a typical Clean Architecture, you might have:
- An Application Layer in a namespace like
application.use_cases
. - An Infrastructure Layer in a namespace like
infrastructure.repositories
. - A Presentation Layer in a namespace like
presentation.views
.
The "Composition Root" is the part of your application that wires everything together. It's a master namespace that knows how to import names from all other namespaces and inject concrete objects where they're needed.
# main.py (The Composition Root)
from infrastructure.repositories.sqlite_user_repo import SQLiteUserRepository
from application.use_cases.get_user_profile import GetUserProfileUseCase
from presentation.user_profile_view import UserProfileView
# 1. Create concrete object from the infrastructure namespace
user_repo = SQLiteUserRepository(...)
# 2. Create use case from the application namespace, injecting the repo
get_user_uc = GetUserProfileUseCase(user_repo=user_repo)
# 3. Create view from the presentation namespace, injecting the use case
profile_view = UserProfileView(get_user_uc=get_user_uc)
The Inheritance Approach to DI
Now for a more unusual pattern: using inheritance for DI. In this model, a base class implicitly extends the namespace of its children, making dependencies available without direct injection into the child's constructor.
The Setup
Imagine a BaseView
that holds a reference to a master AppContainer
. This container knows how to build and provide every service the application needs.
# presentation/base_view.py
from app_bootstrap import AppContainer
class BaseView(QWidget):
def __init__(self, app_container: AppContainer, parent=None):
super().__init__(parent)
# This container instance is now in the BaseView instance's namespace
self._app_container = app_container
# These methods provide access to dependencies
def get_user_profile_use_case(self) -> GetUserProfileUseCase:
return self._app_container.get_get_user_profile_use_case()
def update_user_profile_use_case(self) -> UpdateUserProfileUseCase:
return self._app_container.get_update_user_profile_use_case()
A child class like UserProfileView
now only needs to inherit from BaseView
. It gains access to the dependencies without its module ever needing to import the specific use cases.
# presentation/user_profile_view.py
from presentation.base_view import BaseView
class UserProfileView(BaseView):
def __init__(self, app_container: AppContainer, user_id: str, parent=None):
super().__init__(app_container, parent)
self.user_id = user_id
# ... setup UI ...
def _on_save_profile(self):
# Access the dependency through the inherited namespace!
use_case = self.update_user_profile_use_case()
use_case.execute(self.user_id, {...})
The Namespace Perspective: Pros and Cons
So, what's happening here? DI via inheritance manipulates namespace accessibility in a unique way.
- ✅ Simplified Imports: The child class (
UserProfileView
) has a cleaner import section. It doesn't need to know about every specific dependency, only its parent. - ✅ Centralized Resolution: The
BaseView
and itsAppContainer
act as a single, consistent source for all dependencies. - ⚠️ Potential Obscurity: The convenience comes at a cost. It's no longer explicit from the child class's constructor what its dependencies are. You have to inspect its parent's namespace to see the full picture.
In essence, instead of injecting dependencies directly into a class's instance namespace, the base class creates a "dependency-providing" segment of the namespace that all its children inherit automatically.
Comments
Post a Comment