좋은 설계를 위해선 깔끔한 코드가 필요.
이러한 깔끔한 코드는 SOLID 원칙을 따르는 것이 좋다.
SOLID 원칙을 따르게 된다면 변경에 유연하고, 코드가 이해해지기 쉬우며, 많은 소프트웨어 시스템에 사용 될 수 있는 컴포넌트의 기반이 된다.
단일 모듈의 변경 이유는 오직 하나뿐이어야 한다.
모듈이 SRP를 어기는 징조는 다음과 같다.
데이터와 메서드를 분리하는 방법.
이렇게 분리를하면 병합의 문제는 피할 수 있으나 세 가지 클래스를 인스턴스화하고 추적해야하는 번거로움이 있다.
이런 번거로움을 해결하기 위해 퍼사드 패턴을 활용하는게 좋음.
public void view()
{
Beverage beverage = new Beverage("콜라");
Remote_Control remote= new Remote_Control();
Movie movie = new Movie("어벤져스");
beverage.Prepare(); //음료 준비
remote.Turn_On(); //tv를 켜다
movie.Search_Movie(); //영화를 찾다
movie.Charge_Movie(); // 영화를 결제하다
movie.play_Movie(); //영화를 재생하다
}
해당 코드를 아래와 같이 변경하는 예시가 facade pattern을 도입하는 것이다
public class Remote_Control {
public void Turn_On()
{
System.out.println("TV를 켜다");
}
public void Turn_Off()
{
System.out.println("TV를 끄다");
}
}
public class Movie {
private String name="";
public Movie(String name)
{
this.name = name;
}
public void Search_Movie()
{
System.out.println(name+" 영화를 찾다");
}
public void Charge_Movie()
{
System.out.println("영화를 결제하다");
}
public void play_Movie()
{
System.out.println("영화 재생");
}
}
public class Beverage {
private String name="";
public Beverage(String name)
{
this.name = name;
}
public void Prepare()
{
System.out.println(name+" 음료 준비 완료 ");
}
}
public class Facade {
private String beverage_Name ="";
private String Movie_Name="";
public Facade(String beverage,String Movie_Name)
{
this.beverage_Name=beverage_Name;
this.Movie_Name=Movie_Name;
}
public void view_Movie()
{
Beverage beverage = new Beverage(beverage_Name);
Remote_Control remote= new Remote_Control();
Movie movie = new Movie(Movie_Name);
beverage.Prepare();
remote.Turn_On();
movie.Search_Movie();
movie.Charge_Movie();
movie.play_Movie();
}
}
소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙.
아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다.
조직화를 통해 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
방향성을 제어하여 의존성을 조정해야하고, 정보 은닉을 통해 고수준의 컴포넌트가 저수준의 컴포넌트의 변경으로 부터 자유로운 것을 보장해야 한다.
public interface CalculatorOperation {
void perform();
}
public class Addition implements CalculatorOperation {
private double left;
private double right;
private double result;
// constructor, getters and setters
@Override
public void perform() {
result = left + right;
}
}
public class Division implements CalculatorOperation {
private double left;
private double right;
private double result;
// constructor, getters and setters
@Override
public void perform() {
if (right != 0) {
result = left / right;
}
}
}
public class Calculator {
public void calculate(CalculatorOperation operation) {
if (operation == null) {
throw new InvalidParameterException("Cannot perform operation");
}
operation.perform();
}
}
추후에 CalculatorOperation을 구현한 다른 객체를 추가하는 것이 용이하며 이때 상위 클래스읜 Calculator은 이런 변경으로 부터 보호를 받는다.
부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙.
객체 지향 언어가 발전함에 따라 LSP는 단순 상속을 넘어 인터페이스와 구현체에도 적용되는 더 광범위한 설계 원칙으로 변모.
LSP를 위배하면 시스템 아키텍처가 오염되어 별도의 매커니즘을 추가해야되는 상황이 발생 가능하다.
객체는 자신이 호출하지 않는 메소드에 의존하지 않아야 한다는 원칙.
구현할 객체에게 무의미한 메소드의 구현을 방지하기 위해 필요한 메서드만 상속/구현 하도록 해야되고, 상속할 객체의 규모가 너무 크다면, 해당 객체의 메소드를 작은 인터페이스로 나누는 것이 좋다.
소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템.
그러나 자바에서 String class역시 구현체이지만 변경이 없는 안정적인 구체이며, 이러한 안정적인 구체에는 의존을 하는 것은 괜찮으나, 변경이 잦은 구체에는 의존해서는 안된다.
추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정해야 한다.
이러한 인터페이스의 변동성을 낮추기 위해 애를 써야한다.
안정화된 추상화를 위해선 아래의 사항들을 지켜야 한다.
변동성이 큰 객체는 주의해서 생성을 해야한다.
어떠한 서비스에서 변경이 잦은 객체를 생성할때 팩터리 인테페이스를 만들고 이 팩터리 인터페이스를 구현한 객체에서 변동이 잦은 객체를 생성하는 방식으로 설계를 하는 것이 이상적이다.
이때 소스 코드 의존성은 제어흐름과는 반대 방향으로 역전되는데 이를 의존성 역전이라한다.