클린코드가 가르쳐주지 않는 것
- 무작위로 오른쪽, 아래쪽을 선택
- 대각선 편향
- 무작위로 가로 방향 n회 확장 & 아래 행의 무작의 셀과 연결
- 수직 편향
- 모든 셀을 방문할 때 까지 무작위 보행 (브라운 운동?)
- 편향 없음 / 점점 느려짐
- DFS, 각 셀을 2번씩 방문
- 길다 / 굽이치는 미로 / 막다른 길이 없음
- visited에서 시작, 막다를 때 까지 무작위 보행
- 길다 / 굽이치는 / 적은 메모리
- loop-erased 무작위 보행
- 편향 없음 / 점점 빨라짐
- 신장 나무 알고리즘, but 무작위로 간선 선택
- 비편향 / 균질함
- 정방형이 아닌 미로에 사용
- 신장 나무 알고리즘, but 무작위로 간선 선택
- 깨진 유리같은(radial) / 많은 막다른 길
- 네모의 느낌 / 길쭉한 통로(bottleneck)
- 다양한 변형이 가능
1. OOP 설계 원칙을 따르면서
2. 클린코드 원칙을 생각하면서
class Generator {
private maze;
void generate() {
// update this.maze
}
void draw() {
// canvas.lineTo(...)
}
}
function onLoad() {
let m = new Generator();
m.generate();
m.draw();
}interface IGenerator {
void generate();
void draw();
}
class BinaryTree implements IGenerator {/*...*/}
class Sidewinder implements IGenerator {/*...*/}
function onLoad() {
let m = new BinaryTree(); // or Sidewinder
m.generate();
m.draw();
}
interface IGenerator {
void generate();
void draw();
}
enum MazeType {
BinaryTree,
Sidewinder,
/*...*/
}
class MazeFactory {
IGenerator create(MazeType type) {/*...*/}
}
function onLoad() {
let m = MazeFactory.create(BINARY_TREE);
m.generate();
m.draw();
}
interface IGenerator {
void step();
bool isCompleted();
void draw();
}
async function run() {
let m = new BinaryTree();
while (!m.isCompleted()) {
m.step();
m.draw();
await sleep(50);
}
}
interface IGenerator {
void step();
bool isCompleted();
void draw();
}
let g_delay = 50;
async function run() {
let m = new BinaryTree();
while (!m.isCompleted()) {
m.step();
m.draw();
await sleep(g_delay);
}
}
전역변수가 찝찝하지만 딱 한개니까...
let g_delay = 50;
let g_shouldRestart = false;
await function run() {
let m = new BinaryTree();
while (!m.isCompleted() && !g_shouldRestart) {
m.step();
m.draw();
await sleep(g_delay);
}
if (g_shouldRestart) {
g_shouldRestart = true;
run();
}
}
1. 전역변수가 2개가 되었다.
2. 이번에는 읽는 것 뿐만이 아니라 값을 변경하기까지 한다.
3. 비동기 처리 때문에 코드가 어려워졌다.
4.
동시성 문제가 생길 것 같다.
let g_delay = 50;
let g_shouldRestart = false;
let g_fastForward = false;
function run() {
let m = new BinaryTree();
while (!m.isCompleted() && !g_shouldRestart) {
if (g_fastForward) {
m.generate();
g_fastForward = false;
} else {
m.step();
}
m.draw();
sleep(g_delay);
}
if (g_shouldRestart) {
g_shouldRestart = true;
run();
}
}
Q. 애니메이션 될 때 소리가 나게 해보세요.
let g_delay = 50;
let g_shouldRestart = false;
let g_fastForward = false;
let g_soundEnabled = true;
function run() {
let m = new BinaryTree();
while (!m.isCompleted() && !g_shouldRestart) {
if (g_fastForward) {
m.generate();
g_fastForward = false;
} else {
m.step();
}
m.draw();
if (g_soundEnabled) {
m.playSound();
}
sleep(g_delay);
}
if (g_shouldRestart) {
g_shouldRestart = true;
run();
}
}
1. 전역변수가 어느새 4개
2. 값을 양쪽에서 조작하는 변수가 2개가 되었다.
3. 함수가 너무 많은 일을 하는 것 같다.
4. 확장성이 떨어지고 구현 난이도가 증가한다.
(대부분의 리팩토링 관련된 책에서 지적하는 부분)
연출만 변경할 뿐인데, 로직 구현부가 영향을 받는다.
특정 step이 오래 걸릴 경우 렌더링 성능 저하가 생긴다.
뷰(View)에 해당하는 부분: draw, playSound, sleep
class Renderer {
let _delay = 50;
let _soundEnabled = true;
void render(IGenerator g) {/*...*/}
}
let g_shouldRestart = false;
let g_fastForward = false;
let g_renderer = new Renderer();
function run() {
let m = new BinaryTree();
while (!m.isCompleted() && !g_shouldRestart) {
if (g_fastForward) {
m.generate();
g_fastForward = false;
} else {
m.step();
}
g_renderer.render(m);
}
if (g_shouldRestart) {
g_shouldRestart = true;
run();
}
}
🛠 Renderer 역할을 하는 클래스를 분리해 보자!
테스트 하기 어렵다고 배웠다.
의존성 주입(DI)을 통해 제어를 역전(IoC)하라고 배웠다.
객체의 생성을 외부에 위임(deligation)하라고 배웠다.
그러면 결합(decouple)이 줄어든다고 배웠다.
class ControlConfig {
let shouldRestart;
let fastForward;
}
class BaseMaze implements IGenerator {
constructor(ControlConfig config) {
this.config = config;
}
/* ... */
}
class BinaryTree extends BaseMaze {/*...*/}
function run() {
let m = new BinaryTree();
while (!m.isCompleted()) {
m.generate();
g_renderer.render(m);
}
}
🛠 DI... IoC...
전역 변수가, 다른 클래스들의 지역 변수로 흩어졌다.
Mock Object는 테스트를 도와줄 뿐 구조를 단순하게 만들지 않음.
* 본질적인 복잡도 = 요구사항의 복잡도 + 구현의 복잡도
구현 난이도가 올라갔던 이유는 구현 자체의 문제가 아님.
새로운 상태의 추가가 기존 코드와 얽힘을 만들기 때문.
맨먼스 미신 - 은탄환은 없다 中
(오늘 발표의 제목)
복잡도 = 로직의 복잡도 + 상태의 복잡도
로직의 복잡도 = 필수 로직의 복잡도 + 우발적 로직의 복잡도
상태의 복잡도 = 필수 상태의 복잡도 + 우발적 상태의 복잡도
우발적 로직은 논리형 프로그래밍으로
우발적 상태은 함수형 프로그래밍으로
필수 로직은 informal한 표현을 formal한 표현으로
필수 상태는 관계형 모델링으로
state
state'
state''
f
f
step
step
step
g_shouldRestart → reset seq
g_fastForward → seq.last()
Clojure의 관점이죠!
관찰자(렌더러)의 필요에 따라 미로의 상태가 변함
변경과 관찰이 분리되면 메모리 경합 발생
시간 제어를 위한 기법이 필요해짐
미로는 이산적 변화로 구성된 lazy-seq
외부에서는 특정 시간의 스냅샷을 "관찰"할 뿐
동시성 제어가 불필요
- Live Demo의 비밀
- Essential State는 항상 유지되고 있었기 때문
- React/Redux 철학과 비슷
store, action → store'
store', action → store''
maze, step → maze'
maze', step → maze''
Reducer! (reducing function)
- undo/redo를 위한 커맨드 패턴 (X)
- 작성한 코드: 약 10줄 (깨닫는데 10년)
(defn do-step!
"시퀀스의 head를 그리고 기록한 뒤 한 칸 전진시킴"
[]
(when-let [m (first (:mseq @*state))]
(swap! *state assoc :output m)
(swap! *state update :mseq rest)
(swap! *history conj m)
(redraw)))
(defn time-travel!
"특정 idx의 상태로 돌아가서 렌더링"
[idx]
(let [m (get @*history idx)]
(swap! *state assoc :output m)
(redraw)))Purely Functional Data structures - Chris Okasaki, 1996
Hash Array Mapped Trie - Phill Begwell, 2001
Hickey Tree* - Rich Hickey, 2007
RRB-Trees - Phil Begwell, 2012
CHAMP - Steinforder & Vinju, 2015
*Hickey Tree라는 이름은 임의로 부여한 것입니다.
1. 코드 가독성보다 중요한 것은 복잡도를 낮추는 것
2. 복잡도는 상태 때문이다.
3. 상태 관리를 위한 다양한 접근법에 관심을 갖자
4. 가변성을 멀리하고 불변적을 가까이하자
2. SICP / SDF
3. Meetings With Remarkable Trees
4. Live React: Hot Loading with Time Travel
5. Immutability changes everything