Dart 객체지향 프로그래밍이란!

🦥 OOP

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('저희 멤버는 지수, 제니, 리사, 로제가 있습니다.');
    }
}

contructor 사용

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}가 있습니다.');
    }
}

Named Constructor 란?

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 를 사용해도 인스턴스를 만들 수 있다.

Immutable

불변성 유지

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);   
}

Const Constructor 의 비교

프로그래밍에서는 두개의 값이 다르다고 판단한다. 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 는 데이터를 가져올때 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 를 사용하냐?

그냥 함수 만들면 안됌? 둘의 차이는 기능적인 차이는 없음 그냥 뉘앙스 차이이다.

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 는 무용지물??

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
}

private 변수

한 파일에서만 사용하기 위해 숨김 기능(?)이다. 클래스와 변수나 함수를 현재 그곳에서만 사용가능하게 숨김! 기능이라고 생각하면된다.

그렇기 때문에, import 를 하더라고 private 속성을 사용했으면, 불러오지 못한다. 하지만 port 라는 다른 방법으로 private 함수를 호출할 수 있다. 이건 나중에!

작성법

// priavate 클래스 
class _ExampleClass1 {}

// private 변수
class ExampleClass2 {
	String _name;
}

// private 함수
void _exampleFunction {
	print('private 으로 생성된 함수');
}

Inheritance

상속이다. 클래스는 다른 클래스를 상속 받을 수 있다. 부모 클래스의 모든 속성을 자식 클래스가 부여받는다.

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('저는 여자 아이돌입니다.');
  }
  
}

Override

재정의라는 것이다. 클래스 내부에 있는 함수 (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 이름 그대로 작성해야함!
    @override
    int calculate() {
    	// super.number 는 부모클래스의 number이다. super. 생략가능하다.
    	return super.number * 4; 
    }
    
    // 재정의2) : 부모 클래스의 함수 그대로를 불러와서 그 결과값에다가 추가하기
    @override
    int calculate() {
    	return super.calculate() * 2;
    }
    
}

Static 키워드

static 은 인스턴스에 귀속되지 않고 class 에 귀속된다 이게 무슨 뜻이냐?

클래스는 Employee 이고, 인스턴스는 Employee 라는 클래스로 변수를 생성하는 것이다.

1) 인스턴스에 귀속이 된다.

인스턴스에 귀속이라는 말은 인스턴스를 만들어서 실행을 하거나 값을 바꿀 수 있다.

상황)
main 함수에 인스턴스 변수 두개를 선언해서 name 파라미터에 각자 김똥개, 김박 이라고 값을 넣었다. 김똥개라는 이름만 신희태라는 이름으로 변경한 상태이고, Employee void 함수를 호출했다.

결과)

kimdong.name = '신희태';
kimhong.printNameAndBuilding(); // 제 이름은 신희태입니다. null 건물에서 근무하고 있습니다.
kimpark.printNameAndBuilding(); // 제 이름은 김박입니다. null 건물에서 근무하고 있습니다.

설명) 같은 클래스에서의 인스턴스를 만들었는데, 함수를 실행하거나 값(name)을 가져오면은 각각 만든 변수마다 다르게 출력된다.

2) 클래스에 귀속이 된다.

따로 인스턴스를 만들지 않고도 바로 클래스에 다이렉트로 실행하거나 값을 바꿀 수 있다.

상황) 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

상속과 비슷하지만 다른 개념으로 사용한다. 다른 언어에는 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 입니다.');
    }
}

abstract 란? (추상적)

interface 로 만든 것은 인스턴스화 시키면안된다! 구조를 강제하기 위한 클래스로 interface 를 한것인데, 다른 개발자가 작성하다가 interface 인줄 모르고 인스턴스화 시킬수도 있다. 그것을 막기위한 방법이다.

void main() {
	//  error! abstract 로 작성했으니, 인스턴스로 못만든다고 에러뜬다.
	IdolInterface test = IdolInterface('블랙핑크');
}

// interface
abstract class IdolInterface{
	String name;
    
    IdolInterface(this.name);
    
    void sayName(); // -> 추상적이기 때문에 함수의 body 를 생략할 수 있다.
    
}

interface 타입 비교

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 입니다.');
    }
}

generic

타입을 외부에서 받을 때 사용한다. 지끔까지 클래스외부에서 받았던것들은 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);
    }
}

왜 클래스를 사용해서 코딩하는 것이 OOP 라고 하는가?

Obect Oriented Programming

Test 라는 클래스 안에 아무것도 안 만들고, test 라는 이름으로 인스턴스화한 것을 호출할때 많은 함수들을 호출이 가능한것이 보인다 왜일까? 그 이유는? 모든 클래스는 Object 라는 최상위 클래스를 상속받고 있는데, 이것이 생략되어 있기 때문이다!. 그래서 객체지향 프로그래밍이라고 하는 것이다.

class Test extends Object{} 
// 두개는 같다.
class Text {}

Categories:

Updated:

Leave a comment