c++ api design 품질

39
C++ API 디자인 Ch.2 품질 아꿈사 Cecil

Upload: hyeonseok-choi

Post on 20-May-2015

1.266 views

Category:

Technology


4 download

TRANSCRIPT

Page 1: C++ api design 품질

C++ API 디자인 Ch.2 품질

아꿈사Cecil

Page 2: C++ api design 품질

좋은 API가 갖춰야 할 기본 품질은?

이 장에서는

Page 3: C++ api design 품질

고품질의 API 설계를 가능하게 하는 특정한 품질 속성이 존재하고, 가능하다면 이 기준을 도입해라

!

설계 품질을 낮추는 많은 요인은 반드시 피해라

Page 4: C++ api design 품질

고품질 API 설계를 위한 이 책에서 제시하는 기준

• 문제 도메인

• 구체적인 구현 숨기기

• 작게 완성하기

• 쉬운 사용성

• 느슨한 연결

• 안정화와 문서화, 테스트

Page 5: C++ api design 품질

문제 도메인 모델 API는 문제에 대한 납득할 만한 해결책을 제공해야 한다

Page 6: C++ api design 품질

훌륭한 추상화 제공

• API는 논리적인 추상화를 제공해야 함

• 저수준의 코드 구현 보다 추상화된 API SET을 제공

• But, 훌륭한 추상화 제공은 쉽지 않은 문제

• 일관성 있고 충분한 논리성을 바탕으로 API를 개발해야 함

Page 7: C++ api design 품질

핵심 객체 모델링

• API는 문제 도메인의 핵심 객체를 모델링 해야함

• 객체 모델링의 주요 목표: 주요 객체 식별 및 연결 관계 확인

• API의 핵심 모델은 시간이 지나면 변화가 필요함을 고려해야 함

• 너무 앞서서 필요한 것보다 일반적인 객체 생성은 금물

Page 8: C++ api design 품질

구체적인 구현 숨기기 클라이언트에 영향을 미치지 않고, 내부 로직을 변경할 수 있도록 구체적인 구현을 숨겨야 한다

Page 9: C++ api design 품질

물리적 은닉: 선언 vs 정의

• 선언: 컴파일러에서 심벌의 타입과 이름을 알려줌

• ex) external int i, class MyClass …

• 정의: 전체적인 세부 사항을 제공

• 변수 정의 및 함수의 본문

• 물리적 은닉

• 공개된 인터페이스로부터 분리된 파일에 상세한 내부 로직을 구현

• 가급적이면 API 헤더는 선언만을 제공

• inline 함수 사용 자제

Page 10: C++ api design 품질

논리적 은닉: 캡슐화

• API의 구체적인 로직이 외부로 노출되지 않도록 접근 제한자를 사용

• C++ 접근 제한자

• public: 외부에서 접근이 가능

• 구조체의 기본 접근 수준

• protected: 클래스와 파생된 클래스 내부에서만 접근 가능

• private: 클래스 내부에서만 접근 가능

• 클래스의 기본 접근 수준

Page 11: C++ api design 품질

멤버 변수 감추기

• 멤버 변수의 직접 노출보다 getter/setter 메서드를 제공

• getter/setter의 장점

• 유효성 체크

• 지연된 평가

• 캐싱

• 추가 연산

• 알림

• 디버깅

• 동기화

• 훌륭한 접근 제어

• 바뀌지 않는 관계 유지

• 클래스의 데이터 멤버는 항상 private로 선언

Page 12: C++ api design 품질

메서드 구현 숨기기

• public으로 선언할 필요가 없는 메서드를 감추는 것이 중요

• 클래스는 “무엇을 할 것인지를 정의하는 것”

• C++의 제약 사항

• 클래스를 구현하기 위해 필요한 모든 멤버를 헤더에 선언 해야함

• 해결방안

• Pimple 이디엄 사용 or cpp 파일에 정적 함수를 사용

• Tip: private 멤버에 대한 비상수 포인터나 참조를 리턴(X:캡슐화 위반)

Page 13: C++ api design 품질

클래스 구현 숨기기

