From e1187cdd2519079348210e23b288cd6de40d1336 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:22:29 +0000 Subject: [PATCH 1/3] Initial plan From 22aa921d2e68eee268861ecbda0fbb85f14cf4e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:41:45 +0000 Subject: [PATCH 2/3] Add Chapter 11: Application Development - Layers section with Flask examples --- .cspell.json | 2 + .gitignore | 5 + .../11.0-overview.md | 62 ++++ .../11-application-development/11.1-layers.md | 283 ++++++++++++++++++ .../6.4-pairprogramming.md | 2 +- docs/README.md | 109 ++++--- docs/_sidebar.md | 4 + examples/ch11/example1/README.md | 80 +++++ examples/ch11/example1/app.py | 29 ++ examples/ch11/example1/requirements.txt | 2 + examples/ch11/example1/tests/test_app.py | 15 + examples/ch11/example2/README.md | 165 ++++++++++ examples/ch11/example2/app/__init__.py | 7 + examples/ch11/example2/app/data/__init__.py | 5 + .../ch11/example2/app/data/in_memory_repo.py | 12 + .../ch11/example2/app/data/sqlite_repo.py | 25 ++ examples/ch11/example2/app/models/user.py | 1 + .../ch11/example2/app/presentation/routes.py | 17 ++ .../example2/app/services/user_service.py | 11 + examples/ch11/example2/requirements.txt | 2 + examples/ch11/example2/run.py | 6 + examples/ch11/example2/tests/test_routes.py | 14 + package-lock.json | 20 +- 23 files changed, 819 insertions(+), 59 deletions(-) create mode 100644 docs/11-application-development/11.0-overview.md create mode 100644 docs/11-application-development/11.1-layers.md create mode 100644 examples/ch11/example1/README.md create mode 100644 examples/ch11/example1/app.py create mode 100644 examples/ch11/example1/requirements.txt create mode 100644 examples/ch11/example1/tests/test_app.py create mode 100644 examples/ch11/example2/README.md create mode 100644 examples/ch11/example2/app/__init__.py create mode 100644 examples/ch11/example2/app/data/__init__.py create mode 100644 examples/ch11/example2/app/data/in_memory_repo.py create mode 100644 examples/ch11/example2/app/data/sqlite_repo.py create mode 100644 examples/ch11/example2/app/models/user.py create mode 100644 examples/ch11/example2/app/presentation/routes.py create mode 100644 examples/ch11/example2/app/services/user_service.py create mode 100644 examples/ch11/example2/requirements.txt create mode 100644 examples/ch11/example2/run.py create mode 100644 examples/ch11/example2/tests/test_routes.py diff --git a/.cspell.json b/.cspell.json index 261594e1..c9a0a48d 100644 --- a/.cspell.json +++ b/.cspell.json @@ -54,9 +54,11 @@ "HTTPIP", "Inclusivity", "Intelli", + "isinstance", "Intn", "joda", "jodatime", + "jsonify", "Katacode", "Karpenter", "kdeardorff", diff --git a/.gitignore b/.gitignore index 2ab7db27..6bebe542 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ node_modules/ ## Webpack dist/ +examples/ch11/example1/venv/ +examples/ch11/example2/venv/ +examples/ch11/example2/data.sqlite +**/__pycache__/ +*.pyc diff --git a/docs/11-application-development/11.0-overview.md b/docs/11-application-development/11.0-overview.md new file mode 100644 index 00000000..33247784 --- /dev/null +++ b/docs/11-application-development/11.0-overview.md @@ -0,0 +1,62 @@ +--- +docs/11-application-development/11.0-overview.md: + category: Software Quality + estReadingMinutes: 10 +--- +# Application Development: An Overview + +Building robust, maintainable applications is a core skill in modern software development. As applications grow in size and complexity, the way we structure our code becomes increasingly important. Poor organization leads to tightly coupled code that is difficult to test, maintain, and extend. Good architectural patterns help teams build applications that are easier to understand, modify, and scale. + +This chapter introduces fundamental software architecture patterns that every developer should understand. These patterns provide proven solutions to common design problems and help teams build applications that can evolve with changing requirements. + +## What You'll Learn + +In this chapter, you will explore: + +- **Layered Architecture**: How to separate concerns by organizing code into distinct layers (presentation, business logic, data access) +- **Benefits of Separation**: Why isolating different parts of your application makes code more maintainable and testable +- **Practical Examples**: Hands-on experience refactoring tightly coupled code into well-organized layers +- **Real-World Trade-offs**: When to apply these patterns and how to balance simplicity with structure + +## Why Application Architecture Matters + +When building applications, you'll often face questions like: + +- How do I organize my code so it's easy for others to understand? +- How can I change my data storage without rewriting my entire application? +- How do I test business logic without depending on a database or external APIs? +- How can I prevent small changes from rippling through my entire codebase? + +Good application architecture provides answers to these questions. By learning and applying proven patterns, you'll build applications that are: + +- **Maintainable**: Easy to update and fix +- **Testable**: Simple to verify with automated tests +- **Flexible**: Adaptable to changing requirements +- **Understandable**: Clear for other developers to work with + +## From DevOps to Application Design + +Throughout this bootcamp, you've learned about infrastructure, CI/CD pipelines, containers, and cloud platforms. These tools help you deploy and operate applications efficiently. However, the quality of what you deploy matters just as much as how you deploy it. + +Well-architected applications are easier to: + +- Deploy through CI/CD pipelines +- Run in containers and cloud environments +- Monitor and debug in production +- Scale horizontally or vertically +- Maintain over time + +This chapter focuses on the internal structure of your applications, complementing everything you've learned about external infrastructure and tooling. + +## Getting Started + +The following sections provide hands-on exercises with real code examples. You'll start with a simple tightly coupled application and learn how to refactor it into a layered architecture. Each example uses Python and Flask, but the principles apply to any programming language and framework. + +Let's begin by exploring layered architecture patterns. + +## Deliverables + +- Why is application architecture important in modern software development? +- What problems can arise from tightly coupled code? +- How do good architectural patterns complement DevOps practices? +- What are the key characteristics of maintainable applications? diff --git a/docs/11-application-development/11.1-layers.md b/docs/11-application-development/11.1-layers.md new file mode 100644 index 00000000..b9617a6e --- /dev/null +++ b/docs/11-application-development/11.1-layers.md @@ -0,0 +1,283 @@ +--- +docs/11-application-development/11.1-layers.md: + category: Software Quality + estReadingMinutes: 30 + exercises: + - + name: Compare tightly coupled vs layered architecture + description: Run both example applications, examine their structure, and understand the architectural differences. + estMinutes: 60 + technologies: + - Python + - Flask + - + name: Refactor storage backend from in-memory to SQLite + description: Switch the data storage in both examples from in-memory to SQLite, comparing the effort required in tightly coupled vs layered implementations. + estMinutes: 90 + technologies: + - Python + - Flask + - SQLite +--- +# Layered Architecture + +Layered architecture is a software design pattern that organizes code into distinct layers, each with a specific responsibility. This separation of concerns makes applications easier to understand, test, and maintain. The pattern was popularized by Martin Fowler and has become a fundamental approach in enterprise application development. + +## What is Layered Architecture? + +In a layered architecture, an application is divided into horizontal layers: + +- **Presentation Layer**: Handles user interaction and displays information (e.g., web routes, API endpoints, UI components) +- **Business Logic Layer**: Contains the core application logic, rules, and workflows (e.g., validation, calculations, business rules) +- **Data Access Layer**: Manages data storage and retrieval (e.g., database queries, file I/O, external API calls) + +Each layer should only interact with the layer directly below it. This creates clear boundaries and dependencies. + +### Key Benefits + +**Maintainability**: Changes to one layer typically don't require changes to other layers. For example, switching from one database to another only affects the data access layer. + +**Testability**: Each layer can be tested independently. Business logic can be tested without a real database by using mock data access layers. + +**Reusability**: Business logic can be reused by different presentation layers (web UI, mobile app, API). + +**Clarity**: Developers can quickly understand what each part of the code does and where to make changes. + +## The Problem: Tightly Coupled Code + +Let's start by understanding the problem that layered architecture solves. Consider a simple user management application where all functionality lives in a single file: + +```python +# Everything in one place +@app.route("/users", methods=["GET"]) +def list_users(): + # Presentation, business logic, and data access all mixed together + users = USERS # Direct access to data structure + return jsonify(users) + +@app.route("/users", methods=["POST"]) +def create_user(): + # Route handler contains validation, business rules, and data manipulation + payload = request.get_json() or {} + name = payload.get("name") + if not name: # Validation in route + return jsonify({"error": "name required"}), 400 + user = {"id": NEXT_ID, "name": name} # Direct data manipulation + USERS.append(user) # Direct data access + return jsonify(user), 201 +``` + +### Problems with This Approach + +1. **Hard to Test**: You can't test validation logic without involving the web framework +2. **Hard to Change**: Switching from in-memory storage to a database requires editing route handlers +3. **Hard to Reuse**: Business logic is embedded in routes and can't be used by other interfaces +4. **Hard to Understand**: Everything is mixed together, making it difficult to see what the code does + +## The Solution: Layered Architecture + +Now let's see how layered architecture solves these problems: + +### Presentation Layer (Routes) + +```python +# presentation/routes.py +@bp.route("/users", methods=["GET"]) +def list_users(): + return jsonify(get_users()) # Delegates to business layer + +@bp.route("/users", methods=["POST"]) +def add_user(): + payload = request.get_json() or {} + try: + user = create_user(payload) # Delegates to business layer + except ValueError as e: + return jsonify({"error": str(e)}), 400 + return jsonify(user), 201 +``` + +The presentation layer is thin—it only handles HTTP concerns (parsing requests, formatting responses) and delegates to the business layer. + +### Business Logic Layer (Services) + +```python +# services/user_service.py +def get_users(): + return repo.get_all() # Delegates to data layer + +def create_user(payload): + name = payload.get("name") + if not name or not isinstance(name, str): + raise ValueError("name required") # Business validation + return repo.add({"name": name}) # Delegates to data layer +``` + +The business layer contains all validation and business rules. It knows nothing about HTTP or databases. + +### Data Access Layer (Repository) + +```python +# data/in_memory_repo.py +USERS = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] +NEXT_ID = 3 + +def get_all(): + return USERS.copy() + +def add(user): + global NEXT_ID + u = {"id": NEXT_ID, "name": user["name"]} + NEXT_ID += 1 + USERS.append(u) + return u +``` + +The data layer handles storage. It could be in-memory, a database, or an external API—the business and presentation layers don't need to know. + +## Hands-On Exercise + +We've provided two working examples to demonstrate these concepts: + +### Example 1: Tightly Coupled Application + +A single-file Flask application where routes, business logic, and data access are all mixed together. This represents a typical quick prototype or small script. + +**Location**: `examples/ch11/example1/` + +### Example 2: Layered Application + +The same application refactored into a layered architecture with clear separation between presentation, business logic, and data access. + +**Location**: `examples/ch11/example2/` + +### Running the Examples + +Both examples are fully functional Flask applications with the same API behavior. Follow the README files in each example directory to: + +1. Set up a Python virtual environment +2. Install dependencies +3. Run the application +4. Run the tests + +Try making requests to both applications and verify they behave identically: + +```bash +# List users +curl http://127.0.0.1:5000/users + +# Create a user +curl -X POST http://127.0.0.1:5000/users \ + -H "Content-Type: application/json" \ + -d '{"name": "Charlie"}' +``` + +### Exercise: Change the Storage Backend + +Now comes the key learning moment. Both examples currently use in-memory storage. Your task is to switch both applications to use SQLite for persistent storage. + +**In Example 1 (Tightly Coupled)**: + +You'll need to: +1. Add SQLite imports and connection logic +2. Modify the `list_users()` route to query the database +3. Modify the `create_user()` route to insert into the database +4. Add table creation logic +5. Handle database errors + +This requires editing the core route handlers and thoroughly retesting. + +**In Example 2 (Layered)**: + +You'll need to: +1. Edit `app/data/__init__.py` to change one import statement: + ```python + # from .in_memory_repo import get_all, add + from .sqlite_repo import get_all, add + ``` + +That's it! The presentation and business layers remain completely unchanged. The SQLite repository implementation is already provided. + +## Comparing the Results + +After completing both changes: + +- **Example 1** required modifying route handlers, mixing database code with HTTP code +- **Example 2** required changing a single import statement + +This demonstrates the power of layered architecture: changes are isolated to the appropriate layer, and other layers don't need to know about the implementation details. + +### What Makes This Work? + +The key is the **repository interface**. Both `in_memory_repo.py` and `sqlite_repo.py` provide the same functions: + +- `get_all()` - returns a list of user dictionaries +- `add(user)` - adds a user and returns the created user dictionary + +The business and presentation layers depend on this interface, not on the specific implementation. This is an example of **dependency inversion** - depending on abstractions rather than concrete implementations. + +## When to Use Layered Architecture + +Layered architecture is excellent for: + +- **Business applications** with clear separation between UI, logic, and data +- **APIs** that need to support multiple clients +- **Applications that will grow** in size and complexity +- **Team projects** where different developers work on different concerns + +You might skip layered architecture for: + +- **Quick prototypes** or scripts that won't be maintained +- **Very simple applications** with minimal logic (though even simple apps can benefit) +- **Performance-critical code** where the overhead of layer boundaries matters (rare) + +## Best Practices + +1. **Keep layers thin**: Each layer should do one thing well +2. **Avoid leaking abstractions**: Don't pass database objects through to the presentation layer +3. **Use consistent interfaces**: Repository functions should have clear, predictable contracts +4. **Test each layer independently**: Unit test business logic without databases; integration test with real data stores +5. **Document layer boundaries**: Make it clear what each layer is responsible for + +## Additional Patterns + +Layered architecture is foundational, but there are related patterns to explore: + +- **Hexagonal Architecture (Ports and Adapters)**: Similar to layers but emphasizes the business logic at the core +- **Clean Architecture**: Robert Martin's approach emphasizing dependency inversion +- **Domain-Driven Design**: Organizing code around business domains rather than technical layers +- **MVC (Model-View-Controller)**: A specific type of layered architecture for UI applications + +Each pattern has trade-offs, but they all share the goal of separating concerns and making code more maintainable. + +## Real-World Considerations + +### Performance + +Layer boundaries do add some overhead (function calls, data transformations). In practice, this overhead is negligible for most applications. Profile before optimizing. + +### Complexity + +Layered architecture adds structure, which some might consider "complexity." However, this structure pays dividends as applications grow. Even small applications benefit from clear organization. + +### Over-Engineering + +Not every application needs multiple layers. Use judgment: a 50-line script doesn't need layers, but a team application that will be maintained for years probably does. + +## Conclusion + +Layered architecture is a proven pattern for building maintainable applications. By separating presentation, business logic, and data access into distinct layers, you create code that is easier to understand, test, and modify. + +The hands-on examples demonstrate this concretely: changing storage backends in a layered application is trivial, while the same change in tightly coupled code requires significant refactoring. + +As you build applications in your DevOps journey, consider how layered architecture can help you create software that's easier to deploy, maintain, and evolve over time. + +## Deliverables + +- What are the three primary layers in a layered architecture, and what is each responsible for? +- Why is it easier to change the data storage in a layered application compared to a tightly coupled application? +- What is a repository interface, and why is it important? +- Run both example applications and verify they work identically +- Complete the exercise: switch both examples from in-memory to SQLite storage +- Compare your experience refactoring Example 1 vs Example 2 +- When would you choose not to use layered architecture? +- How does layered architecture improve testability? diff --git a/docs/6-software-development-practices/6.4-pairprogramming.md b/docs/6-software-development-practices/6.4-pairprogramming.md index bf7f9358..1fcc53a7 100644 --- a/docs/6-software-development-practices/6.4-pairprogramming.md +++ b/docs/6-software-development-practices/6.4-pairprogramming.md @@ -4,7 +4,7 @@ docs/6-software-development-practices/6.4-pairprogramming.md: estReadingMinutes: 20 exercises: - - name: Pair Programing + name: Pair Programming description: Using 'Live Share' or some equivillant try pair programming a 'Hello World' app in the language of your choice estMinutes: 30 technologies: diff --git a/docs/README.md b/docs/README.md index ec7af5d5..721752f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -223,6 +223,31 @@ docs/10-platform-engineering/10.2-platforms.md: - Kubernetes - ArgoCD - ExternalSecrets +docs/11-application-development/11.0-overview.md: + category: Software Quality + estReadingMinutes: 10 +docs/11-application-development/11.1-layers.md: + category: Software Quality + estReadingMinutes: 30 + exercises: + - name: Compare tightly coupled vs layered architecture + description: >- + Run both example applications, examine their structure, and understand + the architectural differences. + estMinutes: 60 + technologies: + - Python + - Flask + - name: Refactor storage backend from in-memory to SQLite + description: >- + Switch the data storage in both examples from in-memory to SQLite, + comparing the effort required in tightly coupled vs layered + implementations. + estMinutes: 90 + technologies: + - Python + - Flask + - SQLite docs/2-Github/2.2-Actions.md: category: CI/CD estReadingMinutes: 20 @@ -801,7 +826,7 @@ docs/6-software-development-practices/6.4-pairprogramming.md: category: Agile Development estReadingMinutes: 20 exercises: - - name: Pair Programing + - name: Pair Programming description: >- Using 'Live Share' or some equivillant try pair programming a 'Hello World' app in the language of your choice @@ -1103,6 +1128,47 @@ docs/8-infrastructure-configuration-management/8.1.3-terraform-modules.md: technologies: - Terraform - AWS S3 +docs/8-infrastructure-configuration-management/8.2-ansible.md: + category: Infrastructure as Code + estReadingMinutes: 15 + exercises: + - name: Vagrant and Ansible + description: >- + Provision a virtual machine and install a GitHub self-hosted runner + using Ansible as a provisioner in Vagrant. + estMinutes: 300 + technologies: + - Ansible + - Vagrant + - GitHub self-hosted runner + - name: Idempotency + description: >- + Provision a virtual machine and install a GitHub self-hosted runner + using Ansible as a provisioner in Vagrant while maintaining idempotency. + estMinutes: 300 + technologies: + - Ansible + - Vagrant + - GitHub self-hosted runner + - name: Ansible and AWS EC2 + description: >- + Provision an AWS EC2 instance and install a GitHub self-hosted runner + using Ansible. + estMinutes: 300 + technologies: + - Ansible + - AWS EC2 + - GitHub self-hosted runner + - name: Terraform and Ansible + description: >- + Provision an EC2 instance using Terraform and install a GitHub + self-hosted runner with Ansible. + estMinutes: 360 + technologies: + - Terraform + - Ansible + - AWS EC2 + - GitHub self-hosted runner docs/8-infrastructure-configuration-management/8.3-terraform-providers.md: category: Infrastructure as Code estReadingMinutes: 20 @@ -1147,47 +1213,6 @@ docs/8-infrastructure-configuration-management/8.3-terraform-providers.md: technologies: - Terraform - Go -docs/8-infrastructure-configuration-management/8.2-ansible.md: - category: Infrastructure as Code - estReadingMinutes: 15 - exercises: - - name: Vagrant and Ansible - description: >- - Provision a virtual machine and install a GitHub self-hosted runner - using Ansible as a provisioner in Vagrant. - estMinutes: 300 - technologies: - - Ansible - - Vagrant - - GitHub self-hosted runner - - name: Idempotency - description: >- - Provision a virtual machine and install a GitHub self-hosted runner - using Ansible as a provisioner in Vagrant while maintaining idempotency. - estMinutes: 300 - technologies: - - Ansible - - Vagrant - - GitHub self-hosted runner - - name: Ansible and AWS EC2 - description: >- - Provision an AWS EC2 instance and install a GitHub self-hosted runner - using Ansible. - estMinutes: 300 - technologies: - - Ansible - - AWS EC2 - - GitHub self-hosted runner - - name: Terraform and Ansible - description: >- - Provision an EC2 instance using Terraform and install a GitHub - self-hosted runner with Ansible. - estMinutes: 360 - technologies: - - Terraform - - Ansible - - AWS EC2 - - GitHub self-hosted runner docs/9-kubernetes-container-orchestration/9.1-kubectl-ref.md: category: Container Orchestration estReadingMinutes: 120 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index dcbe9d8c..c9e4ec1d 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -159,6 +159,10 @@ - [10.1.4 - Plugins](10-platform-engineering/10.1.4-plugins.md) - [10.2 - Platforms](10-platform-engineering/10.2-platforms.md) +- **Chapter 11** +- [11.0 - Application Development Overview](11-application-development/11.0-overview.md) +- [11.1 - Layers](11-application-development/11.1-layers.md) + - **Addendum** - [Addendum](addendum/addendum-overview.md) diff --git a/examples/ch11/example1/README.md b/examples/ch11/example1/README.md new file mode 100644 index 00000000..ec789687 --- /dev/null +++ b/examples/ch11/example1/README.md @@ -0,0 +1,80 @@ +# Example 1: Tightly Coupled Flask Application + +This example demonstrates a simple Flask application where all concerns (presentation, business logic, and data access) are mixed together in a single file. This is a common pattern for quick prototypes, but it becomes difficult to maintain as applications grow. + +## Structure + +``` +example1/ +├── app.py # Single file containing routes, logic, and data +├── requirements.txt # Python dependencies +├── tests/ +│ └── test_app.py # Basic tests +└── README.md # This file +``` + +## What This Example Shows + +- **Tightly Coupled Code**: All functionality in one place +- **Direct Data Access**: Routes directly manipulate the in-memory data structure +- **Mixed Concerns**: HTTP handling, validation, and data storage all in the same functions + +## Running the Application + +### Setup + +```bash +# Create and activate a virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### Run the Server + +```bash +python app.py +``` + +The server will start on `http://127.0.0.1:5000` + +### Test the API + +```bash +# List all users +curl http://127.0.0.1:5000/users + +# Create a new user +curl -X POST http://127.0.0.1:5000/users \ + -H "Content-Type: application/json" \ + -d '{"name": "Charlie"}' +``` + +### Run Tests + +```bash +PYTHONPATH=. pytest -q +``` + +## Exercise: Add SQLite Storage + +Try modifying this application to use SQLite instead of in-memory storage. You'll need to: + +1. Import `sqlite3` +2. Create a database file and table +3. Modify `list_users()` to query the database +4. Modify `create_user()` to insert into the database +5. Handle connection management and errors + +Notice how many places you need to touch and how database code gets mixed with HTTP handling code. + +## Key Observations + +- **Everything is in one file**: Easy to start with, but hard to navigate as it grows +- **No clear boundaries**: Where does the HTTP handling end and business logic begin? +- **Testing requires Flask**: You can't test validation logic without the web framework +- **Changes are risky**: Modifying data access might break HTTP handling + +Compare this with Example 2 to see how layered architecture addresses these issues. diff --git a/examples/ch11/example1/app.py b/examples/ch11/example1/app.py new file mode 100644 index 00000000..27ee5566 --- /dev/null +++ b/examples/ch11/example1/app.py @@ -0,0 +1,29 @@ +from flask import Flask, request, jsonify + +app = Flask(__name__) + +# In-memory "DB" +USERS = [ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"} +] +NEXT_ID = 3 + +@app.route("/users", methods=["GET"]) +def list_users(): + return jsonify(USERS) + +@app.route("/users", methods=["POST"]) +def create_user(): + global NEXT_ID + payload = request.get_json() or {} + name = payload.get("name") + if not name: + return jsonify({"error": "name required"}), 400 + user = {"id": NEXT_ID, "name": name} + NEXT_ID += 1 + USERS.append(user) + return jsonify(user), 201 + +if __name__ == "__main__": + app.run(port=5000, debug=True) diff --git a/examples/ch11/example1/requirements.txt b/examples/ch11/example1/requirements.txt new file mode 100644 index 00000000..3eae1641 --- /dev/null +++ b/examples/ch11/example1/requirements.txt @@ -0,0 +1,2 @@ +Flask>=2.0,<3.0 +pytest>=6.0 diff --git a/examples/ch11/example1/tests/test_app.py b/examples/ch11/example1/tests/test_app.py new file mode 100644 index 00000000..f19f5a29 --- /dev/null +++ b/examples/ch11/example1/tests/test_app.py @@ -0,0 +1,15 @@ +from app import app +import json + +def test_list_users(): + client = app.test_client() + r = client.get("/users") + assert r.status_code == 200 + data = r.get_json() + assert isinstance(data, list) + +def test_create_user(): + client = app.test_client() + r = client.post("/users", json={"name": "Charlie"}) + assert r.status_code == 201 + assert r.get_json()["name"] == "Charlie" diff --git a/examples/ch11/example2/README.md b/examples/ch11/example2/README.md new file mode 100644 index 00000000..7ba0fe1d --- /dev/null +++ b/examples/ch11/example2/README.md @@ -0,0 +1,165 @@ +# Example 2: Layered Flask Application + +This example demonstrates a Flask application organized using layered architecture. The code is separated into distinct layers: presentation (routes), business logic (services), and data access (repositories). This structure makes the code more maintainable, testable, and flexible. + +## Structure + +``` +example2/ +├── run.py # Application entry point +├── requirements.txt # Python dependencies +├── app/ +│ ├── __init__.py # Application factory +│ ├── presentation/ +│ │ └── routes.py # HTTP routes (presentation layer) +│ ├── services/ +│ │ └── user_service.py # Business logic (service layer) +│ ├── data/ +│ │ ├── __init__.py # Repository interface (single swap point) +│ │ ├── in_memory_repo.py # In-memory implementation +│ │ └── sqlite_repo.py # SQLite implementation +│ └── models/ +│ └── user.py # Data models (placeholder) +├── tests/ +│ └── test_routes.py # Tests +└── README.md # This file +``` + +## What This Example Shows + +- **Separation of Concerns**: Each layer has a single responsibility +- **Clear Boundaries**: Presentation doesn't know about data storage, business logic doesn't know about HTTP +- **Easy to Test**: Business logic can be tested independently +- **Easy to Change**: Switching storage backends requires changing one import + +## Running the Application + +### Setup + +```bash +# Create and activate a virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### Run the Server + +```bash +python run.py +``` + +The server will start on `http://127.0.0.1:5001` + +### Test the API + +```bash +# List all users +curl http://127.0.0.1:5001/users + +# Create a new user +curl -X POST http://127.0.0.1:5001/users \ + -H "Content-Type: application/json" \ + -d '{"name": "Charlie"}' +``` + +### Run Tests + +```bash +PYTHONPATH=. pytest -q +``` + +## Understanding the Layers + +### Presentation Layer (`app/presentation/routes.py`) + +Handles HTTP concerns: +- Parsing requests +- Calling business logic functions +- Formatting responses +- HTTP status codes + +### Business Logic Layer (`app/services/user_service.py`) + +Contains business rules: +- Input validation +- Business logic (none in this simple example, but this is where it would go) +- Calls data layer for persistence + +### Data Access Layer (`app/data/`) + +Manages data storage: +- Repository interface (`__init__.py`) +- In-memory implementation (`in_memory_repo.py`) +- SQLite implementation (`sqlite_repo.py`) + +## Exercise: Switch to SQLite Storage + +This example makes it trivial to change the storage backend. Here's how: + +### Step 1: Edit the Repository Interface + +Open `app/data/__init__.py` and change the import: + +```python +# Comment out the in-memory import +# from .in_memory_repo import get_all, add + +# Uncomment the SQLite import +from .sqlite_repo import get_all, add +``` + +### Step 2: Run the Application + +```bash +python run.py +``` + +That's it! The application now uses SQLite for storage. A `data.sqlite` file will be created in the example2 directory. + +### What Happened? + +- The presentation layer (routes) didn't change +- The business logic layer (services) didn't change +- Only the data layer changed (one import statement) + +This demonstrates the power of layered architecture and dependency inversion. + +## Key Observations + +- **Clear Organization**: Easy to find where things are +- **Single Responsibility**: Each file has one job +- **Easy to Test**: You can test business logic without Flask or a database +- **Easy to Change**: Swapping implementations is trivial +- **Team Friendly**: Different developers can work on different layers + +## Comparing with Example 1 + +Go back and try the same exercise in Example 1: + +1. Example 1 requires editing route handlers directly +2. Example 1 mixes HTTP code with database code +3. Example 1 is harder to test in isolation +4. Example 2 required changing only one import statement + +This is the benefit of layered architecture in practice. + +## Extension Ideas + +Try these exercises to deepen your understanding: + +1. Add a `get_user_by_id(user_id)` function to all three layers +2. Add a `delete_user(user_id)` function +3. Create a third repository implementation that uses a JSON file +4. Add more business logic (e.g., name must be at least 2 characters) +5. Write unit tests for the service layer that mock the repository + +## Best Practices Demonstrated + +1. **Application Factory Pattern**: `create_app()` avoids circular imports +2. **Blueprint Registration**: Routes are organized in blueprints +3. **Repository Pattern**: Data access is abstracted behind an interface +4. **Explicit Dependencies**: Imports show the dependency flow +5. **Error Handling**: Business layer raises exceptions, presentation layer handles them diff --git a/examples/ch11/example2/app/__init__.py b/examples/ch11/example2/app/__init__.py new file mode 100644 index 00000000..7edace64 --- /dev/null +++ b/examples/ch11/example2/app/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask +from .presentation.routes import bp as user_bp + +def create_app(): + app = Flask(__name__) + app.register_blueprint(user_bp) + return app diff --git a/examples/ch11/example2/app/data/__init__.py b/examples/ch11/example2/app/data/__init__.py new file mode 100644 index 00000000..359b3285 --- /dev/null +++ b/examples/ch11/example2/app/data/__init__.py @@ -0,0 +1,5 @@ +# Default to in-memory repo. To switch backend, change this import only. +from .in_memory_repo import get_all, add + +# To use sqlite, replace above with: +# from .sqlite_repo import get_all, add diff --git a/examples/ch11/example2/app/data/in_memory_repo.py b/examples/ch11/example2/app/data/in_memory_repo.py new file mode 100644 index 00000000..5021b0a1 --- /dev/null +++ b/examples/ch11/example2/app/data/in_memory_repo.py @@ -0,0 +1,12 @@ +USERS = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] +NEXT_ID = 3 + +def get_all(): + return USERS.copy() + +def add(user): + global NEXT_ID + u = {"id": NEXT_ID, "name": user["name"]} + NEXT_ID += 1 + USERS.append(u) + return u diff --git a/examples/ch11/example2/app/data/sqlite_repo.py b/examples/ch11/example2/app/data/sqlite_repo.py new file mode 100644 index 00000000..ebe33407 --- /dev/null +++ b/examples/ch11/example2/app/data/sqlite_repo.py @@ -0,0 +1,25 @@ +import sqlite3 +from pathlib import Path + +DB_PATH = Path(__file__).parent.parent.parent / "data.sqlite" + +def _get_conn(): + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + return conn + +def _ensure_table(): + with _get_conn() as c: + c.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + +def get_all(): + _ensure_table() + with _get_conn() as c: + rows = c.execute("SELECT id, name FROM users").fetchall() + return [dict(r) for r in rows] + +def add(user): + _ensure_table() + with _get_conn() as c: + cur = c.execute("INSERT INTO users (name) VALUES (?)", (user["name"],)) + return {"id": cur.lastrowid, "name": user["name"]} diff --git a/examples/ch11/example2/app/models/user.py b/examples/ch11/example2/app/models/user.py new file mode 100644 index 00000000..d40c343b --- /dev/null +++ b/examples/ch11/example2/app/models/user.py @@ -0,0 +1 @@ +# simple dict-based model for this example; left for extension diff --git a/examples/ch11/example2/app/presentation/routes.py b/examples/ch11/example2/app/presentation/routes.py new file mode 100644 index 00000000..be28c7ec --- /dev/null +++ b/examples/ch11/example2/app/presentation/routes.py @@ -0,0 +1,17 @@ +from flask import Blueprint, request, jsonify +from ..services.user_service import get_users, create_user + +bp = Blueprint("users", __name__) + +@bp.route("/users", methods=["GET"]) +def list_users(): + return jsonify(get_users()) + +@bp.route("/users", methods=["POST"]) +def add_user(): + payload = request.get_json() or {} + try: + user = create_user(payload) + except ValueError as e: + return jsonify({"error": str(e)}), 400 + return jsonify(user), 201 diff --git a/examples/ch11/example2/app/services/user_service.py b/examples/ch11/example2/app/services/user_service.py new file mode 100644 index 00000000..96939dc0 --- /dev/null +++ b/examples/ch11/example2/app/services/user_service.py @@ -0,0 +1,11 @@ +# keep business logic here (validation, business rules) +from ..data import get_all, add + +def get_users(): + return get_all() + +def create_user(payload): + name = payload.get("name") + if not name or not isinstance(name, str): + raise ValueError("name required") + return add({"name": name}) diff --git a/examples/ch11/example2/requirements.txt b/examples/ch11/example2/requirements.txt new file mode 100644 index 00000000..3eae1641 --- /dev/null +++ b/examples/ch11/example2/requirements.txt @@ -0,0 +1,2 @@ +Flask>=2.0,<3.0 +pytest>=6.0 diff --git a/examples/ch11/example2/run.py b/examples/ch11/example2/run.py new file mode 100644 index 00000000..033ab038 --- /dev/null +++ b/examples/ch11/example2/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(port=5001, debug=True) diff --git a/examples/ch11/example2/tests/test_routes.py b/examples/ch11/example2/tests/test_routes.py new file mode 100644 index 00000000..26bd39d8 --- /dev/null +++ b/examples/ch11/example2/tests/test_routes.py @@ -0,0 +1,14 @@ +from app import create_app +import json + +def test_list_users(): + client = create_app().test_client() + r = client.get("/users") + assert r.status_code == 200 + assert isinstance(r.get_json(), list) + +def test_create_user(): + client = create_app().test_client() + r = client.post("/users", json={"name": "Diana"}) + assert r.status_code == 201 + assert r.get_json()["name"] == "Diana" diff --git a/package-lock.json b/package-lock.json index 23f25335..11459db7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1734,8 +1733,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-dart": { "version": "2.3.1", @@ -1875,16 +1873,14 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -2082,8 +2078,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -2853,7 +2848,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2879,7 +2873,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -3456,7 +3449,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3712,7 +3704,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -9010,7 +9001,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9774,7 +9764,6 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -9824,7 +9813,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", From f5959546ee20b0988e1f13685bd32954c8333481 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:44:44 +0000 Subject: [PATCH 3/3] Add security comments about Flask debug mode in examples --- examples/ch11/example1/README.md | 2 +- examples/ch11/example1/app.py | 1 + examples/ch11/example2/README.md | 2 +- examples/ch11/example2/run.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/ch11/example1/README.md b/examples/ch11/example1/README.md index ec789687..0825047e 100644 --- a/examples/ch11/example1/README.md +++ b/examples/ch11/example1/README.md @@ -4,7 +4,7 @@ This example demonstrates a simple Flask application where all concerns (present ## Structure -``` +```text example1/ ├── app.py # Single file containing routes, logic, and data ├── requirements.txt # Python dependencies diff --git a/examples/ch11/example1/app.py b/examples/ch11/example1/app.py index 27ee5566..bb04ebbf 100644 --- a/examples/ch11/example1/app.py +++ b/examples/ch11/example1/app.py @@ -26,4 +26,5 @@ def create_user(): return jsonify(user), 201 if __name__ == "__main__": + # NOTE: debug=True is for development only. Disable in production. app.run(port=5000, debug=True) diff --git a/examples/ch11/example2/README.md b/examples/ch11/example2/README.md index 7ba0fe1d..c78d9ce1 100644 --- a/examples/ch11/example2/README.md +++ b/examples/ch11/example2/README.md @@ -4,7 +4,7 @@ This example demonstrates a Flask application organized using layered architectu ## Structure -``` +```text example2/ ├── run.py # Application entry point ├── requirements.txt # Python dependencies diff --git a/examples/ch11/example2/run.py b/examples/ch11/example2/run.py index 033ab038..c12937b3 100644 --- a/examples/ch11/example2/run.py +++ b/examples/ch11/example2/run.py @@ -3,4 +3,5 @@ app = create_app() if __name__ == "__main__": + # NOTE: debug=True is for development only. Disable in production. app.run(port=5001, debug=True)