컨테이너 및 포장

8703 단어
실제 프로젝트에서 용기가 매개 변수로 되어 서로 다른 대상 간에 전달되는 것을 자주 볼 수 있다.이렇게 하면 무슨 문제가 있습니까?

내중성이 부족하다


진일보한 토론을 하기 전에, 우리 먼저 아래의 두 표현식 사이에 어떤 차이가 있는지 봅시다.
int value; 
std::list values; 

흔히 얻는 답은 전자는 primitive의 데이터이고 후자는 하나의 대상이다.전자에 대해 당신은 기본적인 수치 연산만 실행할 수 있습니다.후자의 유형std::list은 하나의 유형으로 너는 그것을 호출할 수 있다. 예를 들어 다음과 같다.
values.push_back(5);

이 답안은 결코 무슨 잘못이 없다.그럼 다음 두 표현식의 차이는 어디에 있습니까?
Object object;
std::list objects;

이 문제에 대해 이전의 답안은 더 이상 유효하지 않다.이 예에서 둘 다 대상이기 때문이다.다른 점은 Object 대상에 대한 방법 호출은 구체적인 업무 대상에 대한 조작이다.후자는 용기 대상에 대한 조작이다.
이제 우리는 문제를 다음과 같이 바꾸었다. 이 두 예에서 앞뒤 두 표현식의 공통된 차이는 무엇입니까?
만약 네가 비교적 예민하다면 이미 답을 얻었을 것이다. 전자는 하나의 데이터 (대상) 를 대표하고, 후자는 하나의 데이터 (대상) 를 대표한다.
따라서 용기 자체는 하나의 대상이지만 더욱 본질적으로 하나의 데이터를 대표하고 이 데이터의 업무 논리를 둘러싸고 용기 대상 자체는 언급되지 않았다.
따라서 용기를 업무 대상에 봉하는 것이 아니라 용기에 직접 접근한다.이것은 추상적인 데이터 형식에 데이터를 봉인하는 것이 아니라 하나의 데이터를 직접 조작하는 것과 본질적으로 아무런 차이가 없다.그것들은 모두 데이터와 그것을 조작하는 행위를 함께 놓아야 한다는 고내집 원칙을 위반했다.

안정성이 결여되다


이제 다음 세 가지 표현식의 공통점은 무엇입니까?
Object objects[100];
std::vector objects;
std::list   objects;

답은 간단하다. 모두 여러 개의 Object 대상의 집합을 대표한다.그들 간의 실현 기술은 비록 다르지만 추상적인 차원에서 이 세 가지 실현 방식이 표현하고자 하는 개념은 결코 다르지 않다.
실현 기술은 구속의 변화에 따라 변화할 수 있지만 사용자의 추상적인 수요가 변화가 없으면 사용자의 코드는 구체적으로 기술 변화를 실현하는 영향을 받지 말아야 한다.
따라서 용기 대상에 사용자에게 직접 접근시키는 것은 고내집 원칙뿐만 아니라 안정적인 방향으로 의존한다는 원칙에도 어긋난다.

컨테이너 포장


상술한 토론을 바탕으로 우리는 다음과 같은 결론을 얻을 수 있다. 시스템에 하나의 집합 개념이 존재할 때 이 집합 개념을 포함하는 단일 개념이 무엇인지를 고려하고 이 단일 개념에 따라 집합을 봉인해야 한다.
예를 들어 한 반에는 많은 학생들이 포함되어 있다.나쁜 방법은:
typedef std::list SchoolClass; 

다른 대상에서 한 반의 평균 성적을 계산해야 할 때 다음과 같은 코드가 나온다.
struct Foo 
{ 
  void f(const SchoolClass& cls) 
  { 
    unsigned int averageScore = getAverageScoreOfClass(cls); 
    
    // ... 
  }
   
private: 
  unsigned int getAverageScoreOfClass(const SchoolClass& cls) 
  { 
    unsigned int totalScore = 0;    
    for( SchoolClass::const_iterator i=cls.begin(); i != cls.end(); ++i)
    {
      totalScore += (*i).getScore();
    }
    
    return totalScore/cls.size(); 
  } 
  // ... 
};

한 가지 합리적인 방법은 다음과 같다.
struct SchoolClass 
{ 
  unsigned int getAverageScore() const 
  { 
    // ... 
  } 
  // ... 
  
private:  
  std::list students; 
};
 
struct Foo 
{ 
  void f(const SchoolClass& cls) 
  { 
    unsigned int averageScore = cls.getAverageScore(); 
    // ... 
  } 
  // ... 
};