• 실제 구현 코드를 가능하면 감춰라.#include<vector> !class Fireworks { public: Fireworks(); void SetOrigin(double x, double y); void SetColor(float r, float g, float b); void SetGravity(float g); void 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을 외부에 구현하기 보다는 내부 클래스로 구현

Page 14: C++ api design 품질

작게 완성하기 좋은 API라면 최소한의 크기로 완성되어야 한다

Page 15: C++ api design 품질

지나친 약속은 금지

• API의 모든 public 인터페이스는 약속이다

• 새로운 기능 추가는 쉽지만 기존의 것의 변경이 어렵다

• “더더더” 일반적인 해결책을 찾기 위한 노력을 피해야 하는 이유

• 보다 더한 일반화가 필요한 순간은 오지 않을 수 있다

• 만약 그런날이 온다면, 경험으로 인한 더 좋은 해결책을 내놓을 수 있다

• 추가 기능이 필요하다면, 복잡한 곳보다는 간단한 API에 추가하는 것이 쉽다

Page 16: C++ api design 품질

가상 함수의 추가는 신중하게

• 상속과 추상화

• 의도 했던 것 보다 더 많은 기능들을 노출시킬 수 있는 방법

• 상속의 함정

• “깨지기 쉬운 베이스 클래스 문제”: 베이스 클래스의 변경이 클라이언트에 영향을 줌

• 클라이언트는 API 개발자가 의도치 않았던 방법으로 API를 사용할 수 있다

• 클라이언트는 API를 오류가 많이 발생하도록 확장할 수 있다 (동기화 등)

• 클래스 통합을 방해: 기존 함수의 정책에 위반되는 행위를 수행할 경우

Page 17: C++ api design 품질

C++ 가상 함수 사용시 생각할 점

• 가상 함수 호출은 런타임시 vtable을 탐색

• 가상함수를 사용할 수록 객체의 크기도 비례해서 증가(vtable)

• 가상 함수는 인라인이 될 수 없다

• 가상 함수를 오버로드 하는 것은 어렵다

• 추가적으로 기억할 것

• 소멸자는 항상 가상 함수로 선언

• 메소드 호출 관계를 문서화

• 생성자나 호출자에서는 절대 가상함수를 호출하지 않음

Page 18: C++ api design 품질

편리한 API

• 기능에 초점을 맞춘 순수 API 제공 vs. 편리한 래퍼 API 제공

• 순수 API 제공

• 경량화되고 기능에 집중, 구현 코드의 복잡성을 줄임

• 래퍼 API 제공

• 클라이언트는 적은 양의 코드를 통해서 기본적인 기능이 동작

• 최소화시킨 핵심 API를 기반으로 분리된 모듈이나 라이브러리를 통해서 사용하기 편리한 API를 제공

GLUquadric *qobj gluNewQuadric();gluSphere(qobj, radius, slices, stacks);gluDeleteQuadric(qobj);

This is a great example of how to maintain the minimal design and focus of a core API while alsoproviding additional convenience routines that make it easier to use that API. In fact, other APIsbuilt on top of OpenGL provide further utility classes, such as Mark Kilgard’s OpenGL UtilityToolkit (GLUT). This API offers routines to create various solid and wireframe geometric primitives(including the Utah teapot), as well as simple window management functions and event processing.Figure 2.4 shows the relationship among GL, GLU, and GLUT.

Ken Arnold refers to this concept as progressive disclosure, meaning that your API should pre-sent the basic functionality via an easy-to-use interface while reserving advanced functionality fora separate layer (Arnold, 2005). He notes that this concept is often seen in GUI designs in the formof an Advanced or Export button that discloses further complexity. In this way, you can still providea powerful API while ensuring that the expert use cases don’t obfuscate the basic workflows.

TIP

Add convenience APIs as separate modules or libraries that sit on top of your minimal core API.

2.4 EASY TO USEA well-designed API should make simple tasks easy and obvious. For example, it should be possiblefor a client to look at the method signatures of your API and be able to glean how to use it, withoutany additional documentation. This API quality follows closely from the quality of minimalism: ifyour API is simple, it should be easy to understand. Similarly, it should follow the rule of least sur-prise. This is done by employing existing models and patterns so that the user can focus on the taskat hand rather than being distracted by the novelty or involution of your interface (Raymond, 2003).

