«   2022/08   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
Archives
Today
84
Total
275,421
08-11 11:09
관리 메뉴

lancelot.com

엘레강트 오브젝트 : 새로운 관점에서 바라 본 객체지향 - yegor Bugayenko 저 / 조영호 역 본문

보고 듣고, 느끼고

엘레강트 오브젝트 : 새로운 관점에서 바라 본 객체지향 - yegor Bugayenko 저 / 조영호 역

lancelot50 2021. 9. 27. 20:12

절차적 방식의 프로그래밍에서 벗어나, 진정한 OOP를 사용하는 객체지향 프로그래밍을 해야한하고, 그렇게하려면 어떻게 해야하는지를 구체적인 예시를 통해 이야기하는 책이다. 

절차적 방식의 프로그래밍에 익숙해진 사람들은, 객체지향 방식으로 사고하고 프로그래밍을 하는게 힘든데, 이 책에서는 다양한 경우에 볼 수 있는 설득력있는 예제를 가지고 설명해준다. 각 주제마다 붙은 토론하기 링크를 따라가서 읽어보면 더 다양한 예시와 다른 사람들의 의견을 들을 수 있어서 좋다.

처음 접해보는 사람들에게도 좋겠지만, 한 번 정도 객체지향설계나 애자일 개발에 관해 접해본 사람이 사람들이라면 이해가 훨씬 더 쉬울 수 있을 것 같다.

 

객체의 일생을 인생에 비유해서 설명하는게 특징인데 각 챕터 제목은,

  1. 출생(Birth)
  2. 학습(Education)
  3. 취업(Employment)
  4. 은퇴(Retirement)

