코드스피츠80_OOP design with game (2)- 2. 모델 (베이스 레이어)

코드스피츠80_OOP design with game (2)- 2. 모델 (베이스 레이어)

목차

코드스피츠 강의 정리록

생소한 도메인으로 배우는게 좋다.
익숙한 도메인들은 익숙한 처리방법으로 처리하기때문에 객체지향을 배우기 어렵다.
때문에 80기는 게임을 통해서 진행할 예정.


1. 저번시간의 코드에서 잘못된 점을 찾아보자.

1.1 베이스 클래스에 네이티브 지식이 포함되어있다.

1
2
3
4
5
6
7
8
const Block = class {
static GET(type = parseInt(Math.random() * 5)){ return new Block(type);}
constructor(type) {
this._type = type;
}
get image(){ return 👉 `url('img/block${this._type}.png'`;}
get type(){ return this._type}
}
  • 블럭 모델에 네이티브 지식이 들어가있다. (css 지식)
  • 베이스 레이어에 들어오면 안되는 지식이다.

아키텍처는 코드에 가질 수 있는 역할과 가질 수 없는 역할,
기질 수 있는 책임과 가질수 없는 책임을 명시할 수 있고,
그것으로 코드가 바른지 안바른지 판단하는 기준표가 된다.

  • 기존 모델에서 지금 아키텍처와 안 맞는 부분을 지목할 수 있다.

1.2 통제권을 가진 제왕이 존재한다. (1) - 블럭 위치값의 지식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
👉 const data = [];
...
return tid => {
table = document.querySelector(tid);
👉 for (let i = 0; i < row; i++){
const r = [];
data.push(r);
👉 for(let j = 0; j < column; j++) r[j] = Block.GET();
}
table.addEventListener('mousedown', down);
table.addEventListener('mouseup', up);
table.addEventListener('mouseleave', up);
table.addEventListener('mousemove', move);
render();
};

cf__1. 프로시저적인 생각을 한다? 를 어디서 알 수 있냐면,

= 모든 연산을 data를 갖고 하려고 한다.

  • data가 왕이다. 통제의 왕

일반적으로 프로시저로 짜면 메모리를 적게 사용하게 된다.

  • 메모리의 효율성이 높아지고, 연산의 횟수가 줄어든다.
  • 하지만 모든 복잡성을 본인이 감당해야한다.
    • 예를들어 현재는 2차원배열로 작업했지만,
      요건이 만약 3차원의 개념으로 바뀐다면?
      3차원배열로 바꿔야하는데 그럼.. 로직을 아예 바꿔야함

블럭의 위치값

2차원 배열에 들어가는데, 블럭에 X값 Y값이 들어가있나?

  • 안들어가있음. 실제 객체는(Block) 모르고 있다.
  • 블럭은 x, y값을 모르는데 자기 스스로 move나 판정을 할 수 있을까?
    row와 column값은(i와 j) Block의 지식일까? data의 지식일까?
  • 현재는 data가 통제의 왕이기 때문에 data가 블럭의 판정통제권을 들고 있다.(i, j)

객체지향은 내 할일이 아니면 위임하는 것이다.

  • 자기일이 아닌거는 계속 미룬다. 미룰사람이 누군지만 알고 있으면 된다.
  • 말로만 객체지향이나 역할을 거의 수행할 수 없는 상태.
    현재 블럭은 x,y에 대한 역할을 수행할 수 없다.

1.3 통제권을 가진 제왕이 존재한다. (2) - 선택된 블럭의 지식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const selected = [], getBlock = (x, y) => {...}

const down = ({pageX: x, pageY: y}) => {
if(isDown) return;
const curr = getBlock(x,y);
if(!curr) return;
isDown = true;
👉 selected.length = 0;
👉 selected[0] = startBlock = currBlock = curr;
render();
};

const move = ({ pageX: x, pageY: y }) => {
if (!isDown) return;
const curr = getBlock(x, y);
if (!crr || curr.type !== startBlock.type || !isNext(curr)) return;
👉 if (selected.indexOf(curr) == -1) selected.push(curr);
👉 else if (selected[selected.length - 1] == curr) selected.pop();
currBlock = curr;
render();
};

