Refactoring trong Java

1. Nguyên Tắc Refactoring

1.1 Định Nghĩa

  • Refactoring là gì?
  • Tại sao cần refactoring?
  • Khi nào nên refactoring?
  • Lợi ích của refactoring
  • Rủi ro và thách thức

1.2 Các Loại Refactoring

  • Method-level Refactoring
  • Class-level Refactoring
  • Data-level Refactoring
  • Architecture-level Refactoring
  • Performance Refactoring

2. Method-level Refactoring

2.1 Extract Method

// Before
public void processOrder(Order order) {
    // Validate order
    if (order == null || order.getItems() == null) {
        throw new ValidationException("Invalid order");
    }
    if (order.getItems().isEmpty()) {
        throw new ValidationException("Order must have items");
    }

    // Calculate total
    BigDecimal total = BigDecimal.ZERO;
    for (OrderItem item : order.getItems()) {
        total = total.add(item.getPrice()
            .multiply(new BigDecimal(item.getQuantity())));
    }
    order.setTotal(total);

    // Process payment
    paymentService.process(order);
}

// After
public void processOrder(Order order) {
    validateOrder(order);
    calculateTotal(order);
    processPayment(order);
}

private void validateOrder(Order order) {
    if (order == null || order.getItems() == null) {
        throw new ValidationException("Invalid order");
    }
    if (order.getItems().isEmpty()) {
        throw new ValidationException("Order must have items");
    }
}

