3. Class Design and Inheritance

20. Class Mechanics

이 클래스는 뭐가 잘못되었는가?

class Complex 
{
public:
  Complex( double real, double imaginary = 0 )
    : _real(real), _imaginary(imaginary)
  {
  }
  void operator+ ( Complex other )
  {
    _real = _real + other._real;
    _imaginary = _imaginary + other._imaginary;
  }
  void operator<<( ostream os )
  {
    os << "(" << _real << "," << _imaginary << ")";
  }
  Complex operator++()
  {
    ++_real;
    return *this;
  }
  Complex operator++( int )
  {
    Complex temp = *this;
    ++_real;
    return temp;
  }
private:
  double _real, _imaginary;
};

먼저 std::complex를 재사용하지 않았다. 코드를 직접 만드는 대신 재사용하라. 특히 표준 라이브러리 코드를 재사용하라. 더 빠르고, 쉽고, 안전하다.

일단 위의 코드는 생성자가 묵시적 변환을 한다. 묵시적 변환으로 생기는 숨겨진 임시 오브젝트를 주의하라. 이를 피하는 좋은 방법은 가능할 때마다 생성자를 explicit로 쓰고 변환 연산자를 쓰지 않는 것이다. 또한 operator+가 비효율적이다. 오브젝트는 값이 아니라 const&로 받아라. a = a op b 대신 a op=b로 쓰라. 더 깔끔하고 종종 더 효율적이다. 또한, operator+가 멤버 함수여선 안 된다. 연산자의 독립된 버전을 제공한다면, 그 연산자의 대입 버전을 제공하고, 독립된 버전을 대입된 버전으로 구현하라. op와 op=간 자연적인 관계를 보존하라. 표준은 operator= () [] ->가 멤버 함수이도록 요구한다. 또한, 클래스 특정 연산자인 operator new, new[], delete, delete[]는 static 멤버 함수이도록 요구한다. operator+가 오브젝트를 수정하는 것도 잘못되었다. operator<<가 멤버 함수여선 안 된다. operator<<나 operator>>는 스트림 참조를 반환하도록 하라. operator++, operator++(int)의 리턴 타입도 잘못되었다. 일관성을 위해, 후위 증가 연산자는 항상 전위 증가 연산자를 통해 구현하라. 보존된 이름을 쓰지 마라.

21. Overriding Virtual Functions

아래 코드는 어디가 잘못되었는가?

#include <iostream> 
#include <complex>
using namespace std;
class Base
{
public:
  virtual void f( int );
  virtual void f( double );
  virtual void g( int i = 10 );
};
void Base::f( int )
{
  cout << "Base::f(int)" << endl;
}
void Base::f( double )
{
  cout << "Base::f(double)" << endl;
}
void Base::g( int i )
{
  cout << i << endl;
}
class Derived: public Base
{
public:
  void f( complex<double> );
  void g( int i = 20 );
};
void Derived::f( complex<double> )
{
  cout << "Derived::f(complex)" << endl;
}
void Derived::g( int i )
{
  cout << "Derived::g() " << i << endl;
}
void main()
{
  Base    b;
  Derived d;
  Base*   pb = new Derived;
  b.f(1.0);
  d.f(1.0);
  pb->f(1.0);
  b.g();
  d.g();
  pb->g();
  delete pb;
}

void main()이 잘못되었다. delete pb;는 안전하지 않다. 기반 클래스의 소멸자는 virtual로 선언하라. Derived::f는 오버로드 역할을 하지 않는다. 함수에 상속된 함수로서 같은 이름을 제공하려 할 때는, 상속된 함수를 숨기고 싶지 않다면 using 선언문 스코프 내로 가져오자. Derived::g가 Base::g를 오버라이드 할 때 기본 매개변수를 바꾸면 안 된다. 오버라이드된 상속된 함수의 기본 매개변수를 바꾸지 말라.

22. Class Relationships-Part 1

아래의 객체지향 설계 실수는 무엇일까?

class BasicProtocol /* : possible base classes */ 
{
public:
  BasicProtocol();
  virtual ~BasicProtocol();
  bool BasicMsgA( /*...*/ );
  bool BasicMsgB( /*...*/ );
  bool BasicMsgC( /*...*/ );
};

class Protocol1 : public BasicProtocol
{
public:
  Protocol1();
  ~Protocol1();
  bool DoMsg1( /*...*/ );
  bool DoMsg2( /*...*/ );
  bool DoMsg3( /*...*/ );
  bool DoMsg4( /*...*/ );
};

