Abstract Loop
이전에 우리가 사용했던 루프는 ‘목적이 있는 루프’이다.
for, while… 등등
목적이 있는 루프를 만들면, 목적을 살짝만 비틀어도 루프를 다시 짜야된다.
이것이 제어문을 그냥 사용했을 때의 가장 큰 문제점이다.
이전에도 말했지만 문을 식으로 바꿔야된다.
그 말은 구조만 남겨야된다는 뜻이다. (프레임워크, 라이브러리처럼..)
const Compx = class {
constructor(data) {this.data = data;}
*gene(){
const data = [JSON.parse(JSON.stringify(this.data))];
let v;
while (v = data.shift()) {
if (!(v instanceof Object)) yield v;
else {
if (!Array.isArray(v)) v = Object.values(v);
data.unshift(...v);
}
}
}
};
const a = new Compx([{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9]);
console.log([...a.gene()]);
console.log([...a.gene()]);
어떻게 구조만남길까?
재활용을 하기 위해선 구조화해야된다.
문은 실행되면 사라지기 때문에 객체로 바꿔줘야된다. 그리고 재활용을 위해 구조화해야된다.
-> 즉 객체를 이용해 루프 실행기를 별도로 구현하는 방법을 활용한다.
루프를 처리해주는 객체 시리즈를 만들어 놓고 여기에다 값을 넣거나 이 값을 이용하는 쪽으로 따로 분리해줘야된다는 말이다.
(data, f) => {
let v;
while(v = data.shift()){
if (!(v instanceof Object)) {
f(v);
} else {
if (!Array.isArray(v)) v = Object.values(v);
data.unshift(...v);
}
}
}
if 문은 제거할 수 없다. 하지만 제거하고 싶다.
어떻게하면 if문을 제거할 수 있을까?
if로 나뉘어지는 경우의 수만큼의 값을 미리 만들어놓고 바깥쪽에서 그 값을 선택해서 들어오게할 수밖에 없다.
이렇게 하면 안쪽의 if 조건이 하나 사라진다.
이걸 겹겹이 쌓으면 if 문을 하나씩 제거해 나갈 수 있다.
위의 코드에선 if가 세개가 있다. (원시값 / 객체 / 배열)
이 세개만큼의 객체를 미리 설정, 그리고 이것을 바깥쪽에서 선택하게 한다.
그렇게하면 if문 하나를 제거할 수 있다.
(디자인 패턴 중 하나이다. 전략객체 패턴, 상태 패턴 등등..)
Abstract Loop - Factory + Composite
// 팩토리
const Operator = class {
static factory(v){
if (v instanceof Object) {
if (!Array.isArray(v)) v = Object.values(v);
return new ArrayOp(v.map(v => Operator.factory(v)));
}else return new PrimaOp(v);
}
constructor(v) {this.v = v;}
operation(f) {throw 'override';}
};
// 컴포지트
const PrimaOp = class extends Operator{
constructor(v) {super(v);}
operation(f){f(this.v);}
}
const ArrayOp = class extends Operator {
constructor(v) {super(v);}
operation(f){for (const v of this.v) {v.operation(f); console.log(v);}}
}
Operator.factory([1,2,3, {a:4, b:5}, 6, 7]).operation(console.log);
// 1
// PrimaOp {v: 1}
// 2
// PrimaOp {v: 2}
// 3
// PrimaOp {v: 3}
// 4
// PrimaOp {v: 4}
// 5
// PrimaOp {v: 5}
// ArrayOp {PrimaOp {v: 4}, PrimaOp {v: 5}}
// 6
// PrimaOp {v: 6}
// 7
// PrimaOp {v: 7}
object의 static 키워드로 정적 메서드 - 위에서는 factory - 를 정의한다.
정적 메서드는 클래스의 인스턴스없이 호출이 가능하며 클래스가 인스턴스화되면 호출할 수 없다.
정적 메서드는 종종 어플리케이션의 유틸리티 함수를 만드는데 사용한다.
- 참고 : Mdn 정적 메서드
object의 extends 키워드로 Operator 클래스의 자식을 만든다.
자식 클래스의 super 키워드는 부모 오브젝트의 함수를 호출할 때 사용합니다.
자식 클래스에 반드시 넣어줘야하는 키워드입니다.
- 참고 : Mdn extends
- 참고 : Mdn super
위 코드 풀이
- [1,2,3, {a:4, b:5}, 6, 7] 배열이 factory ‘정적 메서드’ 인자로 들어간다.
- [1,2,3, {a:4, b:5}, 6, 7]는 Object 이다.
v instanceof Object
= true - [1,2,3, {a:4, b:5}, 6, 7]는 Object 이면서 배열이다.
(!Array.isArray(v))
에서 false 반환, optional 건너뛰고 mandatory 실행 Mandatory 코드인
return new ArrayOp(v.map(v => Operator.factory(v)))
실행- ArrayOp([Operator.factory(1), Operator.factory(2), Operator.factory(3), Operator.factory({a:4, b:5}), Operator.factory(6), Operator.factory(7)]) : 이렇게 ArrayOp 가 실행
- ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([4, 5]), PrimaOp(6), PrimaOp(7)])
- ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([Operator.factory(4), Operator.factory(5)]), PrimaOp(6), PrimaOp(7)])
ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([PrimaOp(4), PrimaOp(5)]), PrimaOp(6), PrimaOp(7)])
for (const v of this.v)
를 통해 constv
값으로 [PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([PrimaOp(4), PrimaOp(5)]), PrimaOp(6), PrimaOp(7)] 의PrimaOp(1)이 추출
된다.PrimaOp(1).operation(f)
-> f의 인자로는 console.log가 전달된다. 즉, console.log(1) -> 콘솔창에 1이 찍힌다.
그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 1}이 찍힌다.- const v 값으로 두번째 PrimaOp(2) 가 전달되고, 같은 원리에 의해 console.log(2) -> 콘솔창에 2가 찍힌다.
그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 2}가 찍힌다. const v 값으로 세번째 PrimaOp(3) 가 전달되고, 같은 원리에 의해 console.log(3) -> 콘솔창에 3이 찍힌다.
그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 3}이 찍힌다.- const v 값으로 네번째
ArrayOp([PrimaOp(4), PrimaOp(5)])
가 전달된다. ArrayOp([PrimaOp(4), PrimaOp(5)]).operation(f)
가 실행된다.
그 다음에 있는console.log(v)
에 ArrayOp([PrimaOp(4), PrimaOp(5)]) 이 전달되는 건
ArrayOp([PrimaOp(4), PrimaOp(5)]).operation(f)
가 실행된 이후이다.
.operation(f)
가 실행되고 인해f
인자로console.log
가 전달되고,
for (const v of this.v)
의this.v
엔 [PrimaOp(4), PrimaOp(5)] 가 전달된다.
그리고 다들 알다시피v
값으로 PrimaOp(4) / PrimaOp(5) 각각 따로따로 날라가면서 알고있는대로 실행된다.즉, ArrayOp([PrimaOp(4), PrimaOp(5)]) 은 같은 원리로 4와 PrimaOp(4), 5와 PrimaOp(5)를 순서대로 콘솔창에 출력한다.
- 위 과정 이후에 ArrayOp([PrimaOp(4), PrimaOp(5)])이 console.log(v)로 넘어가 콘솔창에 출력된다.
- 6, PrimaOp(6), 7, PrimaOp(7)도 마찬가지 과정을 거쳐 콘솔창에 출력된다.
위 코드 해석하면서 궁금한점
아래 궁금증 해결됨 Operator 객체의 operation(f) {throw ‘override’;} 이건 아직 잘 모르겠음..
- ArrayOp의 operation(f){for (const v of this.v) v.operation(f);}은 무슨역할이지..?
- Operator 객체의 operation(f) {throw ‘override’;} 이건 무슨 의미일까? 분명 쓰이는게 맞는거 같은데..
throw문은 사용자 정의 예외를 던질 수 있습니다.
현재 상황이 중지되어야하는 상황에 닥쳐서 throw 뒤에 있는 명령문들이 실행되지 않는다면,
그리고 try {} catch (e) {} 구문이 있다면, throw 뒤에 있는 것들을 catch 문으로 전달합니다.
위와 같이하면 if 문을 늘려나가지 않고 분기처리만 한 다음에 그 후 객체를 늘려가는식으로 if를 해결할 수 있다.
원시값을 한번에 처리하는 것이 아닌 string 처리기는 따로 나누고 싶을 땐?
// 팩토리
const Operator = class {
static factory(v){
if (v instanceof Object) {
if (!Array.isArray(v)) v = Object.values(v);
return new ArrayOp(v.map(v => Operator.factory(v)));
}else return typeof v === "string" ? new StringOp(v) : new PrimaOp(v);
}
constructor(v) {this.v = v;}
operation(f) {throw 'override';}
};
// 컴포지트
const StringOp = class extends Operator{
constructor(v) {super(v);}
operation(f){for (const a of this.v) f(a)}
}
const PrimaOp = class extends Operator{
constructor(v) {super(v);}
operation(f){f(this.v);}
}
const ArrayOp = class extends Operator {
constructor(v) {super(v);}
operation(f){for (const v of this.v) v.operation(f);}
}
Operator.factory([1,2,3, {a:'abc', b:5}, 6, 7]).operation(console.log);
// 1
// 2
// 3
// a
// b
// c
// 5
// 6
// 7
위와 같이 분기처리하고 객체 선언하면 된다.
이것이 if 문을 줄이는 유일한 방법이다.
if문이 많아지면 감당이 안되기 때문에,, 아키텍쳐도 다 이거하려고 하는 것이다.
그리고 if 는 ‘문’이기 때문에 확정인데 비해서, 객체는 ‘동적’으로 추가할 수 있다.
앞에꺼 하나도 안 건드리고 객체만 추가하면 된다.
후기 확장이 용이하다.
플러그인, 애드온 형태 등이 모두 이걸 활용한다.