클래스는 응집력있고 잘정의된 긴으을 공유하는 데이터와 루틴의 모음이다.

 

추상데이터형(ADT) - 데이터와 데이터를 다루는 연산의 집합.

 

만약에 폰트를 제어한다면?

currentFont.size = 16  || currentFont.size = PointsToPixel(12) || currentFont.sizeInPixels = PointsToPixels(12) ... 크기를 제어하려고 해도 이렇게 부담스럽다

currentFont.bold = true. etc

ADT 사용시의 혜택

 

-세부적인 구현 사항을 감출수 있다.

-변경이 전체프로그램에 영향을 미치지 않는다 : 폰트에 보다 다양하고 많은 연산들 하고자 하면 한 곳에서 추가해주면 된다.

-인터페이스가 보다 많은 정보를 제공하도록 만들수 있다.:currentFont.size = 16과 같은 코드는 16이 픽셀단위인지 포인트단위인지 모호하다. 하지만 모아놓으면 의미                                                                              가 보다 명확해진다.

-성능을 향상시키기 쉽다 : 폰트의 성능을 향상시켜야 한다면, 프로그램 전체를 향상시키기보다 잘 정의된 루틴을 재작성할수 있다

-외관상으로 프로그램이 정확하다는 것을 더 잘 알 것이다. currentFont.attribute = 0x02 식으로 쓴다면 값이 정확한지 입력이 된 것인지 동작이 제대로 된 것인지 판단하기 어렵다.

-프로그램이 보다 더 스스로를 설명하게 된다. : 현재 사용하는 변수나 이름 입력값에 대한 정의가 이루어져 있어 보다 의미가 명확해진다

-프로그램에 모든 데이터를 넘길 필요가 없다

-저수준 구현 구조체 대신 실세계의 개체들을 다룰수 있다

ADT사용의 예제

currentFont.SetSizeInPoints(sizeInPoints)

currentFont.SetSizeInPixels(sizeInPixels)

currentFont.SetBoldOn()

currentFont.SetBoldOff()

currentFont.SetItalicOn()

currentFont.SetItalicOff()

 

좋은 추상화

클래스 인터페이스에서 일관된 추상화수준을 표현한다.

 

클래스가 구현하고 있는 추상화가 무엇인지 확실하게 이해하도록 하라.

 

서로 반대되는 기능을 갖는 서비스 쌍(pair)을 제공하라. : 클래스를 설계할때 각 public 루틴들에 반대되는 기능이 필요한지를 검사한다. 불필요하게 반대되는 기능을 만들어서는 안되지만, 필요한지는 확인하도록 한다

 

 

관련없는 정보를 다른 클래스로 이동시켜라

 

가능하다면, 인터페이스를 의미론적이기보다는 프로그래밍적으로 만들어라.:루틴 A는 루틴B 전에 호출되어야 한다 혹은 데이타1은 루틴A에 입력되기 전에 초기화 되어야 한다는 류의 의미보다는 프로그래밍 언어로 표현하고자 해야 한다.

 

수정시 인터페이스의 추상화가 손상되는 것을 조심하라

 

인터페이스 추상화와 어긋나는 public 멤버를 추가하지 말라.

 

추상화와 응집도를 함께 고려해라.

 

좋은캡슐화

추상화는 세부적인 구현 사항을 무시할수 있는 모델을 제공함으로서 복잡성 관리에 도움을 준다. 반면에 캡슐화는 세부사항을 알고 싶어 할때에도 이를 차단해버리는 경향이 있다.추상화와 캡슐화는 둘다 있던지 없던지 간에 하나만 있어서는 의미가 없다.

 

클래스와 멤버에 대한 접근성을 최소화하라 : 인터페이스 추상화와 무결성을 가장 잘 유지할수 있는 가에 따라 제한자를 구별해서 사용한다. 만약 노출시키는 루틴이 추상화와 일관성이 있다면 노출시켜도 별 문제가 없다. 먄약 확신할수 없다면, 일반적으로 숨기는 것이 숨기지 않는 것보다 좋다.

 

멤버데이터를 public으로 노출시키지 마라. : 멤버변수(멤버데이터)는 노출시키지 말고 멤버변수를 제어하고자 하는 멤버변수는 노출시켜라.

 

