소프트웨어 개발에서 유지 관리, 확장 가능, 효율적인 애플리케이션을 생성하려면 깔끔한 코드 관행을 준수하는 것이 필수적입니다. 이러한 규율을 장려하는 접근 방식 중 하나는 개발자가 더 나은 객체 지향 코드를 작성하도록 안내하기 위해 고안된 일련의 규칙인 객체 미용 체조입니다.
이번 게시물에서는 객체 미용 체조의 원리와 이를 적용하여 더욱 깔끔하고 구조화된 코드를 작성하는 방법을 살펴보겠습니다. 실용적인 코드 예제에 대해 더 자세히 알아보거나 이러한 원칙이 실제 프로젝트에서 어떻게 구현되는지 알아보고 싶다면 내 GitHub 저장소에서 자세한 내용을 확인하세요.
자세한 설명을 보려면 내 GitHub 저장소를 방문하세요.
객체 미용 체조는 객체 지향 원칙을 준수하도록 권장하는 9가지 규칙으로 구성된 프로그래밍 연습입니다. 규칙 자체는 Jeff Bay의 저서 The ThoughtWorks Anthology에서 소개되었습니다. 목표는 이러한 제약 조건을 준수하여 코드의 디자인과 구조에 더 깊이 집중하도록 장려하는 것입니다.
9가지 규칙은 다음과 같습니다.
이 원칙에 따르면 각 메서드에는 한 수준의 들여쓰기만 있어야 합니다. 들여쓰기 수준을 낮게 유지하면 가독성이 향상되고 코드 유지 관리가 더 쉬워집니다. 메서드의 들여쓰기 수준이 너무 많으면 내부 논리가 너무 복잡하여 더 작은 조각으로 나누어야 한다는 신호일 때가 많습니다.
구현예
다음은 이 원칙을 따르지 않는 방법의 예입니다.
public class OrderService { public void processOrder(Order order) { if (order.isValid()) { if (order.hasStock()) { if (order.hasEnoughBalance()) { order.ship(); } else { System.out.println("Insufficient balance."); } } else { System.out.println("Stock unavailable."); } } else { System.out.println("Invalid order."); } } }
위 코드의 processOrder 메소드에는 여러 수준의 들여쓰기가 있어 특히 로직이 더 복잡해지면 읽기가 더 어려워집니다.
이제 논리를 단순화하여 어떻게 원리를 따를 수 있는지 살펴보겠습니다.
public class OrderService { public void processOrder(Order order) { if (!isOrderValid(order)) return; if (!hasStock(order)) return; if (!hasEnoughBalance(order)) return; shipOrder(order); } private boolean isOrderValid(Order order) { if (!order.isValid()) { System.out.println("Invalid order."); return false; } return true; } private boolean hasStock(Order order) { if (!order.hasStock()) { System.out.println("Stock unavailable."); return false; } return true; } private boolean hasEnoughBalance(Order order) { if (!order.hasEnoughBalance()) { System.out.println("Insufficient balance."); return false; } return true; } private void shipOrder(Order order) { order.ship(); } }
이 버전에는 논리 블록당 한 수준의 들여쓰기가 있어 코드가 더 깔끔하고 유지 관리가 더 쉽습니다.
객체 지향 프로그래밍에서 else 키워드를 사용하면 코드가 복잡해지고 유지 관리가 어려워지는 경우가 많습니다. else 문에 의존하는 대신 조기 반환이나 다형성을 사용하여 코드를 구조화할 수 있습니다. 이 접근 방식을 사용하면 가독성이 향상되고 복잡성이 줄어들며 코드 유지 관리가 더욱 쉬워집니다.
먼저 else 키워드를 사용하는 예를 살펴보겠습니다.
public class Order { public void processOrder(boolean hasStock, boolean isPaid) { if (hasStock && isPaid) { System.out.println("Order processed."); } else { System.out.println("Order cannot be processed."); } } }
이 예에서 else 문은 부정 사례를 명시적으로 처리하도록 강제하여 복잡성을 더합니다. 이제 else를 사용하지 않도록 리팩터링해 보겠습니다.
public class Order { public void processOrder(boolean hasStock, boolean isPaid) { if (!hasStock) { System.out.println("Order cannot be processed: out of stock."); return; } if (!isPaid) { System.out.println("Order cannot be processed: payment pending."); return; } System.out.println("Order processed."); } }
다른 항목을 제거하면 각 섹션이 단일 책임을 처리하는 보다 집중적인 코드를 작성할 수 있습니다. 이는 "단일 책임 원칙"에 부합하며 더욱 깨끗하고 유지 관리하기 쉬운 소프트웨어를 만드는 데 도움이 됩니다.
Object Calisthenics의 주요 방법 중 하나는 사용자 정의 클래스에서 모든 기본 요소와 문자열을 래핑하는 것입니다. 이 규칙은 개발자가 원시 기본 유형이나 문자열을 코드에서 직접 사용하지 않도록 권장합니다. 대신 의미 있는 객체 내에 캡슐화해야 합니다. 이러한 접근 방식을 사용하면 표현력이 더 풍부하고 이해하기 쉬우며 관리하기 어려워지지 않으면서 복잡성이 커질 수 있는 코드가 탄생합니다.
기본 유형과 문자열을 직접 사용하는 예를 고려해 보겠습니다.
public class Order { private int quantity; private double pricePerUnit; private String productName; public Order(int quantity, double pricePerUnit, String productName) { this.quantity = quantity; this.pricePerUnit = pricePerUnit; this.productName = productName; } public double calculateTotal() { return quantity * pricePerUnit; } }
이 코드에서는 int, double, String을 직접 사용합니다. 단순해 보일 수도 있지만 각 값의 의도를 명확하게 표현하지 않으며 나중에 해당 값과 관련된 새로운 동작이나 제약 조건을 도입하기가 더 어렵습니다.
이제 이 코드를 리팩터링하여 프리미티브와 문자열을 래핑합니다.
public class Quantity { private final int value; public Quantity(int value) { if (value < 1) { throw new IllegalArgumentException("Quantity must be greater than zero."); } this.value = value; } public int getValue() { return value; } } public class Price { private final double value; public Price(double value) { if (value <= 0) { throw new IllegalArgumentException("Price must be positive."); } this.value = value; } public double getValue() { return value; } } public class ProductName { private final String name; public ProductName(String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Product name cannot be empty."); } this.name = name; } public String getValue() { return name; } } public class Order { private final Quantity quantity; private final Price pricePerUnit; private final ProductName productName; public Order(Quantity quantity, Price pricePerUnit, ProductName productName) { this.quantity = quantity; this.pricePerUnit = pricePerUnit; this.productName = productName; } public double calculateTotal() { return quantity.getValue() * pricePerUnit.getValue(); } }
주요 변경 사항:
목록이나 지도와 같은 컬렉션을 사용할 때마다 전용 개체로 만드세요. 이렇게 하면 코드가 깔끔하게 유지되고 컬렉션과 관련된 로직이 캡슐화됩니다.
Problem: Using a Raw Collection Directly
public class Order { private List<String> items; public Order() { this.items = new ArrayList<>(); } public void addItem(String item) { items.add(item); } public List<String> getItems() { return items; } }
In this example, Order directly uses a List
Solution: Using a First-Class Collection
public class Order { private Items items; public Order() { this.items = new Items(); } public void addItem(String item) { items.add(item); } public List<String> getItems() { return items.getAll(); } } class Items { private List<String> items; public Items() { this.items = new ArrayList<>(); } public void add(String item) { items.add(item); } public List<String> getAll() { return new ArrayList<>(items); // Return a copy to prevent external modification } public int size() { return items.size(); } }
Explanation:
Benefits of First-Class Collections:
One Dot Per Line is an Object Calisthenics rule that encourages developers to avoid chaining multiple method calls on a single line. The principle focuses on readability, maintainability, and debugging ease by ensuring that each line in the code only contains a single method call (represented by one "dot").
The Problem with Method Chaining (Multiple Dots Per Line)
When multiple methods are chained together on one line, it can become difficult to:
Example: Multiple Dots Per Line (Anti-Pattern)
public class OrderProcessor { public void process(Order order) { String city = order.getCustomer().getAddress().getCity().toUpperCase(); } }
In this example, we have multiple method calls chained together (getCustomer(), getAddress(), getCity(), toUpperCase()). This makes the line of code compact but harder to understand and maintain.
Solution: One Dot Per Line
By breaking down the method chain, we make the code easier to read and debug:
public class OrderProcessor { public void process(Order order) { Customer customer = order.getCustomer(); // One dot: getCustomer() Address address = customer.getAddress(); // One dot: getAddress() String city = address.getCity(); // One dot: getCity() String upperCaseCity = city.toUpperCase(); // One dot: toUpperCase() } }
This code follows the One Dot Per Line rule by breaking down a method chain into smaller, readable pieces, ensuring that each line performs only one action. This approach increases readability, making it clear what each step does, and improves maintainability by making future changes easier to implement. Moreover, this method helps during debugging, since each operation is isolated and can be checked independently if any errors occur.
In short, the code emphasizes clarity, separation of concerns, and ease of future modifications, which are key benefits of applying Object Calisthenics rules such as One Dot Per Line.
One of the key rules in Object Calisthenics is Don’t Abbreviate, which emphasizes clarity and maintainability by avoiding cryptic or shortened variable names. When we name variables, methods, or classes, it’s important to use full, descriptive names that accurately convey their purpose.
Example of Bad Practice (With Abbreviations):
public class EmpMgr { public void calcSal(Emp emp) { double sal = emp.getSal(); // Salary calculation logic } }
Example of Good Practice (Without Abbreviations):
public class EmployeeManager { public void calculateSalary(Employee employee) { double salary = employee.getSalary(); // Salary calculation logic } }
By avoiding abbreviations, we make our code more understandable and maintainable, especially for other developers (or even ourselves) who will work on it in the future.
In Object Calisthenics, one of the fundamental rules is Keep Entities Small, which encourages us to break down large classes or methods into smaller, more manageable ones. Each entity (class, method, etc.) should ideally have one responsibility, making it easier to understand, maintain, and extend.
Example of Bad Practice (Large Class with Multiple Responsibilities):
public class Employee { private String name; private String address; private String department; public void updateAddress(String newAddress) { this.address = newAddress; } public void promote(String newDepartment) { this.department = newDepartment; } public void generatePaySlip() { // Logic for generating a payslip } public void calculateTaxes() { // Logic for calculating taxes } }
In this example, the Employee class is doing too much. It’s handling address updates, promotions, payslip generation, and tax calculation. This makes the class harder to manage and violates the Single Responsibility Principle.
Example of Good Practice (Small Entities with Single Responsibilities):
public class Employee { private String name; private String address; private String department; public void updateAddress(String newAddress) { this.address = newAddress; } public void promote(String newDepartment) { this.department = newDepartment; } } public class PaySlipGenerator { public void generate(Employee employee) { // Logic for generating a payslip for an employee } } public class TaxCalculator { public double calculate(Employee employee) { // Logic for calculating taxes for an employee return 0.0; } }
By breaking the class into smaller entities, we:
The principle of avoiding getters and setters encourages encapsulation by ensuring that classes manage their own data and behavior. Instead of exposing internal state, we should focus on providing meaningful methods that perform actions relevant to the class's responsibility.
Consider the following example that uses getters and setters:
public class BankAccount { private double balance; public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public void deposit(double amount) { balance += amount; } public void withdraw(double amount) { if (amount <= balance) { balance -= amount; } } }
In this example, the getBalance method exposes the internal state of the BankAccount class, allowing direct access to the balance variable.
A better approach would be to avoid exposing the balance directly and provide methods that represent the actions that can be performed on the account:
public class BankAccount { private double balance; public void deposit(double amount) { balance += amount; } public void withdraw(double amount) { if (amount <= balance) { balance -= amount; } } public boolean hasSufficientFunds(double amount) { return amount <= balance; } }
In this revised example, the BankAccount class no longer has getters or setters. Instead, it provides methods for depositing, withdrawing, and checking if there are sufficient funds. This approach maintains the integrity of the internal state and enforces rules about how the data can be manipulated, promoting better encapsulation and adherence to the single responsibility principle.
The principle of separating user interface (UI) code from business logic is essential for creating maintainable and testable applications. By keeping these concerns distinct, we can make changes to the UI without affecting the underlying business rules and vice versa.
Consider the following example where the UI and business logic are intertwined:
public class UserRegistration { public void registerUser(String username, String password) { // UI Logic: Validation if (username.isEmpty() || password.length() < 6) { System.out.println("Invalid username or password."); return; } // Business Logic: Registration System.out.println("User " + username + " registered successfully."); } }
In this example, the registerUser method contains both UI logic (input validation) and business logic (registration). This makes the method harder to test and maintain.
A better approach is to separate the UI from the business logic, as shown in the following example:
public class UserRegistration { public void registerUser(User user) { // Business Logic: Registration System.out.println("User " + user.getUsername() + " registered successfully."); } } public class UserRegistrationController { private UserRegistration userRegistration; public UserRegistrationController(UserRegistration userRegistration) { this.userRegistration = userRegistration; } public void handleRegistration(String username, String password) { // UI Logic: Validation if (username.isEmpty() || password.length() < 6) { System.out.println("Invalid username or password."); return; } User user = new User(username, password); userRegistration.registerUser(user); } } class User { private String username; private String password; public User(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } }
In this improved design, the UserRegistration class contains only the business logic for registering a user. The UserRegistrationController is responsible for handling UI logic, such as input validation and user interaction. This separation makes it easier to test and maintain the business logic without being affected by changes in the UI.
By adhering to this principle, we enhance the maintainability and testability of our applications, making them easier to adapt to future requirements.
Object Calisthenics might seem a bit strict at first, but give it a try! It's all about writing cleaner, more maintainable code that feels good to work with. So, why not challenge yourself? Start small, and before you know it, you’ll be crafting code that not only works but also shines. Happy coding!
위 내용은 객체 미용 체조 이해: 더욱 깔끔한 코드 작성의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!