FIGURE 2.4

An example of a core API (OpenGL) separated from convenience APIs layered on top of it (GLU and GLUT).

392.4 Easy to use

Page 19: C++ api design 품질

쉬운 사용성 잘 설계한 API라면 간단한 작업을 쉽고 명확하게 만들어야 한다

Page 20: C++ api design 품질

한눈에 들어오는

• 사용자가 API를 어떻게 사용해야 할지 한눈에 이해

• 클래스와 함수 이름을 잘 선택해서 직관적이고 논리적인 객체를 모델

• 자세한 내용은 4장에서

Page 21: C++ api design 품질

• 좋은 API라면 잘못 사용하기에도 어려워야 함

!

!

!

!

• 코드의 가독성을 높이기 위해서 Boolean보다는 enum을 사용

• Tip: 함수에 같은 타입의 파라미터를 여러개 사용하지 말자

잘못 사용하기에도 어렵게

Of course, this is not an excuse for you to ignore the need for good supporting documentation.In fact, it should make the task of writing documentation much easier. As we all know, a goodexample can go a long way. Providing sample code can greatly aid the ease of use of your API.Good developers should be able to read example code written using your API and understand howto apply it to their own tasks.

The following sections discuss various aspects and techniques to make your API easier to under-stand and ultimately easier to use. Before I do so though, it should be noted that an API may provideadditional complex functionality for expert users that is not so easy to use. However, this should notbe done at the expense of keeping the simple case easy.

2.4.1 DiscoverableA discoverable API is one where users are able to work out how to use the API on their own, withoutany accompanying explanation or documentation. To illustrate this with a counterexample from thefield of UI design, the Start button in Windows XP does not provide a very discoverable interface forlocating the option to shut down the computer. Likewise, the Restart option is accessed rather unin-tuitively by clicking on the Turn Off Computer button.

Discoverability does not necessarily lead to ease of use. For example, it’s possible for an API tobe easy for a first-time user to learn but cumbersome for an expert user to use on a regular basis.However, in general, discoverability should help you produce a more usable interface.

There are a number of ways in which you can promote discoverability when you design your APIs.Devising an intuitive and logical object model is one important way, as is choosing good names foryour classes and functions. Indeed, coming up with clear, descriptive, and appropriate names can beone of the most difficult tasks in API design. I present specific recommendations for class and functionnames in Chapter 4 when I discuss API design techniques. Avoiding the use of abbreviations can alsoplay a factor in discoverability (Blanchette, 2008) so that users don’t have to remember if your APIuses GetCurrentValue(), GetCurrValue(), GetCurValue(), or GetCurVal().

2.4.2 Difficult to MisuseA good API, in addition to being easy to use, should also be difficult to misuse. Scott Meyers sug-gests that this is the most important general interface design guideline (Meyers, 2004). Some of themost common ways to misuse an API include passing the wrong arguments to a method or passingillegal values to a method. These can happen when you have multiple arguments of the same typeand the user forgets the correct order of the arguments or where you use an int to represent a smallrange of values instead of a more constrained enum type (Bloch, 2008). For example, consider thefollowing method signature:

std::string FindString(const std::string &text,bool search forward,bool case sensitive);

It would be easy for users to forget whether the first bool argument is the search direction or thecase sensitivity flag. Passing the flags in the wrong order would result in unexpected behavior andprobably cause the user to waste a few minutes debugging the problem, until they realized that theyhad transposed the bool arguments. However, you could design the method so that the compilercatches this kind of error for them by introducing a new enum type for each option. For example,

40 CHAPTER 2 Qualities

