CODE COMPLETE 2nd Edition
목차
6장클래스다루기 7장 고급루틴
6장클래스다루기
20세기에는 루틴 관점에서 프로그래밍을 생각했으며, 21세기에는 클래스 관점에서 프로그래밍을 이해한다,
클래스는 연관성이 높고 잘 정의된 기능을 공유하는 데이터와 루티의 모음이다.
6.1 클래스의 토대 : 추상 데이터형(ADT)
ADT : 데이터와 테이터를 처리하는 연산의 집합이다.
ADT 가 필요한 예
여러가지 글꼴과 크기, 폰트특성을 사용해 화면에 출력하는 텍스트를 제어하는 프로그램을 작성한다고 가정하자.
ADT를 사용한다면 데이터(글꼴이름, 크기, 폰트특성) 와 이데이터를 다루는 일련의 폰트 루틴을 작성할 것이다.
ADT를 사용하지 않는다면 폰트를 다루기위해 currentFont.size = 16;
루틴을 미리 구축해 놓았다면 currentFont.size = PonitsToPixels(16);
ADT 사용할 때 좋은점
구현 세부 사항을 감출 수 있다. 변경이 전체에 영향을 미치지 않는다. 인터페이스가 더 많은 정보를 제공하도록 만들수 있다. 성능을 향상시키기 쉽다. 프로그램이 명백하게 정확해진다. 프로그램의 가독성이 높아진다. 전체 프로그램에 데이터를 넘길 필요가 없다. 저수준 구현 구조체 대신 현실 세계의 개체를 다룰 수 있다.
ADT 의 또다른 예제
몇가지 원칙 전형적인 저수준 테이터형을 저수준 데이터형이 아닌 ADT로 만들거나 사용하라. 파일과 같은 일반적인 객체를 ADT 로 취급하라, 각단한 객체도 ADT 로 취급하라. ADT 가 저장된 매체와 독립적으로 ADT를 참조하라.
비객체지향 프로그래밍 환경에서 ADT 로 여러개의 데이터 인스턴스 다루기
ADT에서 인터페이스를 다루는 방법 방법1 : ADT 서비스를 사용할 때마다 명시적으로 인스턴스를 식별한다. 방법2 : ADT 서비스에서 사용하는 데이터를 명시적으로 제공한다. 방법3 : 암시적인 인스턴스르 사용하라(각별한 주의가 요구된다)
ADT 와 클래스
추상 데이터형은 클래스의 기본 개념의 바탕을 이룬다. 추상 데이터형에 상속과 다형성을 더한 것을 클래스로 생각해도 된다.
6.2 좋은 클래스 인터페이스
고급 클래스를 만들기 위한 가장 중요한 단계는 좋은 인터페이스를 만드는 것이다.
좋은 추상화
추상화는 복잡한 연산을 단순한 형태로 보여주는 능력이다. 클래스 인터페이스가 일관된 추상화 수준을 갖도록 한다. 클래스가 구현하고 있는 추상화가 무엇인지 이해해야 한다. 서로 반대되는 기능을 갖는 서비스 쌍(pair)를 제공하라 관련이 없는 정보를 다른 클래스로 옮겨라. 가능하면 인터페이스를 의미론적이기보다는 프로그래밍적으로 만들라. 코드 변경시 인터페이스의 추상화가 망가지지 않도록 주의한다. 인터페이스 추상화에 맞지 않는 공개멤버를 추가하지 말라 추상화와 응집도를 함께 고려하라
좋은 캡슐화
캡슐화는 추상화보다 더 강력한 개념이다. 추상화는 구현 세부사항을 무시할 수 있는 모델을 제공함으로써 복잡성 관리에 도움을 준다. 캡슐화는 세부사항을 알고 싶어할 때 조다 이를 원천적으로 봉쇄해버리는 더 강력한 벙법이다.
캡슐화 없이는 추상화가 깨질수 있기 때문에 두개념은 연관되어 있다.
클래스와 멤버의 접근성을 최소화하라.
멤버데이터를 pulblic로 노출하지 말라.
내부 구현 세부사항을 클래스의 인터페이스에 입력하지 말라
클래스의 사용자를 가정하지 말라
프랜드 클래스를 피하라.
어떤 루틴이 공개루틴만 사용한다고 해서 public 인터페이스에 두지말라
코드를 작성할 때의 편의성보다 가독성이 높은 코드를 작성하라.
캡슐화의 의미론적인 위반을 각별히 주의하라.
다음의 얘는 클래스의 사용자가 의미론적 캡슐화를 망가뜨리는 몇가지 방법이다.
- 클래스 A의 PerformFirstOperation() 루틴이 InitializeOperations() 루틴을 자동으로 호출하는 것을 알고 있기때문에 클래스 A의 InitializeOperations() 루틴을 호출하지 않는다.
- employee.Retrieve() 함수가 데이터베이스에 연결되어 있지 않을 때 데이터베이스에 연결한다는 것을 알고 있기 때문에 employee.Retrieve(database) 함수를 호출하기 전에 database.Connect() 루틴을 호출하지 않는다.
- 클래스 A 의 PerformFirstOperation() 루틴이 이미 호출되었다는 것을 알고 있기 때문에 클래스 A의 Terminate()루틴을 호출하지 않는다.
- ObjectA가 ObjectB를 정적 공간에 보관해서 ObjectB 는 계속접근할 수 있다는 것을 알고 있기 때문에 ObjectA 에 의해서 생성된 ObjectB 에 대한 포인터나 참조를 ObjectA 가 영역을 벗어난 후에도 사용한다.
- 두 상수의값이 같다는 것을 알고 있기 때문에 ClassA.MAXIMUN_ELEMENTS 대신 클래스 B의 MAXIMUM_ELEMENTS 상수를 사용한다.
이 예시의 문제는 클라이언트 코드가 클래스의 pulbic 인터페이스에 의존하지 않고 private 구현에 의존하고 있다는 점이다. 인터페이스 문서만으로 클래스를 어떻게 사용해야할지 알아낼 수 없을때 소스코드를 꺼내서 구현부를 보는 것은 올바른 대처가 아니다,
지나치게 밀접한 결합을 주의하라.
- 클래스와 멤버의 접근성을 최소화하라.
- 프랜드 클래스는 너무 밀접하게 결합되기 때문에 피하라
- 파생 클래스와 기본클래스가 느슨하게 연결되도록 기본클래스의 데이터를 protected 가 아닌 private 으로 선언하라.
- 클래스의 공개 인터페이스에서 멤버 데이터를 노출하지 말라
- 의미론적인 캡슐화를 유지하라
- 데미테르의 법칙을 준수하라.
6.3 설계와 구현 문제
포함 ( has a 관계)
포함은 클래스가 데이터 요소나 객체를 포함한다는 간단한 개념이다.
포함을 통해서 "갖다"를 구현하라.
최후의 수단으로 비공개 상속을 통해 "has a"를 구현하라.
약 7개 이상의 데이터 멤버를 포함하는 클래스를 주의하라.
상속(is a 관계)
공개 상속을 통해 "이다(is a)"를 구현하라
상속을 고려해서 설계하고 문서화하라. 그게 아니면 상속을 금지하라.
리스코프 치환 원칙을 따르라.: 서브 클래스는 사용자가 그 차이점을 모른채 기본 클래스의 인터페이스를 통해서 사용할 수 있어야 한다. 다시말하면 기본 클래스에 정의된 모든 루틴은 파생클래스에서 사용될 때도 의미가 같아야 한다. Account 라는 기본 클래스가 있고, CheckingAccount, SavingsAccount, AutoLoadAccount 라는 파생클래스가 있다면 개발자는 Account 객체의 상속클래스가 모엇이든지 Account 클래스로부터 파생된 모든 루틴을 호출할 수 있어야한다.
상속받고 싶을때만 상속받게하라. 인터페이스는 필요없고, 구현만 사용하고 싶다면 상속대신 포함을 사용하라
오버라이드가 불가능한 멤버 함수를 "오버라이드 "하지 말라.
공통으로 사용되는 인터페이스와 데이터, 행위를 상속 단계에서 가능한 한 가장 높은 곳으로 옮겨라. 루틴을 더 높이 옮겼을때 해당 객체의 추상화를 깬다면 거기서 멈춘다.
인스턴스가 하나쁜인 클래스를 의심히라.
파생클래스가 하나쁜인 기본클래스를 의심하라. 필요한 것 이상으로 상속구조를 만들어서는 안된다.
루틴을 오버라이드했는데 파생된 루틴 내부에서는 아무것도 하지 않는 클래스들을 의심하라.
깊은 상속 구조를 피하라.
광범위한 타입검사보다 다형성을 택하라.
모든 데이터를 보호가 아닌 비공개로 만들어라
상속을 언제 사용하고 언제 포함을 사용할지를 요약한 내용이다
- 다중클래스가 공통 데이터는 공유하지만 행위를 공유하지 않는다면 그 클래스가 포함할 수 있는 공통객체를 만든다.
- 다중클래스가 공통 행위는 공유하지만 데이터를 공유하지 않는다면 공통적인 류틴을 정이한 기본 공통 클래스를 상속받는다.
- 다중 클래스가 공통 데이터와 행위를 공유한다면 공통적인 테이터와 루틴을 정의한 기본공통클래스를 상속받는다.
- 인터페이스를 제어하기 위한 기본 클래스가 필요할때는 상속을 하고 인터페이스를 제어햐고 싶다면 포함한다.
멤버함수와 데이터
멤버함수와 데이터를 효과적으로 구현하기 위한 몇가지 지침
클래스에 가능한한 적은 수의 루틴을 유지하라.
원하지 않는 멤버함수와 연산자가 암묵적으로 생성되지 않도록 하라.
클래스에서 호출되는 루틴의 수를 최소화하라
다른 클래스에 대한 간접적인 루틴 호출을 최소화하라.
일반적으로 클래스가 다른 클래스와 협력하는 정도를 최소화하라.
생성자
가능하다면 모든 멤버 데이터를 모든 상성자에서 초기화하라.
비공개 생성자를 사용해 실글턴 속성을 구현하라.
다른 사실이 증명될 때까지 얕은 복사사보다 깊은복사를 택하라.
6.4 클래스를 작성하는 이유
- 현실세계의 객체를 모델링한다.
- 추상객체를 모델링한다.
- 복잡성을 줄인다.
- 복잡성을 고립시킨다.
- 구현세부사항을 숨긴다.
- 변경효과를 제한한다.
- 전역데이터를 숨긴다.
- 매개변수 전달은 간소화한다.
- 중앙집중관리한다.
- 코드 재사용을 돕는다.
- 프로그램 전체를 고혀한다.
- 연관된 기능을 패키지로 구성한다.
- 특정한 리팩터링을 수행한다.
피해야할 클래스
- 신클래스를 생성하지 말라.
- 관련없는 클래스를 제거하라.
- 동사를 뒤에 붙이는 클래스를 피하라. 데이터는 없이 행위만 구성된 클래스는 일반적으로 클래스가 아니다.
7장 고급루틴
루틴이란? 한가지 ㅗㄱ적을 위해서 호출할 수 있는 개별 매서드나 프로시저를 말한다. (자바의 메서드)
나쁜 루틴
- 루틴이름이 좋지않다.
- 루틴에 대한 설명이 없다.
- 루틴의 레이아웃이 엉망이다.
- 루틴의 입력변수인 InputRect가 변경되었다.
- 루틴이 전역변수를 읽고 쓴다.
- 루틴의 목적이 하나가 아니다.
- 루틴이 잘못된 데이터로부터 자신을 방어하지 않는다.(예:나누기 0)
- 루틴에 여러가지 매직넘버를 사용하고 있다.
- 루틴의 매개변수 중 몇개를 사용하지 않는다.
- 루틴의 매개변수 중 하나가 잘못 전달되었다.
- 루틴의 매개변수가 너무 많다.(매개변수의 최대한계는 7개다.)
- 루틴의 매개변수가 잘못정렬되어 있으며 문서화도 되어있지 않다.
7.1 루틴을 작성하는 이유
- 복잡성을 줄인다.
- 이해하기 쉬운 중간 단계의 추상화를 도입한다.
- 중복코드를 피한다.
- 서브클래싱을 지원한다.
- 코드의 실행 순서를 감춘다.
- 포인터 연산을 감춘다.
- 이식성을 높인다.
- 복잡한 불린테스트를 단순화한다.
- 모든 루틴의 길이를 짧게 만들기 위해서
루틴으로 작성하기에는 너무 단순해 보이는 연산
코드가 두세줄 뿐인 루틴을 작성하면 너무 지나쳐보일수 있지만, 경험상 매우 유용하다.
7.2 루틴 수준의 설계
루틴에서의 응집성은 루틴에 있는 연산들이 얼마나 밀접하게 연관되어 있는지를 나타낸다. Cosine() 과 같은 함수는 한가지 기능만 수행하므로 응집성이 강하다. CosineAndTan() 과 같은 함수는 한가지 이상의 작업을 수행하기 때문에 응집성이 약하다. 응집성이 높을수록 코드의 오류가 적다.
완벽하지는 않지만 알아둘 만한 응집성
- 순차적 응집성: 루틴이 특정한 순서대로 수행되어야 하고 단계마다 정보를 공유하며 동시에 수행될 때 완전한 기능을 제공하지 못하는 연산을 할때 존재한다.
- 통신적 응집성; 루틴에 있는 연산들이 같은 데이터를 사용하지만, 서로 아무런 연관이 없을때 발생한다.
- 시간적 응집성: 여려 연산이 동시에 수행되어야 해서 하나의 루틴으로 결합할때 발생한다.
지양해야할 응집성
- 절차적응집성: 루틴에 있는 연산들이 정해진 순서대로 처리할 때 발생한다.
- 논리적응집성: 여러가지 기능을 한 루틴에서 수행할때 전달디는 조건에 따라 수행하는 기능이 다른경우에 발생한다.
- 우연적응집성: 루틴에 있는 연산이 특별한 관계를 맺지 않을때 발생한다.
7.3 좋은 루틴이름
- 루틴이 하는 모든것을 표현하라
- 의미가 없거나 모호하거나 뚜렷한 특징이 없는 동사를 사용하지 말라
- 루틴의 이름을 숫자만으로 구분하지 말라
- 루틴이름의 길이에 신경쓰지마라: 연구에 의하면 적절한 길이는 9~15
- 함수의 이름을 지을때는 리턴값에 관해서 설명하라
- 프로시져의 이름을 지을때 확실한 의미가 있는 동사를 객체이름과 함께 사용하라.
- 반의어를 정확하게 사용하라.
- 공통적인 연산을 위한 규약을 만들어라
7.4 루틴의 길이에 대한 문제
결과적으로 200줄 이상의 긴 루틴을 작성하고자 할때는 주의해야한다.
7.5 루틴 매개변수 처리
인터페이스 문제를 최소화하는 몇가지 지침
- 매개변수를 입력-수정-출력 순서로 입력한다.
- 고유한 in 과 out 키워드의 사용을 고려한다.
- 유사한 매개변수가 여러루틴에서 사용된다면 해당 매개변수를 항상 같은 순서로 입력한다.
- 모든 매개변수를 사용한다.
- 상태변수나 오류변수를 마지막에 입력한다.
- 루틴의 매개변수 연산을 위한 변수로 사용하지 않는다.
- 매개변수에 대한 제약사항을 주석으로 작성한다.
- 루틴 매개변수의 수를 7개 정도로 제한한다.
- 매개변수에 사용할 입력, 수정, 출력이름 규약을 고려한다.
- 루틴이 인터페이스 추상화를 유지할 수 있도록 변수나 객체를 전달한다.
- 이름 매개변수를 사용한다.
- 실질적인 매개변수가 형식적인 매개변수와 일치하는지 확인한다.
7.6 함수를 사용할 때 특별히 고려해야 할 사항
함수는 값을 반환하고 프로시저는 아무 값도 반환하지 않는 루틴이다. 문법적인 구분과 의미론적인 구분이 있는데 의미론적인 구분을 따라야한다.
함수를 사용할 때와 프로시저를 사용할 때
루틴의 일차적인 목적이 함수의 이름에서 가리키고 있는 값을 반환하는 것이라면 함수를 사용하라 그렇지 않다면 프로시저를 사용하라.
함수 리턴값 설정
- 가능한 모든 리턴 경로를 검사하라.
- 지역 데이터에 대한 참조나 포인터를 리턴하지 말라.
7.7 매크로 루틴과 인라인 루틴
- 매크로 표현식을 괄호로 묶어라.
- 다중 명령문 매크로를 중괄호로 감싸라
- 필요한 경우 루틴으로 대체될 수 있도록 매크로의 이름을 루틴과 비슷하게 작성하라