private void calculateTotal(Order order) {
    BigDecimal total = order.getItems().stream()
        .map(item -> item.getPrice()
            .multiply(new BigDecimal(item.getQuantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    order.setTotal(total);
}

private void processPayment(Order order) {
    paymentService.process(order);
}

2.2 Replace Temp with Query

// Before
public double calculateTotal() {
    double basePrice = quantity * itemPrice;
    double discount = Math.max(0, quantity - 500) * itemPrice * 0.05;
    double shipping = Math.min(basePrice * 0.1, 100.0);
    return basePrice - discount + shipping;
}

// After
public double calculateTotal() {
    return getBasePrice() - getDiscount() + getShipping();
}

private double getBasePrice() {
    return quantity * itemPrice;
}

private double getDiscount() {
    return Math.max(0, quantity - 500) * itemPrice * 0.05;
}

private double getShipping() {
    return Math.min(getBasePrice() * 0.1, 100.0);
}

3. Class-level Refactoring

3.1 Extract Class

// Before
public class Order {
    private List<OrderItem> items;
    private String customerName;
    private String customerEmail;
    private String customerPhone;
    private String shippingAddress;
    private String billingAddress;

    // Methods using customer information
}

// After
public class Customer {
    private String name;
    private String email;
    private String phone;
    private Address shippingAddress;
    private Address billingAddress;

    // Customer-related methods
}

public class Order {
    private List<OrderItem> items;
    private Customer customer;

    // Order-specific methods
}

public class Address {
    private String street;
    private String city;
    private String state;
    private String zipCode;

    // Address-related methods
}

3.2 Move Method

// Before
public class Order {
    private Customer customer;

    public boolean isValidCustomer() {
        return customer.getEmail() != null &&
               customer.getPhone() != null &&
               isValidEmail(customer.getEmail()) &&
               isValidPhone(customer.getPhone());
    }

    private boolean isValidEmail(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }

    private boolean isValidPhone(String phone) {
        return phone.matches("^\\d{10}$");
    }
}

// After
public class Customer {
    private String email;
    private String phone;

    public boolean isValid() {
        return email != null &&
               phone != null &&
               isValidEmail(email) &&
               isValidPhone(phone);
    }

    private boolean isValidEmail(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }

    private boolean isValidPhone(String phone) {
        return phone.matches("^\\d{10}$");
    }
}

public class Order {
    private Customer customer;

    public boolean isValidCustomer() {
        return customer.isValid();
    }
}

4. Data-level Refactoring

4.1 Replace Type Code with Class

// Before
public class Order {
    private static final int STATUS_PENDING = 0;
    private static final int STATUS_PROCESSING = 1;
    private static final int STATUS_COMPLETED = 2;
    private static final int STATUS_CANCELLED = 3;

    private int status;

    public void setStatus(int status) {
        this.status = status;
    }

    public boolean isComplete() {
        return status == STATUS_COMPLETED;
    }
}

// After
public enum OrderStatus {
    PENDING,
    PROCESSING,
    COMPLETED,
    CANCELLED;

    public boolean isComplete() {
        return this == COMPLETED;
    }
}

public class Order {
    private OrderStatus status;

    public void setStatus(OrderStatus status) {
        this.status = status;
    }

    public boolean isComplete() {
        return status.isComplete();
    }
}

4.2 Encapsulate Collection

// Before
public class Order {
    private List<OrderItem> items;

    public List<OrderItem> getItems() {
        return items;
    }

    public void setItems(List<OrderItem> items) {
        this.items = items;
    }
}

// After
public class Order {
    private final List<OrderItem> items = new ArrayList<>();

    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    public void addItem(OrderItem item) {
        items.add(item);
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
    }

    public int getItemCount() {
        return items.size();
    }
}

5. Architecture-level Refactoring

5.1 Layer Separation

// Before
@RestController
public class OrderController {
    @PostMapping("/orders")
    public Order createOrder(@RequestBody OrderRequest request) {
        // Validation
        if (request == null || request.getItems().isEmpty()) {
            throw new ValidationException("Invalid order");
        }

        // Business logic
        Order order = new Order();
        order.setItems(request.getItems());
        order.calculateTotal();

        // Database operation
        Connection conn = DriverManager.getConnection(DB_URL);
        PreparedStatement stmt = conn.prepareStatement(
            "INSERT INTO orders ...");
        stmt.executeUpdate();

        return order;
    }
}

// After
@RestController
public class OrderController {
    private final OrderService orderService;
    private final OrderRequestValidator validator;

    @PostMapping("/orders")
    public OrderResponse createOrder(@RequestBody OrderRequest request) {
        validator.validate(request);
        Order order = orderService.createOrder(request);
        return OrderResponse.from(order);
    }
}

@Service
public class OrderService {
    private final OrderRepository repository;
    private final OrderFactory factory;

    public Order createOrder(OrderRequest request) {
        Order order = factory.createFrom(request);
        order.calculateTotal();
        return repository.save(order);
    }
}

@Repository
public class OrderRepository {
    private final JdbcTemplate jdbcTemplate;

    public Order save(Order order) {
        // Database operations
    }
}

5.2 Dependency Injection

// Before
public class OrderService {
    private final PaymentService paymentService = new PaymentService();
    private final InventoryService inventoryService = new InventoryService();

    public void processOrder(Order order) {
        paymentService.process(order);
        inventoryService.update(order);
    }
}

// After
@Configuration
public class ServiceConfig {
    @Bean
    public OrderService orderService(
            PaymentService paymentService,
            InventoryService inventoryService) {
        return new OrderService(paymentService, inventoryService);
    }
}

@Service
public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;

    public OrderService(
            PaymentService paymentService,
            InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }

    public void processOrder(Order order) {
        paymentService.process(order);
        inventoryService.update(order);
    }
}

6. Performance Refactoring

6.1 Caching

// Before
@Service
public class ProductService {
    private final ProductRepository repository;

    public Product getProduct(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new NotFoundException("Product not found"));
    }
}

// After
@Service
public class ProductService {
    private final ProductRepository repository;
    private final Cache<Long, Product> cache;

    public Product getProduct(Long id) {
        return cache.get(id, key -> repository.findById(key)
            .orElseThrow(() -> new NotFoundException("Product not found")));
    }
}

@Configuration
public class CacheConfig {
    @Bean
    public Cache<Long, Product> productCache() {
        return Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build();
    }
}

6.2 Batch Processing

// Before
public void processOrders(List<Order> orders) {
    for (Order order : orders) {
        processOrder(order);
    }
}

// After
public void processOrders(List<Order> orders) {
    // Group orders by status
    Map<OrderStatus, List<Order>> ordersByStatus = orders.stream()
        .collect(Collectors.groupingBy(Order::getStatus));

    // Process each group in parallel
    ordersByStatus.entrySet().parallelStream().forEach(entry -> {
        List<Order> groupOrders = entry.getValue();
        switch (entry.getKey()) {
            case NEW:
                processBatch(groupOrders, this::validateAndInitialize);
                break;
            case PENDING:
                processBatch(groupOrders, this::processPayment);
                break;
            case PAID:
                processBatch(groupOrders, this::updateInventory);
                break;
        }
    });
}

private void processBatch(List<Order> orders, 
        Consumer<List<Order>> processor) {
    int batchSize = 100;
    for (int i = 0; i < orders.size(); i += batchSize) {
        List<Order> batch = orders.subList(i, 
            Math.min(i + batchSize, orders.size()));
        processor.accept(batch);
    }
}

7. Testing During Refactoring

7.1 Unit Testing

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    @Mock
    private PaymentService paymentService;

    @Mock
    private InventoryService inventoryService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void whenProcessOrder_thenSuccess() {
        // Given
        Order order = createTestOrder();

        // When
        orderService.processOrder(order);

        // Then
        verify(paymentService).process(order);
        verify(inventoryService).update(order);
    }

    @Test
    void whenValidateOrder_thenSuccess() {
        // Given
        Order order = createValidOrder();

        // When/Then
        assertDoesNotThrow(() -> 
            orderService.validateOrder(order));
    }
}

7.2 Integration Testing

@SpringBootTest
public class OrderServiceIntegrationTest {
    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    @Transactional
    void whenCreateOrder_thenSavedInDatabase() {
        // Given
        OrderRequest request = createValidRequest();

        // When
        Order result = orderService.createOrder(request);

        // Then
        Optional<Order> savedOrder = 
            orderRepository.findById(result.getId());
        assertThat(savedOrder).isPresent();
        assertThat(savedOrder.get().getItems())
            .hasSameSizeAs(request.getItems());
    }
}

8. Tools và Automation

8.1 IDE Refactoring Tools

  • Extract Method
  • Extract Class
  • Move Method
  • Rename
  • Change Method Signature
  • Extract Interface
  • Pull Up/Push Down
  • Encapsulate Field

8.2 Static Analysis Tools

<!-- pom.xml -->
<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>${sonar.version}</version>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <version>${checkstyle.version}</version>
</plugin>

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>${spotbugs.version}</version>
</plugin>

9. References và Further Reading

9.1 Books

  • Refactoring: Improving the Design of Existing Code (Martin Fowler)
  • Working Effectively with Legacy Code (Michael Feathers)
  • Clean Code (Robert C. Martin)
  • Patterns of Enterprise Application Architecture (Martin Fowler)
  • Domain-Driven Design (Eric Evans)

9.2 Online Resources