enum SearchDirection {FORWARD,BACKWARD

};enum CaseSensitivity {

CASE SENSITIVECASE 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 gen-erate a compilation error, but also the code they have to write is now more self-descriptive. Compare

result FindString(text, true, false);

with

result FindString(text, FORWARD, CASE INSENSITIVE);

TIP

Prefer enums to booleans to improve code readability.

For more complex cases where an enum is insufficient, you could even introduce new classes toensure that each argument has a unique type. For example, Scott Meyers illustrates this approachwith use of a Date class that is constructed by specifying three integers (Meyers, 2004, 2005):

class Date{public:

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

};

Meyers notes that in this design clients could pass the year, month, and day values in the wrong order,and they could also specify illegal values, such as a month of 13. To get around these problems, hesuggests the introduction of specific classes to represent a year, month, and day value, such as

class Year{public:

explicit Year(int y) : mYear(y) {}int GetYear() const { return mYear; }

private:int mYear;

};

412.4 Easy to use

<boolean 사용>

<enum 사용>

Page 22: C++ api design 품질

일관성 있는

• API 설계 관점에서의 일관성

• 명명 규칙, 파라미터의 순서, 표준 패턴의 사용, 의미론적인 메모리 모델, 예외 및 오류 처리 등.

• 일관성을 지키지 못한 사례

!

!

• Tip: 함수의 이름, 파라미터의 순서를 일관성 있게 유지하라

2.4.3 ConsistentA good API should apply a consistent design approach so that its conventions are easy to remember,and therefore easy to adopt (Blanchette, 2008). This applies to all aspects of API design, such asnaming conventions, parameter order, the use of standard patterns, memory model semantics, theuse of exceptions, error handling, and so on.

In terms of the first of these, consistent naming conventions imply reuse of the same words for thesame concepts across the API. For example, if you have decided to use the verb pairs Begin and End,you should not mingle the terms Start and Finish. As another example, the Qt3 API mixes the use ofabbreviations in several of its method names, such as prevValue() and previousSibling(). This isanother example of why the use of abbreviations should be avoided at all costs.

The use of consistent method signatures is an equally critical design quality. If you have severalmethods that accept similar argument lists, you should endeavor to keep a consistent number andorder for those arguments. To give a counterexample, I refer you to the following functions fromthe standard C library:

void bcopy(const void *s1, void *s2, size t n);char *strncpy(char *restrict s1, const char *restrict s2, size t n);

Both of these functions involve copying n bytes of data from one area of memory to another.However, the bcopy() function copies data from s1 into s2, whereas strncpy() copies from s2 intos1. This can give rise to subtle memory errors if a developer were to decide to switch usage betweenthe two functions without a close reading of the respective man pages. To be sure, there is a clue tothe conflicting specifications in the function signatures: note the use of the const pointer in each case.However, this could be missed easily and won’t be caught by a compiler if the source pointer is notdeclared to be const.

Note also the inconsistent use of the words “copy” and “cpy.”Let’s take another example from the standard C library. The familiar malloc() function is used to

allocate a contiguous block of memory, and the calloc() function performs the same operation withthe addition that it initializes the reserved memory with zero bytes. However, despite their similarpurpose, they have different function signatures:

void *calloc(size t count, size t size);void *malloc(size t size);

The malloc() function accepts a size in terms of bytes, whereas calloc() allocates (count * size)bytes. In addition to being inconsistent, this violates the principle of least surprise. As a furtherexample, the read() and write() standard C functions accept a file descriptor as their first para-meter, whereas the fgets() and fputs() functions require the file descriptor to be specified last(Henning, 2009).

TIP

Use consistent function naming and parameter ordering.

These examples have been focused on a function or method level, but of course consistencyis important at a class level, too. Classes that have similar roles should offer a similar interface.

432.4 Easy to use

2.4.3 ConsistentA good API should apply a consistent design approach so that its conventions are easy to remember,and therefore easy to adopt (Blanchette, 2008). This applies to all aspects of API design, such asnaming conventions, parameter order, the use of standard patterns, memory model semantics, theuse of exceptions, error handling, and so on.

In terms of the first of these, consistent naming conventions imply reuse of the same words for thesame concepts across the API. For example, if you have decided to use the verb pairs Begin and End,you should not mingle the terms Start and Finish. As another example, the Qt3 API mixes the use ofabbreviations in several of its method names, such as prevValue() and previousSibling(). This isanother example of why the use of abbreviations should be avoided at all costs.

The use of consistent method signatures is an equally critical design quality. If you have severalmethods that accept similar argument lists, you should endeavor to keep a consistent number andorder for those arguments. To give a counterexample, I refer you to the following functions fromthe standard C library:

void bcopy(const void *s1, void *s2, size t n);char *strncpy(char *restrict s1, const char *restrict s2, size t n);

Both of these functions involve copying n bytes of data from one area of memory to another.However, the bcopy() function copies data from s1 into s2, whereas strncpy() copies from s2 intos1. This can give rise to subtle memory errors if a developer were to decide to switch usage betweenthe two functions without a close reading of the respective man pages. To be sure, there is a clue tothe conflicting specifications in the function signatures: note the use of the const pointer in each case.However, this could be missed easily and won’t be caught by a compiler if the source pointer is notdeclared to be const.

Note also the inconsistent use of the words “copy” and “cpy.”Let’s take another example from the standard C library. The familiar malloc() function is used to

allocate a contiguous block of memory, and the calloc() function performs the same operation withthe addition that it initializes the reserved memory with zero bytes. However, despite their similarpurpose, they have different function signatures:

void *calloc(size t count, size t size);void *malloc(size t size);

The malloc() function accepts a size in terms of bytes, whereas calloc() allocates (count * size)bytes. In addition to being inconsistent, this violates the principle of least surprise. As a furtherexample, the read() and write() standard C functions accept a file descriptor as their first para-meter, whereas the fgets() and fputs() functions require the file descriptor to be specified last(Henning, 2009).

TIP

Use consistent function naming and parameter ordering.

These examples have been focused on a function or method level, but of course consistencyis important at a class level, too. Classes that have similar roles should offer a similar interface.

432.4 Easy to use

Page 23: C++ api design 품질

클래스 수준의 일관성

• 비슷한 기능을 제공하는 클래스들은 비슷한 인터페이스를 제공

• ex) STL, std::vector, std::set, std::map의 size 함수

