developer-ellen
인간 디버거의 로그 찍기
developer-ellen
전체 방문자
오늘
어제
  • 분류 전체보기 (217)
    • 회고록 (0)
    • 취뽀 및 커리어 여정의 Stack (2)
      • SSAFY 7기 (2)
    • 프로그래밍공부 (24)
      • c++ (0)
      • JAVA (3)
      • Spring (5)
      • design pattern (3)
      • BackDB (1)
      • Servlet&JSP (3)
      • Vue (4)
      • JPA (4)
      • Infra (1)
      • Linux (0)
    • AI (3)
      • papers (3)
      • trend (0)
    • 프로젝트진행 (0)
      • 데이터베이스 (0)
      • 서버개발 (0)
      • 인공지능 (0)
      • 하루정리 (0)
    • 포트폴리오 (0)
    • 알고리즘 (158)
      • 알고리즘문풀 (155)
      • 알고리즘공부 (3)
    • 통계공부 (15)
      • 시계열분석 (15)
      • 회귀분석 (0)
    • CS (14)
      • 컴퓨터네트워크 (4)
      • 운영체제 (8)
      • 데이터베이스 (2)
    • 주저리주저리 (0)
      • 필사 (0)
    • 취업관련정보 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 데이터분석
  • 삼성코테기출
  • 삼성코테자바꿀팁
  • 카카오코테
  • 삼성코테준비
  • 삼성코테구현문제추천
  • 삼성코테자바풀이
  • AR모형
  • Arima
  • 삼성코테파이썬
  • 삼성코테기출자바풀이
  • c++디자인패턴
  • 시계열
  • 삼성코테자바준비
  • 삼성코테파이썬준비
  • 코테파이썬
  • 통계분석
  • SW역량테스트파이썬
  • BOJ파이썬풀이
  • ARIMA모형
  • 삼성코테구현풀이
  • SW역량테스트파이썬풀이
  • 운영체제인터럽트
  • c++ 빌더 패턴
  • 카카오코테java풀이
  • 통계학
  • 백준파이썬풀이
  • 시계열분석
  • 삼성코테파이썬풀이
  • MA모형

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
developer-ellen

인간 디버거의 로그 찍기

프로그래밍공부/design pattern

[모던 c++의 디자인 패턴] 1장. SOLID 디자인 원칙

2024. 3. 4. 23:31

디자인 패턴

  • 2000년대 초 로버튼 마틴(Robert C. Martin)에 의해 소개됨
  • SOLID 디자인 패턴은, 우리가 앞으로 살펴볼 디자인 패턴에 전반적으로 녹아져 있음

 

SOLID 디자인 패턴

1) 단일 책임 원칙(Single Responsibility Principle, SRP)

  • 단일 책임 원칙에서 각 클래스는 단 한 가지의 책임을 부여받아, 수정할 이유가 단 한가지여야 한다.
  • 전지전능한 객체 (여러 기능을 담고 있는)는 SPR를 위배한다.
  • 기록을 위한 메모장 클래스가 있을 때, 이 클래스는 vector에 파라미터로 주어지는 값을 추가하는 함수가 존재한다.
  • 이때, 추가로 영구적인 파일을 저장하는 기능을 만든다고 할 때 디스크에 파일을 쓰는 기능 또한 메모장 클래스의 역할일까?
  • 작은 수정을 여러 클래스에 걸쳐서 해야 하나다면 아키텍처에 뭔가 문제가 있다는 징조이다.

 

2) 열림-닫힘 원칙 (Open-Closed Principle, OCP)

  • 열림-닫힘 원칙은 타입이 확장에는 열려 있지만, 수정에는 닫혀 있도록 강제하는 것을 뜻한다.
    • 따라서, 기존 코드의 수정없이 기능을 확장할 수 있어야 한다.
  • 색상과 크기로 구별되는 상품들이 존재하고, 이를 필터링하는 기능을 만들 때 코드는 다음과 같다.
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };

struct Product
{
    string name;
    Color color;
    Size size;
}

struct ProductFiler
{
    typedef vector<Product *> Items;
}

ProductFilter::Items ProductFilters::by_color(Items item, Color color)
{
    Item result;
    for (auto &i : items)
        if (i->color == color)
            result.push_back(i);
    return result;
}

 

  • 만약 색상과 크리를 모두 지정해서 필터링해야 하는 요구 상황이 생긴다면?
    • by_color_and_size와 같은 새로운 함수를 또 추가해서 구현할 수도 있다.
    • 하지만, 요구사항은 언제든 변경될 가능성이 있어서 기존의 코드 수정없이 필터링을 확장할 수 있는 방법이 필요하다.
      • 필터링 절차를 두 개의 부분으로 나눈다. (필터와 명세)
