본문 바로가기

프로그래밍/TypeScript

07.클래스와 인터페이스 기반의 다형성

7.3.3 클래스와 인터페이스 기반의 다형성

1. 클래스의 다형성
---------------------------------------------------------
class Planet {
    public diameter: number;    // 지름
    public isTransduction: boolean = true;  // 공전

    getIsTransduction(): boolean {
        return this.isTransduction;
    }
    stopTransduction(): void {
        console.log("stop1");
        this.isTransduction = false;
    }
}

class Earth extends Planet {
    public features: string[] =["soil", "water", "oxyzen"];
    stopTransduction(): void {
        console.log("stop2");
        this.isTransduction = false;
    }
}

let earth: Planet = new Earth();
earth.diameter = 12656.2;
console.log("1번 : " + earth.diameter);
console.log("2번 : " + earth.getIsTransduction());
earth.stopTransduction();
console.log("3번 : " + earth.getIsTransduction());
---------------------------------------------------------

부모 클래스 Planet가 변수의 타입으로 지정되면, 자식 클래스 Earth에 할당될 수 있습니다.
이때 부모 클래스 Planet는 부모 클래스를 상속하는 어떤 자식클래스의 타입이라도 받아들일 수 있는 
다형 타입(polymorphic type)이 됩니다. 

위와 같은 상속관계에서 부모클래스 Plant 의 타입으로 지정된 객체 참조변수는 
자식 클래스의 객체를 할당받더라도 실제 동작은 부모클래스를 기준으로 동작합니다.

let earth: Planet = new Earth();

따라서 객체 참조변수 earth는 부모 클래스에 선언된 메소드에 접근할 수 있지만, 
자식 클래스에 선언된 멤버 변수에는 접근할 수 없습니다. 

여기서 유의해볼 것은 stopTransduction() 함수입니다.
이 함수는 부모 클래스의 함수를 자식 클래스로 오버라이딩 한 것입니다. 

아래의 명령문을 실행하면 부모 클래스가 아닌 자식 클래스의 stopTransduction() 함수가 실행됩니다.
왜냐하면 오버라이든 메소드보다 오버라이딩 메소드가 우선으로 호출되기 때문입니다.

런타임 다형성 (runtime polymorphism)
: 런타임 시에 호출될 메소드가 결정되는 특성
런타임 다형성의 대표적인 예로 덕 타이핑이 있습니다. 


2. 인터페이스의 다형성
---------------------------------------------------------
interface IPerson {
    height: number;
    getAlias: () => string;
    getAge(): number;
}

class PoliceOfficer implements IPerson {
    height: number;
    getAlias = () => "happy";
    getAge(): number {
        return 10;
    }
    hasClub() {
        return true;
    }
}

let policeMan: IPerson = new PoliceOfficer();
console.log(policeMan.getAlias());
console.log(policeMan.getAge());
// console.log(policeMan.hasClub());
---------------------------------------------------------

클래스의 다형성과 크게 다르지 않습니다.
객체 참조변수 policeMan 은 인터페이스에 정의된 멤버변수나 메소드에는 접근할 수 있지만, 
구현 클래스에 새롭게 추가된 hasClub 메소드에는 접근할 수 없습니다. 


3. 매개변수의 다형성

1) 유니언 타입을 이용한 경우
---------------------------------------------------------
class MonitorDisplayTest {
    // 매개변수에 타입을 넣는 경우는 typeof
    // 클래스 타입을 넣는 경우는 instanceof
    display1(monitor: Led | Oled) {
        if (monitor instanceof Led) {
            let myMonitor: Led =  monitor;
            return myMonitor.getName();
        } else if (monitor instanceof Oled) {
            let myMonitor: Oled =  monitor;
            return myMonitor.getName();
        }
    }
}
class Monitor {
    getName(): string {
        return "";
    }
}
class Led extends Monitor { }
class Oled extends Monitor { }
---------------------------------------------------------

