Modularization & Layer (Presentation, Domain, Data)

บทนำ: ทำไมต้องแบ่ง Layers?

เมื่อเขียนโปรแกรมขนาดใหญ่ หากไม่แบ่ง layers code จะ chaos:

text❌ ปัญหาของ Monolithic Code:
- Database code ปนกับ UI code
- Business logic ปนกับ data access
- ต้อง change UI ต้อง touch database code
- ต้อง change database ต้อง update UI
- ไม่รู้ flow ของ data

✓ Solutions:
- Organize code into layers
- Each layer ต่างกันชัดเจน
- Layers communicate through interfaces
- Change one layer ไม่ affect others

Layered Architecture = Organizing code into horizontal layers, each with specific responsibility


3-Layer Architecture: The Standard

Layer Structure

text┌─────────────────────────────────────────┐
│   PRESENTATION LAYER                    │
│  (UI, Controllers, API endpoints)       │
│  - Display data to user                 │
│  - Collect user input                   │
│  - Format responses                     │
└──────────────┬──────────────────────────┘
               │ Calls
               ▼
┌─────────────────────────────────────────┐
│   DOMAIN/BUSINESS LOGIC LAYER           │
│  (Services, Business Rules)             │
│  - Process data                         │
│  - Apply business rules                 │
│  - Make decisions                       │
└──────────────┬──────────────────────────┘
               │ Calls
               ▼
┌─────────────────────────────────────────┐
│   DATA/PERSISTENCE LAYER                │
│  (Repositories, Database Access)        │
│  - Read from database                   │
│  - Write to database                    │
│  - Data mapping                         │
└─────────────────────────────────────────┘
               │ Connects
               ▼
        [Database]

ตัวอย่างที่ 1: Monolithic vs Layered

❌ Monolithic (Bad)

java// ❌ ไม่ดี: Everything mixed together
@RestController
public class OrderController {
    
    @PostMapping("/orders")
    public String createOrder(@RequestBody OrderData data) {
        // Step 1: Validate input (Presentation concern)
        if (data.getCustomerId() == null) {
            return "ERROR: Customer ID required";
        }
        
        // Step 2: Query database (Data concern)
        Connection conn = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
            PreparedStatement stmt = conn.prepareStatement(
                "SELECT * FROM customers WHERE id = ?"
            );
            stmt.setString(1, data.getCustomerId());
            ResultSet rs = stmt.executeQuery();
            
            if (!rs.next()) {
                return "ERROR: Customer not found";
            }
            
            // Step 3: Business logic (Business concern)
            double subtotal = 0;
            for (OrderItem item : data.getItems()) {
                // Query product price from database
                PreparedStatement productStmt = conn.prepareStatement(
                    "SELECT price FROM products WHERE id = ?"
                );
                productStmt.setString(1, item.getProductId());
                ResultSet productRs = productStmt.executeQuery();
                
                if (productRs.next()) {
                    double price = productRs.getDouble("price");
                    subtotal += price * item.getQuantity();
                }
            }
            
            // Apply discount logic
            if (subtotal > 1000) {
                subtotal *= 0.9;  // 10% off
            }
            
            // Step 4: Update database
            PreparedStatement updateStmt = conn.prepareStatement(
                "INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?)"
            );
            updateStmt.setString(1, data.getCustomerId());
            updateStmt.setDouble(2, subtotal);
            updateStmt.setString(3, "PENDING");
            updateStmt.executeUpdate();
            
            // Step 5: Format response
            return "SUCCESS: Order created with total: " + subtotal;
            
        } catch (SQLException e) {
            e.printStackTrace();
            return "ERROR: Database error";
        } finally {
            if (conn != null) {
                try { conn.close(); } catch (SQLException e) {}
            }
        }
    }
}

// PROBLEMS:
// ❌ Controller ทำทุกอย่าง
// ❌ Database code mixed with business logic
// ❌ Can't test business logic without database
// ❌ Can't reuse business logic in other controllers
// ❌ Change database affect UI code
// ❌ 100+ lines in one method

✓ Layered (Good)

textProject structure:
├── controller/          (Presentation)
│   └── OrderController.java
├── service/             (Domain/Business)
│   ├── OrderService.java
│   └── DiscountService.java
├── repository/          (Data)
│   ├── OrderRepository.java
│   ├── CustomerRepository.java
│   └── ProductRepository.java
├── model/               (Shared)
│   ├── Order.java
│   ├── Customer.java
│   └── Product.java
└── config/
    └── DatabaseConfig.java