class Protocol2 : public BasicProtocol
{
public:
  Protocol2();
  ~Protocol2();
  bool DoMsg1( /*...*/ );
  bool DoMsg2( /*...*/ );
  bool DoMsg3( /*...*/ );
  bool DoMsg4( /*...*/ );
  bool DoMsg5( /*...*/ );
};

리스코프의 IS-A 또는 WORKS-LIKE-A를 쓸 때가 아니면 public 상속을 쓰지 말라. 오버라이드된 모든 멤버 함수는 더 많은 것을 요구해선 안 되며 더 적은 것을 보장해선 안 된다. 기반 클래스의 코드를 재사용하기 위한 목적으로 public 상속을 하지 말라. public 상속은 재사용되기 위해 하는 것이다. is implemented in terms of을 모델링할 때는 항상 멤버십/포함 관계를 쓰라. private 상속은 상속이 꼭 필요할 때만 하라. protected 멤버에 접근하거나 가상 함수를 오버라이딩할 때. 코드 재사용을 위해 public 상속을 하지 말라.

23. Class Relationships-Part 2

아래의 디자인 패턴을 개선시킬 수 있는가?

//--------------------------------------------------- 
// File gta.h
//---------------------------------------------------
class GenericTableAlgorithm
{
public:
  GenericTableAlgorithm( const string& table );
  virtual ~GenericTableAlgorithm();

  // Process() returns true if and only if successful.
  // It does all the work: a) physically reads
  // the table's records, calling Filter() on each
  // to determine whether it should be included
  // in the rows to be processed; and b) when the
  // list of rows to operate upon is complete, calls
  // ProcessRow() for each such row.
  //
  bool Process();

private:
  // Filter() returns true if and only if the row should be
  // included in the ones to be processed. The
  // default action is to return true (to include
  // every row).
  //
  virtual bool Filter( const Record& );

  // ProcessRow() is called once per record that
  // was included for processing. This is where
  // the concrete class does its specialized work.
  // (Note: This means every row to be processed
  // will be read twice, but assume that that is
  // necessary and not an efficiency consideration.)
  //
  virtual bool ProcessRow( const PrimaryKey& ) =0;

  struct GenericTableAlgorithmImpl* pimpl_; // MYOB
};

class MyAlgorithm : public GenericTableAlgorithm 
{
  // ... override Filter() and ProcessRow() to
  //     implement a specific operation ...
};
int main()
{
  MyAlgorithm a( "Customer" );
  a.Process();
}

public 가상 함수를 피하라. 템플릿 메소드 패턴을 선호하라. 디자인 패턴을 알고 사용하라. 널리 쓰이는 클래스들에 대해서는 컴파일러 방화벽 관용구(pimpl 이디엄)을 선호해 세부 구현을 숨겨라. 상태와 멤버 함수를 담는 private 멤버에 대한 불투명 포인터를 사용하라. 응집을 선호하라. 코드 부분-각 모듈, 각 클래스, 각 함수-가 단일의 잘 정의된 책임을 다하도록 하라.

24. Uses and Abuses of Inheritance

다음 코드는 어떻게 개선시킬 수 있는가?

// Example 1 
//
template <class T>
class MyList
{
public:
  bool   Insert( const T&, size_t index );
  T      Access( size_t index ) const;
  size_t Size() const;
private:
  T*     buf_;
  size_t bufsize_;
};

// Example 1(a) 
//
template <class T>
class MySet1 : private MyList<T>
{
public:
  bool   Add( const T& ); // calls Insert()
  T      Get( size_t index ) const;
                          // calls Access()
  using MyList<T>::Size;
  //...
};

// Example 1(b)
//
template <class T>
class MySet2
{
public:
  bool   Add( const T& ); // calls impl_.Insert()
  T      Get( size_t index ) const;
                          // calls impl_.Access()
  size_t Size() const;    // calls impl_.Size();
  //...
private:
  MyList<T> impl_;
};

포함(컴포지션, 레이어링, HAS-A, 위임)을 상속에 비해 선호하라. IS-IMPLEMENTED-IN-TERMS-OF를 모델링할 때, 항상 상속이 아닌 포함으로 모델링하는 것을 선호하라. 리스코프의 IS-A 또는 WORKS-LIKE-A를 쓸 때가 아니면 public 상속을 쓰지 말라. 오버라이드된 모든 멤버 함수는 더 많은 것을 요구해선 안 되며 더 적은 것을 보장해선 안 된다. 기반 클래스의 코드를 재사용하기 위한 목적으로 public 상속을 하지 말라. public 상속은 재사용되기 위해 하는 것이다.

25. Object-Oriented Programming

C++은 객체지향 프로그래밍 언어일까? 그렇진 않다. 멀티패러다임 언어이다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중