const up = () => (👉 selected.length > 2 ? remove() : reset());
  1. selected를 사방에서 직접 컨트롤하고 있다.

    • 만약 선택의 조건이 현재 요건과 달라진다면?
      down, move, up 모두 바꿔야 한다.
  2. selected도 블럭이 선택되어있는지 아닌지에 대한 지식이기 때문에
    Block의 지식이다.

    • 선택된 그룹도 블럭이 알고 있는 것이고,
    • 블럭이 블럭 인스턴스끼리 협력하는 코드는 블럭내의 지식이다.
    • 블럭은 내 형제가 누군지에 대해서 블럭이라는 클래스 지식내에서 해결할 수 있다.
  3. 우리는 완전히 독립되어있는 어떤 데이터를 바라보고 있는
    프로시저를 짜서 해결하려고 한다.

    • 때문에 데이터 구조가 달라지면 다 바꿔야한다.
  4. 끊임없는 상태에 대한 변화의 책임을
    블럭이 갖고 있어야한다.

    • 가장 중요한 권한은 블럭의 type이 아니라
      끊임없는 상태에 대한 변화의 책임을 블럭이 갖고 있어야한다.
    • 상태를 갖고 있으면 관리가 어렵기 때문에 해당 책임을 갖고 있는 객체가 알아서 하게끔 권한을 주는 것.
    • 현재 코드에서 블럭의 상태는
      거의 상태가 없는 객체나 마찬가지이다.

2. 프로시저를 객체지향으로 바꿔보자.

2.1 모델 (feat. 유틸)

2.1.1 심플한 책임

1
2
3
4
5
6
7
8
const Block = class {
static GET(type = parseInt(Math.random() * 5)){ return new Block(type);}
constructor(type) {
this._type = type;
}
get image(){ return `url('img/block${this._type}.png'`;}
get type(){ return this._type}
}

👇👇👇