Layer 1: Presentation Layer

Responsibility: Handle HTTP requests, format responses

java// Model for request/response
public class CreateOrderRequest {
    private String customerId;
    private List<OrderItemRequest> items;
    
    // Getters/Setters
    public String getCustomerId() { return customerId; }
    public List<OrderItemRequest> getItems() { return items; }
}

public class OrderItemRequest {
    private String productId;
    private int quantity;
    
    // Getters/Setters
    public String getProductId() { return productId; }
    public int getQuantity() { return quantity; }
}

public class OrderResponse {
    private String status;
    private String orderId;
    private double total;
    
    public OrderResponse(String status, String orderId, double total) {
        this.status = status;
        this.orderId = orderId;
        this.total = total;
    }
    
    // Getters
    public String getStatus() { return status; }
    public String getOrderId() { return orderId; }
    public double getTotal() { return total; }
}

// Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    // Inject service (dependency injection)
    private OrderService orderService;
    
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    /**
     * Create new order
     * Responsibility: Parse input, call service, format response
     */
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
        @RequestBody CreateOrderRequest request) {
        
        try {
            // Validate input (presentation concern)
            if (request.getCustomerId() == null || request.getCustomerId().isEmpty()) {
                return ResponseEntity.badRequest()
                    .body(new OrderResponse("ERROR", "", 0));
            }
            
            // ← Call service (delegate business logic)
            Order order = orderService.createOrder(
                request.getCustomerId(),
                request.getItems()
            );
            
            // ← Format response
            OrderResponse response = new OrderResponse(
                "SUCCESS",
                order.getId(),
                order.getTotal()
            );
            
            return ResponseEntity.ok(response);
            
        } catch (CustomerNotFoundException e) {
            return ResponseEntity.notFound().build();
        } catch (Exception e) {
            return ResponseEntity.status(500)
                .body(new OrderResponse("ERROR", "", 0));
        }
    }
    
    @GetMapping("/{orderId}")
    public ResponseEntity<Order> getOrder(@PathVariable String orderId) {
        try {
            Order order = orderService.getOrder(orderId);
            return ResponseEntity.ok(order);
        } catch (OrderNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
}

// RESPONSIBILITY:
// ✓ Parse HTTP request
// ✓ Validate input format
// ✓ Call appropriate service
// ✓ Format HTTP response
// ✓ Handle HTTP-specific exceptions

Layer 2: Domain/Business Logic Layer

Responsibility: Process data, apply business rules

java// Business logic service
@Service
public class OrderService {
    
    // Inject repositories (data access)
    private OrderRepository orderRepository;
    private CustomerRepository customerRepository;
    private ProductRepository productRepository;
    private DiscountService discountService;
    
    public OrderService(
        OrderRepository orderRepository,
        CustomerRepository customerRepository,
        ProductRepository productRepository,
        DiscountService discountService) {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
        this.productRepository = productRepository;
        this.discountService = discountService;
    }
    
    /**
     * Create new order
     * Responsibility: Business logic only
     */
    public Order createOrder(String customerId, List<OrderItemRequest> items) {
        // Step 1: Verify customer exists
        Customer customer = customerRepository.findById(customerId);
        if (customer == null) {
            throw new CustomerNotFoundException("Customer not found: " + customerId);
        }
        
        // Step 2: Validate items
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("Order must have items");
        }
        
        // Step 3: Calculate order total
        double subtotal = calculateSubtotal(items);
        
        // Step 4: Apply discount
        double discount = discountService.calculateDiscount(customer, subtotal);
        double total = subtotal - discount;
        
        // Step 5: Create order object
        Order order = new Order();
        order.setCustomerId(customerId);
        order.setItems(items);
        order.setSubtotal(subtotal);
        order.setDiscount(discount);
        order.setTotal(total);
        order.setStatus("PENDING");
        order.setCreatedAt(new Date());
        
        // Step 6: Persist order (delegate to repository)
        return orderRepository.save(order);
    }
    
    /**
     * Calculate subtotal for items
     */
    private double calculateSubtotal(List<OrderItemRequest> items) {
        double subtotal = 0;
        
        for (OrderItemRequest item : items) {
            // Get product info (repository handles data access)
            Product product = productRepository.findById(item.getProductId());
            
            if (product == null) {
                throw new ProductNotFoundException(
                    "Product not found: " + item.getProductId()
                );
            }
            
            // Apply business rule: can't order more than available stock
            if (item.getQuantity() > product.getStock()) {
                throw new InsufficientStockException(
                    "Not enough stock for: " + product.getName()
                );
            }
            
            subtotal += product.getPrice() * item.getQuantity();
        }
        
        return subtotal;
    }
    
    public Order getOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        if (order == null) {
            throw new OrderNotFoundException("Order not found: " + orderId);
        }
        return order;
    }
}

