수정에 있어서 용이하게 구현을 해놓는것이 곧 생산성과 직결되며 유능한 개발자로 보여질 수 있다.
Robert C. Martin은 5가지 원칙(S.O.L.I.D)을 제시하였다.
해당 원칙은 디자인 패턴에 녹아있으며
가독성과 유지보수가 좋은 프로그램을 개발할 수 있게 도와준다.
1. Single Responsibility Principle (단일 책임 원칙)
- 각 클래스는 단 한가지의 책임을 부여받아 수정할 이유가 한가지여야 한다.
제목 하나에 내용이 들어있는 메모장과 메모장의 내용을 파일에 저장하는 기능을 개발한다고 하자.
class Journal {
public :
explicit Journal(const string &title) :title(title){}
~Journal() {}
string title;
vector<string> entries;
void add(const string& entry) {
entries.push_back(entry);
}
void save(const string& filename) {
ofstream ofs(filename);
for (auto& s : entries)
ofs << s << endl;
}
};
| cs |
위와 같이 add,save함수를 이용하여 구현할 수 있다.
하지만 Journal class의 목적은 메모 항목들을 기입/관리하는 것이지 디스크에 쓰는것은 아니다.
앞선 방법으로 구현하게 되면 차후 수정사항에 대해 고통스러운 상황에 처할 수 있다.
따라서 Journal class의 저장 기능을 새로운 class에서 수행하는것이 옳다.
class Journal {
public :
explicit Journal(const string &title) :title(title){}
~Journal() {}
string title;
vector<string> entries;
void add(const string& entry) {
entries.push_back(entry);
}
};
class PersistenceManager {
public:
static void save(const Journal &journal, const string& filename) {
ofstream ofs(filename, std::ofstream::out);
for (auto& s : journal.entries)
ofs << s << endl;
}
};
int main() {
Journal memo("design pattern");
memo.add("builder pattern");
memo.add("factory pattern");
PersistenceManager::save(memo, "Design Pattern.txt");
return 0;
}
| cs |
2. Open-Closed Principle (열림-닫힘 원칙)
- 확장에는 열려있지만 수정에는 닫혀있어야 한다.
단순히 파일에만 저장하는 방식이 아닌 서버에 저장하거나 다른 종류의 저장공간에 저장하는것을 구현한다고 하자.
class PersistenceManager {
public:
static void save_file(const Journal &journal, const string& filename) {
ofstream ofs(filename, std::ofstream::out);
for (auto& s : journal.entries)
ofs << s << endl;
}
static void save_server(const Journal& journal, const Server& server) {
// ...
}
static void save_blabla(const Journal& journal, ...) {
// ...
}
};
| cs |
위의 구현은 OCP를 만족하지 않고있다.
새로운 저장방식이 추가됨에 따라 해당 클래스를 수정해야하기 때문이다.
class SavingMethod {
public:
virtual void saving(const Journal &journal, const string &filename) = 0;
};
class FileSave : public SavingMethod{
public :
void saving(const Journal& journal, const string& filename) override{
ofstream ofs(filename, std::ofstream::out);
for (auto& s : journal.entries)
ofs << s << endl;
}
};
class ServerSave :public SavingMethod {
public :
void saving(const Journal& journal, const string& filename) override {
// ...
}
};
class PersistenceManager {
public:
static void save(const Journal &journal, const string& filename, SavingMethod &saving_method) {
saving_method.saving(journal, filename);
}
};
int main() {
Journal memo("design pattern");
memo.add("builder pattern");
memo.add("factory pattern");
SavingMethod *file_save = new FileSave;
SavingMethod *server_save = new ServerSave;
PersistenceManager::save(memo, "Design Pattern.txt", *file_save);
PersistenceManager::save(memo, "Design Pattern.jar", *server_save);
delete file_save;
delete server_save;
return 0;
}
| cs |
SavingMethod class를 이용해서 기존의 PersistenceManager의 save 기능은 유지하되(수정)
추가 저장 기능이 요구될때(확장) 인터페이스를 건드리지 않는 방식을 제시할 수 있다.
3. Liskov Substitution Principle (리스코프 치환 원칙)
- 객체는 프로그램의 정확성을 깨지 않으면서 하위 타입의 인터페이스 객체로 교체될 수 있어야한다.
상당히 많은 글에서 Rectangle과 Square를 예시로 리스코프 치환 원칙을 설명한다.
class Rectangle {public:Rectangle() = default;void set_width(int width) {this->width_ = width;}void set_height(int height) {this->height_ = height;}protected:int width_, height_;};class Square : public Rectangle {public:Square() = default;void set_width(int width) {this->width_ = width;this->height_ = width;}void set_height(int height) {this->height_ = height;this->width_ = height;}};// Client methodvoid SetRectangle(Rectangle *obj, int width, int height) {obj->set_width(width);obj->set_height(height);}
위와 같이 Rectangle의 하위 클래스를 Square로 상속받게하면 width와 height을 저장할 때 문제가 발생한다.
다른 Client에서 Rectangle의 원소값을 설정할 때 Square의 너비와 높이가 다르게 설정이 될 수 있다.