mojo's Blog

SOLID 원칙 본문

Design Patterns

SOLID 원칙

_mojo_ 2024. 5. 6. 22:49

Single Responsibility Principle (SR.P)

 

클래스는 변경해야 할 이유가 단 하나뿐이여야 한다.

 

※ Responsibility

- 책임이 많아질수록 변화 가능성이 많아짐

- 클래스 변경이 많아지면, 버그가 나타날 가능성이 높아짐

- 결국 변화는 다른 사람들에게 영향을 줄 수 있음

 

※ SR.P

- 결합된 책임들을 별도의 클래스로 분리함

 

 

SR.P 위반 케이스로 Rectangle 클래스는 두 개의 책임을 가지고 있다. (낮은 응집력)

(1) CGA 전용 area() 

(2) GA 전용 draw()

일관된 개념 표현이 아닌, 응집력을 고려하지 않고 필요한 기능을 묶어버렸다.

Geometry와 Graphical은 자연스럽게 함께 속하지 않는 것이 맞다.

 

 

결합된 책임들을 별도의 클래스로 분리하였다.

(1) Rectangle은 기하학적 속성을 가지도록 모델링

(2) DrawableRectangle은 시각적 속성을 가지도록 모델링

위와 같이 분리함으로써 두 클래스 모두 쉽게 재사용할 수 있다.

책임에 대한 변경만 영향을 미치게 된다.

 

※ Identifying Responsibilities Can be Tricky

interface Modem {
    public void dial(String num);
    public void hangup();
    public void send(char c);
    public char receive();
}

 

위와 같이 여러 개의 책임들을 보는 것이 어려울 수 있다.

 

interface DataChannel {
    public void send(char c);
    public char receive();
}

interface Connection {
    public void dial(String num);
    public void hangup();
}

 

두 개의 책임들로 위와 같이 분리할 수 있다.

(1) Connection management

(2) Data communication

애플리케이션이 어떻게 변화하는지에 따라 다르다.

불필요한 복잡성을 방지하며, 증상이 없는 경우 SR.P 적용은 현명하지 않다.

 

Open Closed Principle (OCP)

 

Open for extension

- 모듈의 동작을 확장할 수 있어야 함

- 모듈의 작동 방식을 변경할 수 있어야 함

 

※  Closed for modification

- 동작을 확장하면 모듈의 아키텍처 변경과 같은 과도한 수정이 발생하면 안됨

 

void decAll(Employee[] emps) {
    for (int i = 0; i < emps.size(); i++) {
    	if (emps[i].empType == FACULTY)
        	decFacultySalary((Faculty)emps[i]);
        else if (emps[i].empType == STAFF)
        	decStaffSalary((Staff)emps[i]);
        else if (emps[i].empType == SECRETARY)
        	decSecretarySalary((Secretary)emps[i]);
    }
}

  

위 코드를 예시로, 새로운 직원 유형을 추가하려면 상당한 변경이 필요하다.

많은 switch/case 문을 사용하거나, if/else 문을 사용해야 한다.

=> 새로운 모듈을 여러개 추가할 때마다 수정이 필요하게 됨

 

void decAll(Employee[] emps) {
    for (int i = 0; i < emps.size(); i++)
    	emps[i].decSalary();
}

 

위와 같이 코드를 작성하면 새로운 모듈 추가시 해당 모듈은 변경에 신경쓰지 않아도 된다.

=> This design is open to extension, closed for modification.

 

※ Abstraction

- 추상화는 고정되어 있지만, 가능한 동작의 무한한 그룹을 나타낼 수 있음

- 추상화 기반 클래스는 고정되어 있음

- 추상화 기반 클래스로부터 파생된 모든 클래스는 가능한 무한한 그룹으로 나타냄

 

Liskov Substitution Principle (LSP)

 

※ LSP

- 하위 타입은 기본 타입으로 대체 가능해야 함 (파생 클래스는 기본 클래스로 대체 가능)

- 상속을 사용할지 여부를 결정할 때, 확인하는 규칙

- C가 P의 하위 타입일 때, P 타입의 객체를 C 타입의 객체로 대체할 수 있어야 함

 

class Rectangle {
	private width;
	private height;
	public function setHeight(height) {
		this->height = height;
	}
	public function getHeight() {
		return this->height;
	}
	public function setWidth(width) {
		this->width = width;
	}
	public function getWidth() {
		return this->width;
	}
   	public function area() {
		return this->width * this->height;
	}
}

class Square extends Rectangle {
	public function setHeight(value) {
		this->width = value;
		this->height = value;
	}
	public function setWidth(value) {
		this->width = value;
		this->height = value;
	}
}

 

기본 클래스인 직사각형 클래스와 파생 클래스인 정사각형 클래스가 있다.

그리고 기본 클래스인 직사각형 클래스에는 넓이를 구하는 메서드가 있다.

 