template <typename T> struct Specification
{
    virtual bool is_saisfied(T* item) = 0;
};

template <typename T> struct Filter
{
    virtual vector<T*> filter (
        vector<T*> items,
        Specification<T>& spec) = 0;
    )
};

struct BetterFilter : Filter<Product>
{
    vector<Product *> filter(
        vector<Product*> items,
        Specification<Product*>& spec) override
    {
        vector<Product *> result;
        for (auto &p : items)
            if (spec.is_saisfied(p))
                result.push_back(p);
        return result;
    }
};

 

  • 여기서, 크기와 색상을 동시에 피렅링 조건으로 하는 경우는 어떻게 만들 수 있을까?
    • 복합 명세를 만들면 된다. 여기서는 C++의 강력한 연산자 오버로딩을 활용해서 훨씬 더 단순하게 구현하겠다.
    • && 연산자를 이용하면 두 개 이상의 Specification<T> 객체를 대단히 쉽게 복합 명세로 엮을 수 있다.
template <typename T> struct AndSpecification : Specification<T>
{
    Specification<T>& first;
    Specification<T>& second;

    AndSpecification(Specification<T>& first, Specification<T>& second)
        : first(first), second(second) {}
    
    bool is_satis
};

template<typename T> struct Specification
{
    virtual bool is_satisfied(T* item) = 0;

    AndSpecification<T> operator &&(Specification && other)
    {
        return AndSpecification<T>(*this, other);
    }
};

SizeSpecification large(Size::Large);
ColorSpecification green(Color::Green);
AndSpecification<Product> green_and_large{ large, green };

auto big_green_things = ColorSpecification(Color::Green) && SizeSpecification(Size::Large);
for (auto& x : big_green_things)
    cout << x->name << " is large and green" << endl;

 

 

 

3) 리스코프(Liskov) 치환 원칙(Liskov Subsititution Principle, LSP)

  • 어떤 자식 객체에 접근할 때, 그 부모 객체의 인터페이스에 접근하더라도 아무 문제가 없어야 하는 원칙이다.
  • 예를 들어, 부모 객체인 Rectangle class(사각형)과 자식 객체인 Squre(정사각형)가 있다고 가정한다.
    • 사각형내에 넓이를 구하는 함수에서 get_width(), set_width(), get_height(), set_heigh()를 구현했다.
    • 그러나 정사각형 내 상속받은 함수인 set_width()와 set_height()는 width나 height를 set하면서, 동시에 height나 width도 동일하게 set하도록 구현했다.
    • 여기서, 이 객체를 그 부모인 Rectangle 객체로 접근해서 area(width*height)를 구하면, 의도치 않은 상황이 발생한다.
void process(Rectangle& r)
{
    int w = r.get_width();
    r.set_height(10);
    
    cout << "expected area = " << ( w*10 )
     << ", got " << r.area() << endl;
}


Square{5};
process(s); // 기대된 결과 = 50, 구해진 값 = 25

 

  • 여기서  해결책은, 애당초 서브 클래스를 만들지 않아야 한다. 
  • 서브 클래스를 만드는 대신 아래와 같이 Factory 클래스를 두어 직사각형과 정사각형을 따로따로 생성한다.
struct RectangleFactory
{
    static Rectangle create_rectangle(int w, int h);
    static Rectangle create_squre(int size);
}

 

4) 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

  • 인터페이스 분리 원칙이 의미하는 바는 필요에 따라 구현할 대상을 선별할 수 있도록 인터페이스를 별개로 두어야 한다는 것이다.
  • 한 덩어리의 복잡한 인터페이스를 목적에 따라 구분하여, 인터페이스 모든 항목에 대한 구현을 강제하지 않고 실제 필요한 인터페이스만 구현할 수 있도록 하는 것이다.
  • 예를 들어, 복합 기능 프린터를 구현할 떄 프린트, 스캔 팩스 기능이 합쳐져 있다.
