02. 다트 객체지향 프로그래밍
- 다트 언어는 높은 완성도로 객체지향 프로그래밍을 지원함.
- 플러터 역시 객체지향 프로그래밍 중심으로 설계된 프레임워크.
2.1. 객체지향 프로그래밍의 필요성
- Map을 사용하면 단순히 값을 저장하는 것 외에 추가적인 편의기능을 구현할 수 없음.
- 클래스를 만들어 사용하면 필요한 값들만 입력하도록 제한하고 클래스에 특화된 하뭇들을 선언할 수 있음.
- 클래스는 일종의 설계또로서 데이터가 보유할 속성과 기능을 정의하는 자료구조.
- 클래스가 설계도, 인스터스화가 실물(실제 사용할 수 있는 데이터 생성).
2.2. 클래스
- 객체지향 프로그래밍의 기본 = 클래스(class)
- 예제:
class Idol {
String name = 'BANDIBOODI'; // 종속된 변수 = 멤버 변수,
void sayName() { // 종속된 함수 = 메서드
// 스코프 안에 값은 속성 이름이 하나만 존재한다면 this 생략 가능.
print('저는 ${this.name}입니다.');
}
}
void main() {
Idol bandiboodi = Idol();
bandiboodi.sayName(); // 저는 BANDIBOODI입니다.
}
- 함수는 메서드를 포함하는 더 큰 개념임.
- 클래스에 정의된 함수인 메서드는 클래스의 기능을 정의한 함수
생성자
- constructor = 클래스의 인스턴스를 생성하는 메서드.
class Idol {
final String name;
Idol(String name) : this.name = name;
}
// 생성자의 매개변수를 변수에 저장하는 과정을 생략할 수도 있음.
class Idol2 {
final String name;
// this를 사용할 경우, 해당되는 변수에 자동으로 매개변수가 저장됨.
Idol(this.name);
}
- 생성자에서 입력받을 변수는 일반적으로 final로 선언함 (인스턴스화한 다음에 변수의 값을 변경하지 못하도록 하기 위해.)
네임드 생성자
- named constructor = named parameter와 비슷한 개념.
- 일반적으로 클래스를 생성하는 여러 방법을 명시하고 싶을 때 사용.
class Idol {
final String name;
final int membersCount;
Idol(String name, int membersCount)
: this.name = name,
this.membersCount = membersCount'
// 네임드 생성자
Idol.fromMap(Map<String, dynamic> map)
: this.name = map['name'],
this.membersCount = map['membersCount'];
}
프라이빗 변수
- 다트에서의 private variable은 다른 언어와 정의가 조금 다름.
- 일반적으로 private variable은 class 내부에서만 사용하는 변수를 칭하지만, 다트 언어에서는 같은 파일에서만 접근 가능한 변수임.
class Idol {
// _ 로 변수명을 시작하면 private variable을 선언할 수 있음.
String _name;
Idol(this._name);
}
void main() {
Idol bandiboodi = Idol('BANDIBOODI');
// 같은 파일에서는 _name 변수에 접근할 수 있지만,
// 다른 파일에서는 _name 변수에 접근할 수 없음.
print(bandiboodi._name); // BANDIBOODI
}
Getter / Setter
- 최근에는 객체지향 프로그래밍을 사용할 때 변수의 값에 불변성(immutable: 인스턴스화 후 변경할 수 없는)을 특성으로 사용하기 때문에 Setter는 거의 사용하지 않음.
class Idol {
String _name = 'BANDIBOODI';
String get name {
return this._name;
}
// Setter는 매개변수로 딱 하나의 변수를 받을 수 있음.
set name(String name) {
this._name = name;
}
}
void main() {
Idol bandiboodi = Idol();
bandiboodi.name = 'test'; // setter 사용.
print(bandiboodi.name); // getter 사용. 결과값: test
}
2.3. 상속
- extends 키워드를 사용해 상속(inheritance)할 수 있음.
- 상속은 어떤 클래스의 기능을 다늘 클래스가 사용할 수 있게 하는 기법.
class BoyGroup extends Idol {
// 상속받은 생성자
BoyGroup(
String name,
int membersCount,
) : super(
name,
memberCount,
);
}
- super는 상속한 부모 클래스를 지칭
- 부모 클래스에 기본 생성자가 있기때문에 BoyGroup에서는 Idol 클래스의 생성자를 실행하는 구조.
2.4. 오버라이드
- override = 부모 클래스 또는 interface에 정의된 메서드를 재정의할 때 사용됨.
- 다트에서는 override 키워드를 생략할 수 있기 때문에 override 키워드를 사용하지 않고도 메서드를 재정의할 수 있음.
class GirlGroup extends Idol {
// 생성자의 매개변수로 직접 super 키워드를 사용할 수도 있음.
GirlGroup(
super.name,
super. membersCount,
);
@override
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
}
- 한 클래스에 이름이 값은 메서드가 존재할 수 없기 때문에 부모 클래스나 인터페이스에 이미 존재하는 메서드명을 입력하면 override 키워드를 생략해도 메서드가 덮어써짐.
- 하지만 직접 명시하는 게 협업 및 유지보수에 유리함.
2.5. 인터페이스
- 상속은 공유되는 기능을 이어받는 개념.
- interface = 공통으로 필요한 기능을 정의만 해두는 역할.
- 다트에는 interface를 지정하는 키워드가 따로 없음.
- 상속은 단하나의 클래스만 할 수 있지만 인터페이스는 적용 개수에 제한이 없음.
class GirlGroup implements Idol {
final String name;
final int membersCount;
GirlGroup(
this.name,
this.membersCount,
);
void sayName() {
// ...
}
void sayMembersCount() {
// ...
}
}
- 반드시 재정의할 필요가 있는 기능을 정의하는 용도로 인터페이스 사용. 실수로 빼먹는 일을 방지.
2.6. 믹스인
- mixin = 특정 클래스에 원하는 기능들만 골라 넣을 수 있는 기능.
- 특정 클래스를 지정해서 속성들을 정의할 수 있으며, 지정한 클래스를 상속하는 클래스에서도 사용할 수 있음.
- 한 개의 클래스에 여러 개의 믹스인을 적용할 수도 있음.
mixin IdolSingMixin on Idol {
void sing() {
print('${this.name}가 노래르 부른다.');
}
}
// mixin을 적용할 때는 with 키워드 사용
class BoyGroup extends Idol with IdolSingMixin {
BoyGroup(
super.name,
super.membersCount,
);
}
void main() {
BoyGroup bandi = BoyGroup('BANDI', 6);
// 믹스인에 정의된 sing() 함수 사용 가능
bandi.sing(); // BANDI가 노래를 부른다.
}
2.7. 추상
- abstract = 상속이나 인터페이스로 사용하는 데 필요한 속성만 정의하고 인스턴스화할 수 없도록 하는 기능.
- 클래스를 인터페이스화할 일이 없다면, 클래스를 추상 클래스로 선언해서 해당 클래스의 인스턴스화를 방지하고 메서드 정의를 자식 클래스에 위임하는 방식.
- 추상 크랠스는 추상 메서드를 선언.
- 추상 메서드는 함수의 반환 타입, 이름, 매개변수만 정의.
abstract class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName();
void sayMembersCount();
}
2.8. 제네릭
- generic = 객체지향 프로그래밍에서 가장 아름다운(?) 기능.
- 제네릭은 클래스나 함수의 정의 → 클래스를 선언할 때 X, 인스턴스화하거나 실행할 때로 미룬다.
- 특정 변수의 타입을 하나의 타입으로 제한하고 싶지 않을 때 자주 사용.
- 예를 들어, 정수를 받는 함수, 문자열을 받는 함수를 각각
setInt()
,setString()
처럼 따로 만들지 않아도, 제네릭을 사용해set()
함수 하나로 여러 자료형을 입력받게 처리할 수 있음. - Map, List, Set 등에서 사용한
<>
사이에 입력되는 값이 제네릭 문자임.
// 인스턴스화할 때 입력받을 타입을 T로 정의함.
class Cache<T> {
// data의 타입을 추후 입력될 T 타입으로 지정함.
final T data;
Cache({
required this.data,
});
}
void main() {
// T의 타입을 List<int>로 입력함.
final cache = Cache<List<int>>(
data: [1,2,3],
);
// 제네릭에 입력된 값을 통해 data 변수의 타입이 자동으로 유추됨.
print(cache.data.reduce((value, element) => value + element)); // 6
}
- 일반적으로 개발자들이 사용하는 제네릭 문자들
- T: 변수 타입 표현
- E: 리스트 내부 요소들의 타입을 표현
- K: 키를 표헌
- V: 값을 표현
2.9. Static
- static = 클래스의 인스턴스에 귀속되지 않고, 클래스 자체에 귀속됨.
class Counter {
static int i = 0;
Counter() {
print(i++);
}
}
void main() {
Counter(); // 1
Counter(); // 2
Counter(); // 3
}
- 정적 변수 i는 Counter class에 귀속되기 때문에 instance를 호출할 때마다 1 씩 증가됨.
- 따라서, static 키워드는 인스턴스끼리 공유해야하는 정보에 지정.
2.10. cascade operator
- cascade operator = 인스턴스에서 해당 인스턴스의 속성이나 맴버 함수를 연속해서 사용하는 기능.
- 사용 기호는
..
, 장점은 더 간결한 코드 작성 가능.
void main() {
// cascade operator를 사용하면 선선한 변수의 메서드를 연속으로 실행 가능.
Idol bandiboodi = Idol('BANDIBOODI', 13)
..sayName() // 저는 BANDIBOODI입니다.
..sayMembersCount(); // BANDIBOODI 멤버는 13명입니다.
}
핵심 요약:
- Class 키워드를 사용해서 클래스를 선언할 수 있음.
- 클래스를 인스턴스화하면 클래스의 인스턴스를 변수로 지정할 수 있음.
- 상속받으면 부모 클래스의 모든 속성을 물려받음.
- extends 키워드 사용
- 하나의 자식 클래스는 하나의 부모 클래스만 상속 받을 수 있음.
- Override는 이미 선언되어 있는 속성을 덮어쓰는 기능.
- Interfac는 클래스의 필수 속성들을 정의하고 강제할 수 있는 기능.
- Mixin은 상속처럼 모든 속성을 물려받지 않고 원하는 기능만 골라서 적용할 수 있음.
- with 키워드를 사용해서 믹스인을 적용
- 하나의 클래스에 여러 개의 믹스인을 적용할 수 있음.
- Generic은 변수 타입의 정의를 인스턴스화까지 미룰 수 있음.
- Static은 클래스에 직접 귀속되는 속성.
- Cascade 연산자는 인스턴스에서 해당 인스턴스의 속성이나 멤버 함수를 연속해서 호출할 때 사용.