cleanCode/FiveLinesOfCode

[FiveLinesOfCode] 6장: 유사한 코드 융합하기

Mo_bi!e 2023. 10. 29. 11:20

6장 데이터 보호

의문점

  • 학습목표

  1. getter와 setter를 사용하지 말것’ 으로 캡슐화 강제하기란?
  2. getter와 setter 제거하기’ 로 getter제거하기란?
  3. ‘공통 접사를 사용하지말것’을 위한 ‘데이터 캡슐화’ 사용이란?
  4. ‘순서 강제화’로 불변속성제거란?

키워드

  • 6.1 getter없이 캡슐화 하기

    • 정의

      • Boolean 이 아닌 필드에 getter나 setter 사용하지 말것
      • 즉 직접할당하거나 반환하는 메서드를 의미함
      • getter 존재시
        • 캡슐화를 해제하고 불변속성을 전역적으로 만듬
        • 객체 반환 후 반환받은 곳은 객체를 다른곳어 더 전달이 가능하고 이것은 제어 불가능
      • setter 존재시
        • setter수정시 시그니처 유지할 수있는 또 다른 간접적 레이어 도입할 수있음
        • 실제로는 setter를 통한 새로운 데이터 구조를 반환하도록 getter를 수정하는 것임
        • 밀결합(tight coupling)의 한 형태임
        • 가변(mutable) 객체에서만 발생하는 문제임
      • 필드를 비공개하는것의 장점
        • 푸시기반(push-based)의 아키텍처 장려함
          • 데이터에 가깝에 연산을 이관함
        • 풀기반(pull-based)의 아케틱처
          • 데이터를 가져와 중앙에서 연산함
      • 풀기반 아키텍처
        • 수동적인 데이터 클래스 & 소수의 관리자 클래스로 이어짐
        • 데이터와 관리자 사이, 데이터 클래스 간 밀결합을 가져옴
      • 푸시기반 아키텍처
        • 인자로 데이터를 전달함
        • 효용에 따라 분산이 됨
    • 스멜

      • ‘낯선 사람’에게 말하지말라 요약하는 디미터 법칙에서 유래함
      • 여기서 낯선사람이란 직접접근은 못하지만 ‘참조얻을 수 있는 객체임’
      • getter를 통해 발생함
    • 의도

      • ‘참조얻을 수 있는 객체임’ 와 상호작용시 문제점(pull-based)은 객체 가져오는 방식과 밀결합함
        • 객체 소유자의 내부 구조에 대해 어느정도 알고 있어야함
      • push-bsased 아케틱처는 서비스와 같은 메서드를 노출함 → 메서드 사용자는 메서드 내부구조 신경쓰지 안아도 됨
      // 1. 비공개 설정 및, 클로스로 코드이관
      class KeyConfiguration {
      constructor(private color: string, private _1: boolean, private removeStrategy: RemoveStrategy) { }
      getColor() { return this.color; }
      is1() { return this._1; }
      
      //삭제 -> 더이상 사용되지 않으므로
      //   private getRemoveStrategy() { return this.removeStrategy; }
      
      removeLock(){ //새로운 메소드
        remove(this.removeStrategy); 
      }
      
      }
      
      class Key implements Tile {
      constructor(private keyConf: KeyConfiguration) { }
      isAir() { return false; }
      isLock1() { return false; }
      isLock2() { return false; }
      draw(g: CanvasRenderingContext2D, x: number, y: number) {
        g.fillStyle = this.keyConf.getColor();
        g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      }
      moveHorizontal(dx: number) {
        // remove(this.keyConf.getRemoveStrategy());
        this.keyConf.removeLock(); //이관
        moveToTile(playerx + dx, playery);
      }
      moveVertical(dy: number) {
        // remove(this.keyConf.getRemoveStrategy());
        this.keyConf.removeLock(); //이관
        moveToTile(playerx, playery + dy);
      }
      update(x: number, y: number) { }
      }
  • 6.1 리팩터링 패턴 : getter 와 setter 제거하기

    • 설명

      • 이 패턴으로 기능을 데이터에 더 가깝게 이동하여 getter, setter 제거가능
    • 절차

      1. getter, setter 사용되는 모든곳에서 오류 발생하도록 private 로 설정
      2. 클래스로의 코드 이관으로 오류수정
      3. 클래스로의 코드 이관의 일부로 인라인화 함 → 기존것 삭제
    • 실습

        // // getter와 setter제거 초기코드
        // class Website {
        //     constructor (private url: string){}
        //     getUrl() { return this. url;}
        // }
        // class User {
        //     constructor (private username: string) {}
        //     getusername() {return this.username};
        // }
        // class BlogPost {
        //     constructor (private author: User, private id: string) {}
        //     getId() { return this.id; } 
        //     getAuthor () { return this. author; }
        // }
      
        // function generatePostLink(website: Website, post: BlogPost) {
        //     let url = website.getUrl();
        //     let user = post.getAuthor();
        //     let name = user.getUsername();
        //     let postId = post.getId();
        //     return url + name + postId;
        // }
      
        // 1. private로 변경 / 클래스로 코드 이관으로 오류수정
        class Website {
            constructor (private url: string){}
            getUrl() { return this. url;}
        }
        class User {
            constructor (private username: string) {}
            getusername() {return this.username};
      
        }
        class BlogPost {
            constructor (private author: User, private id: string) {}
            getId() { return this.id; } 
            // private getAuthor() { return this.author; } -> 사용하지 않으므로 삭제
      
            getAuthorName(){
                return this.author.getUsername(); //push 형으로 username을 바로 받음
            };
        }
      
        function generatePostLink(website: Website, post: BlogPost) {
            let url = website.getUrl();
            // let user = post.getAuthor(); // pull형으로 접근할 필요가 없어짐
            let name = user.getUsername();
            let postId = post.getId();
            return url + name + postId;
        }
  • 6.2 규칙 : 공통접사를 사용하지 말것 (간단한 데이터 캡슐화 하기)

    • 정의

      • 코드에는 ‘공통 접두사, 접미사’ 있는 메서드나 변수가 없어야함
      • 메서드, 변수에 접미사 접두사 붙이는 경우 있음 → 컨텍스트를 잘 전달함 → 요소간 긴밀성 나타냄
      • 방법은 클래스 사용으로 메서드,변수 그룹화 장점은 외부 인터페이스를 완전히 제어할 수있음
      • 데이터를 숨김으로서 불변속성이 클래스 내에서 관리되게 할 수있음 → 유지보수성 향상
    • 스멜

      • 단일책임원칙이라고 함
    • 의도

      • 단일책임으로 클래스 설계하려면 원칙과 개요가 필요함 → 이 규칙은 하위책임 식별에 도움이 됨
    • 규칙 적용하기

      • player 클래스 옮기기
    • 참고 (getter, setter 제거)

        // 전
        class Air implements Tile {
          isAir() { return true; }
          isLock1() { return false; }
          isLock2() { return false; }
          draw(g: CanvasRenderingContext2D, x: number, y: number) { }
          moveHorizontal(player: Player, dx: number) {
            moveToTile(player, player.getX() + dx, player.getY());
          }
          moveVertical(player: Player, dy: number) {
            moveToTile(player, player.getX(), player.getY() + dy);
          }
          update(x: number, y: number) { }
          getBlockOnTopState() { return new Falling(); }
        }
      
        class Flux implements Tile {
          isAir() { return false; }
          isLock1() { return false; }
          isLock2() { return false; }
          draw(g: CanvasRenderingContext2D, x: number, y: number) {
            g.fillStyle = "#ccffcc";
            g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
          }
          moveHorizontal(player: Player, dx: number) {
            moveToTile(player, player.getX() + dx, player.getY());
          }
          moveVertical(player: Player, dy: number) {
            moveToTile(player, player.getX(), player.getY() + dy);
          }
          update(x: number, y: number) { }
          getBlockOnTopState() { return new Resting(); }
        }
        class Player {
          private x = 1;
          private y = 1;
          getX() { return this.x; }
          getY() { return this.y; }
          setX(x: number) { this.x = x; }
          setY(y: number) { this.y = y; }
          draw(g: CanvasRenderingContext2D) {
            g.fillStyle = "#ff0000";
            g.fillRect(player.getX() * TILE_SIZE, player.getY() * TILE_SIZE, TILE_SIZE, TILE_SIZE);
          }
        }
      
        // 후
        class Air implements Tile {
          isAir() { return true; }
          isLock1() { return false; }
          isLock2() { return false; }
          draw(g: CanvasRenderingContext2D, x: number, y: number) { }
          moveHorizontal(player: Player, dx: number) {
            player.move(dx, 0);
          }
          moveVertical(player: Player, dy: number) {
            player.move(0, dy);
          }
          update(x: number, y: number) { }
          getBlockOnTopState() { return new Falling(); }
        }
      
        class Flux implements Tile {
          isAir() { return false; }
          isLock1() { return false; }
          isLock2() { return false; }
          draw(g: CanvasRenderingContext2D, x: number, y: number) {
            g.fillStyle = "#ccffcc";
            g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
          }
          moveHorizontal(player: Player, dx: number) {
            player.move(dx, 0);
          }
          moveVertical(player: Player, dy: number) {
            player.move(0, dy);
          }
          update(x: number, y: number) { }
          getBlockOnTopState() { return new Resting(); }
        }
      
        class Player {
          private x = 1;
          private y = 1;
          draw(g: CanvasRenderingContext2D) {
            g.fillStyle = "#ff0000";
            g.fillRect(this.x * TILE_SIZE, this.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
          }
          moveHorizontal(dx: number) {
            map[this.y][this.x + dx].moveHorizontal(this, dx);
          }
          moveVertical(dy: number) {
            map[this.y + dy][this.x].moveVertical(this, dy);
          }
          move(dx: number, dy: number) {
            moveToTile(this, this.x + dx, this.y + dy);
          }
          pushHorizontal(tile: Tile, dx: number) {
            if (map[this.y][this.x + dx + dx].isAir()
              && !map[this.y + 1][this.x + dx].isAir()) {
              map[this.y][this.x + dx + dx] = tile;
              moveToTile(this, this.x + dx, this.y);
            }
          }
          moveToTile(newx: number, newy: number) {
            map[this.y][this.x] = new Air();
            map[newy][newx] = new PlayerTile();
            this.x = newx;
            this.y = newy;
          }
        }
  • 6.3 리팩터링 패턴 : 데이터 캡슐화

    • 설명

      • 접근할 수있는 지점을 제한하고, 구조를 명확하게 함
      • 이름을 단순하게, 응집력을 명확히
      • 이 경우 더 많은 수의 작은 클래스가 도입됨 → 유익함 → 소극적이지 말자
      • 중요 이점은 변수를 캡슐화 하는데있음
      • 범위 제한 시 클래스 내의 메서드만 데이터를 수정할 수있음 → 메서드
    • 절차

      1. 클래스를 만듭니다.
      2. 변수를 새로운 클래스로 이동하고 let을 private으로 바꿔니다. 변수의 이름을 단순한 것으로 정하고 변수에 대한 getter와 setter를 만듭니다.
      3. 변수가 더 이상 전역 범위에 없기 때문에 컴파일러가 오류를 발생시켜 모든 참조를 찾을 수 있게 해줍니다. 다음의 다섯 절차를 통해 이 오류들을 수정합니다.
        1. 새 클래스의 인스턴스에 적합한 변수 이름을 선택합니다.
        2. 접근을 가상의 변수에 대한 getter 또는 setter로 바꿉니다.
        3. 2개 이상의 다른 메서드에서 오류가 발생한 경우 이전의 변수명을 가진 매개변수를 첫 번째 매개변수로 추가하고, 동일한 변수를 첫 번째 인자로 호출하는 쪽에 넣습니다.
        4. 한 메서드에서만 오류가 발생할 때까지 반복합니다.
        5. 변수를 캡슐화했다면 변수가 선언된 지점에서 새로운 클래스를 인스턴스화합니다. 그렇지 않으면 오류가 발생한 메서드에 인스턴스화하는 부분을 만듭니다.
      // // 초기코드
      // let counter = 0;
      // function incrementCunter(){
      //     counter++;
      // }
      
      // function main(){
      //     for(let i = 0 ; i < 20 ; i++){
      //         incrementCunter();
      //         console.log(counter);
      //     }
      // }
      
      // //1단계 , 2단계
      
      // function main(){
      //     for(let i = 0 ; i < 20 ; i++){
      //         incrementCunter();
      //         console.log(counter);
      //     }
      // }
      
      // class Counter{
      //     private counter = 0;
      //     getcounter() {return = 0;}
      //     setcounter(c : number){
      //         this.counter = c;
      //     }
      // }
      
      // 3단계
      class Counter{
        private counter = 0;
        getCounter() {return 0;}
        setCounter(c : number){
            this.counter = c;
        }
      }
      
      function incrementCunter(counter : Counter){
        counter.setCounter(
            counter.getCounter() + 1 //setter로 대체
        )
      }
      
      function main(){
        let counter = new Counter();
      
        for(let i = 0 ; i < 20 ; i++){
            incrementCunter(counter); //인자로 전달되는 가장변수 (이후 만들어줌)
            console.log(counter.getCounter());
        }
      }
  • 6.3 복잡한 데이터 캡슐화

    • 데이터 캡슐화 → 메서드 처리 위해 ‘클래스로의 코드이관’ → 메서드의 인라인화 → getter, setter 제거하기
  • 6.4 리팩터링 패턴 : 순서 강제화 (순서에 존재하는 불변속성 제거하기)

      class Map {
        private map: Tile[][];
        transform() {
          this.map = new Array(rawMap.length);
          for (let y = 0; y < rawMap.length; y++) {
            this.map[y] = new Array(rawMap[y].length);
            for (let x = 0; x < rawMap[y].length; x++) {
              this.map[y][x] = transformTile(rawMap[y][x]);
            }
          }
        }
    
      window.onload = () => {
        map.transform();
        gameLoop(map);
      }
    
    • 다른 메서드보다 먼저 map.transform() 을 호출해야하는 불변속성 제거 효과 있음

      • 순수 불변속성(sequence invariant)라고 함 → 순서강제화
    • 설명

      • 가장 바람직한 리팩터링은 컴파일러에게 프로그램이 어떻게 실행되는지 가르쳐주고 → 원하는대로 실행되는지 확인이 가능함
      • OOP에서 생성자가 객체 메서드 보다 먼저 호출되어야함 →특정 순서로 발생하게 할 수있음
      • 강제하려는 실행절차별로 하나의 클래스를 도입은 간단하게 가능함
      //초기 코드
      // function deposit(to: string, amount: number)
      // {
      //     let accountId = database.find(to);
      //     database.updateOne(
      //         accountId,
      //         { $inc: {balance: amount }});
      // }
      
      //1단계 : 마지막 실행 메서드에 캡술화 적용
      // class Transfer{
      //     deposit(to: string, amount: number)
      //     {
      //         let accountId = database.find(to);
      //         database.updateOne(
      //             accountId,
      //             { $inc: {balance: amount }});
      //     }
      // }
      
      //2단계 : 생성자에 첫 실행 메서드 호출
      // class Transfer{
      //     constructor(from: string, amount:number){
      //         this.deposit(from, -amount);
      //     }
      
      //     deposit(to: string, amount: number){
      //         let accountId = database.find(to);
      //         database.updateOne(
      //             accountId,
      //             { $inc: {balance: amount }});
      //     }
      // }
      
      //3단계 : 개선
      class Transfer{
        constructor(from: string, private amount:number){
            this.depositHelper(from, -this.amount);
        }
      
        deposit(to: string){ //돈 그냥 사라질수있음 주의!! -> 다른클래스로 감추는것 추천
            this.depositHelper(to, this.amount);
        }
      
        //도우미 메서드
        private depositHelper(to: string, amount: number){
            let accountId = database.find(to);
            database.updateOne(accountId, {$inc: {balance: amount }});
        }
      }
  • 6.5 열거형을 제거하는 또 다른 방법

    • 비공개 생성자를 통한 열거
      • 열거형에 대한 메서드 지원하지 않는 경우 private 생성자를 사용해서 이를 우회할수 있는 기법 존재
      • 모든 객체는 생성자를 호출해서 생성해야함 → 생성자를 private로 만들면 클래스 내부에서만 객체 생성 가능
    • 숫자를 클래스에 다시 매핑하기
      • 일부 언어에서는 열거형이 이름을 가진 정수처럼 처리되기 때문에 미서드 가질 수없음
      • 숫자를 새로운 클래스의 인스턴스로 변환하느 방법이 필요함
      • 방법은 열거형에서와 같은 순서로 모든값을 포함하는 배열을 만드는것임
      • 즉 숫자를 알맞은 인슽너스에 매핑할 수있음

요약

  1. getter와 setter를 사용하지 말것’ 으로 캡슐화 강제하기란?
    1. getter와 setter를 사용하지 말것 이란 getter,setter로 간접적으로 비공개 필드 노출해서는 안됨
  2. getter와 setter 제거하기’ 로 getter제거하기란?
    1. 이 리팩터링 패턴 사용해서 getter, setter 제거가능함
  3. ‘공통 접사를 사용하지말것’을 위한 ‘데이터 캡슐화’ 사용이란?
    1. 공통 접두사, 접미사 있는 메서드 변수는 한 클래스에 함께 있어야함
    2. ‘데이터 캡술화’리팩터링 패턴 사용가능함
  4. ‘순서 강제화’로 불변속성제거란?
    1. 컴파일러가 실행순서를 강제하도록 하는것으로 ‘순서 불변속성’ 제거
    2. 비공개 생성자 있는 클래스 사용 → 열거형과 스위치문 추가 제거 가능

나눔

  • 푸시기반 아키텍처 : 묻지 말고 시켜라 (디미터원칙) → 물으면 데이터 접근위한 체이닝 발생함