디자인패턴과 뷰패턴(3)-2

minchan jung·2022년 3월 7일
0

Composit Pattern code-example

To-do list Design

  1. 화면 뷰 or 뷰 디자인 도안에서 domain-Entity 분류
  2. 단일 역할 : 단일 권한, 단일 책임을 기준으로 설계
  3. Host 입장 simulate 코드 작성
  4. 가장 쉬운, 가장 독립적, 가장 의존적이지 않는 Entity 부터 설계
  5. 나머지 Entity 설계
  6. 확장성을 위한 Composition pattern으로 설계한다.
  7. Renderer 설계
  8. Host Test

1. Entity 분류

  1. Todo : TaskList
  2. Task : Task
  3. Sort : 정렬 logic (not sure to have own Entity or not)

2. Entity 단일 역할 설계

  1. TaskList
    • field : todoList제목, Task 를 List 형태로 가짐
    • 기능1 : Task 를 추가, 제거 기능을 수행
    • 기능2 : Task 정렬 기능
  2. Task
    • field : task 제목, 생성시간, 완료여부
    • 기능 : 완료설정 및 취소 기능
    • getter : 완료여부 -외부에서 알아야할 field (render에 직접적으로 알려야함)

3. Host 입장, simulation 코드 작성

// host가 할 수 있는일을 정의하가 수행해 본다. 
const list1 = new TaskList('s3');
const task1 = new Task('3강 교안작성');
const task2 = new Task('코드정리');
list1.add(task1);
list1.add(task2);

list1.remove(task2);
list1.add(new Task('코드리팩토링'));

const list2 = new TaskList('s4');
list2.add(task1);
list2.add(new Task('4강 교안작성'));
// 등등..

4. 가장 독립적인 Entity 설계

Task

const Task = class{
  constructor(title, date = new Date.now()){
    if(!title) err('invalid title');
    this._title = title; // 은닉화
    this._date = date; // 은닉화
    this._isComplete = false; // 은닉화
  }
  isComplete(){ return this._isComplete; } // getter
  // 캡슐화 : 내부 isComplete 정보를 모른채 외부에서 사용되어져야 함
  toggle(){ this.isComplete = !this._isComplete }  // 완료 설정 기능 

}

5. 나머지 Entity 설계

TaskList

const TaskList = class{
  constructor(title){
    if(!title) return err('invalid title');
    this._title = title;
    this._list = [];
  }
  add(title, date = Date.now()){ this._list.push(new Task(title, date)); }
  remove(task){
    const list = this._list; 
    if(list.includes(task)) 	list.splice(list.indexOf(task),1);
  }
  byTitle(task){ return this._getList('title'); }
  byDate(){ return this._getList('date'); }
  _getList(sorting){
	const list = this._list
    return sorting === 'title' 
      ? list.sort((a,b)=>a._title > b._title)
      : list.sort((a,b)=>a._date > b._date) 
  }
}

sort기능

  • byTitle : this._getList('title')
  • byDate : this._getList('date')
  • _getList : Task 의 내부 field가 모두 공개되며 사용된다.
    - 캡슐화 실패 : Task의 내부 사정이 TaskList에서 알게 됨
    • Task에게 전체를 위임하거나, 공개되는 field 로직을 부분 위임!

5-1 TaskList 및 Task 설계 수정

Task

// ..중략.. 
// static method for TaskList to get ref of sorting 
  static title(a,b){return a.sortTitle(b);}
  static date(a,b){return a.sortDate(b);}

  sortTitle(task){// 기능상의 api 추가
    return this._title > task._title;
  }
  sortDate(task){// 기능상의 api 추가
    return this._date > task._date; 
  }

TaskList

//	...중략...
  byTitle(){ return this._getList('title'); }
  byDate(){ return this._getList('date'); }
  _getList(sorting){
    const list = this._list;
    return list.sort(Task[sorting]);// Task로 sorting 구체화는 위임됨 
    // static method 활용 - Task의 sorting method 선택 및 구체화 
  }

Sorting 관련 기능 추가

  • 정렬 기능시 완료된 List만 따로 볼것인지,
  • 완료되지 않은것도 포함해서 모두 같이 정렬해서 볼것인지를 선택해서 볼 필요가 있음

5-2 Task, TaskList (Sorting 관련 기능 수정)

Task = 동일

TaskList