// Discount calculation service
@Service
public class DiscountService {
    
    public double calculateDiscount(Customer customer, double subtotal) {
        // Business rule: Premium customers get 10% off on orders > 1000
        if (customer.isPremium() && subtotal > 1000) {
            return subtotal * 0.1;
        }
        
        // Business rule: Orders > 500 get 5% off
        if (subtotal > 500) {
            return subtotal * 0.05;
        }
        
        return 0;
    }
}

// RESPONSIBILITY:
// ✓ Apply business rules
// ✓ Validate business constraints
// ✓ Calculate/transform data
// ✓ Make business decisions
// ✓ Delegate data access to repositories
// ✗ NO database queries
// ✗ NO HTTP concerns

Layer 3: Data/Persistence Layer

Responsibility: Access and persist data

java// Repository interface
public interface OrderRepository {
    Order findById(String orderId);
    List<Order> findByCustomerId(String customerId);
    Order save(Order order);
    void update(Order order);
    void delete(String orderId);
}

// Repository implementation
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    
    @Override
    public Order findById(String orderId) {
        String sql = "SELECT * FROM orders WHERE id = ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setString(1, orderId);
            
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return mapResultSetToOrder(rs);
                }
            }
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
        
        return null;
    }
    
    @Override
    public Order save(Order order) {
        String sql = "INSERT INTO orders (id, customer_id, total, discount, status, created_at) " +
                     "VALUES (?, ?, ?, ?, ?, ?)";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            String orderId = UUID.randomUUID().toString();
            
            pstmt.setString(1, orderId);
            pstmt.setString(2, order.getCustomerId());
            pstmt.setDouble(3, order.getTotal());
            pstmt.setDouble(4, order.getDiscount());
            pstmt.setString(5, order.getStatus());
            pstmt.setTimestamp(6, new java.sql.Timestamp(order.getCreatedAt().getTime()));
            
            pstmt.executeUpdate();
            
            order.setId(orderId);
            return order;
            
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
        
        return null;
    }
    
    private Order mapResultSetToOrder(ResultSet rs) throws SQLException {
        Order order = new Order();
        order.setId(rs.getString("id"));
        order.setCustomerId(rs.getString("customer_id"));
        order.setTotal(rs.getDouble("total"));
        order.setDiscount(rs.getDouble("discount"));
        order.setStatus(rs.getString("status"));
        order.setCreatedAt(new Date(rs.getTimestamp("created_at").getTime()));
        return order;
    }
}

// Repository interfaces for other entities
public interface CustomerRepository {
    Customer findById(String customerId);
    void save(Customer customer);
}

public interface ProductRepository {
    Product findById(String productId);
    List<Product> findAll();
}

// RESPONSIBILITY:
// ✓ Query database
// ✓ Map database rows to objects
// ✓ Persist objects to database
// ✗ NO business logic
// ✗ NO HTTP concerns
// ✗ NO formatting

ตัวอย่างที่ 2: How Layers Communicate

java// REQUEST FLOW (Controller → Service → Repository)

// 1. User sends HTTP request
POST /api/orders
Content-Type: application/json

{
  "customerId": "CUST-001",
  "items": [
    {"productId": "PROD-001", "quantity": 2},
    {"productId": "PROD-002", "quantity": 1}
  ]
}

// 2. Controller receives request
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(
    @RequestBody CreateOrderRequest request) {
    
    // ← Parse JSON to Java object (Presentation)
    // ← Validate format
    
    Order order = orderService.createOrder(
        request.getCustomerId(),
        request.getItems()
    );
    
    // ← Convert to response (Presentation)
    return ResponseEntity.ok(new OrderResponse(...));
}

