상속, 추상 클래스, 인스턴스, 믹스인

휘Bin·2023년 6월 27일
0
post-thumbnail

상속

'상속'은, 클래스를 재활용하는 객체지향 프로그래밍의 핵심 기능이다.
여기서 기존 클래스를 '부모 클래스', 상속받은 새 클래스를 '자식 클래스'라고 한다.
다트도 상속을 제공하고 큰 차이는 없다. 또한, 다트에서 클래스를 선언할 때 어떤 클래스를 상속받기 위해서는 'extends 예약어'를 사용한다.

class AClass{
	int number = 3;
    void growth(){
    	print('thanks');
    }
}

class BClass extends AClass{
}

main(List<String> args){
	var obj = BClass();
    obj.growth();
    print('i told you : ${obj.number}');
}

위 코드에서 BClass는 AClass를 상속받아 선언했다. 그러면 BClass의 객체로 AClass에 선언된 멤버를 사용할 수 있다.

이렇게 클래스를 상속받으면 부모 클래스에 선언된 멤버를 자식 클래스에서 그대로 사용할 수 있고, 원한다면 재정의도 할 수 있다. 이를 '오버라이딩' 이라 한다.

class AClass{
	int number = 3;
    void growth(){
    	print('thanks');
    }
}

class BClass extends AClass{
	int number = 9;
    void growth(){
    	print('thanks2');
    }
}

main(List<String> args){
	var obj = BClass();
    obj.growth();
    print('i told you : ${obj.number}');
}

위는 부모 클래스의 멤버를 자식 클래스에서 재정의한 예시이다. 이러면 부모 클래스에 선언된 멤버는 자식 더이상 자식 클래스에 상속되지 않는다. 즉, 자식 클래스의 객체는 자신의 멤버에 접근하게 된다.

그래서 위 코드를 실행하면 thanks2, 9 가 나온다.

만약, 자식 클래스에서 부모 클래스에 선언된 멤버를 재정의할 때, 부모 클래스에 선언된 똑같은 이름의 멤버를 이용하고 싶다면 'super 예약어'를 사용한다.

class AClass{
	int number = 3;
    void growth(){
    	print('thanks');
    }
}

class BClass extends AClass{
	int number = 9;
    void growth(){
   		super.growth();
    	print('#{number}, ${super.number}');
    }
}

main(List<String> args){
	var obj = BClass();
    obj.growth();
}

이러면 thanks 9 3 이 출력이된다.

부모 생성자 호출하기

클래스 상속에서 매우 중요한 부분이 있다. 바로 '부모 클래스의 생성자 호출'이다.
자식 클래스의 객체를 생성할 때 자신의 생성자가 호출되는데, 이 때 부모 클래스의 생성자도 반드시 호출되게 해줘야 한다.

위에 예시에서 오류가 안났던 이유는, 컴파일러가 자동으로 부모 클래스의 기본 생성자를 호출해줬기 때문이다.

부모 클래스의 생성자를 호출하려면 super()문을 사용한다.

하지만 만약 부모 생성자가 매개변수나 명명된 생성자를 가진다면, supuer()문을 생략하면 안 되고 반드시 그에 맞는 생성자를 호출해줘야 한다.

class AClass{
	AClass(int arg){}
    AClass.study(){}
}

class BClass extends BClass{
	BClass() : super(){}
}

위와 같이 호출하면 오류가 난다. 어떤 생성자를 호출해야 할지 모르기 때문이다.
컴파일러 자동으로 추가하는 super()코드는 매개변수가 없고 부모 클래스 이름으로 선언된 기본 생성자만 호출한다.

class AClass{
	AClass(int arg){}
    AClass.study(){}
}

class BClass extends BClass{
	BClass() : super(3){}
    BClass.name() : super.study(){}
}

위와 같이 부모 클래스의 생성자 사양에 맞게 super()문을 써줘야 한다.

부모 클래스 초기화

객체를 생성할 때 전달받은 값으로 클래스의 멤버 변수를 초기화할 때는 생성자의 매개변수에 this를 사용한다.

아래와 같이 말이다.

class Student{
	String name;
    int age;
    Student(this.name, this,age);
}

만약, 생성자의 매개변수로 부모 클래스에 선언된 멤버를 초기화해야 할 때는, 부모 클래스의 생성자를 호출하는 super() 구문에 매개변수값을 전달하면 된다.

Class AClass{
	String name;
    int age;
    AClass(this.name, this.age){}
}

class BClass extends AClass{
	BClass(super.name, super.age);
}

즉, 생성자의 매개변수에 super로 부모 클래스의 멤버를 작성하면 해당 값으로 부모 클래스의 생성자가 호출되어 멤버 변수가 초기화되는 것이다.

추상 클래스

상속이 다른 클래스의 멤버를 그대로 사용하는 방법이라면,
추상클래스나 인터페이스를 이용하면 다른 클래스의 멤버를 그대로 사용하지 않고 새로 구현하게 할 수 있다.

'추상 클래스'는 추상 함수만 제공하여 상속받는 클래스에서 반드시 재정의해서 사용하게 강제하는 방법이다. '추상 함수'는 실행문이 작성된 본문이 없는 함수를 의미한다.

본문을 생략한 추상 함수를 선언할 때는 함수가 속한 클래스에 'abstract 예약어'를 붙여 추상 클래스로 선언해야 한다. 즉, 추상 함수는 abstract를 붙인 추상 클래스에서만 선언할 수 있다.

abstract class Student{
	void stduy();
}

