정리한 내용 중 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있다면 꼭 알려주세요.
객체 지향 설계의 5가지 원칙
한 클래스는 하나의 책임만 가져야 한다.
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.
서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다
상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 의존해야 한다.
추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.
조금 더 자세하게 코드로 살펴보자.
class PaymentController {
@PostMapping("/api/payment")
public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
shinhanCardPaymentService.pay(req);
}
}
class ShinhanCardPaymentService {
public void pay(hinhanCardDto.PaymentRequest req) {
shinhanCardApi.apy(req);
}
}
위 코드에서 새로운 카드사가 추가된다면 어떻게 될까?
class PaymentController {
@PostMapping("/api/payment")
public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
if(req.getType() == CardType.SHINHAN) {
shinhanCardPaymentService.pay(req);
} else if (req.getType() == CardType.WOORI) {
wooriCardPaymenyService.pay(req);
}
}
}
위와 같이 작성한다면 PaymentController
에서는 ShinhanCardPaymentService
와 WooriCardPaymenyService
의 의존성을 갖고있고,
카드사가 더 많아진다면 if else문이 계속 생기는 등 확장성에 좋지 않다.
그럼 어떻게 해야할까?
위 그림과 같이 추상화된 인터페이스에 의존하도록 하면 된다.
그렇다면 아래와 같이 코드를 깔끔하게 작성할 수 있다.
class PaymentController {
@PostMapping("/api/payment")
public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
cardPaymentService.pay(req);
}
}
public interface CardPaymentService {
void pay(CardPaymentDto.PaymentRequest req);
}
class ShinhanCardPaymentService implements CardPaymentService {
@Override
public void pay(CardPaymentDto.PaymentRequest req) {
shinhanCardApi.apy(req);
}
}
아래 코드를 보면 함수가 길고, 여러가지 기능이 섞여있는걸 볼 수 있다.
public static String renderPageWithSetupsAndTeardowns(PageData pagedata, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
위 함수를 아래처럼 작게 쪼개고, 함수 내 추상화 수준을 동일하게 맞춘다.
public static String renderPageWithSetupsAndTeardowns(PageData pagedata, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
위 코드에서는 두 가지 문제점이 보인다.
1.계산도 하고, Money도 생성한다. 라는 두가지의 기능이 보인다.
2.새로운 직원 타입이 추가된다면?
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmplyeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmplyeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return CommissionEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
계산과 타입관리를 분리하여 계산은 Employee class 내부에 calculatePay메서드를 통해 계산하고
타입에 대한 처리는 최대한 Factory에서만 하라고 권고한다.
인수의 갯수는 0~2개가 적당하다고 한다.
3개 이상인 경우에는
// 1.객체를 인자로 넘기는 방법👍🏻
Circle makeCircle(double x, double y, double redius); // 👎🏻
Circle makeCircle(Point center, double redius); // 👍🏻
// 2.가변 인자를 넘기기 -> 특별한 경우가 아니라면 잘 사용하지는 않는다. 🤔
String.format(String format, Object... args);
부수효과? 값을 반환하는 함수가 외부 상태를 변경하는 경우
public class UserValidater {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize(); //함수와 관계 없는 외부 상태를 변경시킨다.
return true;
}
}
return false;
}
}
위 코드에서 Session을 초기화 하는 함수가 들어가있는데, 패스워드 체크하는 함수에서
외부의 상태를 변경시키기 때문에, 좋지 않은 코드라고 말할 수 있다.