// 3. Service handles business logic
public Order createOrder(String customerId, List<OrderItemRequest> items) {
    // Verify customer (calls repository)
    Customer customer = customerRepository.findById(customerId);
    
    // Calculate totals (business rules)
    double subtotal = calculateSubtotal(items);
    double discount = discountService.calculateDiscount(customer, subtotal);
    
    // Create order
    Order order = new Order(...);
    
    // Persist (calls repository)
    return orderRepository.save(order);
}

// 4. Repository persists data
public Order save(Order order) {
    // Execute SQL
    String sql = "INSERT INTO orders ...";
    // ... execute ...
    return order;
}

// 5. Response flows back
OrderService.createOrder() → returns Order object
OrderController.createOrder() → formats as OrderResponse
HTTP response:

{
  "status": "SUCCESS",
  "orderId": "ORD-12345",
  "total": 950.00
}

ตัวอย่างที่ 3: Layered Testing

java// Each layer can be tested independently

// TEST 1: Domain logic (no database needed)
@Test
public void testDiscountCalculation() {
    DiscountService discountService = new DiscountService();
    
    // Mock customer (not real from database)
    Customer premiumCustomer = new Customer();
    premiumCustomer.setPremium(true);
    
    // Test business rule
    double discount = discountService.calculateDiscount(
        premiumCustomer, 
        1500.0
    );
    
    assertEquals(150.0, discount);  // 10% of 1500
}

// TEST 2: Service with mocked repository
@Test
public void testCreateOrderSuccess() {
    // Mock repository
    OrderRepository mockOrderRepo = mock(OrderRepository.class);
    CustomerRepository mockCustomerRepo = mock(CustomerRepository.class);
    ProductRepository mockProductRepo = mock(ProductRepository.class);
    DiscountService discountService = new DiscountService();
    
    // Setup mock data
    Customer customer = new Customer();
    customer.setId("CUST-001");
    customer.setPremium(false);
    
    when(mockCustomerRepo.findById("CUST-001"))
        .thenReturn(customer);
    
    Product product = new Product();
    product.setId("PROD-001");
    product.setPrice(100.0);
    product.setStock(10);
    
    when(mockProductRepo.findById("PROD-001"))
        .thenReturn(product);
    
    // Create service with mocks
    OrderService orderService = new OrderService(
        mockOrderRepo,
        mockCustomerRepo,
        mockProductRepo,
        discountService
    );
    
    // Test without real database
    List<OrderItemRequest> items = List.of(
        new OrderItemRequest("PROD-001", 2)
    );
    
    Order order = orderService.createOrder("CUST-001", items);
    
    assertEquals(200.0, order.getSubtotal());
    assertEquals(0, order.getDiscount());
    assertEquals(200.0, order.getTotal());
}

// TEST 3: Repository (with test database)
@Test
public void testOrderRepository() {
    OrderRepository repository = new OrderRepositoryImpl();
    
    Order order = new Order();
    order.setCustomerId("CUST-001");
    order.setTotal(1000.0);
    order.setStatus("PENDING");
    
    Order saved = repository.save(order);
    assertNotNull(saved.getId());
    
    Order retrieved = repository.findById(saved.getId());
    assertEquals(1000.0, retrieved.getTotal());
}

// TEST 4: Controller (with mock service)
@Test
public void testOrderController() {
    OrderService mockService = mock(OrderService.class);
    OrderController controller = new OrderController(mockService);
    
    Order mockOrder = new Order();
    mockOrder.setId("ORD-001");
    mockOrder.setTotal(950.0);
    
    when(mockService.createOrder(any(), any()))
        .thenReturn(mockOrder);
    
    CreateOrderRequest request = new CreateOrderRequest();
    request.setCustomerId("CUST-001");
    
    ResponseEntity<OrderResponse> response = 
        controller.createOrder(request);
    
    assertEquals(HttpStatus.OK, response.getStatusCode());
    assertEquals("SUCCESS", response.getBody().getStatus());
}

// BENEFITS:
// ✓ Test each layer independently
// ✓ Use mocks to isolate layers
// ✓ Fast tests (no database for service tests)
// ✓ Clear what's being tested

Best Practices: Layering Checklist

