SOLID
Single Responsibility Principle (SRP)
단일 책임 원칙
- “클래스가 수정되어야 하는 이유가 2개 이상으로 존재하면 안 된다.”
- 하나의 클래스에는 개념적으로 응집된 기능만을 포함하는 것이 좋다.
- 하나의 클래스에 여러 기능이 포함되어 있는 경우 작은 변화가 이와 연결된 다른 모듈들에 영향을 줄 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42// bad
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
// good
// Authorization은 따로 클래스 정의
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
// Setting과 Authorization 분리
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Open / Closed Principle (OCP)
개방 / 폐쇄 원칙
- “소프트웨어 개체 (e.g. Class, Module, Function, etc.) 는 확장이 용이하지만 변경은 어려워야 한다.”
- 즉, 사용자가 이미 존재하는 코드를 (직접적으로) 수정하지 않아도 새로운 기능을 추가할 수 있도록 설정해야 한다.
확장
에 대해 열려 있다.- 모듈의 동작 (기능) 을 확장할 수 있음
- 요구 사항이 변경될 때, 이에 맞게 새로운 동작을 추가해 모듈을 확장함
- 모듈이 하는 일을 변경하는 것
수정
에 대해 닫혀 있다.- 모듈의 소스 / 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 함
앞서 설명했 듯,
많은 모듈 (클래스) 중 하나를 수정할 때 이에 의존하는 다른 모듈들 또한 수정해야 하는 코드는 좋지 않은 코드를 의미한다.
시스템의 구조를 올바르게
리팩토링
하여 앞선 변경과 같은 유형의 수정 사항이 발생할 경우 더 이상의 수정을 요구하지 않고, 잘 동작하고 있던 기존 코드의 변경 없이 새로운 코드를 추가 하여 기능의 추가와 변경이 가능하도록 한다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
// good
// 즉, REFACTORING 을 잘한 것이 OCP를 적용한 것
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
// good
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
// bad
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
// good
return this.adapter.request(url).then(response => {});
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
Liskov Substitution Principle (LSP)
리스코프 치환 원칙
“만약 S가 T의 하위유형이라면, T 객체는 (바람직한) 속성 (e.g. 정확도, 수행성 등) 을 변경하지 않아도 S 객체로 변경 가능해야 한다.”
즉, 부모 클래스와 자식 클래스가 있을 때, 이들을 서로 바꾸어 적용해도 올바른 결과가 나와야 한다.
정사각형 - 직사각형 클래스에서, 정사각형은 직사각형이지만, 이들을
is-a
관계로 정의한다면 문제가 발생한다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101// bad
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
// good
// 1. Shape 관련 메소드는 분리하여 따로 클래스 정의
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
// 2. 직사각형은 Shape 를 상속받는다.
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
// 3. 정사각형도 Shape 를 상속받지만, 직사각형과 다른 특징을 가지므로 직사각형과 'is-a' 관계로 정의 (=== 상속) 하지 않는다.
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
// 차이점
getArea() {
return this.length * this.length;
}
}
// 4. Shape 를 통해 속성값을 설정해놓아 이를 분리하여 동작을 할 수 있도록 한다. 이로써 여러 모듈 (클래스) 가 엉키지 않고 독립적으로 작용하여 에러를 줄일 수 있으며, 다른 속성을 가진 객체들을 유연하게 다룰 수 있다.
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
// 속성값 constructor의 인자로 전달
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
Interface Segregation Principle (ISP)
인터페이스 분리 원칙
자바스크립트는 인터페이스 (i.e. type) 가 존재하지 않기 때문에 해당 원칙이 명확하게 적용되지는 않지만, type 시스템이 존재하지 않음에도 불구하고 중요하게 작용하는 원칙이다.
- “클라이언트는 그가 사용하지 않는 인터페이스에 의존하도록 강요받지 않아야 한다.”
- 자바스크립트는
duck typing
이므로 여기서 인터페이스는 암시적인 요소로 존재한다.- e.g. 큰 setting 객체들을 요구하는 클래스를 예시로 들면,
- 클라이언트는 대게 모든 setting을 필요로 하지는 않기 때문에, 많은 양의 option을 설정하도록 요구하는 것은 좋지 않다.
- 따라서, Setting을 optional하게 만드는 것은 “fat interface” 를 방지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60// bad
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
// good
class DOMTraverser {
constructor(settings) {
this.settings = settings;
// options 속성을 추가한다.
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
// settings에 직접 접근하지 않고 setupOptions() 메소드를 이용한다.
this.setupOptions();
}
// options를 세팅하는 메소드를 따로 작성한다.
setupOptions() {
// 이로써 options에 접근하도록 한다.
// setter과 같은 역할
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
// 'Most of the time, we won't need to animate when traversing.' 이므로,
// 이를 options로 설정하여 무조건 실행되지 않고 선택적으로 실행될 수 있도록 한다.
options: {
animationModule() {}
}
});
Dependency Inversion Principle (DIP)
의존성 역전 원칙
- 상위 모듈은 하위 모듈에 종속되어서는 안 된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부사항에 의존하면 안 된다. 세부사항이 추상화에 의존해야 한다.
의존성 (이 높을수록 코드를 리팩토링하기 어렵다. 👉Coupling
)나쁜 개발 습관- 상위 모듈이 하위 모듈의 세부사항을 알지 못하므로, 의존성을 감소시킬 수 있다.
- 자바스크립트는 인터페이스가 없어 추상화에 의존한다는 것은 암시적인 것 (약속) 이므로, 다른 객체나 클래스에 노출되는 메소드와 속성 자체가 이에 (암시적인 약속, 추상화) 해당한다고 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
// 암시적인 약속
// InventoryTracker에 대한 모든 요청 모듈은 requestItems 메소드를 가질 것이다.
requestItem(item) {
// ...
}
}
/*
// Requester를 버전 별로 (e.g. V1, V2) 나누어서 클래스로 생성 가능하다.
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
*/
// bad
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a 'specific' request implementation.
// We should just have requestItems 'depend on a request method': `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
// good
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
// requester를 인자로 전달하여 tracker의 default requester로 객체를 설정하(여 코드를 정의하)는 것이 아니라 tracker 생성 시 개별적으로 적용 (생성) 할 수 있게 설정한다.
new InventoryRequesterV2()
);
inventoryTracker.requestItems();