만약에 어느 날 디자이너가 정장수조를 사용하는 것이 더 좋은 선택이라고 생각한다면std::list 메모리 문제로 인한 불확실성 때문에) 모든 수정은 SchoolClass 내부에서 통제되고 Foo, 그리고 그 어떠한 다른 SchoolClass의 고객에게도 영향을 주지 않는다(국부화 영향).

다중 컨테이너


또한 실제 프로젝트에서는 다음과 같은 정의를 자주 볼 수 있습니다.
typedef std::map<:string std::map="" std::string=""> 
             > ConfigFile;

이것은 그래도 가벼운 편이다.사실 내가 겪은 프로젝트 중 3급, 심지어 4급 용기도 드물지 않다.
단일 용기에 비해 다중 용기가 가져오는 문제가 더 많다. 이렇게 복잡한 데이터 구조의 정의 자체가 매우 난해하고 그 처리 코드도 서로 얽혀 이해하기 어려울 뿐만 아니라 매우 취약하다. 그 중에서 어떤 단계의 용기도 변화가 발생하면 전체 데이터 구조의 처리 코드에 영향을 미친다.
예를 들어, 위의 데이터 구조는 다음과 같이 완전히 변경될 수 있습니다.
typedef std::map<:string std::list="" std::string=""> > 
             > ConfigFile;

다단계 용기에 대해 그 처리 방법과 단급 용기의 방법은 별반 다르지 않다. 각 용기를 모두 봉인하는 것이다.예를 들어 방금 이 예에서는 적어도 아래의 봉인과 유사하게 진행할 수 있다.
struct ConfigFile 
{ 
  // ... 
private:  
  std::map<:string configsection=""> sections; 
};
 
struct ConfigSection 
{ 
  // ... 
private:  
  std::map<:string std::string=""> items; 
}; 

의도 불명의 데이터 서브집합


하나의 데이터 집합이 하나의 클래스에 봉인된 후에 이 데이터 집합에 대한 수요는 매우 격렬하게 변화할 수 있다.예를 들어 고객 코드는 다양한 목적을 바탕으로 데이터 집합에서 하나의 데이터 서브집합을 필터하고 이 데이터 서브집합에 대해 자신이 필요로 하는 조작을 수행할 수 있다.
만약에 모든 고객의 의도를 데이터 집합이 있는 클래스에 쌓아서 실현한다면 이런 클래스는 매우 불안정하고 하느님 클래스를 만들기 쉽다.또한 고객 코드의 내집도를 낮출 수 있다.
이런 상황에서 데이터 집합 클래스는 조회 인터페이스를 제공하고 고객이 하나의 필터 조건을 사용자 정의한다. 데이터 집합 클래스는 고객이 사용자 정의한 필터 조건에 따라 고객이 필요로 하는 데이터 서브집합을 얻어 고객 코드가 데이터 서브집합에 대한 정의에 필요한 조작을 하는 것이 오히려 좋은 선택이다.
데이터 집합 클래스에 대해 말하자면, 이러한 데이터 서브집합의 의미는 명확하지 않다. 왜냐하면 고객이 그것의 용도를 알기 때문이다.따라서 이 데이터 서브집합을 봉인할 필요가 있다면 고객의 책임이기도 하다.만약에 고객이 데이터 서브집합을 의미가 명확한 클래스로 봉하고 이 클래스를 출력 매개 변수로 데이터 집합 클래스에 전달하면 데이터 집합 클래스가 이러한 데이터 서브집합 유형에 대한 의존을 초래할 뿐만 아니라 데이터 집합 클래스 인터페이스의 불안정을 초래할 수 있다.
그래서 디자이너들은 데이터 집합 클래스에 다음과 같은 인터페이스와 실현을 제공하는 것을 선택한다.
struct SchoolClass 
{ 
  void getStudentsByFilter 
    ( const Filter& filter //  :  
    , std::list& result //  :  
    ) const 
  { 
    for( SchoolClass::const_iterator i=cls.begin() ; i != cls.end(); ++i)
    {      
      if(filter.matches(*i))
      { 
        result.push_back(*i);
      }
    } 
  } 
  // ... 

private:  
  std::list students; 
};