textPRESENTATION LAYER:
  ☑ Handle HTTP requests/responses only
  ☑ Validate input format
  ☑ Format output for API
  ☑ Don't contain business logic
  ☑ Don't query database directly

DOMAIN/BUSINESS LAYER:
  ☑ Contain business rules
  ☑ Validate business constraints
  ☑ Calculate/transform data
  ☑ Don't contain HTTP code
  ☑ Don't contain SQL code

DATA LAYER:
  ☑ Only data access operations
  ☑ Execute queries
  ☑ Map database to objects
  ☑ Don't contain business logic
  ☑ Don't contain HTTP code

COMMUNICATION:
  ☑ Layers call downward only
  ☑ Use interfaces/abstractions
  ☑ Dependency injection
  ☑ No circular dependencies

MISTAKES TO AVOID:
  ✗ Business logic in controller
  ✗ Database queries in controller
  ✗ Business logic in repository
  ✗ Layers calling upward
  ✗ Direct dependencies (tight coupling)

Visualization: Request Flow

text┌──────────────────────────────────────────────┐
│  CLIENT (Browser, Mobile App, etc)           │
└──────────────┬───────────────────────────────┘
               │ HTTP Request
               ▼
┌──────────────────────────────────────────────┐
│  PRESENTATION LAYER                          │
│  OrderController                             │
│  - Parse JSON                                │
│  - Validate format                           │
│  - Call OrderService                         │
└──────────────┬───────────────────────────────┘
               │ createOrder(customerId, items)
               ▼
┌──────────────────────────────────────────────┐
│  DOMAIN LAYER                                │
│  OrderService                                │
│  - Verify customer (→ repository)            │
│  - Calculate total (business logic)          │
│  - Apply discount (business logic)           │
│  - Create order                              │
│  - Persist (→ repository)                    │
└──────────────┬───────────────────────────────┘
               │ findById(), save()
               ▼
┌──────────────────────────────────────────────┐
│  DATA LAYER                                  │
│  OrderRepository, CustomerRepository, etc    │
│  - Query database                            │
│  - Map results to objects                    │
│  - Execute SQL                               │
└──────────────┬───────────────────────────────┘
               │ SQL queries
               ▼
        [DATABASE]
               │
               ▲
        (returns data)
               │
└──────────────┬───────────────────────────────┘
│  DATA LAYER (returns objects)                │
└──────────────┬───────────────────────────────┘
               │ Order object
               ▼
┌──────────────────────────────────────────────┐
│  DOMAIN LAYER (returns Order object)         │
└──────────────┬───────────────────────────────┘
               │ Order object
               ▼
┌──────────────────────────────────────────────┐
│  PRESENTATION LAYER                          │
│  - Format Order as OrderResponse             │
│  - Convert to JSON                           │
└──────────────┬───────────────────────────────┘
               │ HTTP Response
               ▼
┌──────────────────────────────────────────────┐
│  CLIENT (Browser, Mobile App, etc)           │
└──────────────────────────────────────────────┘

สรุป

Modularization & Layer (Presentation, Domain, Data) คือ foundation ของ scalable architecture:

3 Core Layers:

  • Presentation → Handle UI/API concerns
  • Domain → Apply business logic
  • Data → Manage persistence

Key Principles:

  • Separation of Concerns – Each layer ทำ one thing
  • Layering – Layers communicate downward
  • Abstraction – Use interfaces for loose coupling
  • Testability – Each layer testable independently

ประโยชน์ของ Layering:

  • Clear Structure – Everyone รู้ code ไปไหน
  • Easy to Maintain – Change one layer ไม่affect others
  • Easy to Test – Mock dependencies ได้
  • Easy to Scale – Add features โดยไม่ผสมกับ existing
  • Team Productivity – Multiple developers ทำงานพร้อมกัน

Common Layering Patterns:

  • 3-Layer (Presentation, Business, Data) – Most common
  • 4-Layer (add Service layer) – More separation
  • Domain-Driven (complex business logic)
  • CQRS (Command Query Responsibility Segregation)

Layering ไม่ใช่ “optional” แต่เป็น best practice ที่ professional developers follow – มันให้ structure, maintainability และ scalability ที่ necessary สำหรับ long-term project success ถ้าต้องเขียน code ที่อยู่ได้นาน layering คือ investment ที่ pay off