객체지향 자바스크립트 4회차(MVVM pattern 개선2)

Mar 15th 2020 by jyoon

목표

  • 객체지향 프로그램이란 처음에 이루고자하는 목표에서부터 덩어리진 것을 차근차근 분리하고 깍아내는 과정입니다.
  • 어떻게 깍을지 기준정하는 방법은? "역할"
  • 덩어리진 코드를 클래스로 나누려고 할때 "역할, 기준"이 필요하다.
  • 객체지향에서 "역할, 기준"은 역할, 책임 모델이라고 하는 것입니다.
  • "역할, 책임"은 비슷해보이지만 동전의 양면을 가지고 있다.
  • 책임을 가지고 있다는건? 그 책임에 대한 권한도 가지고 있다.
  • 권한이 있다는건? 권한에 대한 책임이 있다.
  • 그래서 역할을 정의하려면 어떤 권한을 주입 받고, 그 권한으로 부터 무슨 일을 수행하는 권한을 양도 받기위해서는 어떤 책임까지 가져야 하는지 한번에 정의해야 한다.

1. ISP

SOLID 원칙 중 하나 Interface Segregation 인터페이스 분리하는 원칙(이 원칙으로 덩어리진 코드들을 나눌 수 있다.)

  • 한 코드에 여러개가 들어가 있으면 이것들을 인터페이스별로 잘라서 분리 할 수 있다.

    • 여기서 인터페이스는 역할을 얘기합니다.(실제 물리적인 인터페이스를 말하는 것이 아님)
  • 객체지향에서 코드 분리할때 가장 기본적인 방법이다.
  • ViewModel을 이 법칙을 적용해보려고 한다.

1.1 옵저버 서브젝트 파일 리팩토링 하기

  • pdf4, 5, 6
    #isUpdated = new Set; #listeners = new Set;
    addListener(v, _=type(v, ViewModelListener)){
        this.#listeners.add(v);
    }
    removeListener(v, _=type(v, ViewModelListener)){
        this.#listeners.delete(v);
    }
    notify(){
        this.#listeners.forEach(v=>v.viewmodelUpdated(this.#isUpdated));
    }

    static #subjects = new Set;
    static #inited = false;
    static notify(vm){
        this.#subjects.add(vm);
        if(this.#inited) return;
        this.#inited = true;
        const f =_=>{
            this.#subjects.forEach(vm=>{
                if(vm.#isUpdated.size){
                    vm.notify();
                    vm.#isUpdated.clear();
                }
            });
            requestAnimationFrame(f);
        };
        requestAnimationFrame(f);
    }
  • 뷰모델에는 어울리지 않다고 보인다
  • 옵저버패턴에 서브젝트 역할 -> 분리하자!

    • 메소드 코드를 분리할때 변수도 같이 이동해야한다.
    • 메소드 설계시 의존하고 있는 필드를 역할별로 필드를 같이 쓰지 않게 잘 설계해야 한다.
  • SOLID 원칙을 보며 고민... Strategy - SOLID원칙 보고 어떻게 뷰모델에서 옵저버 서브젝트 파일을 빼낼까 고민 중... - 상속모델을 사용할 것이다 우측 하단과 비슷한 모델을 사용 - 서브젝트모델 === 인터페이스A/
    뷰모델 === 객체 === 서브젝트모델을 상속받은 모델