//	...중략...
// 완료 여부에 따른 Sorting을 하기위한, flag para를 기본=true하며 설정 
  byTitle(){ return this._getList('title', stateGroup = true); }
  byDate(){ return this._getList('date', stateGroup = true); }
  _getList(sorting, stateGroup){
    const list = this._list;
    return !stateGroup 
      ? list.sort(Task[sorting]);
      : [ 
      	  ...list.filter(v=> !v.isComplete()).sort(Task[sorting]), //완료x 목록
          ...list.filter(v=> v.isComplete()).sort(Task[sorting]) // 완료 목록
        ]

  }

  • 기능 추가 사항 : Task 에서 nested 한 Task를 만들 수 있어야 한다.
  • Task1 하위에 또 다른 Task 목록이 존재 하도록 확장성을 가지게 만들 필요 있다.
  • Composit pattern 설계가 필요

6. Composition Pattern Design

Task

  • Task내 또 다른 하위 Task를 가질 수 있다.
  • 재귀적 List 구현이 필요 (Composition Pattern)
  • 새로운 자료형, 새로운 객체, 새로운 하위 클래스 타입으로 sub-Task를 정의할 필요가 있다.

TaskItem

  • Entity 추가
  • 기존의 Task 로직을 그대로 수행
  • Task의 상속 모델

TaskList

  • Task 자체가 하위-List를 가지며, Sort를 자체적으로 정의 하고 Get할 수 있게 되었다.
  • 기존의 add, remove 관련 로직 또한 Task에서 수행 하도록 할 수 있다.
  • Task를 sorting 하고 List화 하는 로직은 Task를 상속하여 부모 로직으로 대체가능함

Task의 상속 형태로 전환

  1. Task에서 List 관련 로직
  • add, remove, sort, getList 를 수행
  • 나머지 구체적 method를 자식이 수행
  1. TaskItem 캡슐화
  • isComplete(), toggle(), sortTitle(), sortDate() 수행
  • sortTitle(), sortDate() : 부모 Task 와 별개로 캡슐화가 되어야 한다.
    • 실제 구체적인 Task를 알 수 있는 곳은 이제 Task가 아닌 TaskItem 이기 때문에 TaskItem에서 method 구현되어야 캡슐화가 가능하다.
  1. TaskList Task를 상속
  • 필요한 기능들이 Task(부모)에서 수행 (자식 대체 가능)
  • Rendering시 실제 Task의 상위 요소로 존재해야 하며, 부모 node의 역할이 기대됨

Task

const Task = class{
  // static method to bind Hook
  static title(a,b){return a.sortTitle(b);}
  static date(a,b){return a.sortDate(b);}
 
  constructor(title){
    if(!title) return err('invalid title');
    this._title = title; // 은닉화
    this._list = []; // 은닉화
  }
  // add Task instance to list
  add(task){ return task instanceof Task ? this._list.push(task) : err('invalid instance of Task') }
  remove(task){
    const list = this._list; 
    if(list.includes(task)) list.splice(list.indexOf(task),1);
  }
  getResult(sorting, stateGroup){
    const list = this._list; 
    return {
      item : this, // Renderer 에서 indentity를 해결
      children : (!stateGroup ? [...list].sort(Task[sorting]) : [ 
        ...list.filter(v=>v.isComplete()).sort(Task[sorting])
      ]).map(v=>v.getResult(sorting, stateGroup)) 
      // Composit pattern, 자신의 동일 method 와 자식의 동일 method를 call (재귀)
    }
  } 
  // Hook
  isComplete(){ err('override') };
  sortTitle(){ err('override') };
  sortDate(){ err('override') };
}
	

TaskItem

const TaskItem = class extends Task{
  constructor(title, date= Date.now()){
    super(title);
    this._date = new Date(date);
    this._isComplete = false; 
  }
  isComplete(){ return this._isComplete; }
  sortTitle(task){ return this._title > task._title; }
  sortDate(task){ return this._date > task._date; }
  
  //고유 mothed
  toggle(){ this.isComplete = !this._isComplete }
}

TaskList

const TaskList = class extends Task{
  constructor(title){ super(title); }
  isComplete(){}
  sortTitle(){ return this; }
  sortDate(){ return this; }
}

7. Renderer 설계

el-create util 함수 정의

