2019년 9월 15일 일요일

SOLID 원칙 - SRP, OCP, LSP

개발에 있어서 구조(Architecture)는 매우 중요하다.
수정에 있어서 용이하게 구현을 해놓는것이 곧 생산성과 직결되며 유능한 개발자로 보여질 수 있다.

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 method
void SetRectangle(Rectangle *obj, int width, int height) {
obj->set_width(width);
obj->set_height(height);
}

위와 같이 Rectangle의 하위 클래스를 Square로 상속받게하면 width와 height을 저장할 때 문제가 발생한다.
다른 Client에서 Rectangle의 원소값을 설정할 때 Square의 너비와 높이가 다르게 설정이 될 수 있다.