객체 지향 프로그래밍 (또는 개체 지향 프로그래밍이라고도 한다,
Object Oriented Programming인데 개체가 더 와닫는 말 같다.. )
객체 지향 프로그래밍을 설계할 때 따라야할 원칙들이 있다.
각 원칙의 한 글자씩 따서 SOLID라 한다.
1. Single Responsibility Principle (SRP) 단일 책임 원칙
2. Open-Closed Principle (OCP) 개방-폐쇄 원칙
3. Liskov Substitution Principle (LSP) 리스코프 치환 원칙
4. Interface Segregation Principle (LSP) 인터페이스 분리 원칙
5. Dependency Inversion Principle (DIP) 의존성 역전 원칙
OOP는 문제해결을 위해 문제를 작게 쪼개서 큰 문제를 해결하는 Bottom-up 방식이다.
개체들을 독립적이게 만들어서 재사용성을 높이고 효율적인 개발을 할 수 있게 한다.
시간은 금이라고, 친구!
개발 시간 = 돈이기에 효율적인 개발은 비용을 줄이게 해준다. (매우 중요)
Single Responsibility Principle : 단일 책임 원칙
클래스는 하나의 책임(목적)만 가져야 한다.
프린터라는 클래스가 있다고 하자.
프린터는 출력을 하는 기능말고 스캔이나 팩스, 변신(?) 등 다양한 기능이 있다면
코드가 길어지고 복잡해진다. 곧 유지 보수가 어려워지는 것이다.
#include<iostream>
using namespace std;
class Printer {
public:
void Print() {
// print...
};
};
// 좋지 않다.
class Printer2 {
public:
void Print() {
// print..
};
void Scan() {
// scan..
};
void Transform() {
// transform...
};
};
Open-Closed Principle : 개방-폐쇄 원칙
Class는 확장에 개방 되어 있어야 하고 수정에는 닫혀 있어야 한다.
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 설계하는 것이다.
프린터라는 클래스에서 문서를 출력하는 프린터와 사진을 출력하는 프린터가 파생된다고 하자
부모 클래스에서 각각 문서와 사진을 출력하는 기능이 구현 되어있다고 할 때
새롭게 3D 프린터를 만들고 싶다면 다시 부모 클래스를 수정해야할 것이다.
부모클래스에서는 기능에 대한 정의를 해주고 자식 클래스에서는 각자 자신들의 역할에 맡은 기능을 구현하면 된다.
#include<iostream>
using namespace std;
//==========================노노..==================================
class Printer {
public:
void PrintDocument();
void PrintImage();
void Print3D();
};
class DocPrinter : public Printer {
public:
void ScanDoc();
};
class ImagePrinter : public Printer {
public:
void ScanImage();
};
//===========================굳굳..=================================
class Printer {
public:
virtual void Print();
};
class DocPrinter : public Printer {
public:
void PrintDocument() override;
void ScaneDocument();
};
class ImagePrinter : public Printer {
public:
void PrintImage() override;
void ScanImage();
};
Liskov Substitution Principle : 리스코프 치환 원칙
하위 클래스는 상위 클래스의 자리를 대체할 수 있어야 한다.
프로그램에서 상위 클래스를 사용하는 부분을 하위 클래스로 대체해도 정상작동 해야한다는데 어떻게?
프린터의 출력이라는 기능을 정의하였다면 프린터를 상속받은 다른 형식의 프린터들은
문서를 프린트 하거나, 사진을 프린트 하거나 부모 클래스인 프린트의 기능.
무언가를 프린트 한다는 기능의 범위를 벗어나면 안된다.
프린터기가 무언가를 출력하는 기능을 가져야지 갑자기 트랜스포머가 되어버리면 출력하는 기능을 상실하는 것이다.
#include<iostream>
using namespace std;
class Printer {
public:
virtual void Print();
};
class DocPrinter : public Printer {
public:
void Print() override {
cout << "Print Documents... \n";
};
};
class TransformerPrinter : public DocPrinter {
public:
void Print() override {
cout << "I am Optimus Prime..\n";
};
};
void Printing(Printer& p) {
p.Print();
}
int main() {
DocPrinter myDocPrinter;
TransformerPrinter myTransformerPrinter;
Printing(myDocPrinter);
Printing(myTransformerPrinter);
return 0;
}
Interface Segregation Principle : 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
큰 인터페이스를 작고 구체적인 인터페이스로 분리한다.
*인터페이스란 OOP에서 클래스들이 구현해야하는 메서드들의 집합이다.
인터페이스에서 기능을 정의하고 하위 클래스에서 기능을 구현한다.
다음같은 특징이 있다.
- 인터페이스는 구현하지 않음 : 메서드의 이름, 매개변서, 반환 타입 등을 정의하지만
어떻게 부분은 구현하지 않는다. 인터페이스 자체를 개체화 할 수 없다. (구현부분이 없으니까!) - 계약 : 인터페이스는 상속받은 클래스가 따라야하는 계약을 정의한다. 뭔소리냐면, 인터페이스를
상속받으면 인터페이스에 정의된 모든 메서드를 구현하는 것을 강제한다. - 다형성 지원 : 인터페이스를 통해 다양한 클래스들이 동일한 인터페이스를 구현할 수 있다.
동물이라는 인터페이스를 통해 개, 고양이, 사람, 등등 다양한 개체를 다룰 수 있음. - 유연성 및 확장성 : 새로운 클래스는 기존 인터페이스를 구현함으로써 기존 코드를 수정하지 않고
기능을 확장할 수 있음.
작은 단위로 분리된 인터페이스는 수정하게 될 시 최소한의 영향을 받도록 하는게 목표이다.
단일 책임 원칙과 비슷한 느낌이긴하다.
프린트, 스캔, 팩스 기능이 있는 복합기가 있다고 할 때, 복합기는 SRP를 충족한다.
복합기가 해야하는 기능만 있기 때문이다.
만약 복합기를 상속받아서 프린터기, 스캐너, 팩스기를 만든다고 할 때
이는 ISP를 충족하지 않는다.
프린터기는 프린트 기능 외에 스캔이나 팩스 같은 사용하지 않는 기능들을 구현해야 하기 때문이다.
프린터가 프린트 기능만 구현하면 오류가 뜬다.
인터페이스의 계약으로 인해 인터페이스의 모든 것을 구현해야한다.
#include<iostream>
using namespace std;
class IPrint {
public:
virtual void Print() = 0;
virtual ~IPrint() = default;
};
class IScan {
public:
virtual void Scan() = 0;
virtual ~IScan() = default;
};
class IFax {
public:
virtual void Fax() = 0;
virtual ~IFax() = default;
};
class MultifunctionPrinter : public IPrint, public IScan, public IFax {
void Print() override {
cout << "Print... \n";
};
void Scan() override {
cout << "Scan... \n";
};
void Fax() override {
cout << "Fax... \n";
};
};
class NormalPrinter : public IPrint {
public:
void Print() override {
cout << "Print Documents... \n";
};
};
기능을 작게 쪼겐 인터페이스를 다중 상속 받아서 활용한다면 ISP와 SRP 모두 만족시킨다.
Dependency Inversion Principle : 의존성 역전 원칙
고수준 모듈이 저수준 모듈에 의존하지 않고 추상화된 인터페이스에 의존하도록 한다.
이 원칙의 목표는 코드의 유연성과 재사용성을 높이고 모듈 간의 결합도를 낮추는 것이다.
고수준 모듈은 결정이나 로직을 포함한 모듈이고
저수준 모듈은 기본적인 기능을 가진 모듈으로 고수준 모듈에서 호출하는 모듈이다.
추상화란 인터페이스 클래스 또는 추상 클래스로 구체적인 구현은 감추고, 모듈 간의 결합도를 낮춘다.
딱 와닿지 않는데
요리로 따지자면 고수준 모듈은 요리사이다.
요리사(고수준 모듈)는 어떤 방식의 요리를 만들지 알고 있으며 전체적인 요리의 흐름을 책임진다.
저수준 모듈은 보조 요리사들이다.
보조 요리사들(저수준 모듈)은 각각의 역할 분담이 되어있다.
면을 삶는 보조, 스테이크를 굽는 보조, 재료를 손질하는 보조 등
요리사가 요청하는 대로 맡은 역할을 수행한다.
만약 요리사가 보조 요리사에 의존하게 된다면 보조 요리사가 바뀔 경우 요리사도 그에 맞춰 바뀌어야 한다.
이를 결합도(coupling)가 높다고 한다. 각각의 모듈이 서로 강하게 의존하게 되면 하나의 모듈의 변화가
큰 영향을 미치게 되는 것이다.
#include <iostream>
class FryingPan {
public:
void fry() {
std::cout << "Frying with a frying pan." << std::endl;
}
};
class Chef {
public:
void cook() {
FryingPan pan;
pan.fry(); // 요리사가 직접 프라이팬에 의존
}
};
int main() {
Chef chef;
chef.cook();
return 0;
}
요리사가 특정 보조 요리사(FryingPan)을 가지고 호출하게 되면 보조 요리사가 변경될 때
요리사 코드도 수정해야한다.
#include <iostream>
using namespace std;
class ICooking{
public:
virtual void cook() = 0;
virtual ~ICooking() = default;
};
class FryingPan : public ICooking {
public:
void cook() override {
cout << "Frying... \n";
};
};
class BakingBread : public ICooking {
public:
void cook() override {
cout << "baking... \n";
};
};
class Chef {
private:
ICooking* cook;
public:
Chef(ICooking* cook) : cook(cook) {}
void changeCook(ICooking* newCook){
this->cook = newCook;
}
void makingDish()
{
cook->cook();
}
};
int main()
{
ICooking* cook1 = new FryingPan();
ICooking* cook2 = new BakingBread();
Chef myChef(cook1);
myChef.makingDish();
myChef.changeCook(cook2);
myChef.makingDish();
return 0;
}
보조 요리사를 추상화된 인터페이스로 만든다. (Class ICooking)
특정 역할을 하는 보조 요리사는 추상화된 인터페이스를 상속받아 구현한다. (Class FryingPan)
요리사는 추상화된 인터페이스(ICooking)에 의존하며 같이 일할 보조 요리사를 매개변수로 받아 생성된다. (Class Chef)
요리사는 어떤 보조 요리사인지 알 필요가 없다.
필요한 순간에 호출하기만 하면 된다.(의존도가 낮음, 결합도가 떨어짐, 독립적 모듈)
보조 요리사가 바뀌어도 요리사는 영향을 받지 않게 된다.
개체 지향 프로그래밍 설계의 원칙에 대해 정리해보았다.
막상 또 이렇게 정리하니 좀 더 구조적으로 코드를 짤 수 있을 거 같다.
'C++' 카테고리의 다른 글
#include <memory> Smart Pointer (0) | 2024.07.01 |
---|---|
Struct vs Class (0) | 2024.06.20 |
OOP - Object Oriented Programming 객체 지향 프로그래밍 (0) | 2024.06.03 |
std::sort() & Lamda (0) | 2024.05.14 |
C++ 부수기 - 1- Vector (0) | 2024.03.20 |