배경
<windows.h> 의 RECT는 단순히 초기화 리스트에 left, top, right, bottom 순으로 LONG 인수를 주면 초기화가 가능하다.
하지만 게임에서 직사각형을 생성할 때 대부분 리터럴로 생성하기보단 여러 변수가 조합된 상대적 위치로 생성하는 경우가 많다.
이럴 때 사각형의 잘못된 생성으로 오류가 나면, 코드의 가독성이 떨어져 어디가 잘못되었는지 알아보기 힘들다.
- RECT inventory_rect = { client.right - INVEN_RIGHT_SPACE - inventory_width, client.right - INVEN_RIGHT_SPACE, client.top + INVEN_TOP_SPACE, client.top + INVEN_TOP_SPACE + inventory_height };
위 코드는 명백히 오류를 발생시킨다.
RECT의 초기화는 left, top, right, bottom 순이어야 하는데 left, right, top, bottom 순으로 이루어지고 있기 때문이다.
이런 실수를 보고 비웃음칠 수도 있겠다만 성격이 꼼꼼한 사람도 종종 할 수 있는 실수이다.
실수 발생의 여지가 있는 코드는 언제 터질지 모르는 폭탄같은 위험요소이다. 종종 할 수 있는 실수도 존재해서는 안 된다. 실수 발생의 여지가 있다면 그것이 종종 발생하든 자주 발생하든 발생하면 머리를 쥐어뜯으며 디버깅해야 할 것이다.
그 외에 값 자체도 잘못 입력할 수 있다. 특정한 수치를 연산에서 빼먹는다던지, 잘못된 연산을 한다던지 등으로.
나는 left에 height를 더해서 right를 구하고, top과 bottom은 똑같은 수치로 초기화해 본 적이 꽤 있다.
해결책은 left, top, right, bottom을 따로 선언하여 미리 각 수치에 대한 연산을 끝마쳐놓고, RECT는 바로 그 left, top, right, bottom을 통해 생성하는 것으로 가독성을 늘리는 방법인데, 이보다 더 범용성이 좋으면서 변수 선언과 대입 연산을 줄여 코드가 섹시해지는 방법이 있다.
바로 빌더 패턴을 이용하는 것이다.
아이디어
- 빌더 클래스는 자신이 생성할 객체의 setter를 제공한다.
- setter는 자기 참조 반환을 통해 연속적으로 setter를 호출할 수 있도록 한다.
- build()를 최종적으로 호출해 setter를 통해 입력된 변수의 값들로 객체를 생성해 반환한다.
예시
- RECT rect = rect_builder().left(20).top(30).right(40).bottom(50).build();
- RECT rect2 = rect_builder().bottom(50).left(20).right(40).top(30).build();
- auto rect3 = rect_builder().left(100).top(200).width(40).height(30).build();
- auto rect4 = rect_builder().center(POINT{500, 500}).width(100).height(50).build();
setter를 통해 받을 변수들
class rect_builder
{
public:
// ...
private:
RECT _rect;
POINT _center;
int _width;
int _height;
// ...
};
|
cs |
setter
class rect_builder
{
public:
rect_builder& left(const int val)
{
_rect.left = val;
flag |= LEFT;
return *this;
}
rect_builder& top(const int val)
{
_rect.top = val;
flag |= TOP;
return *this;
}
rect_builder& right(const int val)
{
_rect.right = val;
flag |= RIGHT;
return *this;
}
rect_builder& bottom(const int val)
{
_rect.bottom = val;
flag |= BOTTOM;
return *this;
}
rect_builder& center(const POINT val)
{
_center = val;
flag |= CENTER;
return *this;
}
rect_builder& width(const int val)
{
_width = val;
flag |= WIDTH;
return *this;
}
rect_builder& height(const int val)
{
_height = val;
flag |= HEIGHT;
return *this;
}
// ...
};
|
cs |
setter는 *this를 반환한다.
이로써 setter의 연쇄 호출이 가능해진다.
flag 변수에 대해선 다음 항목을 통해 알 수 있다.
비트 플래그
class rect_builder
{
// ...
private:
// ...
enum Flag { LEFT = 0x01, TOP = 0x02, RIGHT = 0x04, BOTTOM = 0x08,
CENTER = 0x10, WIDTH = 0x20, HEIGHT = 0x40 };
BYTE flag;
// ...
};
|
cs |
어떤 setter를 호출했는지에 따라서 사각형의 생성 방법이 달라져야 한다.
일반적으로 다음의 세 경우를 많이 이용한다.
- left, top, right, bottom을 통한 생성
- left, top, width, height를 통한 생성
- center, width, height를 통한 생성
setter의 호출에 맞는 플래그를 활성화 시킨다.
비트 플래그는 각각 20, 21, 22, 23, 24, ... 의 값으로 단 한 개의 비트를 나타낸다.
나중에 & 연산으로 비트 마스크를 씌워 플래그의 활성 여부를 확인할 수 있다.
비트 마스크 기법을 사용함으로써 여러 개의 bool 변수를 선언하는 비효율적인 코드를 효율적이게 만들 수 있다.
build()
class rect_builder
{
public:
// ...
RECT build()
{
// left, top, right, bottom이 간접적으로 유도되는 경우를 처리한 뒤
// RECT{ left, top, right, bottom }을 반환한다
// 직접 대입이 일어나지 않았을 경우 간접 유도를 시도하고,
// 불가능하면 오류를 던진다
if (!(flag & LEFT))
_rect.left = implicit_left();
if (!(flag & TOP))
_rect.top = implicit_top();
if (!(flag & RIGHT))
_rect.right = implicit_right();
if (!(flag & BOTTOM))
_rect.bottom = implicit_bottom();
return _rect;
}
operator RECT()
{
return build();
}
// ...
};
|
cs |
비트 마스크 기법을 통해 flag로부터 특정 setter의 호출 여부를 검사한다.
_rect의 left, top, right, bottom이 직접 설정되지 않았다면 간접적으로 구하려 시도해본다.
그렇게 left, top, right, bottom의 설정이 모두 끝난 _rect를 반환함으로써 build()가 구현된다.
RECT rt{ some_rect_builder }; 과 같이 코딩하더라도 직관상 문제가 없다.
따라서 operator RECT()를 준비하여 rect_builder가 RECT로 암시적 형변환이 가능하도록 한다.
간접 설정
class rect_builder
{
// ... private:
// ...
const int implicit_left()
{
if (flag & WIDTH)
{
if (flag & RIGHT)
return _rect.right - _width;
else if (flag & CENTER)
return _center.x - (_width >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_top()
{
if (flag & HEIGHT)
{
if (flag & BOTTOM)
return _rect.bottom - _height;
else if (flag & CENTER)
return _center.y - (_height >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_right()
{
if (flag & WIDTH)
{
if (flag & LEFT)
return _rect.left + _width;
else if (flag & CENTER)
return _center.x + (_width >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_bottom()
{
if (flag & HEIGHT)
{
if (flag & TOP)
return _rect.top + _height;
else if (flag & CENTER)
return _center.y + (_height >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
// ...
};
|
cs |
left와 right가 직접 설정되지 않았다면 width로부터,
top과 bottom이 직접 설정되지 않았다면 height로부터 간접 설정을 시도한다.
left와 center를 통해서 right를 구하거나, top과 center를 통해서 bottom을 구하는 것도 가능하겠으나, 그러한 코드를 작성할 일은 없다고 확신했으므로 굳이 만들지 않았다.
이 함수들은 명백한 중복 코드라 살짝 심기가 불편해진다.
하지만 일반화할 경우 변수여야 할 부분이 매우 많아 함수의 매개변수 목록이 더러워진다.
클래스가 이미 갖고 있는 변수를 매개변수로 또다시 받는 것도 지저분하므로
유지보수가 많이 이루어질 것으로 예상되는 코드가 아닌 이상 필요한 중복으로 여겨 일부러 중복시키기로 했다.
전체 코드
class rect_builder
{
public:
rect_builder& left(const int val)
{
_rect.left = val;
flag |= LEFT;
return *this;
}
rect_builder& top(const int val)
{
_rect.top = val;
flag |= TOP;
return *this;
}
rect_builder& right(const int val)
{
_rect.right = val;
flag |= RIGHT;
return *this;
}
rect_builder& bottom(const int val)
{
_rect.bottom = val;
flag |= BOTTOM;
return *this;
}
rect_builder& center(const POINT val)
{
_center = val;
flag |= CENTER;
return *this;
}
rect_builder& width(const int val)
{
_width = val;
flag |= WIDTH;
return *this;
}
rect_builder& height(const int val)
{
_height = val;
flag |= HEIGHT;
return *this;
}
RECT build()
{
// left, top, right, bottom이 간접적으로 유도되는 경우를 처리한 뒤
// RECT{ left, top, right, bottom }을 반환한다
// 직접 대입이 일어나지 않았을 경우 간접 유도를 시도하고,
// 불가능하면 오류를 던진다
if (!(flag & LEFT))
_rect.left = implicit_left();
if (!(flag & TOP))
_rect.top = implicit_top();
if (!(flag & RIGHT))
_rect.right = implicit_right();
if (!(flag & BOTTOM))
_rect.bottom = implicit_bottom();
return _rect;
}
operator RECT()
{
return build();
}
rect_builder() : _rect{ -1, -1, -1, -1 }, _center{ -1, -1 },
_width{ -1 }, _height{ -1 }, flag{ 0 } {}
rect_builder(const rect_builder& other) : _rect{ other._rect },
_center{ other._center }, _width{ other._width },
_height{ other._height }, flag{ other.flag } {}
rect_builder& operator=(const rect_builder& other)
{
_rect = other._rect;
_center = other._center;
_width = other._width;
_height = other._height;
flag = other.flag;
}
private:
RECT _rect;
POINT _center;
int _width;
int _height;
enum Flag { LEFT = 0x01, TOP = 0x02, RIGHT = 0x04, BOTTOM = 0x08,
CENTER = 0x10, WIDTH = 0x20, HEIGHT = 0x40 };
BYTE flag;
const int implicit_left()
{
if (flag & WIDTH)
{
if (flag & RIGHT)
return _rect.right - _width;
else if (flag & CENTER)
return _center.x - (_width >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_top()
{
if (flag & HEIGHT)
{
if (flag & BOTTOM)
return _rect.bottom - _height;
else if (flag & CENTER)
return _center.y - (_height >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_right()
{
if (flag & WIDTH)
{
if (flag & LEFT)
return _rect.left + _width;
else if (flag & CENTER)
return _center.x + (_width >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
const int implicit_bottom()
{
if (flag & HEIGHT)
{
if (flag & TOP)
return _rect.top + _height;
else if (flag & CENTER)
return _center.y + (_height >> 1);
else
throw "cannot make rect\n";
}
else
{
throw "cannot make rect\n";
}
}
};
|
cs |
테스트
#include <iostream>
#include "randomvalue.h"
void test_rect_builder()
{
for (int i = 0; i < 10; ++i)
{
auto rt = rect_builder()
.left(random_value(100, 200))
.top(random_value(100, 200))
.right(random_value(300, 400))
.bottom(random_value(300, 400))
.build();
auto rt2 = rect_builder()
.center(POINT{ random_value(400, 600), random_value(400, 600) })
.width(100)
.height(100)
.build();
auto rt3 = rect_builder()
.left(random_value(500, 600))
.top(random_value(500, 600))
.width(200)
.height(300)
.build();
std::cout << "left in [100, 200], top in [100, 200], right in [300, 400], bottom in [300, 400]\n";
std::cout << "{ " << rt.left << ", " << rt.top << ", " << rt.right << ", " << rt.bottom << " }\n\n";
std::cout << "center.x in [400, 600], center.y in [400, 600], width 100, height 100\n";
std::cout << "{ " << rt2.left << ", " << rt2.top << ", " << rt2.right << ", " << rt2.bottom << " }\n\n";
std::cout << "left in [500, 600], top in [500, 600], width 200, height 300\n";
std::cout << "{ " << rt3.left << ", " << rt3.top << ", " << rt3.right << ", " << rt3.bottom << " }\n\n";
}
}
|
cs |
잘 작동한다.
보통 빌더 패턴을 적용할 클래스의 내부 클래스로 빌더를 구현하는 게 관례이지만, 이미 windows.h에 정의된 RECT와 호환되어야 하기 때문에 빌더 클래스를 따로 정의하였다.
또한 빌더 클래스를 통해 명시된 함수의 호출로 직사각형을 생성하는 만큼, 사용자가 충분히 오류 대처를 할 수 있다고 판단하여 사각형을 구성할 수 없는 경우를 제외한 오류 처리는 하지 않았다. ex) left > right || top > bottom
일부러 left > right || top > bottom 인 사각형을 생성할 수도 있다. 원래 RECT의 생성에도 그러한 오류처리는 없으므로 이와 같은 판단은 타당하다.
'Project > Win32API로 던그리드 모작' 카테고리의 다른 글
csv 문서 파싱 (0) | 2021.08.19 |
---|---|
게임 프레임 관리 (0) | 2021.08.19 |
* passkey 패턴 : public 멤버 함수의 접근 권한 관리로 public 함수를 non-public 하게 (0) | 2021.08.18 |
c++식 랜덤값 얻기의 개선안 (0) | 2021.08.15 |
더블버퍼링 (0) | 2021.08.14 |