이런 방법은 데이터 집합 유형의 인터페이스와 실현의 안정성을 거의 보장할 수 있다.'거의'라고 말하는 이유는 std::list 쌍방이 데이터를 교환하는 계약으로서 여전히 너무 구체적이기 때문이다.일단 어떤 원인으로 인해 변화가 발생하면 쌍방의 코드는 모두 영향을 받을 것이다.
그러나 우리는 std::list 구체적이지만 이를 업무 차원에서 봉인할 수 없다는 결론을 내렸다.우리는 마치 재주가 없는 처지에 빠진 것 같다.5 Why분석법은 우리가 왜 그런지 몇 가지 더 물어보면 더욱 안정적인 추상을 찾을 수 있다는 것을 알려준다.
고객이 데이터 서브집합을 받은 후에 반드시 자신의 의도가 있을 것이다. 만약에 인터페이스가 사용자 자신의 의도를 반영하고 데이터 서브집합이 이렇게 구체적인 세부 사항을 실현하는 것이 아니라면 데이터 서브집합은 쓸모없는 중간층이 될 것이다.
그럼 고객의 의도는 무엇입니까?몰라.그러나 우리는 추상적인 강력한 무기를 가지고 있기 때문에 고객의 확실한 의도는 우리에게 더 이상 중요하지 않다.
따라서 위 코드를 다음과 같이 수정할 수 있습니다.
struct Visitor 
{ 
  virtual void visit(const Student& student) = 0; 
  virtual ~Visitor {} 
};
 
struct SchoolClass 
{ 
  void visitStudentsByFilter 
     ( const Filter& filter  //  : 
     , Visitor&      visitor //  :  
     ) const 
  {
    for( SchoolClass::const_iterator i = cls.begin() ; i != cls.end(); ++i) 
    {
      if(filter.matches(*i)) 
      { 
        visitor.visit(*i);
      }
    } 
  } 
  // ... 
private:  
  std::list students; 
};

이러한 실현 방식은 우리가 더욱 직접적으로 고객의 의도를 만족시키는 데 도움을 준다.이는 쌍방의 코드를 더욱 안정적으로 할 뿐만 아니라 많은 장소에서 고객이 조회 결과를 저장할 필요가 없기 때문에 std::list와 같은 데이터 집합을 돌려서 성능을 향상시키고 메모리 관리의 부담을 낮출 수 있다.
예를 들어 한 고객이 합격한 모든 학생을 거르려고 하는데 단지 합격한 학생의 수를 통계하기 위해서이다. 그러면 Visitor를 다음과 같이 실현할 수 있다.
unsigned int Foo::getNumOfPassStudents(const SchoolClass& cls) const 
{ 
  struct PassStudentFilter : Filter 
  { 
    bool matches(const Student& student) const 
    {
      return student.isPass(); 
    }
  } filter; 

  struct PassStudentsCounter : Visitor 
  { 
    PassStudentsCounter() : numOfPassStudents(0) {} 
    
    void visit(const Student& student) { numOfPassStudents++; }
    
    unsigned int numOfPassStudents; 
  } counter; 

  cls.visitStudentsByFilter(filter, counter); 

  return counter.numOfPassStudents; 
} 

이를 통해 우리는 Filter는 불필요한 사실이다. 왜냐하면 고객이 Visitor에서 스스로 필터를 할 수 있기 때문이다.따라서 우리는 이전 데이터 집합 클래스의 실현을 간소화 버전의 방문자 모델로 수정할 것이다(여러 가지 유형의 요소가 없기 때문에 이중 발송이 필요하지 않다).이것은 더욱 통용되는 추상으로 이를 빌려 쌍방의 실현을 간소화할 수 있다.
struct SchoolClass 
{ 
  void accept(Visitor& visitor) const 
  { 
    for( SchoolClass::const_iterator i=cls.begin() ; i != cls.end(); ++i) 
    {
      visitor.visit(*i);
    } 
  } 
  // ... 
private:  
  std::list students; 
};

이전 고객 코드도 단순화됩니다.
unsigned intFoo::getNumOfPassStudents(const SchoolClass& cls) const 
{ 
  struct PassStudentsCounter : Visitor 
  { 
    PassStudentsCounter() : numOfPassStudents(0) {} 
    void visit(const Student& student)
    { 
      if(student.isPass()) numOfPassStudents++; 
    } 
    unsigned int numOfPassStudents; 
  } counter; 
  
  cls.accept(counter);

  return counter.numOfPassStudents; 
}

필터링 결과를 저장해야 하는 고객에게는 다음을 쉽게 수행할 수 있습니다.
struct Bar : private Visitor 
{ 
  void savePassedStudents(const SchoolClass& cls) 
  { 
    cls.accept(*this); 
  } 
  
  // ... 

private:  //   visit   

  void visit(const Student& student) 
  { 
    if(student.isPass()) passedStudents.push_back(student); 
  } 
  
private:
  //  ,  std::list,  
  std::vector passedStudents;
  // ... 
};

이 실현에 사유 상속을 사용했다.사용법에 대한 자세한 내용은 Virtues of Bastard를 참조하십시오.

총결산


본고는 용기에 직접적으로 노출되는 문제점과 어떻게 봉인하여 유지보수성을 높일 수 있는지 연구하고자 한다.봉인에 관해서는 《류와 봉인》을 참고하시오.

좋은 웹페이지 즐겨찾기