Spring Boot Clean Code: Messy Controller Refactoring | Sinhala Guide

Spring Boot Clean Code: Messy Controller Refactoring | Sinhala Guide

ආයුබෝවන් හැමෝටම!

අද අපි කතා කරන්න යන්නේ අපේ Spring Boot Project වල quality එක වැඩි කරගන්න පුළුවන් Super important topic එකක් ගැන. ඒ තමයි "Clean Code". Software Engineering වලදී Clean Code කියන්නේ 단순히 code එකක් වැඩ කරනවට වඩා එහා ගිය දෙයක්. ඒක අපේ code base එක කියවන්න ලේසි, තේරුම් ගන්න ලේසි, Maintain කරන්න ලේසි සහ අනාගතේදී වෙනස්කම් කරන්න පහසු විදියට ලියන එකටයි කියන්නේ.

විශේෂයෙන්ම, අපි මේ guide එකෙන් බලමු අපේ controller layers වලට Clean Code principles කොහොම apply කරල, අවුල් සහගත controller එකක් කොහොම ලස්සනට refactor කරගන්නද කියල. Developers ලා විදිහට අපි හැමෝටම ඕන කරන දෙයක් තමයි මේක. මොකද project එකක් ලොකු වෙනකොට, developers ලා ගොඩක් වැඩ කරනකොට, Clean Code නැතුව ගොඩක් අමාරු වෙනවා.

අපි අද මේ guide එකෙන් බලමු, controller එකක් messy (අවුල්) වෙන්නේ කොහොමද, ඒක identify කරගන්නේ කොහොමද, ඒවගේම ඒක refactor කරන්න පුළුවන් Practical strategies මොනවද කියල. අවසානයේ අපි live example එකක් කරලම බලමු, messy controller එකක් කොහොමද step-by-step clean controller එකක් බවට පත් කරන්නේ කියලා.

Clean Code මූලධර්ම Spring Boot වලට (Clean Code Principles for Spring Boot)

Clean Code කියන්නේ Robert C. Martin ("Uncle Bob") විසින් ජනප්‍රිය කරපු concepts set එකක්. මේවා ඕනම programming language එකකට apply කරන්න පුළුවන් වගේම, Spring Boot project එකකදී අපිට මේවා බොහොම හොඳින් apply කරන්න පුළුවන්. මේ principles follow කරනකොට අපේ application එක Scalable, Testable, Maintainable වෙනවා.

