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
// 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
// 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.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());
}
}
- Extract Method
- Extract Class
- Move Method
- Rename
- Change Method Signature
- Extract Interface
- Pull Up/Push Down
- Encapsulate Field
<!-- 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