LHJ

I'm a FE developer.

25. Abstract Loop & Lazy Execution - Abstract Loop

12 Aug 2020 » codespitz_re

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 - 를 정의한다.
정적 메서드는 클래스의 인스턴스없이 호출이 가능하며 클래스가 인스턴스화되면 호출할 수 없다.
정적 메서드는 종종 어플리케이션의 유틸리티 함수를 만드는데 사용한다.

object의 extends 키워드로 Operator 클래스의 자식을 만든다.
자식 클래스의 super 키워드는 부모 오브젝트의 함수를 호출할 때 사용합니다.
자식 클래스에 반드시 넣어줘야하는 키워드입니다.


위 코드 풀이

  1. [1,2,3, {a:4, b:5}, 6, 7] 배열이 factory ‘정적 메서드’ 인자로 들어간다.
  2. [1,2,3, {a:4, b:5}, 6, 7]는 Object 이다.
    v instanceof Object = true
  3. [1,2,3, {a:4, b:5}, 6, 7]는 Object 이면서 배열이다.
    (!Array.isArray(v)) 에서 false 반환, optional 건너뛰고 mandatory 실행
  4. Mandatory 코드인 return new ArrayOp(v.map(v => Operator.factory(v))) 실행

  5. ArrayOp([Operator.factory(1), Operator.factory(2), Operator.factory(3), Operator.factory({a:4, b:5}), Operator.factory(6), Operator.factory(7)]) : 이렇게 ArrayOp 가 실행
  6. ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([4, 5]), PrimaOp(6), PrimaOp(7)])
  7. ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([Operator.factory(4), Operator.factory(5)]), PrimaOp(6), PrimaOp(7)])
  8. ArrayOp([PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([PrimaOp(4), PrimaOp(5)]), PrimaOp(6), PrimaOp(7)])

  9. for (const v of this.v) 를 통해 const v값으로 [PrimaOp(1), PrimaOp(2), PrimaOp(3), ArrayOp([PrimaOp(4), PrimaOp(5)]), PrimaOp(6), PrimaOp(7)] 의 PrimaOp(1)이 추출된다.
  10. PrimaOp(1).operation(f) -> f의 인자로는 console.log가 전달된다. 즉, console.log(1) -> 콘솔창에 1이 찍힌다.
    그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 1}이 찍힌다.
  11. const v 값으로 두번째 PrimaOp(2) 가 전달되고, 같은 원리에 의해 console.log(2) -> 콘솔창에 2가 찍힌다.
    그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 2}가 찍힌다.
  12. const v 값으로 세번째 PrimaOp(3) 가 전달되고, 같은 원리에 의해 console.log(3) -> 콘솔창에 3이 찍힌다.
    그리고 그 다음에 console.log(v) 가 실행돼서 PrimaOp {v: 3}이 찍힌다.

  13. const v 값으로 네번째 ArrayOp([PrimaOp(4), PrimaOp(5)]) 가 전달된다.
  14. 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) 각각 따로따로 날라가면서 알고있는대로 실행된다.
  15. 즉, ArrayOp([PrimaOp(4), PrimaOp(5)]) 은 같은 원리로 4와 PrimaOp(4), 5와 PrimaOp(5)를 순서대로 콘솔창에 출력한다.

  16. 위 과정 이후에 ArrayOp([PrimaOp(4), PrimaOp(5)])이 console.log(v)로 넘어가 콘솔창에 출력된다.
  17. 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 는 ‘문’이기 때문에 확정인데 비해서, 객체는 ‘동적’으로 추가할 수 있다.
앞에꺼 하나도 안 건드리고 객체만 추가하면 된다.

후기 확장이 용이하다.
플러그인, 애드온 형태 등이 모두 이걸 활용한다.