• 다형성을 적용하면 일관성 얻기가 용이

• 공통 베이스 클래스를 둘 수 없는 경우에도 각 클래스에 공통된 개념들을 같은 이름으로 표현: 정적 다형성

• C++ 템플릿을 사용한 일관성 유지

• ex) Coord2D<int>, Coord2D<float> ..

The STL is a great example of this. The std::vector, std::set, std::map, and even std::string

classes all offer a size() method to return the number of elements in the container. Because theyalso all support the use of iterators, once you know how to iterate through a std::set you canapply the same knowledge to a std::map. This makes it easier to memorize the programming patternsof the API.

You get this kind of consistency for free through polymorphism: by placing the shared func-tionality into a common base class. However, often it doesn’t make sense for all your classes toinherit from a common base class, and you shouldn’t introduce a base class purely for this purpose,as it increases the complexity and class count for your interface. Indeed, it’s noteworthy that theSTL container classes do not inherit from a common base class. Instead, you should explicitlydesign for this by manually identifying the common concepts across your classes and using thesame conventions to represent these concepts in each class. This is often referred to as staticpolymorphism.

You can also make use of C++ templates to help you define and apply this kind of consistency.For example, you could create a template for a 2D coordinate class and specialize it for integers,floats, and doubles. In this way you are assured that each type of coordinate offers exactly the sameinterface. The following code sample offers a simple example of this:

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 x; }void SetY(T y) { mY 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>, Coord2D<float>, andCoord2D<double> and all of these will have exactly the same interface.

A further aspect of consistency is the use of familiar patterns and standard platform idioms. Whenyou buy a new car, you don’t have to relearn how to drive. The concept of using brake and acce-lerator pedals, a steering wheel, and a gear stick (be it manual or automatic) is universal the worldover. If you can drive one car, it’s very likely that you can drive a similar one, even though thetwo cars may be different makes, models, or have the steering wheel on different sides.

44 CHAPTER 2 Qualities

Page 24: C++ api design 품질

수직적인

• 다른 코드에 영향을 미치지 않는 함수

• 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; }