내부의 세부적인 구현 사항들을 클래스의 인터페이스에 입력하지 않는다 : 세부구현 사항들을 노출하는 코드를 읽을때, 구현단서를 찾기 위해서 클래스 인터페이스의 private섹션은 뒤지는 충동을 억제할수 있어야 한다

 

클래스 사용자들을 추측하지 마라 : 클래스는 클래스 인터페이스에 수반되어 있는 계약대로 설계되고 구현되어야 한다. 인터페이스 문서에 적혀 있는 것 이외에는 인터페이스가 어떻게 사용될 것인지에 대해서 추측해서 안된다. (그래서 더욱 애러처리에 신경써줘야 한다)

 

friend 클래스를 피하라

 

루틴이 public 루틴만 사용한다고 해서 public 인터페이스에 놓지마라.: 누가 이 노출된 루틴에 접근을 시도할수 있다

 

코드를 작성할때의 편의성보다 읽을때의 편의성을 추구하라.

 

캡슐화의 의미론적인 위반을 매우.매우! 주의하라

 

지나치케 밀접한 결합을 주의한다. : 일반적으로 결합은 느슨할수록 좋다. 다음은 가이드라인이다.

  • 클래스와 멤버의 접근성을 최소화하라
  • 프렌드 클래스는 밀접하게 결합되기때문에 피하라
  • 파생클래스가 기본 클래와 느슨하게 연결되도록, 기본 클래의 데이터를 protected보다 private로 선언하라
  • 클래스의 public인터페이스에서 멤버데이터를 노출하지 마라
  • 캡슐화의 의미론적 위반을 경계하라
  • "데이테르의 법칙"을 준수하라

 

포함("has a"관계)

예를 들어 직원은 이름을 가지며(has a) 전화번호를 가지며(has a) 세금ID를 갖는다(has a)

 

상속("is a"관계)

상속은 한 클래스가 다른 클래스를 특수화한다는 개념이다. 상속의 목적은 두 개 이상의 파생클래스에서 공통적으로 사용되는 요소들을 지정하는 기본클래스를 정의하여, 보다 간단한 코드를 작성하기 위함이다. 공통적인 요소들은 루틴 인터페이스, 구현부, 데이터멤버, 또는 데이터형이 될 수 있다.

public 상속을 통해서 "is a"를 구현하라

상속을 설계하고 설명하라. 그렇지 않으면 상속을 하지마라

Liskov치환원리(LSP, Liskov Substitutuin Principle)를 따르라 Barbara Liskov는 파생클래스가 기본 클래스의 정확히 특수화된 버전이 아니라면("is a"), 기본클래스로부터 상속받아서는 안된다고 주장했다. 서브클래스는 사용자가 그 차이점을 알 필요 없이 기본 클래스의 인터페이스를 통해서 사용가능해야 한다.

상속받고 싶을때만 상속받도록 한다. : 어떤 것을 상속시키고 시키지 않을지를 결정하라

오버라이드가 불가능한 멤버루틴을 오버라이드하지 마라.

공통적으로 사용되는 인터페이스, 데이터, 행위(behaviour)를 상속 트리에서 가능한 한 가장 높은 곳으로 이동시켜라.

인스턴스가 하나뿐인 클래스를 의심하라 - 싱글톤은 예외상황이다

파생클래스가 하나뿐인 기본 클래스를 의심해라.

루틴을 오버라이드하고 파생된 루틴내부에서 아무것도 하지 않는 클래스들을 의심하라.

깊은 상속트리를 피하라. - 깊은 상속트리를 복잡성을 증가시키고, 오류율의 증가와도 연관이 있다. 중복된 코드를 피하고 복잡성을 최소화하기 위해서 상속을 사용하고 있는지 확인하라.

광범위한 형 검사보다 다형성을 택하라.: 항상 그렇지는 않지만, 때떄로 자주 반복되는 case문은 상속이 더 좋은 설계일수도 있다.

ex 다형성으로 대체되어야 할 것 같은 case문을 C++로 작성한 예제.

switch(shape.type)

{

 case Shape_Circle:

         shape.DrawCircle();

        break;

case Shape_Square:

       shape.DrawSquare();

       break;

...

}

이 예제에서의 shape.DrawCircle();,  shape.DrawSquare();는 shape.Draw()와 같은 단일 루틴으로 대체되어야 한다.

