출처: https://okky.kr/article/409329
추상화, 의존성 분리, 캡슐화 등등... 말은 참 쉽지만 실전에서 와닿기 힘들 수 있습니다.
자바스크립트로 [Button]을 눌렀을 때 [Text]에 버튼을 누른 횟수를 표시하는 코드를 모델링해보겠습니다. (객체를 설계해보겠습니다.)
1. 어설픈 모델링
class Text {
constructor(){
this.view = document.createElement('div');
this.view.textContent = "[0]";
document.body.append(this.view);
}
}
class Button {
constructor(){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
text.view.textContent = `[${this.clicked++}]`;
}
}
}
let text = new Text();
let button = new Button();위 코드는 잘 작동하지만 Button을 사용하기 위해서는 다음과 같은 의존성이 있습니다.
- 전역 스코프에
text라는 객체가 있을 것 text는view라는 객체를 속성으로 가질 것text.view는textContent라는String을 속성으로 가질 것
혹은
- 전역 스코프의
text라는 객체가 있을 것 text는 view라는HTMLElement를 속성으로 가질 것
이 때문에
let text = new Text();
let button = new Button();위 코드를
let button = new Button();
let text = new Text();이렇게만 바꿔도 작동하지 않으며, 현실적으로 재사용하고 확장하기에 무리가 있습니다.
2. 추상화
- 전역 스코프에
text라는 객체가 있을 것 text는view라는 객체를 속성으로 가질 것text.view는textContent라는String을 속성으로 가질 것
이 세가지 항목을 최대한 줄여볼 수 있을까요?
전역 스코프 사용을 자제하고, 모델을 약간 추상화해보겠습니다.
class Text {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
this.render(0);
}
render(number){
this.view.textContent = `[${number}]`;
}
}
class Button {
constructor(text){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
text.render(this.clicked++);
}
}
}
let text = new Text();
let button = new Button(text);이런 방식으로 의존성이 줄어들 수 있겠습니다. 위 코드에서 Button의 의존성은
- 생성자의 인자가
render(Number)를 속성으로 갖는 객체 일 것
의존성을 줄이려다보니, Text에 render라는 메소드를 만들게 됐습니다.
화면에 보이는 결과는 같지만 의존성이 줄었을뿐만 아니라, Text의 모습을 제어하는 코드 역시 자연스럽게 Text에 캡슐화가 되었습니다.
// 의사 코드
interface Renderable {
void render(Number number);
}
class Button {
constructor(Renderable renderer){
// ...
}
}JS가 정적 타입 언어라면, 위처럼 Text의 생성자에 좀 더 제약사항을 걸어서 코드의 안정성과 가독성을 높힐 수도 있겠습니다.
이렇게 Renderable이라는 추상계층을 통해서 의존성을 줄일 수 있습니다.
3. 확장성
의존성이 단순하기 때문에 Button의 역할을 확장하기가 수월합니다.
새로운 요구사항
Button에Renderable한 객체들을 여러개 연결하고 싶음
class Text {
// ...
}
class Balls {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
}
render(number){
this.view.innerHTML = '';
for(let i=0; i<number; i++) {
let ball = document.createElement('div');
ball.style.background = 'orange';
ball.style.width = ball.style.height = '1em';
ball.style.borderRadius = '50%';
ball.style.display = 'inline-block';
this.view.appendChild(ball);
}
}
}
class Button {
constructor(renderers){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
this.clicked++;
renderers.forEach(renderer => renderer.render(this.clicked));
}
}
}
let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);이렇게 해서 Button은 여러 Renderable과 느슨하게 결합 할 수 있습니다.
이제 Button의 의존성은 (엄밀하게 말하면 의존성은 Renderable 인터페이스 뿐입니다)
- 생성자 인자의 타입은
Array<Renderable>일것
4. 동적인 참조
그런데 아직도
let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);를
let button = new Button([text, balls]);
let text = new Text();
let balls = new Balls();로 바꿀 수는 없는 점이 아쉽습니다.
새로운 요구사항
Button을 생성하는 시점이 아니라, 생성된 이후에도Button에Renderable들을 연결하고 싶음
class Text {
// ...
}
class Balls {
// ...
}
class Button {
constructor(renderers = []){ // 기본값을 할당하는 코드입니다.
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.renderers = renderers;
this.view.onclick = () => {
this.clicked++;
this.renderers.forEach(renderer => renderer.render(this.clicked));
}
}
connect(renderer){
this.renderers.push(renderer);
}
}
let text = new Text();
let button = new Button([new Balls()]);
button.connect(text);이제 Button에 동적으로 Renderable들을 연결 할 수 있습니다.
5. 의존성 없애기!
Button이 Renderable들의 render 코드를 호출하는 대신에,
이벤트 주도(감시자 패턴, 출판-구독 패턴..) 방식으로 Renderable이라는 제약사항을 없앨 수도 있습니다.
새로운 요구사항
Balls, Text을Renderable에서 탈피시키며 의존성을 완전히 없애기
class Text {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
this.number = 0;
this.count();
}
count(){
this.view.textContent = `[${this.number++}]`;
}
}
class Balls {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
}
generate(){
this.view.innerHTML = '';
let number = Math.floor(Math.random()*10);
for(let i=0; i<number; i++) {
let ball = document.createElement('div');
ball.style.background = 'orange';
ball.style.width = ball.style.height = '1em';
ball.style.borderRadius = '50%';
ball.style.display = 'inline-block';
this.view.appendChild(ball);
}
}
}
class Button {
constructor(text){
this.view = document.createElement('button');
this.view.textContent = text;
document.body.append(this.view);
this.clickHandlers = [];
this.view.onclick = () => {
this.clicked++;
this.clickHandlers.forEach(handler => handler(this.clicked));
}
}
addClickHandler(handler) {
this.clickHandlers.push(handler);
}
}
let balls = new Balls();
let text = new Text();
// increment button
let button = new Button("INCREMENT");
button.addClickHandler(() => {
text.count();
});
// button for both
let button2 = new Button("CLICK ME");
button2.addClickHandler(() => {
balls.generate();
text.count();
});이제 Button은 인스턴스별로 원하는 hook(callback, handler, listener, observer 등..)을 등록하여 입맛대로 쓸 수 있습니다.
* 아시는 것처럼 DOM에는 당연히 native로 각종 이벤트 핸들러를 부착할 수 있습니다, JS를 예시로 들다보니 같은 작업을 중복한 꼴이 되었습니다만, 지금 예시로 든 GUI 뿐 아니라 그 어떤 코드에서도 의존성에서 탈피하기 위해서 같은 원리를 적용 할 수 있음을 말하고자 합니다.
맺음말.
의존성에서 벗어나야 좋은 코드라는데, 위 처럼 의존성이 하나도 없는게 최고군요!
- 의존성이 없으면 그만큼 확장성이 있지만, 원하는 대로 그 쓰임새를 강제 할수가 없습니다.
예를 들어서 현금,부동산 등을 등록 할 수 있는 회계사가 있다고 해봅시다.회계사는 현금, 부동산과 현금으로평가할수있는 인터페이스로 느슨하게 연결되어 있습니다.
interface 현금으로평가할수있는 {
금액 현금으로평가();
}이를 통해서 회계사는 현금이나 부동산 등을 현금으로 평가하여 다룰 수 있습니다. 또한 비트코인이라는 새로운 객체에도 현금으로평가할수있는이라는 명시적인 인터페이스만 구현해준다면 쉽게 회계사와 연결 할 수 있습니다.
이렇게 적절한 의존성을 통해서 쓰임새의 방향을 어느 정도 강제하고, 확장의 가이드가 될 수 있습니다.
---
디자인 패턴을 공부해도 막상 실제 코드엔 적용하기가 쉽지 않습니다. 저는 코드의 구조에 그 합당한 이유가 있어야 한다는 생각을 합니다. 아무런 필요도 준비도 없이, 디자인패턴들을 되새김질하며 마치 공식처럼 적용하려고 하기보다는, 재사용, 확장성, 추상화의 키워드를 핵심 컨셉으로 "필요에 따라서 리팩토링" 해나가면 좋겠습니다.
'IT > Javascript|Jquery' 카테고리의 다른 글
| 자바스크립트와 이벤트 루프 (0) | 2018.02.01 |
|---|---|
| Scope의 이해 (0) | 2018.01.25 |
| 비밀번호검증 스크립트(연속,반복) (0) | 2018.01.22 |
| 통화형식 (3자리 콤마) 사용하기 예제 (0) | 2018.01.19 |
| Jquery Input Name Array 값 가져오기 (1) | 2018.01.16 |