Page 25: C++ api design 품질

견고한 자원 할당

• C++의 메모리 관련 이슈

• Null 역참조

• 메모리 이중 해제

• 할당자 혼용

• 잘못된 배열 해제

• 메모리 누수

• 관리되는 포인터를 사용

• 공유 포인터: boost::shared_ptr, …

• 약한 포인터: boost::weak_ptr, …

• 범위 한정 포인터: boost::scoped_ptr, …

Page 26: C++ api design 품질

플랫폼 독립적

• 잘 설계된 API라면 특정 플랫폼에 독립적인 #if/#ifdef 코드를 public 헤더에 사용하지 않아야 함2.4.6 Platform IndependentA well-designed C++ API should always avoid platform-specific #if/#ifdef lines in its public head-ers. If your API presents a high-level and logical model for your problem domain, as it should, thereare very few cases where the API should be different for different platforms. About the only caseswhere this may be appropriate are when you are writing an API to interface with a platform-specificresource, such as a routine that draws in a window and requires the appropriate window handle to bepassed in for the native operating system. Barring these kinds of situations, you should never writepublic header files with platform-specific #ifdef lines.

For example, let’s take the case of an API that encapsulates the functionality offered by a mobilephone. Some mobile phones offer built-in GPS devices that can deliver the geographic location ofthe phone, but not all devices offer this capability. However, you should never expose this situationdirectly through your API, such as in the following example:

