🔥 객체지향 프로그래밍(Object-Oriented Programming, OOP)
1. 클래스(Class)
Dart 언어는 객체지향 프로그래밍이므로 아래와 같이 클래스라는 개념이 사용된다.
- 클래스(class) : 데이터가 가지는 속성과 기능을 정의
- 인스턴스(instance) : 클래스를 이용하여 객체 선언시, 해당 객체를 인스턴스라고 함
- Dart에서 클래스를 정의할 경우 아래와 같이 생성이 가능하다.
- 인스턴스를 생성할 경우에는 함수를 실행할 때와 같이 인스턴스화하고 싶은 클래스명 뒤에 괄호()를 붙여준다.
class Fruit { // 클래스명 지정
String name = '사과'; // 클래스 종속변수 지정
void sayName() { // 클래스 메서드(종속함수) 지정
print('이름 : ${this.name}'); // 클래스 내부 속성 지칭 → this (this.name → 클래스 내 name변수 지칭)
print('이름 : $name'); // 클래스 내부 같은 이름 하나만 존재시 this 생략
}
}
void main() {
Fruit apple = Fruit(); // 변수타입을 Fruit으로 지정하고 해당 클래스의 인스턴스를 생성
apple.sayName(); // 메서드 실행
}
1) 생성자(Constructor)
생성자는 클래스의 인스턴스를 생성하는 메서드이다.
아래와 같이 하나의 클래스만으로 여러 개의 해당 클래스의 인스턴스를 생성할 수 있다.
class Fruit {
final String name; // 생성자에서 입력받는 변수는 보통 final이 붙는다.
Fruit(String name) : this.name = name; // 생성자 선언(클래스와 같은 이름)
// 또는 Fruit(this.name)와 같이 매개변수를 변수에 저장하는 과정 생략 가능
void sayName() {
print('이름 : ${this.name}');
}
}
void main() {
// name에 '사과' 저장
Fruit apple = Fruit('사과');
apple.sayName();
// name에 '오렌지' 저장
Fruit orrange = Fruit('오렌지');
orrange.sayName();
}
2) 네임드 생성자(Named constructor)
- 클래스를 생성 방법을 다양하게 명시하고 싶을 때 사용
class Fruit {
final String name;
final int fruitsCount;
Fruit(String name, int fruitsCount) // 생성자 선언
: this.name = name,
this.fruitsCount = fruitsCount;
Fruit.fromMap(Map<String, dynamic> map) // 네임드 생성자 선언
: this.name = map['name'],
this.fruitsCount = map['fruitsCount'];
void sayName() {
print('이름 : ${this.name}, 개수 : ${this.fruitsCount}개');
}
}
void main() {
Fruit apple = Fruit('사과', 4); // 기본 생성자 사용시
apple.sayName();
Fruit orrange = Fruit.fromMap({ // 네임드 생성자 fromMap 사용시
'name': '오렌지',
'fruitsCount': 7,
});
orrange.sayName();
}
3) 프라이빗 변수(Private Variable)
- 같은 파일에서만 접근 가능한 변수를 생성할 때 사용
class Fruit {
String _name; // 프라이빗 변수 선언 : 변수명을 '_'로 시작
Fruit(this._name);
}
void main() {
Fruit apple = Fruit('사과');
print(apple._name);
}
4) 게터(getter) / 세터(setter)
- 값을 가져오거나 지정할 때, 어떤 값을 어떤 형태로 노출하며, 어떤 변수를 변경 가능하게 할 지 지정
- 게터 : 값을 가져올 때 사용, 세터 : 값을 지정할 때 사용
*보통 객체지향 프로그래밍에서는 변수값을 '불변성' 특성으로 사용하여 세터는 거의 사용되지 않고 있음
class Fruit {
String _name= '사과' ;
String get name { // 게터 명시(get 키워드, 매개변수 없음)
return this._name;
}
set name(String name) { // 세터 명시(set 키워드, 매개변수 하나 받음)
this._name = name;
}
}
void main() {
Fruit apple = Fruit();
apple.name = '오렌지'; // 세터
print(apple.name); // 게터
}
// 실행결과
// 오렌지
2. 상속(Inheritance)
- 상속은 특정 클래스의 기능을 다른 클래스에서도 사용할 수 있도록 하는 기법을 말한다.
- 상속은 extends 키워드를 사용하여 {class 자식클래스 extends 부모클래스} 순서로 지정한다.
- 자식클래스는 부모클래스의 모든 기능을 상속받는다.
- super는 상속한 부모 클래스를 지칭한다.
class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
class BoyGroup extends Idol { // 클래스 상속
// 상속받은 생성자
BoyGroup(String name, int membersCount,) : super(name, membersCount,);
// 상속받지 않은 기능
void sayMale() {
print('저는 남자 아이돌입니다.');
}
}
void main() {
BoyGroup bts = BoyGroup('BTS', 7); // 생성자로 객체 생성
bts.sayName(); // 부모 클래스에서 상속받은 메서드
bts.sayMembersCount(); // 부모 클래스에서 상속받은 메서드
bts.sayMale(); // 자식 클래스에서 생성한 메서드
}
// 실행결과
// 저는 BTS입니다.
// BST 멤버는 7명입니다.
// 저는 남자 아이돌입니다.
3. 오버라이드(Override)
- 부모 클래스 또는 인터페이스에 정의된 메서드를 재정의할 때 사용한다. (override 키워드는 생략 가능)
class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
class GirlGroup extends Idol {
GirlGroup(
super.name,
super.membersCount,
);
@override // override 키워드를 사용하여 sayName 메서드 override
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
}
void main() {
GirlGroup redVelvet = GirlGroup('블랙핑크', 4);
redVelvet.sayName(); // 자식 클래스에서 override된 메서드
redVelvet.sayMembersCount(); // 부모 클래스의 메서드
}
// 실행결과
// 저는 여자 아이돌 블랙핑크입니다.
// 블랙핑크 멤버는 4명입니다.
4. 인터페이스(Interface)
- 인터페이스는 공통으로 필요한 기능을 정의하는 역할을 한다.
- 인터페이스를 지정하는 키워드는 따로 없으며, 클래스를 인터페이스로 사용하려면 implement 키워드를 사용한다.
- 여러 인터페이스를 적용할 때에는 콤마(,)를 사용해 인터페이스를 나열하여 사용하면 된다.
- 클래스와 차이점은, 상속받을 때는 부모 클래스의 모든 기능이 상속되므로 재정의할 필요가 없지만, 인터페이스는 반드시 모든 기능을 다시 정의해주어야 한다.
class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
class GirlGroup implements Idol { // implements : 원하는 클래스를 인터페이스로 사용
final String name;
final int membersCount;
GirlGroup(
this.name,
this.membersCount,
);
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
GirlGroup redVelvet = GirlGroup('블랙핑크', 4);
redVelvet.sayName();
redVelvet.sayMembersCount();
}
// 실행결과
// 저는 여자 아이돌 블랙핑크입니다.
// 블랙핑크 멤버는 4명입니다.
5. 믹스인(Mixin)
- 특정 클래스에 원하는 기능들만 골라 넣는 기능이다.
- mixin 믹스인이름 on 클래스명 으로 원하는 기능들로 이루어진 믹스인을 생성하고,
- class 자식클래스명 extends 부모클래스명 with 믹스인명 과 같이 with 키워드로 믹스인을 사용할 수 있다.
class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
mixin IdolSingMixin on Idol{ // mixin
void sing(){
print('${this.name}가 노래를 부릅니다.');
}
}
class BoyGroup extends Idol with IdolSingMixin{ // mixin 적용시 with 키워드 사용
BoyGroup(
super.name,
super.membersCount,
);
void sayMale() {
print('저는 남자 아이돌입니다.');
}
}
void main(){
BoyGroup bts = BoyGroup('BTS', 7);
bts.sing(); // 믹스인에 정의된 sing() 함수 사용 가능
}
// 실행결과
// BTS가 노래를 부릅니다.
6. 추상(abstract class)
- 추상은 부모 클래스를 인스턴스화할 필요는 없으나, 자식 클래스들에 공통적으로 정의되어야 하는 메서드가 존재할 때 사용한다.
- 추상 클래스는 abstract 키워드를 사용하여 지정한다.
abstract class Idol { // abstract 키워드를 사용해 추상 클래스 지정
final String name;
final int membersCount;
Idol(this.name, this.membersCount); // 생성자 선언
void sayName(); // 추상 메서드 선언
void sayMembersCount(); // 추상 메서드 선언
}
class GirlGroup implements Idol { // implements 키워드를 사용해 추상 클래스를 구현하는 클래스
final String name;
final int membersCount;
GirlGroup(
this.name,
this.membersCount,
);
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
GirlGroup redVelvet = GirlGroup('블랙핑크', 4);
redVelvet.sayName();
redVelvet.sayMembersCount();
}
// 실행결과
// 저는 여자 아이돌 블랙핑크입니다.
// 블랙핑크 멤버는 4명입니다.
7. 제네릭(Generic)
- 특정 변수의 타입을 하나로 제한하고 싶지 않을 때 자주 사용됨
(ex. setInt(), setString()처럼 따로 만들지 않고 제네릭을 사용해 set() 함수 하나로 여러 자료형을 입력받게 처리)
- 클래스나 함수의 정의를 선언할 때가 아닌 실행할 때로 미루는 기능
class Cache<T> { // 인스턴스화할 때 입력받을 타입을 T로 지정
final T data; // data의 타입을 T 타입으로 지정
Cache({
required this.data,
});
}
void main() {
final cache = Cache<List<int>>( // T의 타입을 List<int>로 입력
data: [1,2,3],
);
// 제네릭에 입력된 값을통해 data 변수의 타입이 자동으로 유추
print(cache.data.reduce((value, element) => value + element));
}
// 실행결과
// 6
* 흔히 사용되는 제네릭 문자
· T : 변수 타입 (ex. T value;)
· E : 리스트 내부 요소들의 타입 (ex. List<E>)
· K : 키를 표현할 때 사용 (ex. Map<K, V>)
· V : 값을 표현할 때 사용 (ex. Map<K, V>)
8. 스태틱(Static)
- 변수와 메서드 등 속성을 '클래스가 생성한 인스턴스'가 아닌 '클래스 자체'에 귀속되도록 할 경우 사용
class Counter{
static int i= 0; // static 변수 선언
Counter(){
i++;
print(i++);
}
}
void main() {
Counter count1 = Counter();
Counter count2 = Counter();
Counter count3 = Counter();
}
// 실행결과
// 1
// 2
// 3
9. 캐스케이드 연산자(Cascade Operator)
- 캐스케이드 연산자는 .. 기호를 사용하여 인스턴스에서 해당 인스턴스의 속성이나 멤버 함수를 연속해서 사용하게 한다.
class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
// cascade operator (..) : 선언한 변수의 메서드를 연속으로 실행
Idol blackpink= Idol('블랙핑크', 4)
..sayName()
..sayMembersCount();
}
// 실행결과
// 저는 블랙핑크입니다.
// 블랙핑크 멤버는 4명입니다.
'DEV > dart & flutter' 카테고리의 다른 글
다트 비동기 프로그래밍 Future, async/await, Stream (1) | 2024.01.01 |
---|---|
Dart 기본 문법 정리 (0) | 2023.12.27 |
댓글