class Client {
	function areaVerifier(Rectangle r) {
		r->setWidth(5);
		r->setHeight(4);
		if(r->area() != 20) {
			throw new Exception('오류 발생');
		}
		return true;
	}
}

 

위 메서드의 파라메터로 Square 객체가 들어온다면,

r->setHeight(4) 에서 width = height = 4 가 되어서 넓이는 16이 된다.

즉, 파생 클래스인 정사각형 클래스는 기본 클래스인 직사각형 클래스로 대체할 수 없다.

=> LSP를 통해 자식 클래스가 부모 클래스를 대체할 수 있도록 확장해야 한다.

 

Dependency Inversion Principle (DIP)

 

DIP

- 높은 수준의 모듈은 낮은 수준의 모듈에 의존하면 안됨 (둘 다 추상화에 의존해야 함)

- 추상화는 세부 사항에 의존하면 안됨 (세부 사항은 추상화에 의존해야 함)

- 구조화된 분석 및 설계 접근 방식으로 인해 발생하는 종속성을 "역전" 하려고 시도함 

 

 

인터페이스/추상화 클래스들은 높은 레벨의 자원들이며,

구체화된 클레스들은 낮은 레벨의 자원들이다.

 

Interface Segregation Principle (ISP)

 

※ ISP

- 서로 다른 클라이언트에 대한 기능을 하나의 인터페이스로 묶으면,

  클라이언트 간에 불필요한 결합이 생성될 수 있음

- 한 클라이언트가 인터페이스를 변경하면 다른 모든 클라이언트는 재컴파일 해야 함

- 인터페이스를 응집력 있는 그룹으로 분해하여 불필요한 결합을 없애야 함

- ISP 는 비응집성 인터페이스를 해결할 수 있음

 

class StudentEnrollment {
	String getName() { ... }
    String getSSN() { ... }
    String getInvoice() { ... }
    void postPayment() { ... }
}

class Course {
	StudentEnrollment arr[];
    
    void getStudentsName() {
    	for (int i = 0; i < arr.size(); i++) {
        	String name = arr[i].getName();
        }
    }
    void getStudentsSSN() {
    	for (int i = 0; i < arr.size(); i++) {
        	String ssn = arr[i].getSSN();
        }
    }
    void getStudentsInvoice() {
    	for (int i = 0; i < arr.size(); i++) {
        	String invoice = arr[i].getInvoice();
        }
    }
    void StudentsPostPayment() {
    	for (int i = 0; i < arr.size(); i++) {
        	arr[i].postPayment();
        }
    }
}

 

위와 같이 StudentEnrollment 클래스는 이름, SSN, 청구서, 후불 관련 메서드를 가지고 있다.

그리고 Course 클래스는 학생들을 관리하는 배열을 가지고 있으며, 관련 메서드들을 호출할 수 있다.

 

class RoasterApplication {
	void main() {
    	Course c;
        
        ...
        for (int i = 0; i < c.arr.size(); i++) {
        	c.arr[i].getStudentsName();
            c.arr[i].getStudentsSSN();
        }
    }
}

////////////////////////////////////////////////////////

class AccountApplication {
	void main() {
    	Course c;
        
        ...
        for (int i = 0; i < c.arr.size(); i++) {
        	c.arr[i].getStudentsInvoice();
            c.arr[i].postPayment();
        }
    }
}

 

RoasterApplication 클래스는 학생의 이름, SSN 을 가져오려고 하며,

AccountApplication 클래스는 학생의 청구서와 후불을 관리하려고 한다.

그런데 StudentEnrollment 클래스는 두 클래스가 호출하는 메서드를 전부 보유하고 있다.

즉, RoasterApplication, AccountApplication 클래스는 불필요한 메서드 2개를 가지고 있는 셈이다.

=> StudentEnrollment 클래스의 메서드 하나만 변경되더라도, 불필요한 재컴파일을 하게 된다. 

 

interface EnrollmentAPI {
	String getName();
    String getSSN();
}

interface AccountAPI {
	String getInvoice();
    void postPayment();
}

// RoasterApplication 을 위한 구현
class StudentEnrollment implements EnrollmentAPI {
	String getName() { ... }
    String getSSN() { ... }
}

////////////////////////////////////////////////////////

// AccountApplication 을 위한 구현
class StudentEnrollment implements AccountAPI {
	String getInvoice() { ... }
    void postPayment() { ... }
}

 

위와 같이 EnrollmentAPI, AccountAPI 인터페이스를 분리해서,

특정 Application 마다 인터페이스 구현을 다르게 하면 불필요한 메서드 호출이 없어진다.

메서드 변경이 일어나더라도 다른 Application 에서 재컴파일을 하지 않아도 된다.

 

'Design Patterns' 카테고리의 다른 글

Template Method Pattern  (0) 2024.06.06
Observer Pattern  (1) 2024.06.06
Strategy Pattern  (0) 2024.06.06
GRASP  (1) 2024.05.15
객체지향 패러다임  (0) 2024.05.06
Comments