class MobilePhone{public:

bool StartCall(const std::string &number);bool EndCall();

#if defined TARGET OS IPHONEbool GetGPSLocation(double &lat, double &lon);

#endif};

This poor design creates a different API on different platforms. Doing so forces the clients ofyour API to introduce the same platform specificity into their own applications. For example, inthe 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-fined symbol error on other platforms.

Furthermore, if in a later version of the API you also add support for another device class, sayWindows Mobile, then you would have to update the #if line in your public header to includeWIN32 WCE. Then, your clients would have to find all instances in their code where they haveembedded the TARGET OS IPHONE define and extend it to also include WIN32 WCE. This is becauseyou have unwittingly exposed the implementation details of your API.

Instead, you should hide the fact that the function only works on certain platforms and provide amethod to determine whether the implementation offers the desired capabilities on the current plat-form. For example,

class MobilePhone{public:

bool StartCall(const std::string &number);bool EndCall();bool HasGPS() const;bool GetGPSLocation(double &lat, double &lon);

};

512.4 Easy to use

2.4.6 Platform IndependentA well-designed C++ API should always avoid platform-specific #if/#ifdef lines in its public head-ers. If your API presents a high-level and logical model for your problem domain, as it should, thereare very few cases where the API should be different for different platforms. About the only caseswhere this may be appropriate are when you are writing an API to interface with a platform-specificresource, such as a routine that draws in a window and requires the appropriate window handle to bepassed in for the native operating system. Barring these kinds of situations, you should never writepublic header files with platform-specific #ifdef lines.

For example, let’s take the case of an API that encapsulates the functionality offered by a mobilephone. Some mobile phones offer built-in GPS devices that can deliver the geographic location ofthe phone, but not all devices offer this capability. However, you should never expose this situationdirectly through your API, such as in the following example:

class MobilePhone{public:

bool StartCall(const std::string &number);bool EndCall();

#if defined TARGET OS IPHONEbool GetGPSLocation(double &lat, double &lon);

#endif};

This poor design creates a different API on different platforms. Doing so forces the clients ofyour API to introduce the same platform specificity into their own applications. For example, inthe 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-fined symbol error on other platforms.

Furthermore, if in a later version of the API you also add support for another device class, sayWindows Mobile, then you would have to update the #if line in your public header to includeWIN32 WCE. Then, your clients would have to find all instances in their code where they haveembedded the TARGET OS IPHONE define and extend it to also include WIN32 WCE. This is becauseyou have unwittingly exposed the implementation details of your API.

Instead, you should hide the fact that the function only works on certain platforms and provide amethod to determine whether the implementation offers the desired capabilities on the current plat-form. For example,

class MobilePhone{public:

bool StartCall(const std::string &number);bool EndCall();bool HasGPS() const;bool GetGPSLocation(double &lat, double &lon);

};

512.4 Easy to use

Now your API is consistent over all platforms and does not expose the details of which platforms sup-port GPS coordinates. The client can now write code to check whether the current device supports aGPS device, by calling HasGPS(), and if so they can call the GetGPSLocation() method to returnthe actual coordinate. The implementation of the HasGPS() method might look something like

bool MobilePhone::HasGPS() const{#if defined TARGET OS IPHONE

return true;#else

return false;#endif}

This is far superior to the original design because the platform-specific #if statement is now hiddenin the .cpp file instead of being exposed in the header file.

TIP

Never put platform specific #if or #ifdef statements into your public APIs. It exposes implementation details andmakes your API appear different on different platforms.

2.5 LOOSELY COUPLEDIn 1974, Wayne Stevens, Glenford Myers, and Larry Constantine published their seminal paper onstructured software design. This paper introduced the two interrelated concepts of coupling and cohe-sion (Stevens et al., 1974), which I define as

• Coupling. A measure of the strength of interconnection between software components, that is,the degree to which each component depends on other components in the system.

• Cohesion. A measure of how coherent or strongly related the various functions of a singlesoftware component are.

Good software designs tend to correlate with low (or loose) coupling and high cohesion, that is, adesign that minimizes the functional relatedness and connectedness between different components.Achieving this goal allows components to be used, understood, and maintained independently ofeach other.

TIP

Good APIs exhibit loose coupling and high cohesion.

Steve McConnell presents a particularly effective analogy for loose coupling. Model railroad carsuse simple hook or knuckle couplers to connect cars together. These allow easy linking of traincars normally by just pushing two cars together with a single point of connection. This is

52 CHAPTER 2 Qualities

Page 27: C++ api design 품질

느슨한 연결 좋은 API는 느슨한 연결과 높은 결합성을 보인다

Page 28: C++ api design 품질

이름만을 사용한 연결

• 클래스 전체 선언을 참조할 필요가 없다면 전방 선언을 사용

class MyObject; // only need to know the name of MyObject !class MyObjectHolder { public: MyObjectHolder(); void SetObject(MyObject *obj); MyObject *GetObject() const; private: MyObject *mObj; };

Page 29: C++ api design 품질

클래스 연결 줄이기

• 연결 관계를 줄이기 위해 멤버 함수 대신 비멤버, 비프렌드 함수 사용

// 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);

Page 30: C++ api design 품질

의도적인 중복

• 심각한 연결 관계를 잘라내기 위해 적은 양의 중복코드를 추가하는 것이 효과적일 경우

#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에 대한 연결 관계가 없어짐

Page 31: C++ api design 품질

매니저 클래스

• 하위 수준에 여러개의 클래스를 포함하면서 중재하는 역할을 수행

• 여러개의 클래스들을 대상으로 하나 혹의 그 이상의 의존성을 줄임

2.5.4 Manager ClassesA manager class is one that owns and coordinates several lower-level classes. This can be used tobreak 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 themaround a canvas. The program supports several kinds of input devices to let users select and moveobjects, such as a mouse, tablet, and joystick. A naive design would require both select and moveoperations 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 specificinput device classes. In this way, the SelectObject and MoveObject classes only need to dependon this single manager class, and then only the manager class needs to depend on the individual inputdevice 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 differentinterface. Our manager class could therefore put in place a generic input device interface thatabstracts away the specifics of a particular device. The improved, more loosely coupled, design isshown in Figure 2.6.

Note that this design also scales well too. This is because more input devices can be added to thesystem without introducing any further dependencies for SelectObject or MoveObject. Also, if youdecided to add additional manipulation objects, such as RotateObject and ScaleObject, they onlyneed a single dependency on InputManager instead of each introducing further coupling to theunderlying device classes.

SelectObject

+ DoSelect() : void

MoveObject

+ DoMove() : void

MouseInput

+ GetXCoord() : integer+ GetYCoord() : integer+ IsLMBDown() : boolean+ IsMMBDown() : boolean+ IsRMBDown() : boolean

TabletInput JoystickInput

+ GetXCoord() : integer+ GetYCoord() : integer+ IsPenDown() : boolean

+ GetXCoord() : integer+ GetYCoord() : integer+ IsTriggerDown() : boolean

FIGURE 2.5

Multiple high-level classes each coupled to several low-level classes.

58 CHAPTER 2 Qualities

TIP

Manager classes can reduce coupling by encapsulating several lower level classes.

2.5.5 Callbacks, Observers, and NotificationsThe final technique that I’ll present to reduce coupling within an API relates to the problem of noti-fying other classes when some event occurs.

Imagine an online 3D multiplayer game that allows multiple users to play against each other.Internally, each player may be represented with a unique identifier, or UUID, such as e5b43bba-fbf2-4f91-ac71-4f2a12d04847. However, users want to see the names of other players, not

SelectObject

+ DoSelect() : void

MoveObject

+ DoMove() : void

MouseInput

+ GetXCoord() : integer+ GetYCoord() : integer+ IsLMBDown() : boolean+ IsMMBDown() : boolean+ IsRMBDown() : boolean

InputManager

+ GetXCoord() : integer+ GetYCoord() : integer+ IsButton1Down() : boolean+ IsButton2Down() : boolean+ IsButton3Down() : boolean

TabletInput JoystickInput

+ GetXCoord() : integer+ GetYCoord() : integer+ IsPenDown() : boolean

+ GetXCoord() : integer+ GetYCoord() : integer+ IsTriggerDown() : boolean

FIGURE 2.6

Using a manager class to reduce coupling to lower-level classes.

592.5 Loosely coupled

Page 32: C++ api design 품질

콜백과 옵저버, 알림

• 이벤트가 발생했을때 이를 다른 클래스에 알리는 API

• 일반적인 이슈

• 재진입성

• 알림을 받은 코드가 다시 API를 호출하는 경우를 고려

• 수명 관리

• 이벤트에 대한 구독/해지 기능 제공, 또한 중복 이벤트를 받지 않도록 고려

• 이벤트 순서

• 이벤트의 순서를 명확히 정의 (네이밍 등의 방법으로 명시)

Page 33: C++ api design 품질

콜백

• 저수준 코드가 고수준의 코드를 사용할때 의존성을 없애는 방법

• 대규모 프로젝트에서 순환 의존 고리를 제거

!

!

!

!

• 객체지향 프로그램에서 인스턴스 메소드를 콜백으로 사용하는 경우 this 객체 포인터를 전달해야 함

#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); }

Page 34: C++ api design 품질

옵저버

• 콜백은 객체지향에서는 좋은 해결책이 되지 못함

• this 이슈

• 좀 더 객체지향적인 해결책 옵저버

• 3장과 4장에서 …

Page 35: C++ api design 품질

알림

• 콜백과 옵저버는 특정 작업을 위해 생성

• 알림

• 시스템에서 연결되지 않은 부분 사이에서 알림이나 이벤트를 보내는 메커니즘을 중앙화

• 보내는 이와 받는 이 사이의 의존성이 전혀 없음

• 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();

Page 36: C++ api design 품질

안정화와 문서화, 테스트 !

좋은 API라면

- 안정적이어야 하고, 미래 증명적이어야 함.

- 명확히 이해할 수 있게끔 구체적인 문서를 제공해야 함

- 변경이 발생하더라도 기존 기능에 영향이 가지 않도록 자동화된 테스트 프로세스를 갖추어야 함

Page 37: C++ api design 품질

결론 가능하다면 고품질 API 설계를 가능케하는 기준들을 도입하고, 품질을 낮추는 많은 요인들은 반드시 피해라

Page 38: C++ api design 품질

But, 아무리 잘 정리된 규칙도 모든 상황에 적용 불가 각 프로젝트 특성을 고려한 다음 가장 적합한 해결책을 적용

Page 39: C++ api design 품질

References

• Martin Reddy (2014). C++ API 디자인. (천호민, 옮김). 고양시: 지앤선. (원서출판 2013)