9. 물리적 은닉: 선언 vs 정의
•
선언: 컴파일러에서 심벌의 타입과 이름을 알려줌
•
•
정의: 전체적인 세부 사항을 제공
•
•
변수 정의 및 함수의 본문
물리적 은닉
•
•
ex) external int i, class MyClass …
공개된 인터페이스로부터 분리된 파일에 상세한 내부 로직을 구현
가급적이면 API 헤더는 선언만을 제공
•
inline 함수 사용 자제
10. 논리적 은닉: 캡슐화
•
API의 구체적인 로직이 외부로 노출되지 않도록 접근 제한자를 사용
•
C++ 접근 제한자
•
public: 외부에서 접근이 가능
•
구조체의 기본 접근 수준
•
protected: 클래스와 파생된 클래스 내부에서만 접근 가능
•
private: 클래스 내부에서만 접근 가능
•
클래스의 기본 접근 수준
11. 멤버 변수 감추기
•
멤버 변수의 직접 노출보다 getter/setter 메서드를 제공
•
getter/setter의 장점
•
•
지연된 평가
•
캐싱
•
추가 연산
•
알림
•
디버깅
•
동기화
•
훌륭한 접근 제어
•
•
유효성 체크
바뀌지 않는 관계 유지
클래스의 데이터 멤버는 항상 private로 선언
12. 메서드 구현 숨기기
•
public으로 선언할 필요가 없는 메서드를 감추는 것이 중요
•
클래스는 “무엇을 할 것인지를 정의하는 것”
•
C++의 제약 사항
•
클래스를 구현하기 위해 필요한 모든 멤버를 헤더에 선언 해야함
•
해결방안
•
•
Pimple 이디엄 사용 or cpp 파일에 정적 함수를 사용
Tip: private 멤버에 대한 비상수 포인터나 참조를 리턴(X:캡슐화 위반)
13. 클래스 구현 숨기기
•
실제 구현 코드를 가능하면 감춰라.
#include<vector>
!
class Fireworks {
public:
Fireworks();
!
!
!
};
void
void
void
void
SetOrigin(double x, double y);
SetColor(float r, float g, float b);
SetGravity(float g);
SetNumberOfParticles(int num);
void Start();
void Stop();
void Next Frame(float dt);
private:
class FireParticle {
public:
double mX, mY;
double mLifeTime;
};
double mOriginX, mOriginY;
float mRed, mGreen, mBlue;
float mGravity;
float mSpeed;
bool mIsActive;
std::vector<FireParticle *> mParticles;
FireParticle을 외부에 구현하기 보다는
내부 클래스로 구현
15. 지나친 약속은 금지
•
API의 모든 public 인터페이스는 약속이다
•
새로운 기능 추가는 쉽지만 기존의 것의 변경이 어렵다
•
“더더더” 일반적인 해결책을 찾기 위한 노력을 피해야 하는 이유
•
보다 더한 일반화가 필요한 순간은 오지 않을 수 있다
•
만약 그런날이 온다면, 경험으로 인한 더 좋은 해결책을 내놓을 수 있다
•
추가 기능이 필요하다면, 복잡한 곳보다는 간단한 API에 추가하는 것이
쉽다
16. 가상 함수의 추가는 신중하게
•
상속과 추상화
•
•
의도 했던 것 보다 더 많은 기능들을 노출시킬 수 있는 방법
상속의 함정
•
“깨지기 쉬운 베이스 클래스 문제”: 베이스 클래스의 변경이 클라이언트에 영향을 줌
•
클라이언트는 API 개발자가 의도치 않았던 방법으로 API를 사용할 수 있다
•
클라이언트는 API를 오류가 많이 발생하도록 확장할 수 있다 (동기화 등)
•
클래스 통합을 방해: 기존 함수의 정책에 위반되는 행위를 수행할 경우
17. C++ 가상 함수 사용시 생각할 점
•
가상 함수 호출은 런타임시 vtable을 탐색
•
가상함수를 사용할 수록 객체의 크기도 비례해서 증가(vtable)
•
가상 함수는 인라인이 될 수 없다
•
가상 함수를 오버로드 하는 것은 어렵다
•
추가적으로 기억할 것
•
소멸자는 항상 가상 함수로 선언
•
메소드 호출 관계를 문서화
•
생성자나 호출자에서는 절대 가상함수를 호출하지 않음
18. 편리한 API
•
기능에 초점을 맞춘 순수 API 제공 vs. 편리한 래퍼 API 제공
•
순수 API 제공
•
•
래퍼 API 제공
•
•
경량화되고 기능에 집중, 구현 코드의 복잡성을 줄임
클라이언트는 적은 양의 코드를 통해서 기본적인 기능이 동작
2.4 Easy to use
39
최소화시킨 핵심 API를 기반으로 분리된 모듈이나 라이브러리를 통해서 사용하기 편리한 API를 제공
FIGURE 2.4
An example of a core API (OpenGL) separated from convenience APIs layered on top of it (GLU and GLUT).
GLUquadric *qobj
gluNewQuadric();
20. 한눈에 들어오는
•
사용자가 API를 어떻게 사용해야 할지 한눈에 이해
•
클래스와 함수 이름을 잘 선택해서 직관적이고 논리적인 객체를
모델
•
자세한 내용은 4장에서
21. names in Chapter 4 when I discuss API design techniques. Avoiding the use of abbreviations can also
play a factor in discoverability (Blanchette, 2008) so that users don’t have to remember if your API
uses GetCurrentValue(), GetCurrValue(), GetCurValue(), or GetCurVal().
2.4.2 Difficult to Misuse
A good 사용하기에도 use, should also be difficult
being easy to 어렵게
Scott
잘못API, inis addition toimportant general interface design guideline to misuse.2004). Meyersofsuggests that this the most
(Meyers,
Some the
most common ways to misuse an API include passing the wrong arguments to a method or passing
illegal values to a method. These can happen when you have multiple arguments of the same type
and the user forgets the correct order of the arguments or where you use an int to represent a small
•range of values instead of a more constrained enum type (Bloch, 2008). For example, consider the
following method signature:
좋은 API라면 잘못 사용하기에도 어려워야 함
std::string FindString(const std::string &text,
bool search forward,
<boolean 사용>
bool case sensitive);
2.4 Easy to use
41
!
It would be easy for users to forget whether the first bool argument is the search direction or the
caseenum SearchDirection { the flags in the wrong<enum 사용>
sensitivity flag. Passing
order would result in unexpected behavior and
FORWARD,
probably!cause the user to waste a few minutes debugging the problem, until they realized that they
BACKWARD
had };
transposed the bool arguments. However, you could design the method so that the compiler
catches this kind of error for them by introducing a new enum type for each option. For example,
enum CaseSensitivity {
!
CASE SENSITIVE
CASE INSENSITIVE
};
std::string FindString(const std::string &text,
SearchDirection direction,
CaseSensitivity case sensitivity);
!
•
Not only does this mean that users cannot mix up the order of the two flags, because it would generate a compilation error, but also the code they have to write is now more self-descriptive. Compare
코드의 가독성을true, false); 위해서 Boolean보다는 enum을 사용
높이기
result FindString(text,
with
•
Tip: 함수에 같은 타입의 파라미터를 여러개 사용하지 말자
result
FindString(text, FORWARD, CASE INSENSITIVE);
TIP
Prefer enums to booleans to improve code readability.
22. abbreviations in several of its method names, such as prevValue() and previousSibling(). This is
another example of why the use of abbreviations should be avoided at all costs. use
2.4 Easy to
43
The use of consistent method signatures is an equally critical design quality. If you have several
methods that accept similar argument lists, you should endeavor to keep a consistent number and
order for those arguments. To give a counterexample, I refer you to the following functions from
2.4.3 Consistent
the standard C library:
일관성 있는
A good API should apply a consistent design approach so that its conventions are easy to remember,
and void bcopy(const void *s1, void 2008).size applies to all aspects of API design, such as
therefore easy to adopt (Blanchette, *s2, This t n);
char *strncpy(char *restrict s1, const char *restrict s2, size t n);
• naming conventions, parameter order, the use of standard patterns, memory model semantics, the
use Both of these error handling, and so on.
of exceptions, functions involve copying n bytes of data from one area of memory to another.
In terms of bcopy() these, consistent data from s1 into imply reuse strncpy() copies the
However, the the first offunction copies naming conventions s2, whereasof the same words forfrom s2 into
same concepts across the API. For example, if you have decided to use the verb pairs Begin and End,
s1.•This can give rise to subtle memory errors if a developer were to decide to switch usage between
you should not mingle the terms Start and Finish. As another example, the Qt3 API mixes the use of
the two functions without a methodreading such as respective man pages. To be sure, there is a clue to
close names, of the prevValue() and previousSibling(). This is
abbreviations in several of its
the conflicting specifications in the function signatures: note the usecosts. const pointer in each case.
another example of why the use of abbreviations should be avoided at all of the
However, thisconsistent method signatures is won’t be caught by a compiler if the source pointer is not
The use of could be missed easily and an equally critical design quality. If you have several
declared thatbe const.
methods to accept similar argument lists, you should endeavor to keep a consistent number and
• order for also the inconsistent useaof the words “copy” and “cpy.” following functions from
Note those arguments. To give counterexample, I refer you to the
the Let’s take another example from the standard C library. The familiar malloc() function is used to
standard C library:
API 설계 관점에서의 일관성
명명 규칙, 파라미터의 순서, 표준 패턴의 사용, 의미론적인 메
모리 모델, 예외 및 오류 처리 등.
일관성을 지키지 못한 사례
allocate bcopy(const void *s1, void *s2, and then);
void a contiguous block of memory, size t calloc() function performs the same operation with
the!char *strncpy(char *restrict s1, const char *restrict zerosize t n);
addition that it initializes the reserved memory with s2, bytes. However, despite their similar
purpose, they have different function signatures:
Both of these functions involve copying n bytes of data from one area of memory to another.
However,*calloc(size t count, size t size); s2, whereas strncpy() copies from s2 into
void the bcopy() function copies data from s1 into
s1.!This can give rise to subtle memory errors if a developer were to decide to switch usage between
void *malloc(size t size);
the two functions without a close reading of the respective man pages. To be sure, there is a clue to
Theconflicting specifications in the a size insignatures:bytes, whereasthe const pointer in eachcount * size)
the malloc() function accepts function terms of note the use of calloc() allocates ( case.
bytes. In this could be missed inconsistent, this violatesby a compiler if the least surprise.isAs a further
However, addition to being easily and won’t be caught the principle of source pointer not
•example,tothe const. and write() standard C functions accept a file descriptor as their first paradeclared be read()
Note also the the fgets() and fputs() “copy” and “cpy.”
meter, whereas inconsistent use of the words functions require the file descriptor to be specified last
Let’s take another
(Henning, 2009). example from the standard C library. The familiar malloc() function is used to
Tip: 함수의 이름, 파라미터의 순서를 일관성 있게 유지하라
23. 44
CHAPTER 2 Qualities
클래스 수준의 일관성
•
•
•
•
The STL is a great example of this. The std::vector, std::set, std::map, and
classes all offer a size() method to return the number of elements in the cont
also all support the use of iterators, once you know how to iterate through a
apply the same knowledge to a std::map. This makes it easier to memorize the pr
비슷한 기능을 제공하는 클래스들은 비슷한 인터페이스를 of the API. this kind of consistency for free through polymorphism: by placin
제공 get
You
tionality into a common base class. However, often it doesn’t make sense for
inherit from a common base class, and you shouldn’t introduce a base class pure
• ex) STL, std::vector, std::set, std::map의 size 함수 it increases the complexity and class count for your interface. Indeed, it’s n
as
STL container classes do not inherit from a common base class. Instead, yo
design for this by manually identifying the common concepts across your cla
다형성을 적용하면 일관성 얻기가 용이
same conventions to represent these concepts in each class. This is often re
polymorphism.
You can also make use of C++ templates to help you define and apply this k
For example, you could create a template 표현: coordinate class
공통 베이스 클래스를 둘 수 없는 경우에도 각 클래스에 공통된 개념들을 같은 이름으로 for a 2D 정적 다형성and specia
floats, and doubles. In this way you are assured that each type of coordinate offe
interface. The following code sample offers a simple example of this:
C++ 템플릿을 사용한 일관성 유지
•
ex) Coord2D<int>, Coord2D<float> ..
template <typename T>
class Coord2D
{
public:
Coord2D(T x, T y) : mX(x), mY(y) {};
T GetX() const { return mX; }
T GetY() const { return mY; }
void SetX(T x) { mX
void SetY(T y) { mY
x; }
y; }
void Add(T dx, T dy) { mX þ dx; mY þ dy; }
void Multiply(T dx, T dy) { mX * dx; mY * dy; }
private:
T mX;
T mY;
};
With this template definition, you can create variables of type Coord2D<int>, Coo
Coord2D<double> and all of these will have exactly the same interface.
24. 수직적인
•
다른 코드에 영향을 미치지 않는 함수
•
•
ex) 속성 값을 할당하는 메서드 호출은 그 속성 값만 변화
수직적 API 설계시 고려할 사항
•
중복 제거: 같은 정보가 2가지 이상의 방법으로 반복되지 않게 함
•
독립성 증가: 모든 중첩되는 개념들은 각각의 기반 컨포넌트로 분리
float CheapMotelShower::GetTemperature() const {
return mTemperature;
}
!
float CheapMotelShower::GetPower() const {
return mPower;
}
!
void CheapMotelShower::SetPower(float p) {
if (p < 0) p = 0;
if (p > 100) p = 100;
mPower = p;
mTemperature = 42.0f þ sin(p/38.0f) * 45.0f;
}
float IdealShower::GetTemperature() const {
return mTemperature;
}
!
float IdealShower::GetPower() const {
return mPower;
}
!
void IdealShower::SetTemperature(float t) {
if (t < 42) t = 42;
if (t > 85) t = 85;
mTemperature = t;
}
void IdealShower::SetPower(float p) {
if (p < 0) p = 0;
if (p > 100) p = 100;
mPower = p;
}
25. 견고한 자원 할당
•
C++의 메모리 관련 이슈
•
•
메모리 이중 해제
•
할당자 혼용
•
잘못된 배열 해제
•
•
Null 역참조
메모리 누수
관리되는 포인터를 사용
•
공유 포인터: boost::shared_ptr, …
•
약한 포인터: boost::weak_ptr, …
•
범위 한정 포인터: boost::scoped_ptr, …
26. 플랫폼 독립적
•
bool StartCall(const std::string &number);
bool EndCall();
#if defined TARGET OS IPHONE
bool GetGPSLocation(double &lat, double &lon);
#endif
};
This poor design creates a different API on different platforms. Doing so
your API to introduce the same platform specificity into their own applicatio
2.4 Easy to use
51
the aforementioned case, your clients would have to guard any calls to GetGPSL
cisely the same #if conditional statement, otherwise their code may fail to co
fined symbol error on other platforms.
Furthermore, if in a later version of the API you also add support for anot
2.4.6 Platform Independent
Windows Mobile, then you would have to update the #if line in your publ
A well-designed C++ API should always avoid platform-specific #if/#ifdef lines in its public. head- your clients would have to find all instances in their co
WIN32 WCE Then,
ers. If your API presents a high-level and logical model for your problem domain,embedded the TARGET OS IPHONE define and extend it to also include WIN32
as it should, there
are very few cases where the API should be different for different platforms. About the only cases
you have unwittingly exposed the implementation details of your API.
where this may be appropriate are when you are writing an API to interface with a platform-specific
Instead, you should hide the fact that the function only works on certain pla
resource, such as a routine that draws in a window and requires the appropriate window handle to be
method to determine whether the implementation offers the desired capabilitie
passed in for the native operating system. Barring these kinds of situations, you shouldFor example,
form. never write
public header files with platform-specific #ifdef lines.
class MobilePhone
For example, let’s take the case of an API that encapsulates the functionality offered by a mobile
phone. Some mobile phones offer built-in GPS devices that can deliver the geographic location of 2 Qualities
52{
CHAPTER
public:
the phone, but not all devices offer this capability. However, you should never expose this situation
bool StartCall(const std::string &number);
directly through your API, such as in the following example:
잘 설계된 API라면 특정 플랫폼에 독립적인 #if/#ifdef 코드를
public 헤더에 사용하지 않아야 함
class MobilePhone
{
public:
bool StartCall(const std::string &number);
bool EndCall();
#if defined TARGET OS IPHONE
bool GetGPSLocation(double &lat, double &lon);
#endif
};
bool EndCall();
bool HasGPS() const;
Now your API is consistent over all platforms and does not expose the details of wh
bool GetGPSLocation(double &lat, double &lon);
port GPS coordinates. The client can now write code to check whether the current
};
GPS device, by calling HasGPS(), and if so they can call the GetGPSLocation()
the actual coordinate. The implementation of the HasGPS() method might look som
bool MobilePhone::HasGPS() const
{
#if defined TARGET OS IPHONE
return true;
#else
This poor design creates a different API on different platforms. Doing so forces the clients of
return false;
your API to introduce the same platform specificity into their own applications. For example, in
#endif
the aforementioned case, your clients would have to guard any calls to GetGPSLocation() with pre}
cisely the same #if conditional statement, otherwise their code may fail to compile with an unde-
This is far superior to the original design because the platform-specific #if statem
fined symbol error on other platforms.
in the .cpp file instead of being exposed in the header file.
Furthermore, if in a later version of the API you also add support for another device class, say
Windows Mobile, then you would have to update the #if line in your public header to include
TIP
WIN32 WCE. Then, your clients would have to find all instances in their code where they have
This is platform
embedded the TARGET OS IPHONE define and extend it to also include WIN32 WCE.Never put because specific #if or #ifdef statements into your public APIs. It exposes impleme
you have unwittingly exposed the implementation details of your API.
makes your API appear different on different platforms.
Instead, you should hide the fact that the function only works on certain platforms and provide a
28. 이름만을 사용한 연결
•
클래스 전체 선언을 참조할 필요가 없다면 전방 선언을 사용
class MyObject; // only need to know the name of MyObject
!
class MyObjectHolder {
public:
MyObjectHolder();
void SetObject(MyObject *obj); MyObject *GetObject() const;
private:
MyObject *mObj;
};
29. 클래스 연결 줄이기
•
연결 관계를 줄이기 위해 멤버 함수 대신 비멤버, 비프렌드 함수
사용
// myobject.h
class MyObject {
public:
void PrintName() const;
std::string GetName() const;
...
protected:
...
private:
std::string mName; ...
};
// myobject.h
class MyObject {
public:
std::string GetName() const;
...
protected:
...
private:
std::string mName; ...
};
!
void PrintName(const MyObject &obj);
30. 의도적인 중복
•
심각한 연결 관계를 잘라내기 위해 적은 양의 중복코드를 추가하
는 것이 효과적일 경우
#include "ChatUser.h"
#include <string>
#include <vector>
class TextChatLog {
public:
bool AddMessage(const ChatUser &user,
const std::string &msg);
int GetCount() const;
std::string GetMessage(int index);
private:
struct ChatEvent {
ChatUser mUser;
std::string mMessage;
size t mTimestamp;
};
std::vector<ChatEvent> mChatEvents;
};
#include <string>
#include <vector>
class TextChatLog {
public:
bool AddMessage(const std::string &user,
const std::string &msg);
int GetCount() const;
std::string GetMessage(int index);
private:
struct ChatEvent {
std::string mUserName;
std::string mMessage;
size t mTimestamp;
};
std::vector<ChatEvent> mChatEvents;
};
ChatUser에 대한 연결 관계가 없어짐
31. 58
CHAPTER 2 Qualities
2.5.4 Manager Classes
A manager class is one that owns and coordinates several lower-level classes. This can be used to
break the dependency of one or more classes upon a collection of low-level classes. For example,
consider a structured drawing program that lets you create 2D objects, select objects, and move them
around a canvas. The program supports several kinds of input devices to let users select and move
objects, such as a mouse, tablet, and joystick. A naive design would require both select and move
operations to know about each kind of input device, as shown in the UML diagram (Figure 2.5).
Alternatively, you could introduce a manager class to coordinate access to each of the specific
input device classes. In this way, the SelectObject and MoveObject classes only need to depend
on this single manager class, and then only the manager class needs to depend on the individual input
device classes. This may also require creating some form of abstraction for the underlying classes.
For example, note that MouseInput, TabletInput, and JoystickInput each have a slightly different
interface. Our manager class could therefore put in place a generic input device interface that
abstracts away the specifics of a particular device. The improved, more loosely coupled, design is
shown in Figure 2.6.
Note that this design also scales well too. This is because more input devices can be added to the
system without introducing any further dependencies for SelectObject or MoveObject. Also, if you
decided to add additional manipulation objects, such as RotateObject and ScaleObject, they only
need a single dependency on InputManager instead of each introducing further coupling to the
underlying device classes.
매니저 클래스
•
하위 수준에 여러개의 클래스를 포함하면서 중재하는 역할을 수행
•
여러개의 클래스들을 대상으로 하나 혹의 그 이상의 의존성을 줄임
2.5 Loosely coupled
MoveObject
SelectObject
+ DoMove() : void
+ DoSelect() : void
MoveObject
SelectObject
+ DoMove() : void
+ DoSelect() : void
InputManager
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsButton1Down() : boolean
+ IsButton2Down() : boolean
+ IsButton3Down() : boolean
MouseInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsLMBDown() : boolean
+ IsMMBDown() : boolean
+ IsRMBDown() : boolean
TabletInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsPenDown() : boolean
JoystickInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsTriggerDown() : boolean
MouseInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsLMBDown() : boolean
+ IsMMBDown() : boolean
+ IsRMBDown() : boolean
FIGURE 2.5
TabletInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsPenDown() : boolean
Multiple high-level classes each coupled to several low-level classes.
FIGURE 2.6
Using a manager class to reduce coupling to lower-level classes.
JoystickInput
+ GetXCoord() : integer
+ GetYCoord() : integer
+ IsTriggerDown() : boolean
59
32. 콜백과 옵저버, 알림
•
이벤트가 발생했을때 이를 다른 클래스에 알리는 API
•
일반적인 이슈
•
재진입성
•
•
수명 관리
•
•
알림을 받은 코드가 다시 API를 호출하는 경우를 고려
이벤트에 대한 구독/해지 기능 제공, 또한 중복 이벤트를 받지 않도록 고려
이벤트 순서
•
이벤트의 순서를 명확히 정의 (네이밍 등의 방법으로 명시)
33. 콜백
•
저수준 코드가 고수준의 코드를 사용할때 의존성을 없애는 방법
•
대규모 프로젝트에서 순환 의존 고리를 제거
!
!
!
!
•
#include <string>
class ModuleB {
public:
typedef void (*CallbackType)(const std::string &name, void *data);
void SetCallback(CallbackType cb, void *data);
...
private:
CallbackType mCallback; void *mClosure;
};
!
if (mCallback) {
(*mCallback)("Hello World", mClosure);
}
객체지향 프로그램에서 인스턴스 메소드를 콜백으로 사용하는 경우 this 객
체 포인터를 전달해야 함
35. 알림
•
콜백과 옵저버는 특정 작업을 위해 생성
•
알림
•
시스템에서 연결되지 않은 부분 사이에서 알림이나 이벤트를 보내는 메커니즘을 중앙화
•
보내는 이와 받는 이 사이의 의존성이 전혀 없음
•
ex) signal/slot
class MySlot {
public:
void operator()() const {
std::cout << "MySlot called!" << std::endl; }
};
// Create an instance of our MySlot class
MySlot slot;
!
// Create a signal with no arguments and a void return value
boost::signal<void ()> signal;
!
// Connect our slot to this signal
signal.connect(slot);
!
// Emit the signal and thereby call all of the slots
signal();
36. 안정화와 문서화, 테스트
!
좋은 API라면
- 안정적이어야 하고, 미래 증명적이어야 함.
- 명확히 이해할 수 있게끔 구체적인 문서를 제공해야 함
- 변경이 발생하더라도 기존 기능에 영향이 가지 않도록 자동화된 테스트 프로세스를 갖추어야 함