모든데이터를 protected가 아닌 private으로 만들어라. 만약 정말로 파생클래스에서 기본클래스의 특성을 접근해야 한다면, protected 접근 함수를 대신 제공해라.

다중상속 - 강력하나 위험하다. 다이아몬드 상속문제를 야기할수 있다.

 

멤버함수의 데이터.

클래스에 가능한 한 적은 수의 루틴을 유지하라.

여러분이 원하지 않는 멤버함수와 연산자들이 암시적으로 생성되지 않도록 하라

클래스에서 호출되는 루틴의 수를 최소화하라

다른 클래스에 대한 간접 루틴호출을 최소화하라. - 직접적인 연결은 매우 위험하다. account.ContactPerson().DaytimeContactInfo().PhoneNumber()와 같은 간접적인 연결은 훨씬 위험하다. 연구자들은 "데미테르의 법칙"을 고안했다. 이 규칙의 핵심은 객체A가 자신의 루틴은 어느 것이든 호출할수 있다. 만약 객체A가 객체 B를 인스턴스화했다면, 객체B의 루틴은 어느 것이든 호출할수 있다. 하지만 객체 B에 의해서 제공되는 객체들을 호출하지 않아야 한다. 앞에서 살펴본 Account예제라면, account.ContactPerson()은 문제 없지만, account.ContactPerson().DaytimeContactInfo()는 문제가 있다는 것이다.

일반적으로 클래스가 다른 클래스와 협력하는 정도를 최소화하라.

 

생성자

가능하면 모든 멤버데이터를 모든 생성자에서 초기화한다. - 모든 데이터 멤버들을 모든 생성자에서 초기화하는 것은 비싸지 않은 방어적인 프로그래밍 습관이다

private생성자를 사용하여 싱글톤 속성을 구현하라. - 오직 하나의 객체만 인스턴스화되는 것을 허용하려면 클래스의 모든 생성자를 숨기고 클래스의 단일 인스턴스에 접근하기 위한 static GetInstance()루틴을 제공하는 방법으로 구현할수있다.

ex. private 생성자로 싱글톤을 구현한 자바 예제

public class MaxId{

   private MaxId(){} //private 생성자

   public static MaxId GetInstance(){ return m_instance; } //단일 인스턴스에 대한 접근을 제공하는 public 루틴

    private static final MaxId m_instance = new MaxId();

}

다른 사항들이 증명될때까지 얕은 복사보다 깊은 복사를 택하라

 

클래스를 작성하는 이유.

실세계의 객체를 모델링하라

추상 객체들을 모델링하라

복잡성을 줄여라

복잡성을 고립시켜라

세부적인 정보를 숨겨라

변경의 영향을 제한하라

전역데이터를 숨겨라

매개변수의 전달을 간소화하라

중앙집중 관리하라

코드의 재사용성을 도와라 - 코드의 재사용성은 프로젝트가 반복되고 심화될수록 빛을 바란다. 다만, "재사용성을 위한 설계작업"을 하지 않아야 한다. 그래야 불필요한 복잡성 증가를 막을수 있고 진정한 캡슐화와 정보은닉이 쉬워진다.

프로그램군(family)를 위한 계획을 작성하라

연관된 기능을 패키지화하라

특정한 리팩토링을 수행하라

 

피해야할 클래스

만능(god)클래스를 생성하지 마라

관련이 없는 클래스를 제거해라

 

6장의 요점

  • 클래스의 인터페이스는 일관성이 있는 추상화를 제공해야 한다. 많은 문제들이 이 규칙을 어기기떄문에 발생한다
  • 클래스 인터페이스는 시스템 인터페이스나 설계 결정, 또는 세부적인 구현 사항들을 숨겨야 한다
  • 만약 "is a"관계를 모델링하고 있지 않는다면, 일반적으로 상속보다 포함을 선택해야 한다
  • 상속은 유용한 도구이지만 복잡성을 증가시키며, 이는 복잡성을 관리해야 하는 소프트웨어의 일차적인 기술적 의미에 반대된다
  • 클래스는 복잡성을 관리하기 위해서 여러분이 사용할수 있는 일차적인 도구이다. 그러한 목표를 달성할수 있도록 설계에 많은 주의를 기울여야 한다.

 

 

이 글은 스프링노트에서 작성되었습니다.

by 무위자연 2008. 1. 19. 17:07