Object Oriented Programming
객체지향 프로그래밍
클래스는 변수들과 함수들의 기능들을 한곳에 모아둔 곳이라고 할 수 있다.
클래스를 통해서 개발하는 것을 OOP 라고 한다.
클래스를 통해서 결과물을 나타내는 것을 인스턴스라고 한다.
인스턴스를 만들 수 있다.
void main() {
Idol blackPink = Idol();
print(blackPink.name); // 블랙핑크
print(blackPink.members); // ['제니', '지수', '리사', '로제']
blackPink.sayHello(); // 안녕하세요 블랙핑크입니다.
blackPink.introduce(); // 저희 멤버는 지수, 제니, 리사, 로제가 있습니다.
}
// Idol class
class Idol {
// 변수
String name = '블랙핑크';
// 변수
List<String> members = ['제니', '지수', '리사', '로제'];
// constructor (생성자)
Idol(String name, List<String> members)
: this.name = name,
this.members = members;
// 함수
void sayHello() {
print('안녕하세요 블랙핑크입니다.');
}
// 함수
void introduce() {
print('저희 멤버는 지수, 제니, 리사, 로제가 있습니다.');
}
}
constructor를 활용하면 여러개의 인스턴스 활용가능하다.
파라미터를 다르게 넣어서 여러개의 인스턴스를 만들 수 있다.
void main() {
... 블랙핑크도 똑같이 가능
Iodl BTS = Idol(
'방탄 소년단',
['진', '뷔', '제이홉', 'RM', '슈가', '지민', '정국'],
)
}
// Idol class
class Idol {
String name;
List<String> members;
// constructor 이렇게도 생성가능, 자동 추론해줌
Idol(this.name, this.members);
// 함수
void sayHello() {
print('안녕하세요 ${this.name}입니다.');
}
// 함수
void introduce() {
print('저희 멤버는 ${this.members}가 있습니다.');
}
}
Constructor 에 이름을 붙여 사용하는 것이다.
내가 원하는 이름으로 설정 가능하다.
void main() {
Idol BTS = Idol.fromList(
['진', '뷔', '제이홉', 'RM', '슈가', '지민', '정국'],
'BTS',
);
print(BTS.name);
print(BTS.members);
}
class Idol {
String name;
List<String> members;
// 생성자
Idol(this.name, this.members);
// [named parameters] -> fromList()로 지음
Idol.fromList(List values)
: this.members = values[0],
this.name = values[1];
}
즉, 어떤 Costructor 를 사용해도 인스턴스를 만들 수 있다.
final 사용
변수를 선언을 했을때, 변경이 안되게끔 하길 원한다. 애초에 삭제하고 다시 생기길 원하지!
그렇긴 원해서 final 로 선언을 해준다.
void main() {
Idol blakPink = Idol('블랙핑크');
// 변경
blackPink.name = 'BTS';
print(blackPink.name); // error
}
Class Idol {
final String name;
Idol(this.name);
}
const constructor 를 사용
const + constructor 로 불변성을 유지하는 것인데, 생성자 앞에 const 를 붙여주면, 기본 constructor 에 const 로 선언이 가능해진다.
build 타임에 값을 알 수 있어야한다. 그렇기 때문에 변경이 불가해 불변성을 유지해준다.
void main() {
Idol blackPink = const Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
}
class Idol{
final String name;
final List<String> members;
const Idol(this.name, this.members);
}
프로그래밍에서는 두개의 값이 다르다고 판단한다.
blackPink 와 blackPink2 가 같아보이지만, 인스턴스를 생성하면 생성할 때마다 메모리를 차지하고, 다른 인스턴스로 인식을 한다.
그러므로 아무리 값이 같아보여도 두개의 비교는 false 로 출력된다.
void main() {
Idol blackPink = Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
Idol blackPink2 = Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
print(blackPink == blackPink2); // false
}
하지만 const 를 사용하면, 같은 인스턴스가 되어버린다.
void main() {
Idol blackPink = const Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
Idol blackPink2 = const Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
print(blackPink == blackPink2); // true
}
getter 는 데이터를 가져올때
setter 는 데이터를 설정할때
getter 선언
반환타입 get 이름 {
return 반환하고 싶은 값;
}
setter 선언
set 이름(무조건 한개의 파라미터) {
원하는 식 = 파라미터;
}
void main() {
Idol blackPink = Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
// getter 사용
print(blackPink.firstMember); // 제니
// setter 사용
blackPink.firstMember = '김또깡';
print(blackPink.firstMember); // 김또깡
}
class Idol {
String name;
List<String> members;
Idol(this.name, this.members);
// getter
String get firstMember {
return this.members[0];
}
// setter
set firstMember(String name) {
this.member[0] = name;
}
}
그냥 함수 만들면 안됌?
둘의 차이는 기능적인 차이는 없음
그냥 뉘앙스 차이이다.
getter 는 데이터를 가공하는데 사용한다.
함수는 로직적인 부분에 많이 사용한다.
void main() {
Idol blackPink = Idol(
'블랙핑크',
['제니', '지수', '리사', '로제'],
);
print(blackPink.getFirstMember());
print(blackPink.firstMember);
}
...
// 함수
String getFirstMember() {
return this.member[0];
}
// getter
String get firstMember() {
return this.members[0];
}
...
setter 함수를 사용하면, 바뀌어야 하는거 아닌가?
final 로 선언을 해서 불변성을 유지시키면 값을 바꿀 수 가 없다.
List 는 특별하게! final 이여도 members 의 값들을 바꿀 수 있다.
class Idol{
final List<String> members;
// 생성자
Idol(this.members);
// setter : name 파라미터로 받아서 List 의 0번째를 변경하는 식
set firstMember(String name) {
this.members[0] = name;
}
}
안되는 부분은 무엇이냐면, 아예 members 자체를 통째로 파라미터로 받아서 List 를 통째로 변경 시켜주는 것은 안된다!
그래서 솔직히 무용지물이다. 원래 final 의도에서 값을 변하지 않게하려는 것을 벗어나기 때문에 잘 안쓴다.
set firstMember(List<String> members) {
this.members= members; // error
}
한 파일에서만 사용하기 위해 숨김 기능(?)이다.
클래스와 변수나 함수를 현재 그곳에서만 사용가능하게 숨김! 기능이라고 생각하면된다.
그렇기 때문에, import 를 하더라고 private 속성을 사용했으면, 불러오지 못한다.
하지만 port 라는 다른 방법으로 private 함수를 호출할 수 있다. 이건 나중에!
작성법
// priavate 클래스
class _ExampleClass1 {}
// private 변수
class ExampleClass2 {
String _name;
}
// private 함수
void _exampleFunction {
print('private 으로 생성된 함수');
}
상속이다.
클래스는 다른 클래스를 상속 받을 수 있다.
부모 클래스의 모든 속성을 자식 클래스가 부여받는다.
class Dady {
String name;
int money;
Dady({
required this.name,
required this.money,
});
void sayName() {
pritn('나의 이름은 ${this.name} 이다.');
}
void sayMoney() {
print('지갑에 돈이 ${this.money}가 있다.');
}
}
부모의 생성자를 준수해줘야한다.
그래야 상속받은 파라미터들을 사용 가능하다.
부모클래스를 지칭 하는 super();
void main() {
print('아이돌');
Idol apink = Idol(name: '에이핑크', membersCount:5);
print('남돌');
BoyGroup bts = BoyGroup('BTS', 7);
print('여돌');
GirlGroup redVelvet = GirlGroup('레드 벨벳', 7);
}
class Idol {
String name;
int membersCount;
Idol({
required this.name,
required this.membersCount,
});
}
class BoyGroup extends Idol {
BoyGroup(String name, int membersCount):super(name:name, membersCount:membersCount);
void sayMan(){
print('저는 남자입니다.');
}
}
class GirlGroup extends Idol {
GirlGroup(String name, int membersCount,):super(name:name, membersCount:membersCount);
void sayFemale(){
print('저는 여자 아이돌입니다.');
}
}
인스턴스 is 클래스
void main() {
print('아이돌');
Idol apink = Idol(name: '에이핑크', membersCount:5);
print('남돌');
BoyGroup bts = BoyGroup('BTS', 7);
print('여돌');
GirlGroup redVelvet = GirlGroup('레드 벨벳', 7);
print('타입 비교1');
print(apink is Idol); // true
print(apink is BoyGroup); // false
print(apink is GirlGroup); // false
print('타입 비교2');
print(bts is Idol); // true
print(bts is BoyGroup); // true
print(bts is GirlGroup); // false
print('타입 비교3');
print(redVelvet is Idol); // true
print(redVelvet is BoyGroup); // false
print(redVelvet is GirlGroup); // true
}
class Idol {
String name;
int membersCount;
Idol({
required this.name,
required this.membersCount,
});
}
// 자식클래스1 Idol 상속
class BoyGroup extends Idol {
BoyGroup(String name, int membersCount):super(name:name, membersCount:membersCount);
void sayMan(){
print('저는 남자입니다.');
}
}
// 자식클래스2 Idol 상속
class GirlGroup extends Idol {
GirlGroup(String name, int membersCount,):super(name:name, membersCount:membersCount);
void sayFemale(){
print('저는 여자 아이돌입니다.');
}
}
재정의라는 것이다.
클래스 내부에 있는 함수 (method)를 재정의 (override) 하여 사용한다.
void main() {
// 인스턴스 선언
TimeTwo tt = TimesTwo(2);
print(tt.calculate()); // 4
TimeFour tf = TimesFour(2);
// 재정의된 함수 불러옴
print(tf.calculate()); // 8
}
class TimesTwo{
final int number;
TimesTwo(
this.number,
)
//method
int calculate() {
// number 라는 변수가 method 안에 따로 존재하지 않으면, this. 생략가능!
return number * 2;
}
}
class TimesFour extends TimesTwo {
TimesFour(
int number
): super(number);
// 재정의1) : 부모 클래스의 method 를 함수를 전부 수정
// 재정의 방법은 부모상속 받은 method 이름 그대로 작성해야함!
int calculate() {
// super.number 는 부모클래스의 number이다. super. 생략가능하다.
return super.number * 4;
}
// 재정의2) : 부모 클래스의 함수 그대로를 불러와서 그 결과값에다가 추가하기
int calculate() {
return super.calculate() * 2;
}
}
static 은 인스턴스에 귀속되지 않고 class 에 귀속된다 이게 무슨 뜻이냐?
클래스는 Employee 이고, 인스턴스는 Employee 라는 클래스로 변수를 생성하는 것이다.
인스턴스에 귀속이라는 말은 인스턴스를 만들어서 실행을 하거나 값을 바꿀 수 있다.
상황)
main 함수에 인스턴스 변수 두개를 선언해서 name 파라미터에 각자 김똥개, 김박 이라고 값을 넣었다.
김똥개라는 이름만 신희태라는 이름으로 변경한 상태이고, Employee void 함수를 호출했다.
결과)
kimdong.name = '신희태';
kimhong.printNameAndBuilding(); // 제 이름은 신희태입니다. null 건물에서 근무하고 있습니다.
kimpark.printNameAndBuilding(); // 제 이름은 김박입니다. null 건물에서 근무하고 있습니다.
설명)
같은 클래스에서의 인스턴스를 만들었는데, 함수를 실행하거나 값(name)을 가져오면은 각각 만든 변수마다 다르게 출력된다.
따로 인스턴스를 만들지 않고도 바로 클래스에 다이렉트로 실행하거나 값을 바꿀 수 있다.
상황) Employee 클래스에 static 변수로 String? building 을 선언했다.
main 함수에서 따로 생성자를 선언하지 않았는데, Employee.building 이렇게 작성할 수 있고, Employee void 함수를 호출했다.
결과)
kimdong.name = '신희태';
Employee.building = '오투타워';
kimdong.printNameAndBuilding(); // 제 이름은 신희태입니다. 오투타워 건물에서 근무하고 있습니다.
kimpark.printNameAndBuilding(); // 제 이름은 김박입니다. 오투타워 건물에서 근무하고 있습니다.
설명)
서로 다른 인스턴스인데도 불구하고 오투타워라고 나오는 것을 볼수 있다.
둘다 변경이 된 것을 볼 수있다. Emlployee 라는 클래스에서 building 값만 바꿔주기만 했는데, kingdong 과 kimpark 두개 다 buiding 값이 변경 되었다.
static 키워드를 사용해서 어느 변수를 클래스에다가 귀속 시킬 수 있다.
static method 는 ?
void main() {
Employee kimdong = Emloyee('김똥개');
Employee kimpark = Emloyee('김박');
// 인스턴스에 귀속이 된다. (Employee 클래스에 name 의 final 이 없는 상태!)
kimdong.name = '신희태';
kimhong.printNameAndBuilding(); // 제 이름은 신희태입니다. null 건물에서 근무하고 있습니다.
kimpark.printNameAndBuilding(); // 제 이름은 김박입니다. null 건물에서 근무하고 있습니다.
// 클래스에 귀속이 된다.
Employee.building = '오투타워';
kimdong.printNameAndBuilding(); // 제 이름은 신희태입니다. 오투타워 건물에서 근무하고 있습니다.
kimpark.printNameAndBuilding(); // 제 이름은 김박입니다. 오투타워 건물에서 근무하고 있습니다.
// static method 사용
Employee.printBuilding(); // 직원들은 오투타워 건물에서 근무중입니다.
}
class Employee {
// static 은 인스턴스에 귀속되지 않고 class 에 귀속된다.
// 알바생이 일하는 건물
static Stirng? buildeing;
// 알바생 이름
String name;
// 생성자
Employee(
this.name
);
void printNameAndBuilding() {
print('제 이름은 $name입니다. $building 건물에서 근무하고 있습니다.');
}
static void printBuilding(){
print('직원들은 $building 건물에서 근무중입니다.');
}
}
상속과 비슷하지만 다른 개념으로 사용한다.
다른 언어에는 interface 라는 키워드를 사용하지만, dart 언어에는 똑같이 class 를 사용한다.
extends 는 속성과 기능을 물려주기 위해 사용하지만
interface 는 특수한 구조를 강제하는 것이다. 꼭 써줘야지 에러가 안난다!
void main() {
BoyGroup bts = BoyGroup('BTS');
GrilGroup redVelvet = GirlGroup('레드벨벳');
bts.sayName(); // 제 이름은 BTS 입니다.
redVelvet.sayName(); // 제 이름은 레드벨벳 입니다.
}
// interface: 구조 만들기!
class IdolInterface{
String name;
IdolInterface(this.name);
// 함수 안에는 아무것도 넣지 않을거다 -> 실제로 IdolInterface 를 만들때 사용하는 게 아니고, 다른 클래스를 만들때 이 interface 를 사용해서 선언되어있는 형태를 꼭 지킬 수 있도록 강제할떄 사용할거다.
void sayName(){}
}
class BoyGroup implements IdolInterface{
String name;
BodyGroup(this.name);
void sayName() {
print('제 이름은 $name 입니다.');
}
}
class GirlGroup implemtns IdolInterface{
String name;
GirlGroup(this.name);
void sayName() {
print('제 이름은 $name 입니다.');
}
}
interface 로 만든 것은 인스턴스화 시키면안된다!
구조를 강제하기 위한 클래스로 interface 를 한것인데, 다른 개발자가 작성하다가 interface 인줄 모르고 인스턴스화 시킬수도 있다. 그것을 막기위한 방법이다.
void main() {
// error! abstract 로 작성했으니, 인스턴스로 못만든다고 에러뜬다.
IdolInterface test = IdolInterface('블랙핑크');
}
// interface
abstract class IdolInterface{
String name;
IdolInterface(this.name);
void sayName(); // -> 추상적이기 때문에 함수의 body 를 생략할 수 있다.
}
void main() {
BoyGroup bts = BoyGroup('BTS');
GrilGroup redVelvet = GirlGroup('레드벨벳');
print(bts is IdolInterface); // true
print(bts is BoyGroup); // true
print(bts is GirlGroup); // false
print(redVelvet is IdolInterface); // true
print(redVelvet is BoyGroup); // false
print(redVelvet is GirlGroup); // true
}
// interface
abstract class IdolInterface{
String name;
IdolInterface(this.name);
void sayName()
}
class BoyGroup implements IdolInterface{
String name;
BodyGroup(this.name);
void sayName() {
print('제 이름은 $name 입니다.');
}
}
class GirlGroup implemtns IdolInterface{
String name;
GirlGroup(this.name);
void sayName() {
print('제 이름은 $name 입니다.');
}
}
타입을 외부에서 받을 때 사용한다.
지끔까지 클래스외부에서 받았던것들은 constructor 의 paramter 에다가 값을 넘겨줬을떄만이었다.
void main() {
// 외부에서 타입을 넣어줄 수 있다.
Lecture<String> lecture1 = Lecture('123', 'lecture1');
lecture1.printIdType(); // Stirng
Lecture<int> lecture2 = Lecture(123, 'lecture2');
lecture2.printIdType(); // int
}
// <> 를 사용하고 id 의 타입을 외부에서 받기로 해보자!
class Lecture<T> {
final T id;
final String name;
Lecture(this.id, this.name);
void printIdType() {
print(id.runtimeType);
}
}
Obect Oriented Programming
Test 라는 클래스 안에 아무것도 안 만들고, test 라는 이름으로 인스턴스화한 것을 호출할때 많은 함수들을 호출이 가능한것이 보인다 왜일까?
그 이유는?
모든 클래스는 Object 라는 최상위 클래스를 상속받고 있는데, 이것이 생략되어 있기 때문이다!. 그래서 객체지향 프로그래밍이라고 하는 것이다.
class Test extends Object{}
// 두개는 같다.
class Text {}