유니언 타입을 사용한 경우 새로운 클래스가 추가될 때마다
type alias도 else if 문을 매번 업데이트 해줘야 합니다. 

---------------------------------------------------------
class Qled extends Monitor { }
    
display1(monitor: Led | Oled) {
    if (monitor instanceof Led) {
        let myMonitor: Led =  monitor;
        return myMonitor.getName();
    } else if (monitor instanceof Oled) {
        let myMonitor: Oled =  monitor;
        return myMonitor.getName();
    } else if (monitor instanceof Qled) {
        let myMonitor: Qled =  monitor;
        return myMonitor.getName();
    }
}
---------------------------------------------------------

이를 해결하기 위해 동일한 로직을 수행하는 것이라면 인터페이스를 이용해 다형성을 구현해줄 수 있습니다. 

2) 인터페이스 타입을 이용한 경우
---------------------------------------------------------
interface IMonitor {
    getName(): string;
}

class Led implements IMonitor {
    constructor(private name: string) { }
    getName() : string {
        return "LED => " + this.name;
    }
}
class Oled implements IMonitor {
    constructor(private name: string) { }
    getName() : string {
        return "OLED => " + this.name;
    }
}

class MonitorDisplayTest {
    display(monitor: IMonitor) {
        let myMonitor: IMonitor = monitor;
        return myMonitor.getName();
    }
}

let displayTest = new MonitorDisplayTest();
---------------------------------------------------------

구현 클래스 Led, Oled 의 타입은 인터페이스 IMonitor 타입으로 호환이 됩니다. 
그러므로 유니언 타입 대신 인터페이스를 타입으로 이용하면 됩니다. 
class 추가시 display 함수 변경없이 IMonitor 만 상속받아 구현하면 됩니다. 

---------------------------------------------------------
class Qled implements IMonitor {
    constructor(private name: string) { }
    getName() : string {
        return "QLED => " + this.name;
    }
}
---------------------------------------------------------

7.3.4 클래스에서 getter와 setter

자바스크립트에서는 객체의 멤버에 접근할 수 있는 방법으로 ES6 getter와 setter를 지원합니다. 
getter는 접근자 accessor 라고 하고, setter는 설정자 mutator 라고 합니다.
자바스크립트 ES5에서 접근자와 설정자는 보통 객체 리터럴에 추가해 사용합니다.

1) 자바스크립트 (ES5) 객체 리터럴에 getter, setter 를 사용한 경우
---------------------------------------------------------
var obj = {
    set name(name) {
        this.myName = name;
    }, 
    get name() {
        return this.myName;
    },
    myName: ""
}
---------------------------------------------------------

2) 타입 스크립트의 경우
---------------------------------------------------------
class Student2 {
    private studentName: string;
    private studentBirthYear: number;

    get name(): string {
        return this.studentName;
    }

    set name(name: string) {
        if (name.indexOf("happy") !== 0) {
            this.studentName = name;
        }
    }

    get birthYear(): number {
        return this.studentBirthYear;
    }

    set birthYear(year: number) {
        if (year <= 2000) {
            this.studentBirthYear = year;
        }
    }
}
let student2 = new Student2();
---------------------------------------------------------

위 코드를 ES5로 컴파일 한 결과입니다. 