//설명1
const ViewModelSubject = class extends ViewModelListener {
  #info = new Set()
  #listeners = new Set()
  //설명2 add, clear
  add(v, _ = type(v, ViewModelValue)) {
    this.#info.add(v)
  }
  clear() {
    this.#info.clear()
  }
  addListener(v, _ = type(v, ViewModelListener)) {
    this.#listeners.add(v)
    //설명3
    ViewModelSubject.watch(this)
  }
  removeListener(v, _ = type(v, ViewModelListener)) {
    this.#listeners.delete(v)
    //설명4
    if (!this.#listeners.size) ViewModelSubject.unwatch(this)
  }
  notify() {
    this.#listeners.forEach(v => v.viewmodelUpdated(this.#info))
  }
}
  • pdf8, 9
const ViewModelSubject = class extends ViewModelListener {
  #info = new Set()
  #listeners = new Set()
  add(v, _ = type(v, ViewModelValue)) {
    this.#info.add(v)
  }
  clear() {
    this.#info.clear()
  }
  addListener(v, _ = type(v, ViewModelListener)) {
    this.#listeners.add(v)
    ViewModelSubject.watch(this)
  }
  removeListener(v, _ = type(v, ViewModelListener)) {
    this.#listeners.delete(v)
    if (!this.#listeners.size) ViewModelSubject.unwatch(this)
  }
  notify() {
    this.#listeners.forEach(v => v.viewmodelUpdated(this.#info))
  }
}
  • 설명1 상속

    • 3강에서 ViewModel은 ViewModelListener을 상속는데 여기서 ViewModelSubject에 ViewModelListener을 상속 받는 이유는 뭘까?
    • Javascript는 다중 상속이 안돼서 어쩔 수 없이 ViewModelSubject가 ViewModelListener을 상속받고 ViewModel이 ViewModelSubject를 상속받는다

      3장 ViewModel ->                     ViewModelListener
      4장 ViewModel -> ViewModelSubject -> ViewModelListener
  • 설명2 add, clear추가

    • `#isUpdated -> #info
    • `#info는 부모에 있는 private 속성이다. 자식이 못건드리기 때문에 부모에 속성 #info private 속성을 제거, 추가할때는 부모쪽에서 서비스를 내려줘야한다. 그래서 내가 ViewModel #info를 변경하고 싶을때는 부모쪽 메소드에서 ViewModel Value를 직접 넘겨 ViewModel를 직접 넘겨서 ViewModel Value를 부모인 서브젝트가 직접 추가하도록 위임해서 동작하게 해야한다. clear도 부모에게 직접 요청해서 추가해야 한다.
  • 설명3 watch

    • 이전에는 addListener를 하고 ViewModel이 생성되는 시점에 바로 전체 서브젝트 리스트에 등록했다.
    • 지금은 Listener가 들어온순간 서브젝트를 watch로 보낼 것이다. 뷰모델 생성했다고 바로 requestAnimationFrame 넣어서 감시할 필요가 없다. Listener가 하나라도 생겼을때 감시하면 된다!(뷰모델을 감사하는 사람이 없어 변화를 추적할 필요가 없다.)
  • 설명4 unwatch

    • 뮤보델은 구독하는 listener가 없을때 unwatch를 하자!

1.2 notify 리팩토링

  • pdf10
    static #subjects = new Set;
    static #inited = false;
    static notify(vm){
        this.#subjects.add(vm);
        if(this.#inited) return;
        this.#inited = true;
        const f =_=>{
            this.#subjects.forEach(vm=>{
                if(vm.#isUpdated.size){
                    vm.notify();
                    vm.#isUpdated.clear();
                }
            });
            requestAnimationFrame(f);
        };
        requestAnimationFrame(f);
    }
  • requestAnimationFrame에 의해서 subject돌면서 뷰모델에 notify해준다.
  • pdf11-12
const ViewModelSubject = class extends ViewModelListener {
  //... 위 코드 참고

  static #subjects = new Set()
  static #inited = false
  static notify() {
    const f = _ => {
      this.#subjects.forEach(v => {
        if (v.#info.size) {
          v.notify()
          v.clear()
        }
      })
      //설명1
      if (this.#inited) requestAnimationFrame(f)
    }
    requestAnimationFrame(f)
  }

  //설명2
  static watch(vm, _ = type(vm, ViewModelListener)) {
    this.#subjects.add(vm) //set에 add해도 변화 x
    if (!this.#inited) {
      this.#inited = true
      this.notify()
    }
  }

  //설명3
  static unwatch(vm, _ = type(vm, ViewModelListener)) {
    this.#subjects.delete(vm)
    if (!this.#subjects.size) this.#inited = false
  }
}
  • notify가 무조건 돈다.
  • 설명1 flag를 통한 Animation 제어

    • requsetAnimationFrame를 동작 여부를 결정하는 flag "#inited"를 사용하고 있다.
  • 설명2, 설명3 flag를 통한 Animation 제어

    • 리스너가 없는 뷰모델을 여러개 만들어도 requestAnimationFrame은 돌지 않는다. 뷰모델에 리스너를 등록해야지 그 뷰모델을 감시하기 시작하고 리스너를 다 빼서 하나도 없다면 requestAnimation도 멈추게 된다.
    • 기존 notify함수에 this.#subjects.add(vm); 코드로 subject를 관리했는데 이렇게 하면 외부에서는 notify에 #subject에 add한다는 사실을 모른다. 그래서 외부에서 알 수 있도록 watch, unwatch함수 #subject.add, #subject.delete를 통해서 관리해주고 있다.
  • 추가 정보: 플래그변수, 싱글스레드, 멀티스레드

    • Javascript는 싱글스레드이기 때문에 플래그 변수로 제어하기 쉽다. 다른 언어는 멀시스레드이기 때문에 플래그 제어가 안된다.
    • 플래그 기만에 효율적인 알고리즘짜는것을 계속해서 연습해야 한다.
    • 멀티 스레드에서 작업할때는 플래그 기만이 아니라 멀티스레드에서 안정성을 확보하고 효율성을 확보하는 패턴을 배워야한다.
  • 추가 정보: static method

    • notify static private method로 설정해야하는데 Javascript에는 static method를 지원하지 않아서 공개적으로 되어 있다. 
    • watch, unwatch도 외부에 공개 되어야 하지만 public은 아니다. 내부 Framework안에서 돌기 때문에

2. 섬세한 권한 조정

위 ISP방법을 통해서 ViewModel이 가벼워 졌는데 이제는 섬세한 권한 조정을 하려고 한다.

  • 권한 조정이 필요한 이유

    • 권한제어자가 기본적으로 제공되는 언어 java, 코틀린 private, internal, public
    • Java의 기본 권한 private, Javascript 기본권한 public
    • 그래서 생기는 문제는 getter, setter외부에 노출이 되면 코드 조작이 가능해서 문제가 생긴다.
  • 아래 코드에 설명할 부분을 주석으로 6가지를 표시 했으며 아래 설명이 있습니다.
const ViewModel = class extends ViewModelSubject {
  static get(data) {
    return new ViewModel(data)
  }
  styles = {}
  attributes = {}
  properties = {}
  events = {}

  //설명1 readonly
  #subKey = ""
  get subKey() {
    return this.#subKey
  }
  #parent = null
  get parent() {
    return this.#parent
  }

  //설명2 [설명6과 관련]Transaction 연산
  setParent(parent, subKey) {
    this.#parent = type(parent, ViewModel)
    this.#subKey = subKey
    this.addListener(parent)
  }

  constructor(data, _ = type(data, "object")) {
    super()
    Object.entries(data).forEach(([cat, obj]) => {
      if ("styles,attributes,properties".includes(cat)) {
        if (!obj || typeof obj != "object")
          throw `invalid object cat:${cat}, obj:${obj}`
        this[cat] = Object.defineProperties(
          {},
          Object.entries(obj).reduce((r, [k, v]) => {
            r[k] = {
              enumerable: true,
              get: _ => v,
              set: newV => {
                v = newV
                //설명3 update할 객체 세팅 방법 변경
                this.add(new ViewModelValue(this.#subKey, cat, k, v))
              },
            }
            return r
          }, {})
        )
      } else {
        Object.defineProperties(this, {
          [cat]: {
            enumerable: true,
            get: _ => obj,
            set: newV => {
              obj = newV
              //설명3 update할 객체 세팅 방법 변경
              this.add(new ViewModelValue(this.#subKey, "root", cat, obj))
            },
          },
        })
        //설명6 [설명2와 관련]트렌젝션으로 setParent로 한번에 처리
        if (obj instanceof ViewModel) obj.setParent(this, cat)
      }
    })
    //설명4 ViewModel.notify(this); 제거
    //ViewModel.notify(this);
    Object.seal(this)
  }
  //설명5 viewModel
  viewmodelUpdated(updated) {
    updated.forEach(v => this.add(v))
  }
}
  • 설명1 readonly

    • 외부 공개, 쓰기는 private(public getter, private setter pattern)
  • 설명2 Transaction 연산

    • 자바스크립트는 트렌젝션을 따로 지원하지 않기 때문에 함수를 통해서 트렌젝션 연산이라는 것을 알려줘야 한다.
    • Transaction이란? 코드가 한번에 일어나야 하는 구간이다.
      그런데 그 구간에서 하나하나 일어나야 하는건지 덩어리로 한번에 일어나는지 아닌지 구별하는 방법이 없다. 그런데 자바스크립트에서는 덩어리로 한번에 일어나게 하는 방법은 "함수"를 통해서 한번에 일어나야 할 코드들을 표시할 수 있다.
    • 이렇게 setParent 함수로 만든 다른이유는?
    • 코드로 가져 올때 코드가 아닌 함수로 가지고 오기때문에 '전달인자'를 넘겨줘서 문제 없이 세팅할수 있다.
    • 함수가 아닌 코드로 클래스에서 있을경우 함수에서 인자로 넘겨주는 변수들이 '지역변수, 클로저'에 설정되어 있기 때문에 문제가 될수있기 때문에
    • 내부 함수는 "_"를 붙이자.
    • 이 코드는 parent를 세팅할때 있어야 하는 코드(기존에는 constructor안 dfineProperty하는 로직에 있었던 코드다.)
    • 코드에서 꼭 표현해야 할 것! -> "Transaction"이다
  • 설명3 update할 객체 세팅 방법 변경

    • 기존에는 뷰모델 #isUpdated에 직접 Set객체에 add통해 설정했지만 현재는 ViewModelSubject 클래스로 이동해 상속받았기 때문에 부모에게 호출하고 있다.(this.add(...))

      //기존
      vm.#isUpdated.add(new ViewModelValue(vm.subKey, category, k, v));
  • 설명4 ViewModel.notify(this); 제거

    • 기존에는 뷰모델이 직접 자신에게 notify를 등록했는데 지금은 하지 않는다. 왜?
    • 생성시점에 등록 안할거고 addListener할때 lazy하게 등록할 것이기 때문!
    • addListener에 lazy하는것도 뷰모델 서브젝트가 알아서 할 것이다.
  • 설명5 ViewModelListener Class viewmodelUpdated 함수 오버라이딩

    • ViewModelListener Class viewmodelUpdated 함수를 ViewModelSubject에서 오버라이딩 하지 않았다. 그래서 ViewModel에서 오버라이딩 하지 않으면 throw된다.
    • 이번 강의 ViewModel 상속 관계도
      : ViewModel -> ViewModelSubject -> ViewModelListener
    • 추가적으로 설명3과 같이 this.#isUpdated.add(v) -> this.add(v)로 코드가 변경됐다.
  • 설명6 트렌젝션으로 setParent로 한번에 처리

    • 기존에는 아래와 같은 코드를 setPraent 함수로 한번에 처리 했습니다. v.parent = this; v.subKey = k;, v.addListener(this);
  • 추가 설명

    • 그래서 부모 자식간, 필드 접근 권한, 트렌젝션에 의해 계층 권한이 생기는 경우 직접 변수에 접근한는 이유는 다양하다.
    • 권한관계라는건 발생하는건 여러가지 인데 물론 직짜 권한때문에 일어나느 경우가 많지만 트렌젝션때문에 일어나는 경우도 비일비재 하다.
    • 코드를 함수로 빼기위해서 코드를 한번 더 정리하기 때문에 코드 부분이 깔금하게 될 수 있다.
    • 코드가 길다 싶으면 외부함수, 스태틱함수로 변경하는건 좋은 연습니다.

3. Visitor Pattern

  • Scanner?

    • 특정 el 하위를 검색해서 viewmodel대상인 여부를 확인해서 BinderItem을 만들어서 binder에게 넘겨준다.

리팩토링 전 Scanner 소스 설명

const Scanner = class {
  scan(el, _ = type(el, HTMLElement)) {
    const binder = new Binder()
    this.checkItem(binder, el)

    //설명2 Visitor 패턴 적용 대상
    const stack = [el.firstElementChild]
    let target
    while ((target = stack.pop())) {
      this.checkItem(binder, target)
      if (target.firstElementChild) stack.push(target.firstElementChild)
      if (target.nextElementSibling) stack.push(target.nextElementSibling)
    }
    //설명2-end

    return binder
  }
  checkItem(binder, el) {
    //설명1 스캐너 역할
    const vm = el.getAttribute("data-viewmodel")
    if (vm) binder.add(new BinderItem(el, vm))
  }
}
  • 설명1 스캐너의 역할

    • Binder의 Scanner가 아니라 Scanner Class로 빠진이유. (스캐너를 빼낸 이유)
    • vuejs el속성들을 쓰고 싶다면 이곳에서 조작 후 아이템에 꽂아 주면 된다.
  • 설명2 Visitor 패턴

    • dom을 파싱 하는 부분은 Scanner, Binder class역할이라고 보기에 어렵다. 그래서 Visitor 패턴을 사용한다.
    • Visitor 패턴 등장 인물2개/ care taker(원본데이터, 보살핌을 받는애) - Visitor 패턴
    • Visitor에게 caretaker를 주면 Visitor가 돌아준다?
  • binder?

    • 그림을 그릴 대상을 binderItem으로 가지고 있다가 viewmodel에 꽂아 주면 viewmodel에 맞게 그림을 그려주는 역할을 하고 있다.

리팩토링 후 Scanner 소스 설명

const Visitor = class {
  //설명1
  visit(action, target, _0 = type(action, "function")) {
    throw "override"
  }
}

const DomVisitor = class extends Visitor {
  //설명2 Generic
  visit(
    action,
    target,
    _0 = type(action, "function"),
    _1 = type(target, HTMLElement)
  ) {
    const stack = []
    let curr = target.firstElementChild
    do {
      action(curr)
      if (curr.firstElementChild) stack.push(curr.firstElementChild)
      if (curr.nextElementSibling) stack.push(curr.nextElementSibling)
    } while ((curr = stack.pop()))
  }
}

const Scanner = class {
  #visitor
  constructor(visitor, _ = type(visitor, DomVisitor)) {
    this.#visitor = visitor
  }
  scan(target, _ = type9target, HTMLElement) {
    const binder = new Binder()
    const f = el => {
      const vm = el.getAttribute("data-viewmodel")
      if (vm) binder.add(new BinderItem(el, vm))
    }
    f(target)
    this.#visitor.visit(f, target)
    return binder
  }
}
  • 설명1 추상인터페이스

    • 추상인터페이스에서 target이 어떤 타입(HTMLElement, JSON 등등)인지 알 필요가 없습니다.
    • 오버라이드를 하지 않으면 죽기때문에! 즉 오버라이드할때 target의 타입을 지정해줘야 한다.
  • 설명2 Generic

    • 자식에서 구체적인 타입을 알게 되는 것을 Generic이라고 한다.
    • Generic은 원래 class옆에 표시하는건데 자바스크립트에서는 Generic이 있지 않기 때문에 이렇게 표시해준다.
    • 언어가 어떤 기능을 지원하는것보단 그 개념을 어떻게 적용하는지가 중요하다.

4. 추상계층 불일치

  • Scanner 클래스 리팩토링! 추상계층을 일치시켜주는 작업을 한다.
  • 연역적인 케이스를 가지고 연역적으로 원리를(추상적으로) 도출한뒤(귀납적) 다시 연역적으로 다시 도출

    • 귀납적: 원리 -> 현상 예측/ 연역적: 현상 -> 원리 도출

       1        2

  1. Scanner 클래스 constructor에 Domvisitor객체를 주입

    • 왜 Domvisitor를 바로 만들지 않고 Visitor 클래스를 만들어서 상속 받아서 구현했나?
    • 순수한 메모리 객체? Dom에 의존적이 네이티브 객체?
    • 계층이 2, 1개 로 나뉘어 있다.
    • Visitor, Domvisotor (순수한 메모리객체, Dom에 의존적인 네이티브 객체)
    • Scanner
    • 한쪽이 추상계층으로 했다면 다른 한쪽도 추상계층으로 맞춰줘야 한다.

       3

  1. 리팩토링 전후의 Scanner Class 비교

       4

  1. Scanner 클래스 scan에 Element 지식 설명

    • 이것도 추상계층으로 나눈다
    • 리펙토링을 하면 Scanner 클래스는 인메모리 객체만 남게 된다. (네이티브 객체가 하나도 없다)
    • 기존에 Scanner는 dom베이스 객체가 있었는데 없애 버린것이다.
    • Scanner를 추상화 시켜 dom 객체가 없다면 DomScanner는 Scanner를 상속받아 scan 함수를 오버라이딩 한다.
    • DomScanner 클래스에 생성자 함수 DomVisitor를 받는다. 그리고 super로 Scanner 클래스에 전달하는데 Scanner 클래스는 Visitor 클래스를 받는다.

      • 자식은 부모를 대체 가능하기 때문에 문제 없다.
    • Scanner scan 함수에 받는 인자 값 타입이 없었는데 DomScanner scan에 타입을 설정함(제너릭 기능)으로 구상화 했다.

       5

  1. Scanner, Visitor, DomScanner, DomVisitor 클래스 설명(하늘색선 있는 ppt)

    아래 두 계층을 나눈 설명은 이게 바로 추상레이어를 일치 시켜준 것이다.

    • Scanner, Visitor: Dom에 대한 네이티브 객체가 없고 인메모리 객체만 있는 추상 계층이 생기고
    • DomScanner, DomVisitor: 더러운 Dom관련 처리를 해주는 구상클래스 세계
    • 마틴파울러 아저씨는 기능, 도메인 적인 기능을 나눠서 서로 협력하게 만들어야 한다고 얘기 하셨다.
    • 기능: 변하지 않는 부분
    • 도메인: 변하는 부분

      • 그래야 나중에 도메인이 바뀌었을때 그 부분만 교체 가능하다고 얘기 하셨다.
      • 그래서 여기서 Scanner, Visitor는 "기능"적인 부분 DomScanner, DomVisitor은 "도메인(Dom)"을 처리하는 부분이 된다.
    • 그런데 도메인은 상황에 따라 바뀔 수 있다. 어떤경우에는 비즈니스 부분을 보호하고 네이티브에 이것이 스프링, my-sql? 이되던 문제 없이 동작할거야 라고 하면 도메인 쪽이 기능이 되는 것이다. 즉 변한는 부분과 변하지 않는 부분을 구분하라는 것이다.
    • 결과적으로 "변화율"을 고려해서 레이어를 나눈것이다.
    • 변화율때문에 코드를 나눠서 정복한것이있다 그건? 코드를 고치지 않고 코드를 추가함으로 수정할 수 있다.

      • 예를 들어 Dom이 아닌 Canvas를 다룰 때 코드를 고치지 않고 CanvasScanner, CanvasVisitor를 만들면 됩니다. 따라서 기존 코드의 변화가 없고 회귀 테스트가 필요없다.
      • 여기에 "추상레이어"의 장점은 코드를 수정하는게 아니라 코드를 추가함으로 요구사항, 문제가 생겼을때 해결할 수 있다. 여기서는 요구사항에 따라서 어떠한 Scanner, Visitor를 만들어 추가 할 수 있다.
      • 이게 바로 "설계"에 목메다는 이유입니다!!!
    • 추상화 계층을 분리함으로써 "OCP" 를 지키고 있다. 다른말고 OCP를 지키려면 추상화 클래스가 필수다!
    • O: OPEN, C: CLOSE - 확장에는 열려있고 수정에는 닫혀있다!
    • SOLID 법칙은 사실 법칙이 아니라 객체지향설계를 잘하면 얻어질 수 있는 결과물이다.
    • 추상화계층을 만듬으로 "OCP"말고 다른 장점
    • 추상화계층 클래스(Scanner, Visitor 클래스)는 initialize할때 로딩하고 구상클래스틑 원할때 동적 로딩하면 된다. 즉 코드의 늦은 초기화, 클래스 초기화를 유도할 수 있다.
      -> 이게 "의존성 역전의 법칙(DIP)"이다...

       6

  1. 구상 visitor를 구상 Scanner에게 줘서 scaner type 변수에 넘기다.

5. 설계 종합

  • UML diagram은 스팩이 너무 넓기 때문에 아래 캡쳐한것처럼 클래스 관계도를 그려서 설명합니다.

    1

    2

    3

    4

  1. ViewModel

    subject관련된건 ViewModelSubject로 이사갔어요.
    Dom에 의존적인 클래스 없다. 모두 인메모리 객체를 가지고 있다.

  2. ViewModelSubject
  3. 이때 최적화를 했다.
  4. 좋은코드가 나오는 이유는 인간머리의 복잡성을 정복할 정도 쪼개져 있기때문,,, 그런데 쪼개는게 힘들어 왜? 아무렇게나 쪼개면 일관성이 없기 때문에 어떻게 쪼갤지 모르는게 문제야.
  5. 객체 지향은 역할에 따라서 쪼개는것이다 !
  6. ViewModelListener
  7. [대기]
  8. ViewModelValue
  9. 아래와 같이 ViewModelValue은 다음 세개 클래스에 의존 되어 있다. -> ViewModel, ViewModelSubject, ViewModelListener Class
  10. 선이 많이 몰리면 수정하기 어려운 객체

    ViewModel -> ViewModelSubject -> ViewModelListener
    ViewModelValue -> ViewModel
    ViewModelValue -> ViewModelSubject
    ViewModelValue -> ViewModelListener
  11. 이래서 Dom과 관련되 lib, fw에서는 제일 먼저 자기 이벤트를 정복해서 네이티브 이벤트를 보여주지 않으려고 한다.

    • 왜냐하면 네이티브이벤트에 돔과 과련된 이벤트를 짜면 감당이 안되기 때문이다.
    • 이벤트는 옵저버 모델에서 무조건 이렇게 무겁게 된다.
    • 옵저버 모델의 약점이다. ㅠ
  12. 하지만 설계잘했다. ! 왜?

    • 단방향 의존성이기 때문이다. 양향도 없고 순회해서 다시 돌아오는 경우도 없다!
    • 연관은 자연스럽다! 왜? 코드를 객체로 뺏고 그 객체를 빼기전 코드와 연관 지었기 때문에 의존성이 자동으로 생긴다. 하지만 의존성이 발생하는게 나쁜게 아니라 의존성을 단방향으로 유지하는게 중요한 것입니다.
  13. 클래스 관계도를 그리고 코드를 수정할일이 생겼을때 무거운 코드 클래스인지 확인해자.

    5

    6

    7

    8

  1. Scanner, DomScanner
  2. DomScanner에 DomVisitor를 넣어줬긴 했지만

    • DomScanner가 DomVisitor에 의존적이다 물어보면?... 애매해 하지만 간접적으로 알고 있다.
    • DomScanner는 DomVisitor를 생성자 함수에서 받고 있다.
    DomScanner -> Scanner -> Visitor <- DomVisitor

    9

  1. Binder

    결국 Scanner는 Binder를 만들어 낸다.

  2. Binder의 주요 기능은?

    • add를 통해서 items를 set으로 가지고 있다
    • viewModelUpdate Listener를 구현해서 Listener로 부터 업데이트 된것을 render에 반영할 수 있고
    • addProcessor로 rendering에 필요한 것들을 processor 로 가지고
    • 특정 뷰모델은 watch, unwatch, 직접 render할 수 있다.

      DomScanner -> Scanner -> Visitor <- DomVisitor
              Scanner -> Binder

    10

  1. Binder의 의존성을 확인해보자 - 모두 단방향이다. Binder는 Scanner쪽을 모르고 ViewModelValue에서 Binder를 의존하지 않기 때문에 모두 단방향이다. - 나가는 선이 많은(Binder) 같이 변화, 깨지기 쉬운 클래스
    반대로 들어오는 선이이 많은 클래스(ViewModelValue)는 무거운 클래스 Binder -> ViewModel Binder -> ViewModelValue Binder -> ViewModelListener     11
  2. BinderItem
  3. element, viewmodel을 알고 있다.
  4. element가 들어가 있어서 문제가 된다.
  5. Binder -> BinderItem

    12

  1. Processor
  2. element, viewModel을 받아서 처리해주는 클래스
  3. Binder -> Processor

    13

  1. ConcreateProcessor
  2. Processsor는 method만 가지고 있는 것 구상 Processor가 실제적으로 Dom 지식을 가지고 있다.
  3. ConcreateProcessor -> Processor

    14

  1. ConcreateProcessor, DomScanner, DomVisitor 클래스
  2. 실제 Dom 정보를 가지고 있는 클래스는? 그리고 클라이언트에서 작성해야할 클래스는?

    • 위 클래스를 제외 하고 인메모리 객체를 가지고 있다.

6.종합설명

  • MVVM이 좋은점?

    • ConcreateProcessor, DomScanner, DomVisitor 클래스를 교체하면 안드로이드, IOS에서 사용할 수 있다.
  • 옵저버 패턴을 적용해서 ViewModel 클래스 주변으로 "ViewModelSubject, ViewModelListener, ViewModelValue" 다음과 같은 클래스들이 생겨났다.

    • 그림, 설명한것처럼 비용이 싸지 않습니다. 그래서 Binder에서 render를 call하는 경우도 있습니다.
  • 코드를 깍아나갈때 중요한 점 - 추상화 레벨을 맞춰야 한다. _ Binder, Scanner, Visitor이 세개 클래스는 dom 네이티브 객체를 가지고 있지 않는다. _ 이것을 맞추기 위해서 DomScanner, DomVisitor가 탄생한 것
  • 코드를 짤때 궁극적으로 강사님이 원하는건? - dom, node, db가 됐던 특정한 네이티브 지식이 나오면 코드를 분리하자 그리고 그 위에 네이티브 지식을 모르는 것들끼리 통신을 하게 하자 ! 그러면 녹색 클래스를(Dom 네이티브지식을 가진 클래스) 제외하고 재활용 할 수 있다.
    * Binder, Scaner, Visitor 클래스가 어떻게 되어 있는지 확인해보자
  • 다음 시간에는 중요한 패턴 2가지를 배울 것 입니다. - 추상팩토리메소드 패턴 - 커맨드 패턴

7.작업 클래스

const ViewModelListener = class {}
const ViewModelSubject = class extends ViewModelListener {}
const ViewModel = class extends ViewModelSubject {}
const ViewModelValue = class {}

const BinderItem = class {}
const Binder = class extends ViewModelListener {}

const Processor = class {}

const Scanner = class {}
const DomScanner = class extends Scanner {}

const Visitor = class {}
const DomVisitor = class extends Visitor {}

const scanner = new DomScanner(new DomVisitor())

binder.addProcessor(new (class extends Processor {})())
binder.addProcessor(new (class extends Processor {})())
binder.addProcessor(new (class extends Processor {})())
binder.addProcessor(new (class extends Processor {})())

8.작업 코드

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>MVVM 4회</title>
  </head>

  <body>
    <section id="target" data-viewmodel="wrapper">
      <h2 data-viewmodel="title"></h2>
      <section data-viewmodel="contents"></section>
    </section>

    <script></script>
  </body>
</html>
  const type = (target, type) => {
    if (typeof type == "string") {
      if (typeof target != type) throw `invalid type ${target} : ${type}`
    } else if (!(target instanceof type)) {
      throw `invalid type ${target} : ${type}`;
    }
    return target;
  }

  const ViewModelListener = class {
    viewmodelUpdated(updated) {
      throw 'override';
    }
  }

  const ViewModelSubject = class extends ViewModelListener {
    static #subjects = new Set;
    static #inited = false;
    static notify() {
      const f = () => {
        this.#subjects.forEach(v => {
          if (v.#info.size) {
            v.notify();
            v.clear();
          }
        })
        if (this.#inited) requestAnimationFrame(f);
      }
      requestAnimationFrame(f);
    }
    static watch(vm, _ = type(vm, ViewModelListener)) {
      this.#subjects.add(vm);
      if (!this.#inited) {
        this.#inited = true;
        this.notify();
      }
    }
    static unwatch(vm, _ = type(vm, ViewModelListener)) {
      this.#subjects.delete(vm);
      if (!this.#subjects.size) this.#inited = false;
    }
    #info = new Set;
    #listeners = new Set;
    add(v, _ = type(v, ViewModelValue)) {
      this.#info.add(v);
    }
    clear() {
      this.#info.clear();
    }
    addListener(v, _ = type(v, ViewModelListener)) {
      this.#listeners.add(v);
      ViewModelSubject.watch(this);
    }
    removeListener(v, _ = type(v, ViewModelListener)) {
      this.#listeners.delete(v);
      if (!this.#listeners.size) ViewModelSubject.unwatch(this);
    }
    notify() {
      this.#listeners.forEach(v => v.viewmodelUpdated(this.#info));
    }
  }


  const ViewModel = class extends ViewModelSubject {
    static get(data) {
      return new ViewModel(data);
    }
    styles = {};
    attributes = {};
    properties = {};
    events = {};
    #subKey = '';
    #parent = null;

    get subKey() {
      return this.#subKey;
    }
    // read only
    get parent() {
      return this.#parent;
    }
    setParent(parent, subKey) {
      this.#parent = type(parent, ViewModel);
      this.#subKey = subKey;
      this.addListener(parent);
    }

    static descriptor = (vm, category, k, v) => ({
      enumerable: true,
      get: () => v,
      set(newV) {
        v = newV;
        vm.add(new ViewModelValue(vm.subKey, category, k, v));
      }
    })
    static define = (vm, category, obj) => (
      Object.defineProperties(
        obj,
        Object.entries(obj)
          .reduce((r, [k, v]) => (r[k] = ViewModel.descriptor(vm, category, k, v), r), {})
      )
    )
    constructor(data, _ = type(data, 'object')) {
      super();
      Object.entries(data).forEach(([k, v]) => {
        if ('styles,attributes,properties'.includes(k)) {
          if (!v || typeof v != 'object') throw `invalid object k: ${k}, v:${v}`;
          this[k] = ViewModel.define(this, k, v);
        } else {
          Object.defineProperty(this, k, ViewModel.descriptor(this, '', k, v))
          if (v instanceof ViewModel) {
            v.setParent(this, k);
          }
        }
      })
      Object.seal(this);
    }
    viewmodelUpdated(updated) {
      updated.forEach(v => this.add(v));
    }
  }

  const ViewModelValue = class {
    subKey;
    category;
    k;
    v;
    constructor(subKey, category, k, v) {
      Object.assign(this, {
        subKey,
        category,
        k,
        v
      })
      Object.freeze(this);
    }
  }

  const BinderItem = class {
    el;
    vmName;
    constructor(el, vmName, _0 = type(el, HTMLElement), _1 = type(vmName, 'string')) {
      this.el = el;
      this.vmName = vmName;
      Object.freeze(this);
    }
  }

  const Binder = class extends ViewModelListener {
    #items = new Set;
    #processors = {};
    add(v, _ = type(v, BinderItem)) {
      this.#items.add(v);
    }
    addProcessor(v, _ = type(v, Processor)) {
      this.#processors[v.category] = v;
    }
    render(viewmodel, _ = type(viewmodel, ViewModel)) {
      const processores = Object.entries(this.#processors);
      this.#items.forEach(({ vmName, el }) => {
        const vm = type(viewmodel[vmName], ViewModel);
        processores.forEach(([pk, processor]) => {
          Object.entries(vm[pk]).forEach(([k, v]) => {
            processor.process(vm, el, k, v);
          })
        })
      })
    }

    watch(viewmodel, _ = type(viewmodel, ViewModel)) {
      viewmodel.addListener(this);
      this.render(viewmodel);
    }

    unwatch(viewmodel, _ = type(viewmodel, ViewModel)) {
      viewmodel.removeListener(this);
    }

    viewmodelUpdated(updated) {
      const items = {};
      this.#items.forEach(({ vmName, el }) => {
        items[vmName] = [type(rootViewModel[vmName], ViewModel), el];
      })
      updated.forEach(({ subKey, category, k, v }) => {
        if (!items[subKey]) return;
        const [vm, el] = items[subKey], processor = this.#processors[category];
        if (!el || !processor) return;
        processor.process(vm, el, k, v);
      })
    }
  }

  const Processor = class {
    category;
    constructor(category) {
      this.category = category;
      Object.freeze(this);
    }
    process(vm, el, k, v, _0 = type(vm, ViewModel),
      _1 = type(el, HTMLElement),
      _2 = type(k, "string")) {
      this._process(vm, el, k, v)
    }
    _process(vm, el, k, v) {
      throw 'override';
    }
  }

  const Scanner = class {
    #visitor

    constructor(visitor, _ = type(visitor, Visitor)) {
      this.#visitor = visitor;
    }
    visit(f, target) {
      this.#visitor.visit(f, target);
    }
    scan(target) {
      throw `override`;
    }
  }

  const DomScanner = class extends Scanner {
    constructor(visitor, _ = type(visitor, DomVisitor)) {
      super(visitor);
    }
    scan(target, _ = type(target, HTMLElement)) {
      const binder = new Binder;
      const f = el => {
        const vm = el.getAttribute('data-viewmodel');
        if (vm) binder.add(new BinderItem(el, vm));
      }
      f(target);
      this.visit(f, target);
      return binder;
    }
  }

  const Visitor = class {
    visit(action, target, _ = type(action, 'function')) {
      throw 'override';
    }
  }
  const DomVisitor = class extends Visitor {
    visit(action, target, _0 = type(action, 'function'), _1 = type(target, HTMLElement)) {
      const stack = [];
      let curr = target.firstElementChild;
      do {
        action(curr);
        if (curr.firstElementChild) stack.push(curr.firstElementChild);
        if (curr.nextElementSibling) stack.push(curr.nextElementSibling);
      } while (curr = stack.pop());
    }
  }

  const scanner = new DomScanner(new DomVisitor);
  const binder = scanner.scan(document.querySelector('#target'));
  binder.addProcessor(new class extends Processor {
    _process(vm, el, k, v) { el.style[k] = v; }
  }('styles'));
  binder.addProcessor(new class extends Processor {
    _process(vm, el, k, v) { el.setAttribute(k, v); }
  }('attributes'));
  binder.addProcessor(new class extends Processor {
    _process(vm, el, k, v) { el[k] = v; }
  }('properties'));
  binder.addProcessor(new class extends Processor {
    _process(vm, el, k, v) { el[`on${k}`] = e => v.call(el, e, vm); }
  }('events'));

  const getRandom = () => parseInt(Math.random() * 150) + 100;
  const wrapper = ViewModel.get({
    styles: {
      width: '50%',
      background: '#ffa',
      cursor: 'pointer'
    },
    events: {
      click(e, vm) {
        vm.parent.isStop = true
      }
    }
  })
  const title = ViewModel.get({
    properties: {
      innerHTML: 'Title'
    }
  })
  const contents = ViewModel.get({
    properties: {
      innerHTML: 'Contents'
    }
  })
  const rootViewModel = ViewModel.get({
    isStop: false,
    changeContents() {
      this.wrapper.styles.background = `rgb(${getRandom()},${getRandom()},${getRandom()})`
      this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '')
    },
    wrapper,
    title,
    contents
  })

  binder.watch(rootViewModel)
  const f = () => {
    rootViewModel.changeContents();
    if (!rootViewModel.isStop) requestAnimationFrame(f);
  }
  requestAnimationFrame(f);

태그: [대기]