මූලික Clean Code මූලධර්ම (Core Clean Code Principles)

  • KISS (Keep It Simple, Stupid): Code එක හැකිතරම් සරලව තියාගන්න. Complex logic නැතුව, තේරුම් ගන්න ලේසි විදියට ලියන්න. අනවශ්‍ය විදියට දේවල් complicated කරන්න එපා.
  • DRY (Don't Repeat Yourself): එකම code block එක ආයෙ ආයෙ ලියන්නේ නැතුව, reusable components විදියට හදන්න. Common logic එකක් තියෙනවා නම්, ඒක method එකකට හෝ class එකකට දාලා reuse කරන්න.
  • YAGNI (You Aren't Gonna Need It): අනාගතේට ඕන වෙයි කියල හිතල අනවශ්‍ය features, code එකට දාන්න එපා. අදට ඕන දේ විතරක් කරන්න. Over-engineering වලින් වළකින්න.
  • Single Responsibility Principle (SRP): මේක SOLID principles වලින් එකක්. Simple ව කිව්වොත්, class එකකට හෝ method එකකට තියෙන්න ඕන එකම වගකීමක් (one reason to change). Controller එකක වගකීම HTTP request/response handling විතරයි. Business logic, data access ඒවට අයිති නැහැ. ඒවට වෙනම layers තියෙන්න ඕන.
  • Meaningful Names: Variables, methods, classes වලට අර්ථවත් නම් දෙන්න. a, b, temp වගේ නම් නැතුව productName, userService, processOrder වගේ නම් යොදාගන්න. Code එක කියවන ඕනම කෙනෙකුට නමෙන් ඒකේ තේරුම වටහා ගන්න පුළුවන් වෙන්න ඕන.

Messy Controller එකක් හඳුනාගනිමු (Identifying a Messy Controller)

Controller එකක් messy වෙනවා කියන්නේ, ඒක "Big Ball of Mud" වගේ වෙනවා කියන එක. ඒක හඳුනාගන්න පුළුවන් signs කීපයක් තියෙනවා:

  • Fat Controllers: එක controller එකක methods ගොඩක් තියෙනවා නම්, එක method එකක් අතිශය දීර්ඝ නම්. (e.g., 500+ lines in a single method).
  • Business Logic Direct in Controller: Controller එක ඇතුලෙම complex calculations, conditional logic, business rules වගේ දේවල් කෙලින්ම handle කරනවා නම්. මේවා service layer එකට අයිති ඒවා.
  • Direct Data Access: Controller එක ඇතුලෙම Repository එකකට access කරල, database operations කරනවා නම්. Controller එකට database එකක් ගැන දැනගන්න අවශ්‍ය නැහැ.
  • Too Many Dependencies: Controller එකක constructor එකේ dependencies ගොඩක් තියෙනවා නම්, ඒ කියන්නේ ඒ controller එකට වගකීම් ගොඩක් තියෙනවා වෙන්න පුළුවන්. (Constructor parameter list එක දිග වැඩිනම්).
  • Poor Naming Conventions: Methods වලට, variables වලට පැහැදිලි නැති නම් යොදාගෙන තිබ්බොත්. Code එක කියවන කෙනෙකුට ඒක තේරුම් ගන්න අමාරුයි.
  • Lack of Error Handling/Validation: Errors handle කරන්නේ නැතුව, හෝ input validation කරන්නේ නැතුව තිබ්බොත්, නැත්නම් ඒ හැම එකක්ම controller method එක ඇතුලෙම කරනවා නම්.

උදාහරණයක් විදියට, අපි මෙන්න මේ වගේ controller එකක් දිහා බලමු. මේකෙන් Product එකක් create කරනවා, ඒක validate කරනවා, database එකට save කරනවා, ඒවගේම response එකත් හදලා return කරනවා. එක method එක ඇතුලෙම ගොඩක් දේවල් වෙනවා.

package com.example.messycode;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@RestController
@RequestMapping("/messy-products")
public class MessyProductController {

    // Imagine ProductRepository and ProductService are directly here or logic is in controller itself

    @PostMapping
    public ResponseEntity<?> createProduct(@RequestBody Map<String, Object> productData) {
        // 1. Validation logic directly in controller
        if (productData.get("name") == null || ((String) productData.get("name")).isEmpty()) {
            return new ResponseEntity<>("Product name cannot be empty", HttpStatus.BAD_REQUEST);
        }
        if (productData.get("price") == null || !(productData.get("price") instanceof Number)) {
            return new ResponseEntity<>("Product price must be a valid number", HttpStatus.BAD_REQUEST);
        }
        if (((Number) productData.get("price")).doubleValue() <= 0) {
            return new ResponseEntity<>("Product price must be positive", HttpStatus.BAD_REQUEST);
        }

        // 2. Business logic mixed with controller logic
        String productName = (String) productData.get("name");
        Double productPrice = ((Number) productData.get("price")).doubleValue();
        String productDescription = (String) productData.get("description");

        // Imagine a simple Product class for now, no proper entity
        Product newProduct = new Product(); // Assume Product is a simple POJO
        newProduct.setName(productName);
        newProduct.setPrice(productPrice);
        newProduct.setDescription(productDescription);

        // 3. Direct data access or complex operation simulation
        System.out.println("Saving product: " + newProduct.getName() + " with price " + newProduct.getPrice());
        // Simulate saving to DB
        newProduct.setId((long) (Math.random() * 1000)); // Simulate ID generation

        // 4. Complex response construction
        Map<String, Object> response = new HashMap<>();
        response.put("message", "Product created successfully");
        response.put("productId", newProduct.getId());
        response.put("productName", newProduct.getName());
        response.put("status", "success");

        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

    // ... other messy methods
}

// A simple POJO for demonstration (should be a proper entity/DTO)
class Product {
    private Long id;
    private String name;
    private Double price;
    private String description;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
}

Controller Refactoring Strategies (Controller Refactoring Techniques)

අපේ controller එක clean කරගන්න, අපි මේ principles ටික follow කරමු:

Service Layer එකක් පාවිච්චි කරන්න (Leverage Service Layer)

Controller එකේ තියෙන්න ඕන HTTP requests handle කරන එක විතරයි. Business logic, calculations, database interactions වගේ දේවල් service layer එකකට transfer කරන්න. මෙයින් SRP (Single Responsibility Principle) එක maintain වෙනවා වගේම, code එක test කරන්නත් ලේසියි.

Data Transfer Objects (DTOs) සහ Mappers පාවිච්චි කරන්න (Use DTOs and Mappers)

Clients ලා එක්ක communicate කරන්න Entity objects කෙලින්ම use නොකර, DTO (Data Transfer Object) use කරන්න. DTO කියන්නේ client ට අවශ්‍ය data set එක විතරක් තියෙන object එකක්. Entity එකේ හැම field එකක්ම client ට ඕන වෙන්නේ නැති වෙන්න පුළුවන්, සමහර field security risks ඇති කරන්නත් පුළුවන්. Entity එක DTO එකකට convert කරන්න හෝ අනෙක් අතට convert කරන්න Mapper class එකක් (උදා: MapStruct, ModelMapper) පාවිච්චි කරන්න. මේකෙන් presentation layer එක data layer එකෙන් වෙන් කරනවා.

Global Exception Handling (ව්‍යතිරේක හැසිරවීම)

සෑම method එකක් ඇතුලෙම try-catch දාල error handle කරනවා වෙනුවට, @ControllerAdvice සහ @ExceptionHandler පාවිච්චි කරල global exception handling කරන්න. මෙයින් code redundancy අඩු වෙනවා, error responses consistent වෙනවා වගේම, controller methods clean වෙනවා.

Input Validation (දත්ත වලංගු කිරීම)

Business logic validation service layer එකේ තිබ්බත්, basic input validation (e.g., NotNull, Min, Max, NotBlank) controller level එකේදී JSR 303/380 (Bean Validation) annotations පාවිච්චි කරල කරන්න පුළුවන්. @Valid annotation එක controller method parameter එකේ use කරල, automatically validation කරන්න. මේකෙන් controller එකේ අනවශ්‍ය if-else blocks අයින් වෙනවා.

RESTful Endpoint Design

ඔබේ API endpoints RESTful principles වලට අනුව design කරන්න. Resource-based URLs (/products, /users/{id}), Standard HTTP methods (GET, POST, PUT, DELETE), Proper HTTP status codes (200 OK, 201 Created, 400 Bad Request, 404 Not Found) යොදාගන්න. මේකෙන් API එක intuitive සහ predictable වෙනවා.

Dependency Injection

@Autowired annotation එක constructor injection වලට යොදාගන්න. Field injection වලින් වැලකිලා ඉන්න. Constructors clean ව තියාගන්න. මේකෙන් testability එක වැඩි වෙනවා වගේම, dependencies මොනවද කියලා පැහැදිලිව පේනවා.

ප්‍රායෝගික උදාහරණයක්: Refactoring Step-by-Step (A Practical Example: Refactoring Step-by-Step)

අපි දැන් උඩ තිබ්බ MessyProductController එක clean code principles වලට අනුව refactor කරමු. මේ සඳහා අපි Spring Boot project එකක සාමාන්‍යයෙන් භාවිතා කරන layers ටිකක් හදාගමු.

පළමු පියවර: Dependencies සහ Layers වෙන් කිරීම (Step 1: Separating Dependencies and Layers)

අපි ProductService එකක් සහ ProductRepository එකක් හදාගමු. ProductController එකට තියෙන්නේ ProductService එක විතරයි. Product කියන්නේ Entity එකක් විදියට හිතමු, ඒවගේම ProductDTO එකක් හදාගමු.

ProductRepository.java (Repository Interface)

package com.example.cleancode.repository;

import com.example.cleancode.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

Product.java (JPA Entity)

package com.example.cleancode.entity;

import javax.persistence.*; // Or jakarta.persistence for Spring Boot 3+

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Double price;
    private String description;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    // Constructor, equals, hashCode, toString methods can be added for completeness
}

ProductDTO.java (Data Transfer Object for input/output)

package com.example.cleancode.dto;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class ProductDTO {
    private Long id;

    @NotBlank(message = "Product name cannot be empty")
    @Size(min = 3, max = 100, message = "Product name must be between 3 and 100 characters")
    private String name;

    @NotNull(message = "Product price cannot be null")
    @Min(value = 0, message = "Product price must be positive or zero")
    private Double price;

    @Size(max = 500, message = "Product description cannot exceed 500 characters")
    private String description;

    // Getters and Setters
    public ProductDTO() {}

    public ProductDTO(Long id, String name, Double price, String description) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.description = description;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
}

ProductService.java (Service Layer)

package com.example.cleancode.service;

import com.example.cleancode.entity.Product;
import com.example.cleancode.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public Product createProduct(Product product) {
        // Here you can add more complex business logic if needed
        // For example, checking for duplicate product names
        // Or applying special pricing rules
        System.out.println("Business logic: Preparing to save product " + product.getName());
        return productRepository.save(product);
    }

    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }

    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }
    // Other business methods for update, delete, etc. can be added here
}

