인터페이스 설계는 제대로 쓰기엔 쉽게,

엉터리로 쓰기엔 어렵게 하자.


날짜를 나타내는 어떤 클래스가 있다.

class Date{

public:

Date( int month, int day, int year );

...


};


Date d( 1990 , 7 ,13 );

Date d( 20, 12, 2003);

매개변수의 전달 순서가 잘 못 될 수 있다.




해결책

새로운 타입을 들여와 인터페이스를 강화하자.

일,월,연을 구분하는 간단한 랩퍼 타입을 만들고

이 타입을 Date 생성자 안에 둔다.


struct Day{

int val;

explicit Day (int d) : val (d) {};

};

struct Month{

int val;

explicit Day (int m) : val (d) {};

};

struct Year{

int val;

explicit Day (int y) : val (d) {};

};


class Date {

public:

Date( const Month& m , const Day& d , const Year& y );

...

};


Date d (Month(3) , Day(3) , Yeay(1995));




적절한 타입만 제대로 준비되어 있다면, 각 타입에

값에 제약을 가하더라도 괜찮은 경우가 생긴다.

위의 경우, 월이 가질 수 있는 값은 12개 뿐이므로

enum을 사용해서 값에 제약을 줄 수 있다.

하지만 enum의 타입 안전성은 그리 믿음직하지

못하기 때문에, 유효한 Month의 집합을 미리 정의해 두자.


class Month {

private:

explicit Month(int m); --> Month 값이 새로 생성 되지 않도록 하기위해 private로 선언

--> 이미 밑의 함수에서 Month가 리턴되고 있기 때문에 생성될 필요없음.

public:

static Month Jan() {return Month(1);}  --> 왜 함수를 쓰느냐?

...                                                            --> 비지역 정적 객체들의 초기화 문제

static Month Dec() {return Month(12);}

...

};


Date d(Month::Mar(), Day(30), Year(1995));



예상되는 사용자 실수를 막는 다른 방법으로는

어떤 타입이 제약을 부여하여 그 타입을 통해

할 수 있는 일들을 묶어 버리는 방법이 있다.

제약 부여 방법으로 아주 흔히 쓰이는 예가 const 이다. 

앞 전 포스팅에서 다뤘기 때문에 패스~




사용자 정의 타입은 기본제공 타입처럼 동작하게 만들어야 한다.

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인

이유는 일관성 있는 인터페이스를 저공하게 위함이다.

STL의 모든 컨테이너는 size란 멤버 함수를 개방해 놓고 있다.

이 함수는 어떤 컨테이너에 들어 있는 원소의 개수를 알려준다.

만약 이러한 함수가 컨테이너 마다 다르다면? 혼란의 여지가 있다.


사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다.




Investment* createInvestment(); -> 팩토리 함수

요 앞전에 다뤘던 팩토리 함수에서 반환되는 포인터를

auto_ptr 과 shared_ptr로 관리를 해주는 얘기를 했었다.

여기에서도 인터페이스에 관한 얘기를 할 수 있는데,

스마트 포인터를 사용해야 한다는 사실을 잊을 경우 이다.

--> 애초부터 팩토리 함수가 스마트 포인터를 반환하게 만들자.


shared_ptr<Investment> createInvestment();


shared_ptr을 반환하는 구조는 자원 해제에 관련된

상당수의 사용자 실수를 사전 봉쇄할 수도 있어서

여러모로 인터페이스 설계자에게 좋다.

생성 시점에 삭제자를 직접 엮을 수 있는 기능이 있기 때문.




createInvestment를 통해 얻은 investment* 포인터를

직접 삭제하지 않게 하고 getRidOfInvestment라는

함수를 준비하고 여기에서 삭제를 진행한다고 가정해보자.

--> getRidOfInvestment 대신 delete를 쓸 수 있는 가능성이 있다.


shared_ptr에는 두 개의 인자를 받는 생성자가 있다.

첫 번째 인자는 이 놈이 관리할 실제 포인터이고,

두 번째 인자는 참조 카운트가 0이 될 때 호출될 삭제자이다.

shared_ptr이 널 포인터를 물게 함과 동시에 삭제자로

getRidOfInvestment 를 갖게 하는 방법을 쓸 수 있다.




shared_ptr<Investment> pInv(0,getRidOfInvestment);

--> 컴파일 에러가 난다. 생성자의 첫 번째 매개변수로

포인터를 받아야 하는데 0은 포인터가 아니라 int이다.


캐스트를 적용하여 문제를 해결한다.

shared_ptr<Investment>

pInv(static_cast<Investment*>(0),getRidOfInvestment);




이제 위에 놈을 반환하도록 구현해보자.

shared_ptr<investment> createInvestment(){

shared_ptr<investment> retVal (static_cast<Investment*>(0),

,getRidOfInvestment);

...

retVal = ... ;      --> 실제 객체를 가리키도록 만든다.


return retVal;

}


--> 실제 객체의 포인터를 결정하는 시점이

retVal을 생성하는 시점보다 앞설 수 있으면,

위의 코드 처럼 retVal을 널로 초기화하고 대입하는 것보다

실제 객체의 포인터를 바로 생성자에 넘겨버리는 것이 낫다.




shared_ptr은 기본적으로 교차 DLL문제를 미연에 방지해준다.

교차 DLL문제란 객체 생성 시에 어떤 동적 링크 라이브러리의

new를 썻는데 그 객체를 삭제할 때는 이전의 DLL과

다른 DLL의 delete를 썼을 경우이다.


하지만 shared_ptr은 포인터마다 각각의 

삭제자를 자동으로 쓰기 때문에 자신이 가리키는

DLL을 잊지 않고 있다가 해당하는 DLL의 delete를 사용한다.




결론

사용자가 무심코 저지를 수 있는 실수를 없앰으로써

좋은 인터페이스를 만다는 데 쉽게 다가갈 수 있다.


POINT!!

1. 인터페이스 사이의 일관성 잡아주기, 기본제공 타입과 똑같이 동작하게 하기.

2. 사용자의 실수를 방지하는 방법으로 새로운 타입 만들기, 타입에 대한 연산을 제한하기,

객체의 값에 대해 제약 걸기(const), 자원 관리 작업을 사용자 책임으로 놓지 않기(팩토리 함수의 반환값을 스마트포인터로)

3.  shared_ptr은 사용자 정의 삭제자를 지원한다. 때문에 교차 DLL 문제를 막아준다.



+ Recent posts