Reading.CleanArchitecture.DatabaseAsDetail

This chapter reiterates the principle that a database, like frameworks and other implementation details, should not dictate the architecture of your system. Instead, the database is treated as a detail that can be swapped or modified without impacting the core business logic. The focus is on maintaining independence from specific database technologies and ensuring that business rules and use cases are not tightly coupled to persistence logic.

Key Concepts of the Chapter

1. The Database Is a Mechanism, Not the Foundation

  • Traditional approaches often start with database design (schema-first development).
  • Martin argues that this is backwards: the architecture should be designed around business rules and use cases, with the database as a supporting detail.

2. Independence of Business Rules

  • Core business rules should not depend on:
    • Specific database schemas.
    • Database queries or SQL constructs.
    • ORM (Object-Relational Mapping) tools.
  • Instead, the business logic interacts with the database through abstract interfaces.

Example: Abstracting the Database

public interface UserRepository {
    User findById(int id);
    void save(User user);
}

The actual database implementation resides in the outer layer:

public class SqlUserRepository implements UserRepository {
    @Override
    public User findById(int id) {
        // SQL query logic here
    }

    @Override
    public void save(User user) {
        // SQL insert/update logic here
    }
}

3. The Dependency Rule

  • Dependencies must point inward:
    • Business rules (entities, use cases) should not depend on database implementations.
    • Instead, the database interacts with the core through interfaces defined by the inner layers.

4. Benefits of Treating the Database as a Detail

a. Flexibility:
  • The system can adapt to new database technologies or changes in database schema without affecting core business logic.
b. Testability:
  • Business rules and use cases can be tested independently of the database.
  • Example: Use in-memory or mock repositories for testing.
c. Maintainability:
  • Changes to the database (e.g., migrating from MySQL to MongoDB) are isolated to the repository implementations.

5. Repositories and Gateways

  • Repository Pattern:
    • Repositories abstract database access and provide a simple interface for the core logic to interact with.
  • Gateways:
    • Similar to repositories but broader, used to abstract external systems like APIs or file storage.

6. Practical Example: E-Commerce System

Core Logic:
  • Entity:
public class Order {
    private List<Product> products;

    public double calculateTotal() {
        return products.stream().mapToDouble(Product::getPrice).sum();
    }
}

Use Case:

public class PlaceOrderUseCase {
    private OrderRepository orderRepository;

    public PlaceOrderUseCase(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void execute(Order order) {
        orderRepository.save(order);
    }
}

Database Interface:

public interface OrderRepository {
    void save(Order order);
    Order findById(int id);
}

Database Implementation:

public class SqlOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) {
        // SQL INSERT query
    }

    @Override
    public Order findById(int id) {
        // SQL SELECT query
        return new Order();
    }
}

Main Component:

public class Main {
    public static void main(String[] args) {
        OrderRepository repository = new SqlOrderRepository();
        PlaceOrderUseCase placeOrder = new PlaceOrderUseCase(repository);

        Order order = new Order();
        placeOrder.execute(order);
    }
}

7. Common Pitfalls and How to Avoid Them

a. Coupling Logic to Database
  • Bad Example
public void processOrder(int orderId) {
    String query = "SELECT * FROM orders WHERE id = " + orderId;
    // SQL logic here
}
  • Solution:
    • Abstract database queries into repositories.
b. Business Logic in SQL
  • Embedding business logic in stored procedures or SQL queries tightly couples the logic to the database.
  • Solution:
    • Keep business rules in the core logic and use the database for data persistence only.
c. Over-Optimizing for Database Efficiency
  • Avoid premature optimization by tightly coupling the architecture to a specific database or schema.
  • Focus on clarity and maintainability first; optimize only when necessary.

8. Testing Without a Database

Mock Repositories:
  • Use mock implementations to test business logic.
public class InMemoryOrderRepository implements OrderRepository {
    private Map<Integer, Order> dataStore = new HashMap<>();

    @Override
    public void save(Order order) {
        dataStore.put(order.getId(), order);
    }

    @Override
    public Order findById(int id) {
        return dataStore.get(id);
    }
}

Unit Testing Use Cases:

@Test
public void testPlaceOrder() {
    OrderRepository repository = new InMemoryOrderRepository();
    PlaceOrderUseCase useCase = new PlaceOrderUseCase(repository);

    Order order = new Order();
    useCase.execute(order);

    assertNotNull(repository.findById(order.getId()));
}

Key Takeaways

  1. Decouple Business Logic from Database:
    • Treat the database as a detail, interacting with it only through abstract interfaces.
  2. Focus on Business Rules:
    • Design the system around business use cases and workflows, not database schemas.
  3. Enable Flexibility and Testability:
    • Abstracting database logic makes the system easier to test and adapt to new technologies.
  4. Repositories Are Essential:
    • Use repositories to encapsulate database operations and keep them out of the core logic.
This entry was posted in Без рубрики. Bookmark the permalink.