දෙවන පියවර: Mapper එකක් පාවිච්චි කිරීම (Step 2: Using a Mapper)

Entity එකක් DTO එකකට convert කරන්න අපි ProductMapper එකක් හදමු. මේකට MapStruct වගේ library එකක් පාවිච්චි කරන්න පුළුවන්. සරලව අපි අතින්ම ලියමු.

ProductMapper.java

package com.example.cleancode.mapper;

import com.example.cleancode.dto.ProductDTO;
import com.example.cleancode.entity.Product;
import org.springframework.stereotype.Component;

@Component
public class ProductMapper {

    public Product toEntity(ProductDTO productDTO) {
        if (productDTO == null) {
            return null;
        }
        Product product = new Product();
        product.setId(productDTO.getId());
        product.setName(productDTO.getName());
        product.setPrice(productDTO.getPrice());
        product.setDescription(productDTO.getDescription());
        return product;
    }

    public ProductDTO toDto(Product product) {
        if (product == null) {
            return null;
        }
        return new ProductDTO(product.getId(), product.getName(), product.getPrice(), product.getDescription());
    }
}

තෙවන පියවර: Global Exception Handling (Step 3: Global Exception Handling)

Custom exceptions සහ @ControllerAdvice භාවිතයෙන් error handling centralized කරමු.