1
2
3
4
const UTIL = {
el: (v: string) => document.querySelector(v),
prop: (...arg) => Object.assign(...arg)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface IItem {
pos: (x: number, y: number) => void;
select: (item: Item) => void;
unselect: () => void;
}

const Item = class {
static GET(type: number, x: number, y: number){
return new Item(type, x, y);
}

constructor(_type: number, _x: number, _y: number) {
prop(this, {_type, _x, _y, _selected: false, _prev: null})
}
get type(){ return this._type; }
get x(){ return this._x; }
get y(){ return this._y; }
get selected(){ return this.selected; }
get prev(){ return this._prev; }

pos(x: number, y: number){ // 어디로 움직일지
this._x = x;
this._y = y;
}
select(item: Item){ // 어떤 아이템에 선택할지?
this._selected = true;
this._prev = item;
}
unselect(){
this._selected = false;
this._prev = null;
}
}
  1. 싱글 노드 링크드 리스트가 완성되었다.

    • 이전에는 배열로(data) 관리했었는데, 이제는 링크드 리스트로 관리한다.
      나의 지식은 나와 이전(prev) 정도까지밖에 모르기 때문에
  2. item 객체가 이 이상을 알게 되면, 권한 위반이 된다.

    • *권한 책임을 축소하면 연산이 많이 일어난다. *
      (연산이 늘어난 부분은 걱정하지 말자.)
  3. 밖에는 캡슐화 된 메소드로 대화한다.

    • 블럭 상태는 밖에서 관리하지 않고,
    • 밖에서는 pos, select, unselect만 알게하자.

2.1.2 어려운 책임

좀 더 어려운 책임을 바라보게 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
const Item = class {
...
isSelectedList(item: Item){
if(!this._prev) return false;
if(this._prev === item) return true;
else return this._prev.isSelectedList(item)
}

isBorder(item: Item) {
return (Math.abs(this.x - item.x) < 2) &&
(Math.abs(this.y - item.y) < 2)
}
}

1. isSelectedList

selectedList에 포함되어있냐 아니냐를 판단하는 메소드

  • 모든 상황이 안되면, 바로 이전의 item에게 물어본다.
  • 책임 권한을 _prev로 제약하여 메모리와 연산을 교환하였다. (링크드 리스트)
  • 이 모든 건 자료구조의 일부이다.
cf__2. 객체지향에서 책임의 범위와 자료구조
  1. 책임 범위를 축소하면 연산을 많이하게 된다.

    • 타클래스의 메서드를 호출할 경우도 있고,
      본인의 연결되어있는 클래스를 호출하는 경우가 더 많다.
  2. 객체지향에서는 기본적으로 배열같은 어그리게이션(집합)을 쓰는 경우가 거의 없다.

    • 어그리게이션을 쓰면 어그리게이터에 대한 로직을 따로 짜야한다.
    • 코디네이터를 더 만들기 싫으면 본인 안에 링크드 리스트로 연결하는 수밖에 없다.
    • 컬렉터를 만드냐 안만드냐는 아키텍처상 중요한 요소이다.
    • 컬렉터를 추상화하는 것은 굉장히 어려운 것이다.
    • 통합된 자료구조를 안쓰게 되니까 다 연산으로 되어있는 자료구조를 쓰게 된다.
  3. 배열을 링크드 리스트로 치환한 것이다.

  • 같은 자료구조임에도 불구하고, 연산으로 메모리로 치환하거나 메모리를 연산으로 치환할 수 있다.
    • 연산을 메모리로 치환하면 속도가 빨라진다.
      • 메모리를 연산으로 치환하면 좋은 점이
        하나는 메모리가 절감되는 경우가 생기고,
        권한을 축소할 수 있는 권한이 생긴다. (프로시저)
    • 자바스크립트의 프로토타입체인은 대표적으로 연산을 통해서 메모리를 줄이는 시스템이다.
      연산을 통해서 프로토타입 체인에 가져오는 것이다.
      예전 메모리가 없던(넷스케이프)시절 개발된 자바스크립트.
      • 하지만 현대의 크롬브라우저는 속도가 더 중요하기 때문에 모든 프로토타입 체이닝에 잇는 것을 다 copy해서 캐시테이블을 만들어서 거기서 읽는다.
        • 더이상 프로토타입 체인 타고 가서 가져오지 않는다. 때문에 크롬에서 자바사크립트가 빠른 이유이다.
        • 체인을 매번 타고 가는 연산은 귀찮기 때문에

알고리즘의 태반은 자료구조로 되어있고,
자료구조의 태반은 다시 또 알고리즘으로 돌아가게 되어있다.
항상 메모리와 연산은 교환할 수 있다.

2. isBorder

나의 인접 셀을 파악하는 것도 나의 권한이다.

  • 특정 아이템의 x,y가 나의 지식이기 때문에.
  • 나의 x,y와 아이템의 x,y의 차이를 이용해서 나의 인접 셀인지 아닌지를 알 수 있다.
cf__3. 권한 + 책임 = 역할
  1. 우리가 책임을 부여하고 싶으면, 권한을 부여해야한다.
    두개가 어긋나면 둘중 하나가 깨진다.

    • 객체지향에서 권한은 은닉화 되어서 내부 상태로 숨고,
      책임은 표면화되서 외부 메소드를 드러나게 되어있다.
      메소드는 캡슐화해서 표현된다.
      ex_ATM기기
    • 객체지향에서 메소드는 this라는 내부상태의 은닉되어있는 상태를 사용하고 있느냐,
      추상화가 되어있는, 캡슐화 되어있는 메소드냐가 충족되어야
      객체지향에서 메소드라고 부른다.
  2. 얼만큼 캡슐화되어야하는지는 대상에 따라 다르다.

    • 어느 정도로 캡슐화되어있느냐는 공개 범위에 달려있다.
    • 현재 Item의 기준은 게임이다. 게임이 이해할 수 있고, 몰라도 되는 기준으로 캡슐화
    • 그게 내부 인터널에서 사용하는것?
    • 자기들끼리 쓰는것?
    • 부모자식간에 쓰는 것?

내가 만약 프로그램을 짜서 공개되는 코드도 마지막에 은닉과 캡슐화를 처리하는 단계는 항상 머리속에 짱구를 그려야한다.

📚