Dart - 객체 지향 프로그래밍
본 내용은 인프런 중 [코드팩토리] [입문] Dart 언어 4시간만에 완전정복 강의 내용을 정리한 내용입니다.
Object Oriented Programming
Object Oriented Programming(객체 지향 프로그래밍)은 Class 내 변수와 함수를 지정한 프로그래밍으로, Class로 결과물을 만들어내는 것은 Instance이다. 아래와 같이 예시를 보면서 진행해보자.
void main() {
Idol blackPink = Idol();
print(blackPink.name);
print(blackPink.members);
blackPink.sayHello();
blackPink.introduce();
}
// Idol Class 생성
class Idol {
String name = '블랙핑크'; // name(이름) - 변수
List<String> members = ['지수', '제니', '리사', '로제']; // members(멤버들) - 변수
void sayHello(){
print('안녕하세요 블랙핑크입니다.'); // sayHello(인사) - 함수
}
void introduce(){
print('저희 멤버는 지수, 제니, 리사, 로제가 있습니다.'); // introduce(멤버소개) - 함수
}
}
Console 출력창
블랙핑크
[지수, 제니, 리사, 로제]
안녕하세요 블랙핑크입니다.
저희 멤버는 지수, 제니, 리사, 로제가 있습니다.
Constructor
Class를 생성하고 그에 맞는 함수들을 지정해주어서 표현할 수 있다. 여러 개의 인스턴스를 생성해보기 전에 손봐야할 곳이 있다. 아이돌 그룹은 블랙핑크만 있는 것이기 아니기 때문에 아이돌 명에 따라서 멤버와 멤버 이름, 그리고 출력하는 대사들을 변경해줘야 한다. 이를 생성자 기능을 이용하여 변경할 수 있다.
void main() {
Idol blackPink = Idol(
'블랙핑크',
['지수', '제니', '리사', '로제'],
); // Constructor
print(blackPink.name);
print(blackPink.members);
blackPink.sayHello();
blackPink.introduce();
print('----------------');
Idol newjeans = Idol.fromList([
['하니', '해린', '민지', '혜인', '다니엘'],
'Newjeans',
]
); // named Constructor
print(newjeans.name);
print(newjeans.members);
newjeans.sayHello();
newjeans.introduce();
}
// Idol Class 생성
class Idol {
String name;
List<String> members;
Idol(this.name, this.members); // Constructor. parameter로 그룹명과 멤버를 입력 받음.
Idol.fromList(List values) // named Constructor.
: this.members = values[0],
this.name = values[1]; // Constructor의 경우, 본인이 사용하기 편한 방식으로 사용하면 된다.
void sayHello(){
print('안녕하세요 ${this.name}입니다.'); // sayHello(인사) - 함수
}
void introduce(){
print('저희 멤버는${this.members}가 있습니다.'); // introduce(멤버소개) - 함수
}
}
Console 출력창
블랙핑크
[지수, 제니, 리사, 로제]
안녕하세요 블랙핑크입니다.
저희 멤버는[지수, 제니, 리사, 로제]가 있습니다.
----------------
Newjeans
[하니, 해린, 민지, 혜인, 다니엘]
안녕하세요 Newjeans입니다.
저희 멤버는[하니, 해린, 민지, 혜인, 다니엘]가 있습니다.
immutable 프로그래밍
immutable 프로그래밍은 한번 선언한 변수들을 변경이 불가능하다. 아이돌들의 그룹명은 정해져있기 때문에 임의로 변경하면 곤란하다. 이러한 비슷한 이유들로 사용된다. 방법은 간단하다. final 또는 const 기능을 사용하면 된다.
void main() {
Idol blackPink = const Idol(
'블랙핑크',
['지수', '제니', '리사', '로제'],
); // Idol 앞에 const로 선언
//..
// Idol Class 생성
class Idol {
final String name;
final List<String> members; // final로 선언함으로써 변수명을 변경할 수 없게 할 수 있다.
const Idol(this.name, this.members); // const 선언으로 immutable로 만든다
//..
Immutable 프로그래밍은 차후 Flutter에서 효율을 올려주는데 도움이 된다.
Getter & Setter
Getter
getter는 어떤 데이터를 가져올 때 사용한다. 예시를 확인해보자.
//..
blackPink.introduce();
print(blackPink.firstMember); // 선언한 getter 출력
//..
newjeans.introduce();
print(newjeans.firstMember); // 선언한 getter 출력
}
//..
void introduce() {
print('저희 멤버는${this.members}가 있습니다.'); // introduce(멤버소개) - 함수
}
// getter: 데이터를 가져올 때 사용
String get firstMember {
return this.members[0];
}
}
Console 출력창
블랙핑크
[지수, 제니, 리사, 로제]
안녕하세요 블랙핑크입니다.
저희 멤버는[지수, 제니, 리사, 로제]가 있습니다.
지수
----------------
Newjeans
[하니, 해린, 민지,인, 다니엘]
안녕하세요 Newjeans입니다.
저희 멤버는[하니, 해린, 민지, 혜인, 다니엘]가 있습니다.
하니
Setter
setter는 데이터를 새로 지정해주는 기능을 가진다.
//..
print(blackPink.firstMember);
blackPink.firstMember = '윈터'; // 새로운 firstMember 선언
print(blackPink.firstMember);
//..
print(newjeans.firstMember);
newjeans.firstMember = '카리나'; // 새로운 firstMember 선언
print(newjeans.firstMember);
}
//..
// getter: 데이터를 가져올 때 사용
String get firstMember {
return this.members[0];
}
// setter: 데이터를 새로 지정하는 기능
set firstMember(String name){ // set의 파라미터는 무조건 한개의 파라미터만 사용이 가능하다.
this.members[0] = name;
}
}
Console 출력창
블랙핑크
[지수, 제니, 리사, 로제]
안녕하세요 블랙핑크입니다.
저희 멤버는[지수, 제니, 리사, 로제]가 있습니다.
지수
윈터
----------------
Newjeans
[하니, 해린, 민지, 혜인, 다니엘]
안녕하세요 Newjeans입니다.
저희 멤버는[하니, 해린, 민지, 혜인, 다니엘]가 있습니다.
하니
카리나
현대 프로그래밍에서는 immutable 프로그래밍 위주로 진행되기 때문에 setter는 사실상 사용하지 않는다. 그러나, 특정 상황에서는 사용할 수 있기에 알아두면 좋다.
Private 변수
지금 작성하고 있는 예시 코드들은 하나의 파일 안에서 실행된다는 가정하에 진행되고 있다. 허나, 프로젝트나 현업에서는 여러 개의 파일들이 실행될 수 있다. 이 때, 한 파일안에 있는 class 코드를 외부 파일에서 import하더라도 private 변수가 선언된 class는 사용할 수 없다. 선언하는 방식은 변수명 앞에 _
을 붙혀주면 된다.
Inheritance(상속)
상속은 자식 클래스가 부모 클래스의 모든 속성을 부여받는 것을 의미한다.
void main() {
Idol apink = Idol(name: '에이핑크', membersCount: 5);
apink.sayName();
apink.sayMembersCount();
print(' ');
BoyGroup bts = BoyGroup('BTS', 7);
bts.sayName();
bts.sayMembersCount();
bts.sayMale();
print(' ');
GirlGroup redVelvet = GirlGroup('Red Velvet', 5);
redVelvet.sayName();
redVelvet.sayMembersCount();
redVelvet.sayFemale();
}
class Idol {
String name; // 이름
int membersCount; // 멤버 숫자
Idol({
required this.name,
required this.membersCount,
});
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name}은(는) ${this.membersCount}명의 멤버가 있습니다.');
}
}
class BoyGroup extends Idol {
//extends: 상속 받을 클래스를 입력해준다.
BoyGroup(
String name,
int membersCount,
) : super(
//super: 입력받은 파라미터들을 그대로 부모 클래스, 즉 Idol로 넘겨주는 역할을 수행한다.
name: name,
membersCount: membersCount,
);
void sayMale() {
print('저희는 남자아이돌입니다.');
}
}
class GirlGroup extends Idol {
GirlGroup(
String name,
int membersCount,
) : super(
name: name,
membersCount: membersCount,
);
void sayFemale() {
print('저희는 여자아이돌입니다.');
}
}
Console 출력창
저는 에이핑크입니다.
에이핑크은(는) 5명의 멤버가 있습니다.
저는 BTS입니다.
BTS은(는) 7명의 멤버가 있습니다.
저희는 남자아이돌입니다.
저는 Red Velvet입니다.
Red Velvet은(는) 5명의 멤버가 있습니다.
저희는 여자아이돌입니다.
에이핑크와 BTS의 함수명은 다르지만, 출력되는 내용은 동일한 것을 확인할 수 있다. 이는 BoyGroup
클래스가 Idol
클래스를 상속 받아 그 기능을 사용할 수 있기 때문이다. 반면, Idol
클래스에서는 sayMale()
함수를 사용할 수 없다. 자식 클래스는 부모 클래스를 상속받을 수 있지만, 자식 클래스에서 부모클래스로 상속은 불가능하기 때문이다. 또한, 상속받은 클래스들은 다른 상속 받은 클래스에 있는 기능들을 사용할 수 없다. BoyGroup
클래스가 SayFemale()
을 사용하지 못하는 것처럼 GirlGroup
클래스가 SayMale()
을 사용하지 못한다.
Type comparison
이제 앞서 선언한 부모와 자식 클래스들의 타입 비교를 해보도록 해보자.
//..
print('에이핑크는 Idol Class에 속하나요? : ${apink is Idol}');
print('에이핑크는 BoyGroup Class에 속하나요? : ${apink is BoyGroup}');
print('에이핑크는 GirlGroip Class에 속하나요? : ${apink is GirlGroup}');
print(' ');
print('BTS는 Idol Class에 속하나요? : ${bts is Idol}');
print('BTS는 BoyGroup Class에 속하나요? : ${bts is BoyGroup}');
print('BTS는 GirlGroip Class에 속하나요? : ${bts is GirlGroup}');
print(' ');
print('레드벨벳은 Idol Class에 속하나요? : ${redVelvet is Idol}');
print('레드벨벳은 BoyGroup Class에 속하나요? : ${redVelvet is BoyGroup}');
print('레드벨벳은 GirlGroip Class에 속하나요? : ${redVelvet is GirlGroup}');
}
//..
Console 출력창
//..
에이핑크는 Idol Class에 속하나요? : true
에이핑크는 BoyGroup Class에 속하나요? : false
에이핑크는 GirlGroip Class에 속하나요? : false
BTS는 Idol Class에 속하나요? : true
BTS는 BoyGroup Class에 속하나요? : true
BTS는 GirlGroip Class에 속하나요? : false
레드벨벳은 Idol Class에 속하나요? : true
레드벨벳은 BoyGroup Class에 속하나요? : false
레드벨벳은 GirlGroip Class에 속하나요? : true
상속하면 자식클래스는 자식 클래스도 되고 부모 클래스도 되는 것을 확인할 수 있다.
Method Overriding
void main() {
TimesTwo tt = TimesTwo(2);
print(tt.calculate());
TimesFour tf = TimesFour(2);
print(tf.calculate());
}
class TimesTwo {
final int number;
TimesTwo(
this.number,
);
int calculate() {
// method: funcion(class 내부에 있는 함수)
return this.number * 2;
}
}
class TimesFour extends TimesTwo {
TimesFour(
int number,
) : super(number);
@override // override : 덮어쓰기
int calculate(){
return super.number * 4;
}
}
Console 출력창
4
8
Overriding은 상속받은 자식 클래스에서 특정 부분을 덮어쓰고 싶을 때 사용한다. 해당 부모 클래스는 2를 곱하는 클래스지만, 자식클래스는 2를 곱하는 부분을 4를 곱하는 부분으로 덮어썼다. 만약, 부모 클래스의 연산을 살리고 거기다 더하고 싶다면 아래와 같이 작성하면 가능하다.
//..
@override // override : 덮어쓰기
int calculate() {
return super.calculate() * 2;
}
}
Static Keyword
우선 아래 예시를 보면서 이해를 해보도록 하자.
void main() {
Employee minji = Employee('민지');
Employee herin = Employee('해린');
minji.name = '하니';
minji.printNameAndBuilding();
herin.printNameAndBuilding();
print(' ');
print('빌딩을 클래스에 귀속시키기');
Employee.building = 'Hybe';
minji.printNameAndBuilding();
herin.printNameAndBuilding();
Employee.printBuilding();
}
class Employee {
// static은 instance에 귀속되지 않고 class에 귀속된다.
static String? building; // 알바생이 일하고 있는 건물
String name; // 알바생 이름
Employee(
this.name,
);
void printNameAndBuilding(){
print('제 이름은 $name입니다. $building 건물에서 근무하고 있습니다.');
}
static void printBuilding(){
print('저희는 $building 건물에서 근무중입니다.');
}
}
Console 출력창
제 이름은 하니입니다. null 건물에서 근무하고 있습니다.
제 이름은 해린입니다. null 건물에서 근무하고 있습니다.
빌딩을 클래스에 귀속시키기
제 이름은 하니입니다. Hybe 건물에서 근무하고 있습니다.
제 이름은 해린입니다. Hybe 건물에서 근무하고 있습니다.
저희는 Hybe 건물에서 근무중입니다.
Employee
라는 클래스를 만들고 minji
와 herin
이라는 두 개의 인스턴스를 생성하였다. 각각의 인스턴스 중 minji
의 초기 알바생 이름을 ‘하니’로 변경이 가능하다. 이는 인스턴스에 귀속된다고 볼 수 있다. 그리고 중간에 비어있는 빌딩 이름을 지정해주고 두 개의 인스턴스를 다시 실행하니 같은 빌딩이 나오는 것을 확인할 수 있다. 이는 빌딩값은 static
기능을 이용하여 클래스에 귀속시켰기 때문에 인스턴스에서는 수정이 불가능하다.
Interface
인터페이스는 어떤 특수한 구조를 강제하는 역할을 한다.
void main() {
BoyGroup bts = BoyGroup('BTS');
GirlGroup newjeans = GirlGroup('NewJeans');
bts.sayName();
newjeans.sayName();
}
//interface : 특정 규칙을 지정.
// 다른 개발자와 개발할 때 코드 포맷을 만들어서 어떤 형식으로 작성해야하는지 길라잡이 역할 해줌
class IdolInterface{
String name;
IdolInterface(this.name);
void sayName() {}
}
class BoyGroup implements IdolInterface{ //implements : Interface를 사용할 때 사용
String name;
BoyGroup(this.name);
void sayName(){
print('제 이름은 $name입니다');
}
}
class GirlGroup implements IdolInterface{
String name;
GirlGroup(this.name);
void sayName(){
print('제 이름은 $name입니다');
}
}
Console 출력창
제 이름은 BTS입니다
제 이름은 NewJeans입니다
IdolInterface
의 역할은 상속 클래스들이 어떠한 형태로 구성되어야하는지를 알려주는 길라잡이 역할이다. 인스턴스를 생성하는 역할은 아니다. 그러나, 실수로 IdolInterface
클래스로 인스턴스를 생성할 수도 있을 것이다. 이를 방지하기 위해서는 abstract
를 사용한다.
//..
//interface : 특정 규칙을 지정.
abstract class IdolInterface {
String name;
IdolInterface(this.name);
void sayName();
}
//..
Generic
자주는 아니지만 항상 언젠가는 클래스에 받을 변수의 타입이 정해져야할 때가 있다. 그럴 때 Generic 기능을 이용하면 된다.
void main() {
Lecture<String> lecture1 = Lecture('123', 'lecture1');
lecture1.printIDType();
Lecture<int> lecture2 = Lecture(123, 'lecture2');
lecture2.printIDType();
}
// generic : 타입을 외부에서 받을 때 사용
class Lecture<T>{ // <> 안에 변수명을 넣으면 된다. 아무 글자나 상관없다.
// ,수에 따라서 여러개를 받을 수 있다.
final T id;
final String name;
Lecture(this.id, this.name);
void printIDType(){
print(id.runtimeType);
}
}
Console 출력창
String
int
그래서 왜 Object Oriented Programming인가?
이유는 생각보다 단순하다. 왜냐하면 우리가 선언하는 클래스들은 다 Object라는 클래스를 상속받기 때문이다. 따라서 Object Oriented Programming인 것이다.(다소 김이 빠지는 건 기분탓이다.)