이다.

  1. 출생(Birth)
    1. ~er 로 끝나는 이름을 사용하지마라 - 토론하기(https://goo.gl/Uy3wZ6)
      1. 클래스는 객체의 팩토리
          1. 이 아이디어가 가장 잘 드러나 있는 것은 Ruby
        class Cash
        	def initialize(dollar)
            	#
                ...
            end
        end
        Cash five=Cash.new(5)
      2. 기술적으로 클래스는 객체의 템플릿이지만, 그것보다 객체의 능동적 관리자 라고 생각해야한다.
      3. 클래스를 만들때 "무엇을 하는지"가 아니라 "무엇인지"로 규정해라(Manager, Controller, Helper 모두 나쁘다)
      4. 무엇을 하는지로 규정하는 것은 절차적인 언어의 습관일 뿐이다.
        1. SQL은 셀을 조회할 수 있어야한다
        2. 픽셀은 스스로를 바꿀 수 있다
        3. 파일은 디스크에서 내용을 읽어올 수 있다
        4. 인코딩 알고리즘은 인코드 작업을 수행할 수 있다
        5. HTML 문서는 브라우저에서 HTML을 렌더링 할 수 있다
      5. 이렇게 클래스의 이름을 PrimeFinder 가 아니라 PrimeNumbers 로 짓고, PrimeNumbers 스스로 소수를 찾는 역할을 하게만들어야한다.
    2. 생성자 하나를 주 생성자로 만들어라 - 토론하기(https://goo.gl/brqhYS)
      1. 응집도가 높고 견고한 클래스 일수록, 적은 메서드와, 상대적으로 더 많은 수의 생성자를 가진다.
      2. 메서드가 많아지면 클래스의 초점이 흐려지고, 단일책임원칙에 위배된다.
      3. 생성자가 많아야하니까, 그중 하나를 주 생성자로 만들어라.
    3. 생성자에 코드를 넣지마라 - 토론하기(https://goo.gl/DCMFDY)
      • 객체를 생성할 때는
        1. 인스턴스화하고
        2. 우리를 위해 작업을 시킨다
      • 이 두단계를 겹치도록 하지마라.
      • 생성자에는 할당문만 넣고, 다른 행위를 하지않는 것이 코드의 유연성을 높인다
  2. 학습(Education)
    1. 가능하면 적게 캡슐화하라
      • 객체의 멤버가 모두 같으면 같은 객체라야한다.
        • 이것은 좀 논쟁적인 주장이다.
        • 언어 차원에서 지원해야하는 기능일 수 있다.
    2. 최소한 뭔가는 캡슐화해라 - 토론하기(https://goo.gl/QE9aXg)
      • 유틸리티 클래스 같은거 나쁘다는 말 같은데, 책을 뒤까지 다 읽으면 어느 정도 이해가 된다
    3. 항상 인터페이스를 사용해라 - 토론하기(https://goo.gl/vo9F2g)
    4. 메서드 이름을 신중하게 선택해라
      1. 빌더는 명사다
        • 빌더(builder)는 뭔가를 만들어서 새로운 객체를 반환하는 메서드 - 항상 뭔가를 반환하고 이름은 명사
          int pow(int base, int power);
          float speed();
          Employee employee(int id);
          String parsedCell(int x, int y);​
          빌더의 예
        • 제과점에 들러, "브라우니를 주세요", 하지, "브라우니를 요리해주세요", 라고 말하지 않는다.
        • class Bakery
          {
              Food coolBronie();
              Drink brewCupOfCoffee(String flavro);
          }
          위의 Bakery 의 메서드는 메서드가 아니라 프로시져(Procedure)다.  이것은 C 같은 절차적 언어의 방식이것과 비교해보면 명확해진다.  아래처럼 쓰는 것은 명확하게 C방식이다.
        • Food* cook_brownie()
          {
          	// 브라우니를 요리해서 반환한다
          }
      2. 조작자는 동사다
        • 조작자(manibulator - 번역서는 조정자 라고했지만 조정보다는 조작이 더 맞는 뜻 같다)는 객체로 추상화한 실세계의 엔티티를 수정하고 이름은 동사
          void save(String content);
          void put(String key, Float value);
          void remove(Employee emp);
          void quicklyPrint(int id);​
          조작자의 잘된 예
        • 실세계에서 엔티티를 조작해야하는 경우에는 다음과 같이 그 객체가 그 작업을 수행하도록 한다.
          class Pixel
          {
              void paint(Color color);
          }
          
          Pixel center = new PIxel(50,50);
          center.paint(new Color("red"));​
        • 바텐더에게 음악을 틀어달라고 요청하지, 음악을 틀고 볼륨을 반환해주세요, 라고 하지 않는다.
      3. 빌더와 조작자 혼합하기
        • 잘못된 예
          class Document
          {
              int write(Inputstream content)
          }​
        • 바람직하게 변형
          class Document
          {
              OutputPipe output();
          }
          class OutputPipe
          {
              void write(INputStrema content)
              int bytes();
              long time();
          }
      4. boolean 값을 반환하는 경우
        • 위의 원칙대로라면 값을 반환하므로 빌더에 속하고 명사로 지어야한다.
          if(name.emptiness()==true)
          {
              // something to do
          }​
        • 하지만 이것은 영문으로 볼때 자연스럽지 못하다
        • boolean 값을 반환할 경우 이름을 형용사로 짓는게 바람직하다.
          if(name.empty())
          {
              // something to do
          }​
        • 바람직한 예 들
          boolean empty();
          boolean readable();
          boolean negative(();​
    5. Public Constant 를 사용하지마라 - 토론하기(https://goo.gl/QlUoru)
      class Records
      {
          private static final String EOL="\r\n";
          
          void write(Write out)
          {
          	for(Record rec : this.all)
              {
                  out.write(rec.toString());
                  out.wtire(Records.EOL);
              }
          }
      }
      
      class Rows
      {
          private static final String EOL="\r\n";
          
          void print(PrintStream put)
          {
          	for(Row row : this.fetch())
              {
                  put.printf("{ %s }%s", row, Rows.EOL);
              }
          }
      }​
      보통 이와같은 상태로 static 상수를 사용하는데, 여기서 더 나아가면
      #define EOL "\r\n"
          처럼 정의해서 쓰거나
      public class Constants
      {
          public static final String EOL="\r\n";
      }
        이렇게 만들어서 공용으로 쓰기도 한다.  하지만 이것은 전혀 관계없는 클래스의 결합도를 높이므로 아주 잘못된 사용이다.
      • 결합도가 증가한다
      • 응집도가 떨어진다
      • 바람직한 예시 - 데이터를 공유하는게 아니라 기능을 공유하도록 만들어라
        class EOLString
        {
            private final String origin;
            EOLString(String src)
            {
            	this.origin=src;
            }
            @Override String toString()
            {
            	return String.format("%s\r\n", origin);
            }
        }​
        이렇게 만들고
        class Records
        {
            void write(Write out)
            {
            	for(Record rec : this.all)
                {
                    out.write(new EOLString(rec.toString()));
                }
            }
        }
        
        class Rows
        {
            void print(PrintStream pnt)
            {
            	for(Row row : this.fetch)
                {
                    pnt.print(new EOLString(String.format("{ %s }", row)));
                }
            }
        }
            처럼 사용하면 된다.  이제 각줄의 끝에 EOL을 첨가하는 방법은 EOLString Class 가 책임진다 이렇게되면, 만약, "Windows 에서 실행될 경우 \r\n대신 예외를 던지도록 하자", 라는 요구사항이 왔을때
        class EOLString
        {
            private final String origin;
            EOLString(String src)
            {
            	this.origin src;
            }
            String toString()
            {
            	if( /* windows os */)
                {
                    throw new IllegalStateException( "윈도에서 실행중이라 EOL을 사용할 수 없습니다.");
                }
                return String.format("%s\r\n", origin);
            }
        }​
              와 같이 손쉽게 수정할 수 있다.
        다른 예시
        String body=new HttpRequest().method("POST").fetch();​
            이것을
        String body=new HttpRequest().method(HttpMethodsPOST).fetch()
        와 같이 사용하는데
        String body=new PostRequest(new HttpRequest()).fetch();
        처럼 사용하는 것이 낫다
    6.  불변객체로 만들어라 - 토론하기(https://goo.gl/z1XGjO)
      class Cash
      {
          private int dollars;
          public void SetDollars(int val);
          {
          	this.dollars=val;
          }
      }​
      위는 가변 객체 클래스
      cclass Cash
      {
          private final int dollars;
          Cash(int val)
          {
          	this.dollars=val;
          }
      }
      이렇게 변경이 불가능하게 만들라는 이야기.  하지만 이것은 불가능한 경우도 많다.  책의 예시에서 나오듯이 대표적으로 LazyLoad를 구현하는 경우에 그렇다.  저자는 이런 케이스는 언어적으로 지원을 해야한다고 말하고 있다.  (하지만 언어를 바꿀 수는 없는 일 ) - 이런 케이스를 보다 생각이 든 것인데, 객체지향 설계라는 것이, 언어 존재의 추상화레벨 정도와 관련이 없는 것이라서, 언어에 따라서는 언어 자체의 추상화레벨을 침범하는 경우도 많은 것 같다.  모두 지킬 수는 없으나, 중요한 것은 이러한 비전들을 머리 속에 넣고 프로그래밍하는 것. 
      1. 식별자 가변성 - 객체의 상태가 변해서 혼란을 초래
      2. 실패원자성 - 객체 안에서 상태가 일부만 변함
      3. 시간적 결합( Temporal coupling ) - 실행순서에 따라 객체의 상태가 달라짐.
      4. 부수효과 제거( Side effect-free ) - 위의 것들을 신경쓰지 못할때 생기는 부작용
      5. Null 참조 제거 - 객체의 프로퍼티가 일부만 설정됨 ( 2번의 반복)
      6. 스레드 안정성 - 결국 3번
      7. 작고 단순한 객체
    7.  문서를 작성하는 대신 테스트를 만들어라
      • 단위테스트는 실제 예제를 많이 보는 것이 중요한데, 책의 단순한 예제가 아닌 실무에서는 구현하기 까다로운 경우가 많아서 예제도 보기 힘든 경우가 많다.
    8. 모의객체(Mock) 대신 페이크 객체(Fake)를 사용해라 - 토론하기(https://goo.gl/OF3Cev)
      • 여지껏 해왔던 이야기의 UnitTest 버전
      • Mock 객체는 클래스 밖에 있어서 응집도를 떨어뜨리고 유지보수를 힘들게한다.
      • Fake 객체는 클래스와 긴밀히 연관되어 있어서 응집도를 높이고 유지보수가 쉽다.
      • 예제 Cash class
        class Cash
        {
            private final Exchange exchange;
            private final int cents;
            public Cash(Exchange exch, int cnts)
            {
            	this.exchange=exch;
                this.cents=cnts
            }
            public Cash in(String currency)
            {
            	return new Cash(this.exchange, this.cents * this.exchange.rate("USD", currency));
            }
        }​
        사용
        Cash dollar = new Cash(new NYSE("secret"), 100);
        Case euro = dollar.in("EUR")
        NYSE 클래스는 뉴욕증권거래소(NewYork Stock Exchange) 에 위치한 서버에 HTTP 요청을 전송해 정보를 알아내고, "secret"은 패스워드이다.  UnitTest시마다 매번 접속을 하고싶지도 않고, 패스워드도 숨기고 싶다
      • Mock 객체를 사용한 Unit Test 예시
        Exchange exhange=Mockito.mock(Exchange.class);
        Mockito.doReturn(1.15).when(exchange).rate("USD", "EUR")
        Cash dollar = new Cash(exchange, 500);
        Cash euroo=dollar.in("EUR");
        assert "5.75".equals(euro.toString());​
      • Fake 객체를 사용한 Unit Test예시
        interface Exchange
        {
            float rate(String origin, String target);
            final class Fake implements Exchange
            {
            	@Override
                float rate(String origin, String target)
                {
                    return 1.2345;
                }
            }
        }
        이렇게 만들고
          Exchange exchange=new Exchange.Fake();
          Cash dollar = new Cash(exchange, 500); 
          Cash euro = dollar.in("EUR"); 
          assert "6.17".equals(euro.toString());
        처럼 쓴다.  클래스 안에 있기때문에 훨씬 응집성이 좋고, 유닛테스트를 만들 때 내부 동작을 몰라도 된다.
    9. 인터페이스를 짧게 유지하고 스마트(smart)를 사용해라 - 토론하기(https://goo.gl/1Zos9r)
      • 인터페이스는 구현자가 준수해야하는 계약.  아래 예제를 보자
        interface Exchange
        {
            float rate(String target);
            float rate(String source, String target);
        }​
      • 이 계약은 거래소가 환율을 계산하도록 요구하는 동시에, 클라이언트가 환율을 제공하지 않을 경우 기본 환율을 사용하도록 강제한다.
      • 두 rate는 밀접하게 연관되어 있지만 사실 두 개의 독립적인 함수다.
      • 하나의 인자를 받는 rate() 메서드는 이 인터페이스에 포함되어서는 안된다.
      • 이렇게 비슷한 계약조건이 많은 interface는 단일책임원칙( Single Responsibility  Principle )을 위반하는 클래스를 만들도록 부추긴다.  응집도가 낮은 클래스가 만들어질 가능성이 높다
      • 이것을 해결하기위해서는 인터페이스 안에 '스마트(smart)' 클래스를 추가할 수 있다.
        interface Exchange
        {
            float rate(String source, String target);
            final class Smart
            {
                private final Exchange origin;
                public float toUsd(String source)
                {
                    return this.origin.rate(source, "USD");
                }
            }
        }
      • 이렇게 smart 클래스를 구현하면, interface를 구현하는 다른 클래스들 안에 동일한 기능이 중복으로 생기는 것을 막을 수 있다.
      • 중복되는 기능들은 Smart 클래스에 추가하면 된다
  3. 취업(Employmenet)
    1. 5개 이하의 public 메서드만 노출해라
      • 생성자와 priavate 메서드를 제외하고 5개 언더라고 말할 수 있을 정도로 클래스를 작게 만들어라
      • 클래스가 작으면 메서드와 프로퍼티가 더 '가까이'있을 수 있어서 응집도가 높아진다.
      • 각각의 메서드가 모든 프로퍼티를 사용한다.  이것이 응집도의 정의.
      • 클래스에 두 개의 프로퍼티가 있을때, 한 프로퍼티는 두개의 메서드에서만 사용되고, 나머지 프로퍼티는 다른 세 개의 메서드에서만 사용된다면, 거의연관성이 없는 독립적인 두 부분이 하나의 클래스 안에 뭉쳐있는 상황이고, 응집도가 낮다고 할 수 있다.
    2. 정적메서드를 사용하지마라 - 토론하기 (https://goo.gl/8ql2ov
      1. 객체 대 컴퓨터 사고 ( object vs. computer thinking )
        • 컴퓨터처럼 생각하기는 순차적인 사고방식이고 CPU에게 할 일을 지시한다
          int max(int a, int b)
          {
              if(a>b)
              	return a;
              return b;
          }​
        • 객체지향적인 사고는 객체를 정의함으로써 그것을 처리한다
          class Max implements Number
          {
              private final Number a;
              private final Number b;
              public Max(Number left, Number right)
              {
              	this.a=left;
                  this.b=right;
              }
              @Override
              public int intValue()
              {
                  return this.a > this.b ? this.a : this.b;
              }
          }
          사용법
          Number x=new Max(5,9);
          위 코드는 최대값을 계산하지 않는다. 그저 x가 5와 9의 최대값이라는 사실을 정의한다.  이런 식으로 코드를 짜야한다는 측면에서 보면
          int x = Math.max(5,9);
          이 정적 메서드 방식은 최악의 방식이다.
      2. 선언형 스타일 대 명령형 스타일( declarative vs. imperative style )
        • 명령형 프로그래밍(imperative programming) - 프로그램의 상태를 변경하는 문장(statement)을 사용해서 계산 방식을 서술
          • 컴퓨터 처럼 연산을 차례로 실행
        • 선언형 프로그래밍(declarative programming) - 제어 흐름을 서술하지 않고 계산 로직을 표현
          • '엔티티'와 엔티티 사이의 '관계'로 구성되는 자연스러운 사고 패러다임에 가까움
        • 선언형이 더 강력한 기법이기는 하지만, 절차적인 프로그래머가 이해하기에는 명령형 프로그래밍이 훨씬 더 쉽다.
        • 실행 관점에서 선언형 방식이 더 최적화되어있기 때문에 빠르다
        • 코드블록사이의 의존성을 끊기가 더 쉽다
        • 표현력이 좋다
          Collection<Integer> evens = new LinkedList<>();
          for( int number : numbers)
          {
              if(number%2==0)
              {
              	evens.add(number);
              }
          }​
          위와 같은 명령형 코드가 하는 일을 이해하기 위해서는 코드의 실행 경로를 추적해야한다.  코드 안의루프를 마음 속으로 시각화 해야한다.  근본적으로 CPU가 수행해야 하는 일을 코드를 읽는 사람도 동일하게 수행해야 한다.
          Collection<Integer> event = new Filtered( numbers, 
                                                  new Predicated<Integer>()
                                                  {
                                                      @override
                                                      public boolean suitable(Integer number)
                                                      {
                                                          return number%2==0;
                                                      } 
                                                  }
                                              );
          이 코드는 구현과 관련된 세부 사항은 감춰져 있고, 오직 행동만 표현되어있다.
        • 알고리즘(algorithm)과 실행(execution) 대신 객체(object)와 행동(behavior)의 관점에서 사고하므로 영어에 훨씬 더 가깝다.
        • 응집도가 높다 - 앞의 예제에서 계산을 책임지는 곳이 한 곳에 뭉쳐있어서, 실수로라도 분리하기 힘듬
        • 기술적으로 명령형과 선언현 스타일을 조합하는 것은 불가능하다.
        • 정적 메서드를 사용하는 외부라이브러리가 있다면, 캡슐화해서 감추어라.
      3. 유틸리티 클래스( Utility classes )
        • 여기까지 읽었다면, 왜 저자가 유틸리티 클래스를 사용하지 말라고 하는지 알 수 있다.
        • 클래스는 객체의 팩토리고, 코드를 짤때는 객체를 선언하는 식으로 사용해야하는데, 유틸리티 클래스는 절차적이고 명령형 프로그래밍 코드를 클래스에 감싸놓은 식이기 때문이다.
      4. 싱글턴(Singleton) 패턴
        • 싱글턴 패턴은 setinstance() 로 인스턴스를 교체할 수 있다는 점을 제외하면, 논리적으로 static함수, Utility class 와 동일하므로 사용해서는 안된다.
        • 논리적으로 보나 기술적으로 보나 싱글턴은 전역변수 그이상 그이하도 아님
        • OOP에는 global scope 가 없다.  따라서 전역변수를 위한 자리도 없다.
      5. 함수형 프로그래밍
        • 이렇게되면 함수형 프로그래밍과 유사해지지만, 함수형 프로그래밍보다는 OOP가 낫다
          • OOP에서는 객체와 메서드의 조합이 가능하기때문
          • lamba 표현식도 견고함을 약화시키므로 좋지않다
      6. 조합가능한 데코레이터
        • 이 책에서 말하고자하는 OOP 방식의 코드작성의 예시가 아래에 있다.
          name = new Sorted( 
                       new Unique(
                         new Capialized(
                           new Replaced(
                             new FileNames( new Directory("/var/users/*.xml") ),
                              "([&.]+)\\.xml", "$1" 
                               )
                             )
                           ) 
                     );​
          이코드는 순수하게 선언적이다.  어떤일도 실제로 '수행하지는 않지만', 디렉토리(Directory) 안의 모든 파일 이름(FileNames)을 정규 표현식을 이용해서 치환하고(Replaced), 대문자로 변경하고(Capitalized), 중복된 이름을 제거해서 유일하게(Unique) 만들고, 다시 정렬해서(Sorted) 컬렉션에 담은 후, 이 컬렉션을 가리키는 names 객체를 선언한다.  방금 객체를 어떻게 만들었는지 전혀 설명하지 않고도 이 객체가 무엇인지 설명했다.  단지 선언(declared) 했을 뿐인데.
        • 올바르게 설계된 객체지향 소프트웨어라면, 코드 대부분이 위와 같아야한다.
        • 프로그래머는 데코레이터를 조합하는 일을 제외한 다른 일들은 하지 말아야한다.
        • if, for, switch, while 같은 절차적인 문장(statement)이 포함되어 있어서는 안된다.
        • 절차적인 코드 아래 대신에
          float rate;
          if(client.age() > 65)
          	rate=2.5;
          else
          	rate=3.0;​
        • 객체지향적인 코드 아래처럼 만들어야한다.
          // 객체지향적인 버전
          float rate = new If( client.age() > 65, 2.5, 3.0 );
          
          // 조금 더 객체지향적인 버전
          float rate = new If( new Greater( client.age(), 65 ), 2.5, 3.0 );
          
          // 최종 코드
          float rate = new If( new GreaterThan( new AgeOf(Client), 65), 2.5, 3.0);​
          • 음.. 객체지향 최종버전을 보니 좀 현타가 오는데, function call 과 . 같은 operator는 명령 내리는 것을 단순하게 만들어주는 효과도 있는데, 그걸 객체의 생성처럼 복잡한 방식으로 바꾼다면, 영어처럼 읽기 쉬워지는 코드를 위해서 좀 더 복잡하게 코드를 만들자는 것처럼 보이기도 한다.
          • 물론 이것은 예시니까, 꼭 저렇게 하라는 뜻이라기보다는, 거대하고 복잡한 기능을 저런 식으로 나누라는 이야기로 이해하자.
    3. 인자의 값으로 NULL을 절대 허용하지 마라 - 토론하기(http://goo.gl/TzrYbz)
      1. null은 Pointer에서 기원한 것이므로, pointer가 없는 OOP언어에서는 사용해서는 안된다
      2. 아래의 메서드 설계를 보자
        public Iterable<File> find(String mask)
        {
            // 디렉토리를 탐색해서 "*.txt"와 같은 형식의 마스크에 일치하는 모든 파일을 찾는다.
            // 마스크가 NULL 인 경우에는 모든 파일을 반환한다.
        }
      3. 첫번째 설계
        public Iterable<Files> find(String mask)
        {
            if(mask==null)
                // 모든 파일을 찾는다
            else
                // 마스크를 사용해서 파일을 찾는다
        }
        '진짜' 객체라면 대화에 응할 것이고, NULL 이라면 대응하지 않겠다는 식으로 객체와 의사소통 해서는 안됨
      4. 객체를 존중한 설계
      5. public Iterable<File> find(Mask mask)
        {
            if(mask.empty()
                // 모든 파일을 찾는다
            else
                // 마스크를 사용해서 파일을 찾는다.
        }
        
        // 더 개선한 코드
        public Iterable<File> find(Mask mask)
        {
            Collection<File> files = new LinkedList<>();
            for( File file : /* 모든 파일 */ )
                if( mask.matches(file)
                    find.add(file);
            return files;
        }
        
        interface Mask
        {
            boolean matches(File file);
        }
        
        // 일반적인 "*.txt" 와 같은 구현은 Mask를 적절히 캡슐화하고,
        // NULL에 대해서는, null을 전달하는 대신 AnyFile을 생성해서 전달한다
        class AnyFile implement Mask
        {
            @Override
            boolean matches(File file)
            {
                return true;
            }
        }​
    4. 충성스러우면서 불변이거나, 아니면 상수이거나 - 토론하기(https://goo.gl/2UKLds)
      1. Immutable 객체만 사용하도록 하라는데.. 토론 내용과 링크들을 읽어보면, 뭔가 언어적 차원에서 지원해야하는 것인 것 같다.  내용이 확 와닿지는 않는다.
    5. 절대 getter와 setter를 사용하지 마라 - 토론하기(https://goo.gl/LSyvo9)
      1. 객체 대 자료구조
        • C에서 구조체는 자료의 묶음이다.
        • 절차적 프로그래밍을 단순화하는 가장 좋은 방법은, 서브루틴과 데이터의 집합.
        • 서브루틴에 효과적으로 데이터를 넘겨주기위해 존재하는 것이 struct
        • 객체지향언어의 class는 struct와 비슷하지만, 같은 방식으로 사용해서는 안된다.
        • 객체의 property를 알 필요가 없고, 선언해서 사용하는 방식으로 사용해야하기 때문에 객체의 property에 접근하는 getter/setter는 불필요하다
        • getter/setter가 있다는 것은 class가 객체의 팩토리가 아니라 자료의 모음(data bag) 에 불과하다는 이야기
      2. 좋은 의도, 나쁜 결과
      3. 접두사에 관한 모든 것
        1. getProperty() 로 하는 것은 property에 직접 접근하는 것이니 객체지향 설계가 아니다.
    6. 부 생성자 ctor 밖에서는 new를 사용하지 마라
      • 클래스 안에서 new를 사용해서 객체를 생성하게 되면, 클래스간에 의존성이 생긴다.
      • 예제를 살펴보자
        class Cash
        {
            private final int dollars;
            
            public int euro()
            {
                return new Exchange().rate("USD", "EUR") * this.dollars;
            }
        }​
      • 이코드는 아래와 같이 사용할 것이다.
        Cash five=new Cash("5.00");
        print("%5 equals to %d", five.euro());​
      • 하지만 이 코드는 매번 실행할때마다 NYSE 서버와 네트워크 통신이 발생한다. 지금 설계에서는 이렇게 하지않게 만들려면 Cash class를 수정하는 수밖에 없다.  이것은 유지보수를 어렵게 만든다.
      • 메서드 내부에서 직접 new를 사용하지 못하게 하면 객체가 새로운 객체를 직접 생성할 수 없기때문에, 새로운 객체의 ctor를 인자로 전달받아 private 프로퍼티 안에 캡슐화 할 수 밖에 없다.
        class Cash
        {
            private final int dollars;
            private final Exchange exchange;
        
            Cash(int value, Exchange exch)
            {
                this.dollars=value;
                this.exchange=exch;
            }
            
            public int euro()
            {
                return new Exchange().rate("USD", "EUR") * this.dollars;
            }
            
            Cash()    // 부 생성자
            {
                this(0);
            }
            
            Cash(int value)   // 부 생성자
            {
                this(value, new NYSE());
            }
        }   
        
        //사용예
        Cash five = new Cash(5, new FakeExchange());
        print("$5 equals to %d", five.euro());
        Cash는 이제 Exchange에 의존하긴하지만, 의존 주체는 우리다. 다시말해, 객체가 필요한 의존성을 직접 생성하는 대신, 우리가 ctor를 통해 의존성을 주입한다.
      • 부 ctor를 제외한 어떤 곳에서도 new를 사용하지 말것.
      • 이렇게하면 객체들은 상호간에 충분히 분리되고, 테스트 용이성과 유지보수성을 크게 향상 시킬 수 있다.
    7. 인트로스펙션과 캐스팅을 피해라 - 토론하기(https://goo.gl/BoQ2iq)
      1. 타입 인트로스펙션은 reflection이라는 포괄적 용어로 불리는 기법.  이것 역시 해롭다
        public <T> int size(Iterable<T> items)
        {
            if(items instanceof Collection)
            {
                return Collection.class.cast(items).size();
            }
            int size=0;
            for(T item : items)
            {
                ++size;
            }
            return size;
        }​
        이것은 타입에 따라 객체를 차별하는 코드가 되기때문에 OOP의 기본 사상을 훼손한다.  어떠한 식으로 처리할지는 객체가 결정하게 해야한다.  게다가 결합도가 높아진다. 예시에서도 Iterable 과 Collection 이라는 두개의 인터페이스에 의존한다.
      2. 더 나은 설계
        public <T> int size(Collection<T> items)
        {
            return items.size();
        }
        
        public <T> int sze(Iterable<T> items)
        {
            int ssie=0;
            for(T item : items)
            {
                ++size;
            }
            return sze;   
        }​
  4. 은퇴( Retirement )
    1. 절대 NULL을 반환하지 마라 - 토론하기(http://goo.gl/TzrYbz)
      1. 빠르게 실패하기( fail fast ) vs. 안전하게 실패하기 ( fail safe )
        1. 저자는 빠르게 실패하기를 지지한다 - 이것은 경우에 따라 좀 다르지 않은가, 한다.
      2. NULL의 대안
    2.  체크 예외( Checked exception) 만 던져라 - 토론하기(https://goo.gl/5tGDEc)
      1. 꼭 필요한 경우가 아니라면 예외를 잡지마라
      2. 항상 예외를 체이닝해라
      3. 단 한번만 복구해라
      4. 관점-지향 프로그래밍을 사용해라
      5. 하나의 예외 타입만으로도 충분하다
    3. final 이나 abstract 이거나 - 토론하기(https://goo.gl/vo9F2g)
        • 상속은 바르게 사용해야한다.
        • 상속이 복잡하고 이해하기 어려워지는 근본 원인은 상속 그 자체가 아니라 가상 메서드이다
          class Document
          {
              public int length()
              {
                  return this.content().length();
              }
              public byte[] content()
              {
                  // 문서의 내용을 바이트배열로 로드한다
              }    
          }​
          이 예제를 암호화된 문서를 읽을 수 있도록 확장해보자
          class EncryptedDocument extends Document
          {
              @Override
              pubhlic byte[] content()
              {
                  // 문서를 로드해서
                  // 즉시 복호화하고,
                  // 복호화한 내용을 반환한다.
              }
          }
          이 설계가 content() 의 내용을 overriding 했기때문에 length() 의 동작이 바뀌었다.  이것은 의도하지 않은 결과.  이 오류를 알아내는 것도 많은 시간이 걸릴지 모른다.
        • 자식이 부모의 유산을 물려받는 일반적인 상속과는 달리, 메서드 오버라이딩은 부모가 자식의 코드에 접근하는 것을 가능하게 한다.
        • 이것을 피하는 방법 중 하나는, 클래스와 메서드를 final 이나 abstract 둘 중 하나로만 제한하는 것이다.
        • 예를들어서
          final class Document
          {
              public int length() { /* 코드는 동일*/ }
              public buyte[] content() { /* 코드는 동일*/ }
          }​
          클래스 앞의 final 은 클래스의 어떤 메서드도 자식 클래스에서 오버라딩을 할 수 없다는 사실을 컴파일러에게 알려준다.
        • 이 경우에 EncryptedDocument 클래스를 추가하려면 document 를 interface로 만든다
          interface Document
          {
              int length();
              byte[] content();
          }
          
          // 그리고 document 의 이름을 DefaultDocument로 변경하고, 이 클래스가 Document 를 구현하게 만든다
          final class DefaultDocument implements Document
          {
              @Override
              public int length() { /* 코드는 동일 */ }
              @Override
              public byte[] content() { /* 코드는 동일 */ }
          }
          
          // EncryptedDocument를 구현할때 final 클래스를 상속받는 것은 불가능하기때문에, 캡슐화한다
          final class EncryptedDocument implements Document
          {
              private final Document plain;
              EncryptedDocument(Document doc)
              {
                  this.plan=doc;
              }
              @Override
              public int length()
              {
                  this.plan.length();
              }
              @Override
              public byte[] content()
              {
                  byte[] raw = this.plan.content();
                  return /* 원래 내용을 복호화한다 */;
              }
          }​
        • 상속이 사용되는 적절한 경우는 언제일까
        • 클래스의 행동을 확장(extend) 하지않고 정제(refine) 할 때
          • 정제란 부분적으로 불완전한 행동을 완전하게 만드는 일
          • 확장은 침범이다
    4. RAII 를 사용하세요.
      1. Resource Acquisition Is Initialization
      2. 생성자에서 리소스를 얻고, 파괴자에서 리소스를 해제하라는 이야기( C++ 에는 일반적이지만, Java에는 파괴자가 없다)
      3. 아래 예시처럼 사용하라는 이야기
        #include<stdio.h>
        #incldde<string>
        class Text
        {
        public :
            Text(const char* name)
            {
                this->f = fopen(name, "r"); 
            }
            ~Text()
            {
                fclose(this->f); 
            }
            const std::string& content()
            {
                // 파일 내용을 읽어서 UTF-8 문자열로 반환한다 
            }
        };
        
        // 사용법 
        int main()
        {
            Text t("/tmp/test.txt"); 
            t->content(); 
        }​
0 Comments
댓글쓰기 폼