인터페이스란?
Java나 C#과 같은 언어들을 보면 Interface라는 강력한 개념이 존재한다.
인터페이스는 객체에 대한 명세서만 나타내고 구체적인 구현은 각 파생 객체에서 작업하도록 한다.
명세서에는 메서드, 프로퍼티, 이벤트와 같은 것들이 포함될 수 있다.
겉으로만 보면 별로 대단할 것 없어 보이는 개념이지만, 인터페이스를 잘 활용한다면 객체지향 개발에 있어서 신세계를 경험할 수 있다.
C++을 이용한 인터페이스 구현
C++은 언어 차원에서 인터페이스를 명시적으로 지원하지는 않는다. 하지만 C++ 자체에서 갖고 있는 개념인 추상 클래스와 순수 가상함수를 조합하여 인터페이스를 구현할 수 있다.
class IPrintable
{
public:
virtual void Print() = 0; // Pure virtual function
};
파생 클래스에서는 인터페이스의 구현을 작업해야한다.
class Document : public IPrintable
{
public:
virtual void Print() override
{
// Concrete implementation for Document's Print
// ...
}
};
사용방법은 일반적인 다형성이 활용된 클래스들과 동일하다.
int main()
{
IPrintable* printable = new Document();
printable->Print();
delete printable;
return 0;
}
C++ 인터페이스 활용 1 - 다형성에서의 기능 분업화
왜 인터페이스가 강력한지, C++ 인터페이스만이 갖는 장단점은 무엇이 있는지 알아보자.
여러 객체들을 설계하는 과정에서 특정 객체들에게만 특정 기능을 부여해야하는 상황을 상상해보자.
객체 A, B, C가 있는데 A, B만 데이터를 출력시켜주는 기능이 필요한 경우를 구현해야 한다면 어떻게 해야할까?
다음 예제를 보자.
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base(){}
};
class IPrintable
{
public:
virtual void Print() = 0;
};
class DerivedA : public Base, public IPrintable
{
public:
virtual void Print() override { cout << value << endl; }
public:
DerivedA(int value) : value(value){}
int value;
};
class DerivedB : public Base, public IPrintable
{
public:
virtual void Print() override { cout << value << endl; }
public:
DerivedB(string value) : value(value) {}
string value;
};
class DerivedC : public Base
{
};
void Print(Base* base)
{
IPrintable* printable = dynamic_cast<IPrintable*>(base);
if(printable != nullptr)
{
printable->Print();
}
}
int main()
{
Base* baseA = new DerivedA(4);
Base* baseB = new DerivedB("blabla");
Base* baseC = new DerivedC();
Print(baseA);
Print(baseB);
Print(baseC);
delete baseA;
delete baseB;
delete baseC;
return 0;
}
이처럼 IPrintable이라는 인터페이스를 이 기능을 필요로하는 DerivedA, DerivedB 객체에 다중상속시킴으로써 두 객체에만 기능을 추가시킬 수 있다. 참고로 인터페이스는 구현을 갖지 않으므로 C++의 다이아몬드 다중상속 문제로부터 안전하다.
C++ 인터페이스 활용 2 - 프로퍼티 구현방법
C#, Java에서는 변수에 대한 접근도 인터페이스로 명세할 수 있는데 C++에서는 어떻게 할 수 있을까?
이름 데이터를 갖는 객체들을 구분해야하는 상황을 생각해보자. 이 객체들에게 INamable이라는 인터페이스를 통해 이름 데이터를 get, set 할 수 있는 명세를 추가해야할 것이다.
다음 예제를 보자.
class Base
{
public:
virtual ~Base(){}
};
class INameable
{
public:
virtual string GetName() const = 0;
virtual void SetName(const string& name) = 0;
};
class DerivedC : public Base, public INameable
{
public:
virtual string GetName() const override { return name; }
virtual void SetName(const string& name) { this->name = name; }
private:
string name;
};
Getter와 Setter를 인터페이스에서 강제하면 파생 객체는 변수를 만들어서라도 그 구현을 마무리 지어야한다. 이렇게 프로퍼티에 대한 인터페이스도 C++ 문법으로 표현이 가능하다.
C++ 인터페이스 활용 3 - 명세 중복에 대한 유연함
인터페이스의 강력한 장점 중 하나는 파생 객체에서 인터페이스 다중 상속을 통해 명세가 중복되더라도 구현이 중복되지 않기 때문에 문제를 피할 수 있다는 점이다. 예제를 통해 확인해보자.
INamePrintable이라는 인터페이스는 name도 필요로 하고, Print 함수도 필요로 하다. 만약 이 인터페이스와 기존 IPrintable, INameable 인터페이스를 모두 상속한다면 어떻게 될까?
class Base
{
public:
virtual ~Base(){}
};
class IPrintable
{
public:
virtual void Print() = 0;
};
class INameable
{
public:
virtual string GetName() const = 0;
virtual void SetName(const string& name) = 0;
};
class INamePrintable
{
virtual string GetName() const = 0;
virtual void SetName(const string& name) = 0;
virtual void Print() = 0;
};
class DerivedD : public Base, public IPrintable, public INameable, public INamePrintable
{
public:
virtual void Print() override { cout << name << endl; }
virtual string GetName() const override { return name; }
virtual void SetName(const string& name) { this->name = name; }
private:
string name = "dd";
};
명세는 중복되었지만 구현은 중복되지 않기 때문에 문제없이 잘 동작한다. 물론 위와 같은 경우라면 INamePrintable이라는 인터페이스가 INameable, IPrintable 인터페이스를 상속받도록 하는게 더 좋은 방법이겠지만, 이 예제가 의미있는 이유는 인터페이스 설계 중 일부 명세가 겹칠 수 밖에 없는 상황이 발생하기 때문에, 이러한 상황에도 잘 동작한다는 것을 확인했기 때문이다.
C++ 인터페이스 활용 4 - 구현을 포함한 인터페이스
이제부터는 C++에서만 가능한 인터페이스 활용법이다. 여기서부터는 인터페이스라고 하기 어려울 수도 있는 예제들인데, 원래 인터페이스는 구현에 대한 정의를 포함하지 않지만, C++에서는 이게 가능하다. 그래서 사실상 메크로를 사용하지 않고도 Code Generation을 할 수 있다는 의미이다. 물론 이 경우 구현된 함수와 같은 시그니쳐의 함수가 다른 상속한 클래스 있을 때, 문제가 발생할 수 있다.
class Base
{
public:
virtual ~Base(){}
};
class IPrintable
{
public:
virtual void Print() = 0;
static void Print(const string& text) { cout << text << endl; }
static void Print(int value) { cout << value << endl; }
};
class DerivedA : public Base, public IPrintable
{
public:
virtual void Print() override { IPrintable::Print(value); }
public:
DerivedA(int value) : value(value){}
int value;
};
class DerivedB : public Base, public IPrintable
{
public:
virtual void Print() override { IPrintable::Print(value); }
public:
DerivedB(string value) : value(value) {}
string value;
};
'프로그래밍 > C++' 카테고리의 다른 글
[C++] Multithreaded Data Synchronization 총정리 (0) | 2023.09.24 |
---|---|
[C++] C++에서 Partial Class 구현 방법 (0) | 2023.09.23 |
[C++] Casting 총정리 (0) | 2023.09.23 |
[C++] 객체 복사 (0) | 2021.04.27 |
[C++] C++의 데이터 타입 종류 (0) | 2021.04.17 |