또한, 추상 클래스는 객체를 생성할 수 없다. 말한대로 추상 클래스는 자식 클래스에게 함수를 재정의해서 사용하라고 강제하는 수단이다. 그러니 당연히 추상 클래스를 상속받은 자식 클래스에서는 추상 함수를 재정의 해줘야 한다.

인터페이스(interface)

'인터페이스'란, 부모의 특징을 도구로 사용해 새로운 특징을 만들어 사용하는 객체지향 프로그래밍 방법이다. 하지만 다트에서는 interface 예약어를 지원하지 않는다.

같은 객체지향언어인 java는 interface라는 예약어를 지원하고 implements라는 예약어로 인터페이스를 구현하는 클래스를 선언한다. 하지만 다트는 그렇지 않다.
즉, 다트에서는 implements만 지원하고 interface는 지원하지 않는다.

하지만 당연히 인터페이스를 명시적으로 선언하지 않아도 다른 클래스를 도구 삼아 구현하는 방법을 제공한다. 이를 '암시적 인터페이스' 라고 한다.

'암시적 인터페이스'란, 클래스 자체가 인터페이스라는 의미이다.
즉, 클래스를 implements로 선언하면, 다른 클래스를 인터페이스로 활용할 수 있는 것이다.


class Students{
	
    String name;
    int age;
    
    Students(this.name, this.age);
    
    String study(int page) => 'i solved $page';
}

class School implements Students{
}

어? 위와 같이 했는데 오류가 난다.
이유는, 클래스에 implements를 추가해 어떤 클래스를 구현하는 클래스가 되면 대상 클래스에 선언된 모든 멤버를 재정의해야 하기 때문이다.
따라서 아래와 같이 바꿔야 한다.


class Students{
	
    String name;
    int age;
    
    Students(this.name, this.age);
    
    String study(String book) => 'i solved $book';
}

class School implements Students{

	String name = 'hwi';
    int age = 20;
    
    @override
    String study(String book){
    	return 'yanado';
    }

}

하나의 클래스에 여러 인터페이스를 지정해서 선언할 수도 있다.

믹스인 Mixin

일반 클래스는 class 라는 예약어로 선언하고 변수와 함수, 생성자를 정의한다. 하지만 믹스인은 mixin 예약어로 선언하다.

하지만 큰 특징은, 변수와 함수를 선언할 수 있지만, 클래스가 아니므로 생성자는 선언할 수 없다는 것이다.

당연히 믹스인은 생성자를 가질 수 없기에 객체를 생성할 수도 없다.

믹스인 사용

생성자도, 객체도 가질 수 없는데 왜 필요할까?
대부분의 객체지향 언어는 '다중 상속'을 지원하지 않는다. 당연히 다트도 마찬가지이다.
즉, 하나의 클래스만 상속 받을 수 있다.

하지만 여러 클래스에 선언된 멤버를 상속한 것처럼 이용하고 싶을 때! 그 때 믹스인을 사용한다.

믹스인을 사용하면 클래스를 상속하지 않고도 믹스인에 선언한 멤버를 다른 클래스에서 사용할 수 있다. 이 때는 'with 예약어'를 사용한다.

아래와 같이 말이다.


mixin MixinPrac{
	int mixinNumber = 3;
    void mixinTest(){
    	print('thanks');
    }
}

class Prac with MixinPrac{
	void test(){
    	print('number : $mixinNumber');
        mixinTest();
    }
}

위처럼 다트는, 다중 상속을 지원하지 않는 대신에 다른 클래스에서 사용할 수 있는 멤버를 선언할 수 있도록 믹스인을 제공한다.

믹스인의 사용 제약

믹스인은 모든 클래스에서 with 예약어로 사용할 수 있다. 하지만 특정 타입의 클래스에서만 사용하도록 제한을 둘 수도 있다.
이 때는 믹스인 선언부에 on 예약어로 그 타입을 지정한다.

아래 코드를 보면, mixinPrac에 on으로 allTest를 지정했다. 그러니 allTest를 상속받은 someTest에서는 사용할 수 있지만, almostTest에서는 사용할 수 없는 것이다.


mixin mixinPrac on allTest{
}

class allTest{
}

class someTest extends allTest with mixinPrac{
}

class almostTest with mixinPrac{ => 오류
} 

with과 class

with 예약어는 믹스인을 사용할 때 사용한다. 하지만 클래스에도 with을 이용 할 수 있다.


class AClass{
	int number=3;
}

class BClass with AClass{
	void classTest(){
    	print('result : ${number}');
    }
]

위와 같이 with으로 연결했으므로 BClass에서 AClass 멤버를 이용할 수 있다.

보통 클래스는 extends로 상속받아 이용하지만 믹스인처럼 with으로도 이용할 수 있다.
즉, 어떤 클래스에서 다른 클래스나 믹스인에 선언된 멤버를 사용할 때 with을 사용할 수 있는 것이다.

그렇다면 mixin과 class는 무슨 차이가 있을까? 이미 설명했지만 큰 차이가 있다. 바로 클래스는 객체를 사용할 수 있지만, 믹스인은 객체를 생성할 수 없다. 결국, 믹스인은 독립적으로 이용할 수 없으며 다른 클래스에서 공통으로 사용할 변수나 함수를 믹스인에 담아 두고 필요한 클래스에서 with으로 연결해서 사용하는 것이다.

또한, with 예약어로 사용할 클래스에서는 생성자를 선언할 수도 없다.

그러니 아래와 같은 코드는 오류가 나는 것이다.


class ACalss{
	AClass(){}
}

class BClass with AClass{
}
profile
One-step, one-step, steadily growing developer

0개의 댓글