const el = (tag, ...attr)=>{
  const el = document.createElement(tag);
  for(let i = 0 ; i < attr.length ;){
    const k = attr[i++], v = attr[i++]; // key,val,key,val 순으로 attr 잡힐것이라서
    if(typeof el[k] === 'function') el[k](...(Array.isArray(v)? v : [v])); 
    // el[key] 조회시 함수라면 실행시키는데 val를 배열로 통일시켜 ...arg풀어서 인자로 넘긴다
    else if(k[0] === '@') el.style[k.substr(1)] = v; //style attr 라면 표시를 @ 해두고 할당함
    else el[k] = v;// 그외는 attr[key] = val 세팅
  }
  return el;
}

DomRenderer

const DomRenderer = class{
  constructor(list, parent){
    this._parent = parent; //Todo-rendering할  최상위 parent
    this._list = list; // base List (TodoList)
    this._sort = Task.title; // 기본 Sort 값을 title로 지정 
  }
  add(parent, title, date){
    // task추가( data 추가 ) => render (Model render, data 바뀌면 전부 render)
    parent.add( new TaskItem(title, date));
    this.render();
  }
  remove(parent, task){
    // task remove => 모두 render
    parent.reomve(task);
    this.render();
  }
  toggle(task){
    if(task instanceof TaskItem){
      // toggle로 complete 변경 => 모두 render 
      task.toggle();
      this.render();
    }
  }
  render(){ // 재귀과정 전에 초기화 값 세팅하는 도입 함수 ! 
    const parent = this._parent; 
    parent.innerHTML = '';
    parent.appendChild('title,date'.split(',').reduce((nav,c)=>(
      nav.appendChild(
        el('button', 'innerHTML', c,
          '@fontWeight', this._sort === c ? 'bold' : 'normal',
          'addEventListener', ['click', e=>(this._sort = Task[c], this.render())])
      ),nav
    ), el('nav')))
    // 나머지 composition data를 소비하며 render 하기 위한 _render 함수를 실행 (실행 구체 로직을 위임)
    this._render(parent, this._list, this._list.getResult(this._sort),0)
    // parent = 첫번째 composition 으로 appendChild할 부모
    // data상의 부모 = this._list
    // Loop 대상 : this._list.getResult
    // 0 : depth
  }
  _render(base, parent, { item, children }, depth){
    const temp = [];
    base.style.paddingLeft = depth * 10 + 'px';
    if(item instanceof TaskList){
      temp.push(el('h2', 'innerHTML', item._title))
    }else{
      temp.push( // Task 라면 필요한 el-attr 설정을 모두 해주고 temp 배열에 추가
        el('h3', 'innerHTML', item._title, 
          '@textDecoration', item.isComplete() ? 'line-through': 'none'), 
        el('time', 'innerHTML', item._date.toString(), 'datetime', item._date.toString()),
        el('button', 'innerHTML', item.isComplete() ? 'progress': 'compelete',
          'addEventListener', ['click', _ => this.toggle(item)])), 
        el('button', 'innerHTML', 'remove', 
          'addEventListener', ['click', _ => this.remove(parent, item)])
    }
    // children의 새로운 parent node!
    const sub = el('section',
      'appendChild', el('input', 'type', 'text'),
      'appendChild', el('button', 'innerHTML', 'addTask', 
        'addEventListener', ['click', e=>this.add(item, e.target.previousSibling.value)]
      ),
    );
    children.forEach(v=>{  this._render(sub, item, v, depth +1) }); // composition!
    temp.push(sub) // 만들어진 모든 현재 Task 및 sub-하위 Task들을 temp에 밀어 넣고
    // temp 안 요소를 모두 base 최상위 parent 에 추가! 
    temp.forEach(v=> base.appendChild(v))
  }
}
  • Data가 nested 하게 존재하므로
  • 객체를 Composit하게 설계해야 하며,
  • Renderer도 Data를 Composit하게 소비해야함.

8. Host Test

// host 
const list1 = new TaskList('s3-4');
const item1 = new TaskItem('3강 교안작성');
list1.add(item1);

const sub1 = new TaskItem('코드정리');
item1.add(sub1);

const subsub1 = new TaskItem('subsub1');
sub1.add(subsub1);

console.log(list1.getResult(Task.title));
const todo = new DomRenderer(list1, document.querySelector('#todo'))
todo.render();

코드스피츠-디자인패턴과 뷰패턴

0개의 댓글