The Orchestra Conductor: Application Logic in services.py
[home]
The Developer's Diary
The Orchestra Conductor: Application Logic in services.py
With our domain models defined, it's time to add the brains of the operation.
We've laid a solid foundation for our SpLD assessment app. We have our assembler (main.py
), our secret keeper (config.py
), and the heart of our application's language (models.py
). Now, we need to add the logic that brings it all to life. This is the job of the Application Service Layer, which we'll implement in app/core/services.py
.
The Role of the Service Layer
The service layer is the primary entry point for the application's user interface. When a user performs an action, like clicking a "Save" button, the UI calls a method in one of our services. This service then orchestrates the necessary steps to fulfill that request. It acts as a middleman, coordinating between the domain models and the data persistence layer (which we'll call repositories).
Novice Coder (NC): "Got it. So when the user clicks 'Save Profile', the UI calls a function in here, and I write the `UPDATE users SET ...` SQL query right in that function, right?"
Experienced Coder (IC): "Whoa, hold on. This layer is a coordinator, not a low-level worker. It should have zero knowledge of SQL or HTTP or file systems. It orchestrates the use case. A `save_profile` function here would: 1. Receive simple data types from the UI (e.g., name, email). 2. Fetch the `User` domain model from the repository. 3. Call methods on that `User` model to enforce business logic (e.g., `user.change_email(new_email)`, which might validate the format). 4. Finally, pass the updated `User` model back to the repository to be persisted. It delegates, it doesn't do."
This distinction is critical. The service layer doesn't know *how* to save a user to a database; it only knows that it needs to tell the repository to do so. This keeps our concerns neatly separated and makes the entire system more testable and maintainable.
Our Hypothetical services.py
For our SpLD app, a key use case is creating a new assessment for a client. We can define a `HistoryService` to handle this. This service will depend on a repository to handle the actual data storage.
# app/core/services.py
from datetime import date
from typing import List
from .models import Assessment
# We'll define the repository in the next step. For now, we
# can imagine an abstract contract for it.
from ..interfaces.repository import AbstractRepository
class HistoryService:
"""
This service orchestrates the use cases related to
client and assessment histories.
"""
def __init__(self, repo: AbstractRepository):
self.repo = repo
def create_new_assessment(
self,
client_id: int,
assessment_date: date,
hypotheses: List[str]
) -> Assessment:
"""
Use Case: Create a new assessment for a client.
"""
# The service layer doesn't know how assessment_id is generated.
# We can assume the repository handles this.
new_assessment = Assessment(
assessment_id=None, # The database will assign this
client_id=client_id,
assessment_date=assessment_date,
hypotheses=hypotheses
)
# Delegate the persistence task to the repository
created_assessment = self.repo.add_assessment(new_assessment)
return created_assessment
Breaking it down:
- Dependency Injection: The
HistoryService
doesn't create its own repository. Instead, a repository instance is "injected" into it via the__init__
method. This is a core tenet of clean architecture, allowing us to easily swap out the repository implementation (e.g., from a real database to a fake one for testing). - Orchestrating a Use Case: The
create_new_assessment
method is a perfect example of a use case. It takes simple data, uses it to create a domain model (`Assessment`), and then tells the repository to save it. It contains the *what* (the steps of the use case) but not the *how* (the specific SQL commands). - Clear Inputs and Outputs: The method signature is explicit. It accepts primitive types (
int
,date
,List[str]
) and returns a fully-formedAssessment
object. This makes it easy for the UI layer to interact with it. - No Low-Level Details: Notice the complete absence of SQL, file I/O, or any other infrastructure-level code. The service layer remains pure application logic.
With the service layer in place, we have a clean and testable component that executes our application's features. It acts as the perfect bridge between the user interface and the underlying data systems, ensuring each part of our application can evolve independently.
Comments
Post a Comment