struct IMachine
{
    virtual void print(vector<Document*> docs) = 0;
    virtual void fax(vector<Document*> docs) = 0;
    virtual void scan(vector<Document*> docs) = 0;
}
  • 여기서, 만약 프린트와 스캔 기능만 가진 프린터를 구현하고 싶을 땐, fax를 빈 함수로 구현해야 한다.
struct IPrinter
{
    virtual void print(vector<Document*> docs) = 0;
}

struct IScanner
{
    virtual void scan(vector<Document*> docs) = 0;
}

struct IMachine : IPrinter, IScanner
{
};

struct Machine : IMachine
{
    IPrinter& printer;
    IScanner& scanner;

    Machine(IPrinter& printer, IScanner& scnaner)
        : printer(printer),
          scanner(scanner)
    {
    }

    void printer(vector<Document*> docs) override {
        printer.print(docs);
    }

    void scan(vector<Document*> docs) override {
        scanner.scan(docs);
    }
};

 

 

5) 의존성 역전 원칙(Dependency Inversion Principle, DIP)

  • 상위 모듈이 하위 모듈에 종속성을 가져서는 안된다. 양쪽 모두 추상화에 의존해야 한다.
  • 추상화가 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
  • 오늘날, 의존성 역전 원칙을 구현하는 가장 인기 있는 방법은 종속성 주입 테크닉을 활용하는 것이다.
    • 종속성 주입은 Boost.DI와 같은 라이브러를 사용하면, 어떤 컴포넌트의 종속성 요건이 자동적으로 만족되게 한다는 의미이다.
  • 예를 들어, 자동차는 엔진과 로그 기능을 필요하다. 이때, 두 기능에 자동차가 의존성을 가진다.
#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Engine
{
    float volume = 5;
    int horse_power = 400;

    friend ostream& operator<< (ostream& os, const Engine& obj)
    {
        return os
            << "volume: " << obj.volume
            << " horse_power: " << obj.horse_power;
    }
};

struct ILogger
{
    virtual ~ILogger() {}
    virtual void Log(const string& s) = 0;
};

struct ConsoleLogger : ILogger {
    ConsoleLogger() {}

    void Log(const string& s) override
    {
        cout << "LOG: " << s << endl;
    }
};

struct Car
{
    unique_ptr<Engine> engine;
    shared_ptr<ILogger> logger;

    Car(unique_ptr<Engine> engine, 
        const shared_ptr<ILogger>& logger)
        : engine{move(engine)},
          logger{logger}
    {
        logger->Log("making a car");
    }

    friend ostream& operator<<(ostream& os, const Car& obj)
    {
        return os << "car with engine: " << *obj.engine;
    }
};

int main() {
    auto engine = make_unique<Engine>();
    auto logger = make_shared<ConsoleLogger>();

    Car car(move(engine), logger);

    cout << car << endl;

    return 0;
}
  • 여기서 "종속성 주입"인 Boost.DI를 이용하면, ILogger를 ConsoleLogger에 연결하는 bind를 정의한다.
    • 이 정의는 누구든 ILogger를 요청하면 ConsoleLogger를 전달하라"라는 의미이다.
  • 아래의 코드는 온전히 인스턴스화된 Car객체를 가리키는 shared_ptr<Car>를 만든다.
    • 사용할 ILogger 인스턴스의 타입을 바꿀 때, 즉 bind가 수행되는 부분만 수정하면 자동으로 ILogger를 사용하는 모든 곳에서 적용된다는 점이다.
    • 이것은 단 한줄만 수정하여 종속성이 있는 객체에 실제 동작하는 구현객체를 사용할 수 있고, 테스트용 더미 객체를 사용하게 바꿀 수도 있다.
auto injector = di::make_injector(
    di::bind(ILogger)().to<ConsoleLogger>()
);

// injection이 설정된 후, 아래와 같이 Car를 생성해서 이용할 수 있다.
auto car = injector.create<shared_ptr<Car>>();

 

 

모던 c++ 디자인 패턴 책을 보면서 공부한 내용을 정리했습니다.

'프로그래밍공부 > design pattern' 카테고리의 다른 글

[모던 c++의 디자인 패턴] 3장. 팩터리  (0) 2024.04.10
[모던 c++의 디자인 패턴] 2장. 빌더  (0) 2024.04.09
    '프로그래밍공부/design pattern' 카테고리의 다른 글
    • [모던 c++의 디자인 패턴] 3장. 팩터리
    • [모던 c++의 디자인 패턴] 2장. 빌더
    developer-ellen
    developer-ellen

    티스토리툴바