ResourceNotFoundException.java (Custom Exception)

package com.example.cleancode.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

GlobalExceptionHandler.java (Centralized Exception Handler)

package com.example.cleancode.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<String> handleResourceNotFoundException(ResourceNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
                errors.put(error.getField(), error.getDefaultMessage()));
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception ex) {
        return new ResponseEntity<>("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

හතරවන පියවර: Clean Controller එක (Step 4: The Clean Controller)

දැන් අපි ProductController එක නැවත ලියමු. මේකෙ තියෙන්නේ ProductService එකට calls කරන එක විතරයි. බලන්න, මේක කොච්චර clean ද කියලා!

ProductController.java (Refactored & Clean Controller)

package com.example.cleancode.controller;

import com.example.cleancode.dto.ProductDTO;
import com.example.cleancode.entity.Product;
import com.example.cleancode.exception.ResourceNotFoundException;
import com.example.cleancode.mapper.ProductMapper;
import com.example.cleancode.service.ProductService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid; // Or jakarta.validation.Valid for Spring Boot 3+
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    private final ProductService productService;
    private final ProductMapper productMapper;

    // Constructor Injection for dependencies
    public ProductController(ProductService productService, ProductMapper productMapper) {
        this.productService = productService;
        this.productMapper = productMapper;
    }

    @PostMapping
    public ResponseEntity<ProductDTO> createProduct(@Valid @RequestBody ProductDTO productDTO) {
        Product productEntity = productMapper.toEntity(productDTO);
        Product savedProduct = productService.createProduct(productEntity);
        return new ResponseEntity<>(productMapper.toDto(savedProduct), HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
        Product product = productService.getProductById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
        return ResponseEntity.ok(productMapper.toDto(product));
    }

    @GetMapping
    public ResponseEntity<List<ProductDTO>> getAllProducts() {
        List<ProductDTO> products = productService.getAllProducts().stream()
                .map(productMapper::toDto)
                .collect(Collectors.toList());
        return ResponseEntity.ok(products);
    }

    // ... other clean methods for update, delete can be added here
}

Refactoring එකෙන් ලැබුණු වාසි (Benefits of Refactoring)

  • Single Responsibility Principle (SRP): Controller එකට දැන් තියෙන්නේ HTTP request/response handling විතරයි. Business logic සහ data access service සහ repository layers වලට ගිහින්.
  • Readability and Maintainability: Code එක කියවන්න සහ තේරුම් ගන්න දැන් ගොඩක් ලේසියි. අලුත් developers ලාට වුණත් ඉක්මනින් project එකට contribute කරන්න පුළුවන්.
  • Testability: Service layer එකේ business logic test කරන්න දැන් ගොඩක් ලේසියි. Controller එකට mock කරන්න ඕන වෙන්නේ service එක විතරයි.
  • Reduced Duplication (DRY): Validation සහ error handling දැන් centralized කරලා තියෙන්නේ. එකම code block එක ආයෙ ආයෙ ලියන්න අවශ්‍ය නැහැ.
  • Better Separation of Concerns: Project එකේ layers අතර හොඳින් වගකීම් වෙන් වෙලා තියෙනවා. මේකෙන් project එකේ scalability එක වැඩි වෙනවා.

නිගමනය (Conclusion)

අද අපි කතා කරපු "Clean Code principles for Spring Boot controllers" කියන topic එකෙන් ඔයාලට වැදගත් කරුණු ගොඩක් ඉගෙන ගන්න ලැබුණා කියලා මම හිතනවා. Controller එකක් clean කරගැනීමෙන් අපේ code base එක කියවන්න ලේසි වෙනවා, maintain කරන්න ලේසි වෙනවා, අලුත් features add කරන්න ලේසි වෙනවා, ඒ වගේම test කරන්නත් ලේසි වෙනවා.

මතක තියාගන්න, Clean Code කියන්නේ එක් දවසකින් කරන්න පුළුවන් දෙයක් නෙමෙයි. ඒක continuous process එකක්. හැමදාම code කරනකොට මේ principles ගැන හිතලා වැඩ කරන්න. පුංචි පුංචි improvements වලින් වුණත් කාලයත් එක්ක ලොකු වෙනසක් කරන්න පුළුවන්.

ඔයාලගේ project වලත් මේ methods apply කරලා බලන්න. මොන වගේ improvements ද දැක්කේ? ඔයාලගේ අදහස්, ප්‍රශ්න පහල comment section එකේ දාන්න අමතක කරන්න එපා. අපි තවත් මේ වගේ වැදගත් දේවල් එක්ක ආයෙත් හමුවෙමු!