Object-Orientation Design
객체지향 설계
- Modulization(모듈화)에 특화된 설계 방법이다.
* Object (객체)
- Problem Domain에서 어떤 것을 Abtraction 해놓은 개체이다.
- Class에서 정의된 대로 생성되는, 실제 메모리 공간을 차지하는 Instance이다.
- 객체의 구성요소는 아래와 같다.
- Data Member (Attribute, State, Property)
- 정보를 저장한다. - Member Function (Operation, Behavior, Method)
- 기능을 제공한다.
Concepts of Object-Orientation (객체지향의 4가지 주요 개념)
- Abstraction (추상화)
- Encapsulation (캡슐화)
- Inheritance (상속)
- Polymorphism (다형성)
Abstraction (추상화)
- 특정 관점에서 중요하거나 관심 있는 분야를 따로 추출해내는 것을 의미한다.
- 특정 예시들에서 공통된 특징들을 추출하여 일반화된 아이디어나 개념을 도출해내는 것을 의미한다.
- 각 Object에 대해 시스템에서 반드시 필요한 State와 Behavior를 추출하는 것을 의미한다.
- 유무형의 모든 것들이 객체로 추상화될 수 있다.
- 동일한 대상이라 하더라도, Perspective에 따라 Abstraction의 결과가 달라진다.
ex) 지도는 도로·건물 관점, 지형 관점에 따라 다른 결과로 표현된다.
ex) 학과의 관점에서는 학생의 학번, 이름, 성적, 수강과목 등이 중요한 데이터이겠지만,
동아리에서는 학생의 학번, 이름, 특기, 경력사항을 중요한 데이터로 볼 것이다.
Encapsulation (캡슐화)
- 객체지향에서 가장 핵심적인 개념으로, Polymorphism도 Class 차원의 Encapsulation을 구현한 구조이다.
- 객체의 외부 인터페이스에 집중하며, 내부의 구체적인 구현 내용은 숨긴다.
- 외부에서 객체 내부 데이터에 접근하는 것을 제한한다.
- Object, Component, Subsystem 간의 Interface(Public Function)를 정의하는데 필수적인 개념이다.
* Message-Passing
- Object 간의 Communication 수단이다.
- Object들은 다른 Object의 Internal Details를 몰라도(인터페이스만 알아도) 함수에 접근할 수 있다.
Principles of Encapsulation (캡슐화 원칙)
- 캡슐화가 독립적으로 이루어지기 위해서는 아래 원칙들이 준수되어야 한다.
- 최소화
- 외부에 필요한 기능들만 공개하고, 나머지 기능들은 모두 숨긴다.
- Public Function을 "최소화"한다. - 무변경
- 외부에 공개되는 기능은 향후 변경되지 않도록 일반화하여 정의한다.
(공개된 부분이 변경되면, 이와 연관된 모든 기능들도 수정되어야 하기 때문이다.)
- 유지되어야 하는 것(Public Function, Interface)과 변해도 되는 것(Internal Implementation 등)을 분리한다.
Advantages of Encapsulation (캡슐화의 장점)
- 캡슐화를 통해 얻을 수 있는 장점들은 아래와 같다.
- 아래 장점들은 곧 Polymorphism의 장점들과 같다.
- 쉽고 안전한 Reuse
- 내부의 세부적 구현 내용을 몰라도 재사용할 수 있다. - 낮은 Dependency
- Object 혹은 Component 단위로 Modulization 되어 있어 코드 간의 의존성이 낮다.
- 이 객체를 호출하는 다른 객체들의 코드를 수정해야 할 일이 적어진다. (= 낮은 의존성의 의미)
Inheritance (상속)
- 상속은 OOPL(Object-Oriented Programming Language)에서
Generalization(일반화)과 Specialization(특수화)을 구현하는 방법이다.
Approaches to Finding Inheritance Structure (상속 구조를 찾는 방법)
- Bottom-Up Approach
- 클래스들 사이의 유사성을 찾고, 유사성을 통합할 수 있는 Superclass를 정의한다.
- Top-Down Approach
- Superclass와 Subclass들이 이미 생성되어 있는 경우에, Subclass를 추가적으로 정의할 수 있다.
IS-A and HAS-A Relationship (URL)
[C++] Types of Relationship | 클래스간 관계의 유형 (IS-A, HAS-A 등)
- 다수의 클래스를 정의할 때, 공통되는 특성들만을 모아놓은 하나의 클래스를 설계하고, 그 그 클래스에 대한 "IS-A" 또는 "HAS-A" 관계를 밝혀서 나머지 클래스를 구현할 때, 상속을 통해 구현할
dad-rock.tistory.com
- Object들 간의 관계는 IS-A 혹은 HAS-A 관계 둘 말고는 없다.
class Sonata : public Car
{
private:
Engine eng;
...
};
* IS-A Relationship ("a kind of" Relationship) [이즈-어]
- \(x\) IS-A \(y\) \(\iff\) \(x\)는 Subclass, \(y\)는 Superclass인 관계를 의미하며
두 객체 사이의 Inheritance Relationship을 의미한다.
ex) house IS-A building
* HAS-A Relationship [헤즈-어]
- 자신의 객체 내에 다른 객체의 Instance를 포함하는 관계를 의미한다.
- \(x\) HAS-A \(y\) \(\iff\) \(x\)가 \(y\)의 Instance를 포함하는 관계를 의미한다.
ex) car HAS-A engine
Advantages of Inheritance (상속의 장점)
- 상속을 통해 얻을 수 있는 장점들은 아래와 같다.
- Superclass의 Reuse
- 공통된(중복된) Attribute와 Operation을 한데 묶어 재사용할 수 있게 한다. - Reuse 가능성에 대비한 Inheritance 구조 설계가 가능
Polymorphism (다형성) = Multiple-Shaping
"One Interface, Multiple Implementations"
- 즉, 하나의 메시지(One Interface)에 대해 서로 다른 객체가 서로 다른 방법으로 응답할 수 있게 하는 성질을 의미한다.
- Operation Overriding(Inheritance) 메커니즘과 Dynamic Binding(Late Binding) 메커니즘을 통해 구현된다.
- Superclass 하나만 외부에 공개되며, Subclass들은 Encapsulation된다.
- 메시지를 보내는 객체는 어느 객체가 받는지를 신경쓸 필요가 없으며,
메시지를 받는 객체는 각각의 경우에 어떻게 응답해야 하는지를 알고 있다.
Advantages of Polymorphism (다형성의 장점)
- 다형성을 통해 얻을 수 있는 장점들은 아래와 같다.
- Operation Overriding과 Dynamic Binding 메커니즘을 통해 구현된다.
- Operation Overriding
- Subclass가 Superclass 함수의 Prototype과 동일한 함수를 재정의하는 것을 의미한다. - Dynamic Binding
- 포인터 변수가 선언될 당시의 타입이 아닌, 가리키는 객체 타입을 기준으로
Overriding된 함수들 중 호출할 함수를 Runtime에 결정하는 것을 의미한다.
- C++에서는 \(\texttt{virtual}\) Keyword를 통해 이를 구현할 수 있다.
* Operation Overloading
- 함수의 이름은 같게 하되, Signature는 다른 형태의 함수를 정의할 수 있게 하는 기능을 의미한다.
- Overloading과 Overriding은 다른 개념이니 혼동하지 않게 유의해야 한다.
- Polymorphism의 장점은 곧 Encapsulation의 장점들과 같다.
- 쉽고 안전한 Reuse
- 내부의 세부적 구현 내용을 몰라도 재사용할 수 있다.
- 인터페이스(=Superclass)의 사용법만 숙지하고 있으면 된다. - 낮은 Dependency
- Object 혹은 Component 단위로 Modulization 되어 있어 코드 간의 의존성이 낮다.
- 이 객체를 호출하는 다른 객체들의 코드를 수정해야 할 일이 적어진다. (= 낮은 의존성의 의미)
Example. Poorly Structured Polymorphism (C++)
class Driver
{
private:
char driverName[10];
int driverAge;
public:
Driver(const char* driverName, int driverAge);
char* getDriverName() const;
int getDriverAge() const;
};
class TaxiDriver : public Driver // TaxiDriver IS-A Driver
{
private:
int baseSalary;
int bonusMoney;
public:
TaxiDriver(const char* driverName, int driverAge, ...);
int getSalary() const;
void showDriverInfo() const;
};
class BusDriver : public Driver // BusDriver IS-A Driver
{
private:
int workingHours;
int payPerHour;
public:
BusDriver(const char* driverName, int driverAge, ...);
int getSalary() const;
void showDriverInfo() const;
};
class DriverList
{
private:
// Subclass 각각의 객체(TaxiDriver, BusDriver)를 따로 관리해야 한다.
TaxiDriver* taxiDriverList[50]; // DriverList HAS-A TaxiDriver
BusDriver* busDriverList[50]; // DriverList HAS-A BusDriver
int numTaxiDrivers;
int numBusDrivers;
public:
DriverList() : numTaxiDrivers(0), numBusDrivers(0) {};
void addTaxiDriver(TaxiDriver* driver)
{ driverTaxiList[numTaxiDrivers++] = driver; }
void addBusDriver(BusDriver* driver)
{ driverBusList[numBusDriver++] = driver; }
void showAllDriversInfo() const
{
for (int i=0; i<numTaxiDriver; i++)
driverTaxiList[i]->showDriverInfo();
for (int i=0; i<numBusDrivers; i++)
driverBusList[i]->showDriverInfo();
}
void showTotalSalary() const
{
int sum=0;
for (int i=0; i<numTaxiDrivers; i++)
sum += driverTaxiList[i]->getSalary();
for (int i=0; i<numBusDrivers; i++)
sum += driverBusList[i]->getSalary();
cout << "Total Salary: " << sum << endl;
}
~DriverList();
{
for (int i=0; i<numTaxiDrivers; i++)
delete driverTaxiList[i];
for (int i=0; i<numBusDriver; i++)
delete driverBusList[i];
}
};
int main()
{
DriverList drivers;
TaxiDriver* pNewTaxiDriver = null_ptr;
BusDriver* pNewBusDriver = null_ptr;
pNewTaxiDriver = new TaxiDriver("Kim", 33, 300, 50);
drivers.addTaxiDriver(pNewTaxiDriver);
pNewBusDriver = new BusDriver("Park", 36, 40, 5);
drivers.addBusDriver(pNewBusDriver);
drivers.showAllDriverInfo();
drivers.showTotalSalary();
return EXIT_SUCCESS;
}
- 외부에서 클래스의 상속 구조를 알고 있어야하며,
상속 구조가 변경될 경우, 관련 부분을 수정해야될 정도로 의존성이 높다.
Example. Well-Structured Polymorphism (C++)
class Driver
{
private:
char driverName[10];
int driverAge;
public:
Driver(const char* driverName, int driverAge);
char* getDriverName() const;
int getDriverAge() const;
virtual int getSalary() const;
virtual void showDriverInfo() const;
};
class TaxiDriver : public Driver // TaxiDriver IS-A Driver
{
private:
int baseSalary;
int bonusMoney;
public:
TaxiDriver(const char* driverName, int driverAge, ...);
int getSalary() const;
void showDriverInfo() const;
virtual int getSalary() const;
virtual void showDriverInfo() const;
};
class BusDriver : public Driver // BusDriver IS-A Driver
{
private:
int workingHours;
int payPerHour;
public:
BusDriver(const char* driverName, int driverAge, ...);
int getSalary() const;
void showDriverInfo() const;
virtual int getSalary() const;
virtual void showDriverInfo() const;
};
class DriverList
{
private:
// Subclass의 구조를 알 필요 없이, Superclass의 Interface만 알고 있어도 사용이 가능하다.
Driver* driverList[50]; // DriverList HAS-A Driver
int numDrivers;
public:
DriverList() : numDrivers(0) {};
void addDriver(Driver* driver)
{ driverList[numDrivers++] = driver; }
void showAllDriversInfo() const
{
for (int i=0; i<numDriver; i++)
driverList[i]->showDriverInfo();
}
void showTotalSalary() const
{
int sum=0;
for (int i=0; i<numTaxiDrivers; i++)
sum += driverList[i]->getSalary();
cout << "Total Salary: " << sum << endl;
}
~DriverList();
{
for (int i=0; i<numDrivers; i++)
delete driverList[i];
}
};
int main()
{
DriverList drivers;
Driver* pNewDriver = null_ptr;
pNewTaxiDriver = new TaxiDriver("Kim", 33, 300, 50);
drivers.addTaxiDriver(pNewTaxiDriver);
pNewBusDriver = new BusDriver("Park", 36, 40, 5);
drivers.addTaxiDriver(pNewTaxiDriver);
// Superclass의 Interface는 수정하지 않았기 때문에,
// 외부 클래스(DriverList)에서는 기존 그대로 사용할 수 있다.
drivers.showAllDriverInfo();
drivers.showTotalSalary();
return EXIT_SUCCESS;
}
- Virtual Function 구조로 설계하여 Subclass의 구조에 변화가 생기거나, 새로운 Subclass가 추가되어도
외부에서는 이를 알아야 할 필요가 없이, 기존 그대로 Superclass의 Interface로 기능을 이용할 수 있다.
- 잘 설계된 Polymorphism 구조는 위 그림처럼 새로운 Subclass를 추가해도 Superclass를 수정할 필요가 없다.
- 위 그림처럼 새로운 상속구조를 발견하여 새로운 Subclass를 추가하는 것은 Top-Down Approach에 속한다.
Advantages of Object-Orientation (객체지향 기법의 장점)
- 아래는 객체지향 기법의 장점들이다.
- 개발 Cost가 절감된다.
- Reuse하기에 용이한 구조로 이루어져 있어 개발 비용을 줄일 수 있다. - Software Quality를 향상시킬 수 있다.
- Encapsulation(Polymorphism)을 통해 Modularity를 향상시켜 모듈 간 의존성을 낮출 수 있다.
(Sub-System간 Coupling이 낮아진다.)
- 확장하기에 용이하고, Troubleshooting과 Maintain하기 쉽다. - Analysis, Design, Code 사이의 의사소통을 용이하게 한다.
Abstraction Class (C++) vs Interface (Java)
* Pure Virtual Class (URL)
Abstraction Class (C++)
- 하나 이상의 Pure Virtual Function을 포함하고 있는,
객체 생성이 불가능한 Superclass의 일종이다.
Interface (Java)
- Pure Virtual Function으로 구성된 Superclass의 일종이다.
- 즉, Subclass가 일반적인 Class가 아닌, Interface를 상속받는 경우(Overriding하는 경우),
\(\texttt{extends}\) 키워드가 아니라 \(\texttt{implements}\) 키워드를 사용해야 한다.
Principles of Interface(API) Design (인터페이스 설계 원칙)
- 인터페이스 설계시 지켜져야될 원칙들은 캡슐화의 원칙과 같다.
- 최소화
- 외부에 필요한 서비스들만 공개하고, 나머지 기능들은 모두 숨긴다.
- Class 단위에서는 Public Function, Component 단위에서는 Interface(Superclass)가
외부에 필요한 서비스들에 해당된다. - 무변경
- 외부에 공개되는 기능은 향후 변경되지 않도록 일반화하여 정의한다.
(공개된 부분이 변경되면, 이와 연관된 모든 기능들도 수정되어야 하기 때문이다.)
- 유지되어야 하는 것(Public Function, Interface)과 변해도 되는 것(Internal Implementation 등)을 분리한다.
Example. Well-Structured Interface (Java)
public interface OperateCar
{
// Method Signatures
int turn(Direction direction, double radius, double startSpeed, double endSpeed);
int changeLanges(Direction direction, double startSpeed, double endSpeed);
int turnAudioOn(Channel channel, int volume);
int turnAudioOff();
...
};
public class OperateHyundaiSonata implements OperateCar
{
// Implementation for OperateCar
int turn(Direction direction, double radius, double startSpeed, double endSpeed)
{ ... }
int changeLanes(Direction direction, double startSpeed, double endSpeed)
{ ... }
// Implementations for other members
...
};
public class OperateKiaK5 implements OperateCar
{
// Implementation for OperateCar
int turn(Direction direction, double radius, double startSpeed, double endSpeed)
{ ... }
int changeLanes(Direction direction, double startSpeed, double endSpeed)
{ ... }
// Implementations for other members
...
};
Reference: Software Engineering 10th Edition
(Ian Sommerville 저, Pearson, 2016)