---------------------------------------------------------
var Student2 = (function () {
    function Student2() {
    }
    Object.defineProperty(Student2.prototype, "name", {
        get: function () {
            return this.studentName;
        },
        set: function (name) {
            if (name.indexOf("happy") !== 0) {
                this.studentName = name;
            }
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(Student2.prototype, "birthYear", {
        get: function () {
            return this.studentBirthYear;
        },
        set: function (year) {
            if (year <= 2000) {
                this.studentBirthYear = year;
            }
        },
        enumerable: true,
        configurable: true
    });
    return Student2;
}());
var student2 = new Student2();
---------------------------------------------------------

컴파일 결과 Student2 클래스는 즉시 실행함수 형태로 변환됩니다. 
Object.defineProperty() 메소드는 객체에 새로운 속성을 정의할 때 사용하거나 
이미 존재하는 객체를 수정하는 역할을 합니다. 
enumerable 속성은 객체의 키를 열거할 수 있는지에 대한 설정으로 true면 키를 열거할 수 있다는 의미입니다. 
configurable 속성은 해당 속성의 제거 여보 설정으로 true면 특정 속성을 새롭게 정의하거나 삭제할 수 있습니다. 


7.3.5 정적 변수와 정적 메소드

* 싱글턴 패턴에 static 적용하기
- 부지런한 초기화 (eager initialization)
: 프로그램이 구동할 때 초기화가 일어나고
공개된 정적 메소드를 통해 생성된 객체를 얻습니다.
싱글턴 객체는 사용자가 정의한 임의의 변수에 할당되 접근할 수 있습니다.

let eagerLogger: EagerLogger = EagerLogger.getLogger();

- 게으른 초기화 (lazy initilaization)
: 프로그램이 구동할 때 초기화 되지 않지만 공개된 정적 메소드를 호출하는 시점에 객체를 생성합니다. 

---------------------------------------------------------
class EagerLogger {
    // 객체 생성시 초기화
    private static uniqueObject: EagerLogger = new EagerLogger();
    // 생성자에 private을 붙여 객체로 생성되지 않도록 함
    private constructor() { }

    public static getLogger(): EagerLogger {
        return this.uniqueObject;
    }
    public info(message: string) {
        console.log(`[info] ${message}`);
    }
    public warning(message: string) {
        console.log(`[warn] ${message}`);
    }
}
---------------------------------------------------------

객체 생성시 초기화

---------------------------------------------------------
class LazeLogger {
    private static uniqueObject: LazeLogger;
    private LazeLogger() { }
    // 함수 호출시 초기화
    public static getLogger(): LazeLogger {
        if (this.uniqueObject == null) {
            this.uniqueObject = new LazeLogger();
        }
        return this.uniqueObject;
    }

    public info(message: string) {
        console.log(`[info] ${message}`);
    }
    public warning(message: string) {
        console.log(`[warn] ${message}`);
    }
}
---------------------------------------------------------

함수 호출시 초기화


7.3.6 readonly 

* readonly vs const 비교

- const
const test: number = 5;
1) 초기화 필수
2) 값 재할당 불가능
3) 상수
4) 컴파일 후 선언이 유지됨

- readonly
readonly test2: String;
test2 = "";
1) 초기화는 선택 (선언만 가능)
2) 값 재할당 가능
3) 읽기 전용 속성
4) 컴파일 후 선언이 사라짐

readonly는 인터페이스의 멤버변수, 클래스의 멤버변수에 사용할 수 있습니다. 

readonly는 인터페이스나 클래스의 멤버변수, 객체 리터럴의 속성이름에 선언할 수 있습니다.
readonly의 가장 중요한 특징은 초기화를 강제하지 않는다는 점입니다. 
그러나 어떠한 값을 할당해 변수가 초기화돠면 재할당이 불가능합니다. 
이는 클래스 멤버 변수, 객체 리터럴의 속성에도 동일하게 적용됩니다. 

객체리터럴에 타입이 지정됐고, 속성에 readonly가 선언되어있다면 
해당 속성에 어떠한 값도 재할당할 수 없습니다.

let literalObj: { readonly alias: string } = { alias: "happy" }

'프로그래밍 > TypeScript' 카테고리의 다른 글

09. 고급 타입  (0) 2019.10.16
08.모듈  (0) 2019.10.16
07. 클래스와 인터페이스  (0) 2019.10.15
06. 함수  (0) 2019.10.15
04. 조건문, 제어문 / 05. 연산자  (0) 2019.10.15