-
코드팩토리의 플러터 프로그래밍 (책_객체지향 프로그래밍)카테고리 없음 2024. 3. 22. 11:04
객체지향 프로그래밍의 필요성
수천 줄에서 수만 줄의 코드를 작성할 떄 모든 코드를 main()함수에 작성하면 코드 정리가 안 돼 유지보수 및 협업에 상당히 큰 장애물이 된다. 객체지향 프로그래밍을 하면 변수와 메서드를 특정 클래스에 종속되게 코딩할 수 있다. 클래스를 사용해서 밀접한 관계가 있는 함수와 변수를 묶어두면 코드관리가 용이해진다.
클래스를 만들어서 사용하면 필요한 값들만 입력하도록 제한하고 클래스에 특화된 함수들을 선언할 수 있다.
클래스는 일종의 설계도로서 데이터가 보유할 속성과 기능을 정의하는 자료구조이다.
아파트 설계도와 실물 아파트의 관계가 클래스와 인스턴스의 관계이다. 클래스를 설계도, 인스턴스화를 실물 아파트라고 생각하면 된다.
인스턴스화되어야 실제 사용할 수 있는 데이터가 생성된다.
인스턴스
클래스를 이용해서 객체를 선언하면 해당 객체를 클래스의 인스턴스라고 부른다.
인스턴스화
클래스에서 인스턴스(객체)를 생성하는 과정을 말한다.객체지향 프로그래밍의 시작, 클래스
idol클래스 정의
// class 키워드를 입력 후 클래스명을 지정해 클래스를 선언합니다. class Idol { // 1 클래스에 종속되는 변수를 지정할 수 있습니다. String name = '블랙핑크' ; // 2 클래스에 종속되는 함수를 지정할 수 있습니다. // 클래스에 종속되는 함수를 메서드라고 부릅니다. void sayName() { // 3 클래스 내부의 속성을 지칭하고 싶을 때는 this 키워드를 사용하면 됩니다. // 결과적으로 this.name은 Idol 클래스의 name 변수를 지칭합니다. print('저는 ${this.name}입니다.'); // 4 스코프 안에 같은 속성 이름이 하나만 존재한다면 this를 생략할 수 있습니다. print('저는 $name입니다.'); } }
idol클래스 안에 종속된 변수로는 name이, 함수로는 sayName()이 있다.
클래스에 종속된 변수를 멤버 변수, 종속된 함수를 메서드라고 부른다.
클래스 내부 속성을 지칭하는데 this키워드를 사용한다.
this키워드는 현재 클래스를 의미하고 만약에 같은 이름의 속성이 하나만 존재한다면 this를 생략할 수 있다. 다만 sayName()함수에 name이라는 변수가 존재한다면 this키워드를 꼭 사용해야 한다. (함수 내부에 같은 이름의 변수가 없으면 this 키워드 생략 가능)
void main() { // 변수 타입을 Idol로 지정하고 // Idol의 인스턴스를 생성할 수 있습니다. // 인스턴스를 생성할 때는 함수를 실행하는 것처럼 // 인스턴스화하고 싶은 클래스에 괄호를 열고 닫아줍니다. Idol blackPink = Idol(); // 1 Idol 인스턴스 생성 // 메서드를 실행합니다. blackPink.sayName(); }
변수 타입을 idol로 지정해 인스턴스를 생성하고, 인스턴스를 생성할 떄는 함수를 실행하는 것처럼 인스턴스화하고 싶은 클래스명 뒤에 ()를 붙여주면 된다.
생성자
생성자는 클래스의 인스턴스를 생성하는 메서드이다.
class Idol { // 1 생성자에서 입력받는 변수들은 일반적으로 final 키워드 사용 final String name; // 2 생성자 선언 // 클래스와 같은 이름이어야 합니다. // 함수의 매개변수를 선언하는 것처럼 매개변수를 지정해줍니다. Idol(String name) : this.name = name; void sayName() { print('저는 ${this.name}입니다.'); } }
name변수의 값을 외부에서 입력할 수 있게 변경했다. 생성자에서 입력받을 변수를 일반적으로 final로 선언한다.
인스턴스화한 다음에 혹시라도 변수의 값을 변경하는 실수를 막기 위함이다.
클래스 생성자는 클래스와 같은 이름을 사용해야하고 이름 뒤에 ( )를 붙이고 원하는 매개변수를 지정해준다.
: 기호 뒤에 입력받은 매개변수가 저장될 클래스 변수를 지정해준다.
idol클래스로 인스턴스 만들기
void main() { // name에 '블랙핑크' 저장 Idol blackPink = Idol('블랙핑크'); // 저는 블랙핑크입니다. blackPink.sayName(); // name에 'BTS' 저장 Idol bts = Idol('BTS'); // 저는 BTS입니다. bts.sayName(); }
이렇게 하면 Idol클래스 하나로 여러 Idol인스턴스를 생성해 중복 코딩 없이 활용할 수 있게 된다.
class Idol { final String name; // this를 사용할 경우 // 해당되는 변수에 자동으로 매개변수가 저장됩니다. Idol(this.name); void sayName() { print('저는 ${this.name}입니다.'); } }
위에 코드처럼 생성자의 매개변수를 변수에 저장하는 과정을 생략하는 방법도 있다.
네임드 생성자
네임드 생성자는 네임드 파라미터와 상당히 비슷하고, 일반적으로 클래스를 생성하는 여러 방법을 명시하고 싶을때 사용한다.
class Idol { final String name; final int membersCount; // 1 생성자 Idol(String name, int membersCount) // 1개 이상의 변수를 저장하고 싶을 때는 , 기호로 연결해주면 됩니다. : this.name = name, this.membersCount = membersCount; // 2 네임드 생성자 // {클래스명.네임드 생성자명} 형식 // 나머지 과정은 기본 생성자와 같습니다. Idol.fromMap(Map<String, dynamic> map) : this.name = map['name'], this.membersCount = map['membersCount']; void sayName() { print('저는 ${this.name}입니다. ${this.name} 멤버는 ${this.membersCount}명입니다.'); } }
생성자에서 매개변수 2개를 받고, 네임드 생성자를 {클래스명, 네임드 생성자명} 형식으로 지정하면 된다.
프라이빗 변수
일반적으로 프라이빗 변수는 클래스 내부에서만 사용하는 변수를 칭하지만 다트 언어에서는 같은 파일에서만 접근 가능한 변수이다.
class Idol { // 1 '_'로 변수명을 시작하면 // 프라이빗 변수를 선언할 수 있습니다. String _name; Idol(this._name); } void main() { Idol blackPink = Idol('블랙핑크'); // 같은 파일에서는 _name 변수에 접근할 수 있지만 // 다른 파일에서는 _name 변수에 접근할 수 없습니다. print(blackPink._name); }
프라이빗 변수는 변수명을 _ 기호로 시작해서 선언할 수 있다.
게터 / 세터
게터 : 값을 가져올 때 사용
세터 : 값을 지정할 때 사용
가변변수를 선언해도 직접 값을 가져오거나 지정할 수 있지만 게터와 세터를 사용하면 어떤 값이 노출되고 어떤 형태로
노출될지 그리고 어떤 변수를 변경 가능하게 할지 유연하게 정할 수 있다.
상속
extends 키워드를 사용해 상속 할 수 있다. 상속은 어떤 클래스의 기능을 다른 클래스가 사용할 수 있게 하는 기법이다.
기능을 물려주는 클래스를 부모 클래스, 물려받는 클래스를 자식 클래스라고 한다.
[ 부모 클래스 ]
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 자식 클래스 extends 부모 클래스 순서 class BoyGroup extends Idol { // 2 상속받은 생성자 BoyGroup( String name, int membersCount, ) : super( // super는 부모 클래스를 지칭합니다. name, membersCount, ); // 3 상속받지 않은 기능 void sayMale() { print('저는 남자 아이돌입니다.'); } }
자식클래스는 부모 클래스의 모든 기능을 상속 받는다.
현재 클래스를 지칭하는 this와 달리 super는 상속한 부모 클래스를 지칭한다.
부모 클래스인 Idol클래스에 기본 생성자가 있는 만큼 자식 클래스에서 부모 클래스의 생성자를 실행해줘야 한다. 그렇지 않으면
모든 기능을 상ㅅ혹 받아도 변숫값들을 설정하지 않아서 기능을 제대로 사용할 수 없다.
상속받지 않은 메서드나 변수를 새로 추가할 수도 있다.
void main() { BoyGroup bts = BoyGroup('BTS', 7); // 생성자로 객체 생성 bts.sayName(); // 1 부모한테 물려받은 메서드 저는 BTS입니다. bts.sayMembersCount(); // 2 부모한테 물려받은 메서드 BTS 멤버는 7명입니다. bts.sayMale(); // 3 자식이 새로 추가한 메서드 저는 남자 아이돌입니다. }
부모 클래스에 공통으로 사용하는 변수와 메서드를 정의해 상속받으면 결과적으로 자식 코드들은 해당 값들을 사용할 수 있어서 중복코딩하지 않아도 된다.
다만 부모 클래스에서 자식 클래스에 새로 추가한 sayMale()메서드를 호출할 수 없다.
오버라이드
부모 클래스 또는 인터페이스에 정의된 메서드를 재정의할 때 사용된다.
다트에서는 override키워드를 사용하지 않고도 오버라이드 할 수 있다.
class GirlGroup extends Idol { // 2.3 상속에서처럼 super 키워드를 사용해도 되고 다음처럼 생성자의 매개변수로 // 직접 super 키워드를 사용해도 됩니다. GirlGroup( super.name, super.membersCount, ); // 1 override 키워드를 사용해 오버라이드합니다. @override void sayName() { print('저는 여자 아이돌 ${this.name}입니다.'); } }
부모 클래스에 이미 존재하는 메서드를 재정의 할 경우 override키워드를 사용해 메서드 오버라이드를 한다.
다만 override키워드를 생략해도 되지만 직접 명시하는게 협업 및 유지보수에 유리하다.
인터페이스
상속은 공유되는 기능을 상속받는 개념이지만 인터페이스는 공통으로 필요한 기능을 정의만 해두는 역할을 한다.
상속은 단 하나의 클래스만 할 수 있지만 인터페이스는 적용 개수에 제한이 없다.
여러 인터페이스를 적용하려면 ' , ' 기호를 사용해 나열해 입력하면 된다.
// 1 implements 키워드를 사용하면 원하는 클래스를 인터페이스로 사용할 수 있습니다. class GirlGroup implements Idol { 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 blackPink = GirlGroup('블랙핑크', 4); blackPink.sayName(); // 저는 여자 아이들 블랙핑크입니다. blackPink.sayMembersCount(); // 블랙핑크 멤버는 4명입니다. }
믹스인
믹스인은 특정 클래스에 원하는 기능들만 골라 넣을 수 있는 기능이다.
특정 클래스를 지정해서 속성들을 정의할 수 있으며 지정한 클래스를 상속하는 클래스에서도 사용할 수 있다.
인터페이스처럼 한 개의 클래스에 여러 개의 믹스인을 적용할 수도 있다.
mixin IdolSingMixin on Idol{ void sing(){ print('${this.name}이 노래를 부릅니다.'); } } // 믹스인을 적용할 때는 with 키워드 사용 class BoyGroup extends Idol with IdolSingMixin{ BoyGroup( super.name, super.membersCount, ); void sayMale() { print('저는 남자 아이돌입니다.'); } } void main(){ BoyGroup bts = BoyGroup('BTS', 7); // 믹스인에 정의된 sing() 함수 사용 가능 bts.sing(); }
추상
추상은 상속이나 인터페이스로 사용하는 데 필요한 속성만 정의하고 인스턴스화할 수 없도록 하는 기능이다.
아이돌 클래스를 인터페이스로 사용하고 따로 인스턴스화할 일이 없다면, 아이돌 클래스를 추상 클래스로 선언해서 인스턴스화를 방지하고
메서드 정의를 자식 클래스에 위임할 수 있다.
추상클레스는 추상 메서드를 선언할 수 있으며 추상메서드는 함수의 반환타입, 이름, 매개변수만 정의하고 함수
바디의 선언을 자식 클래스에서 필수로 정의하도록 강제한다.
// 1 abstract 키워드를 사용해 추상 클래스 지정 abstract class Idol { final String name; final int membersCount; Idol(this.name, this.membersCount); // 2 생성자 선언 void sayName(); // 3 추상 메서드 선언 void sayMembersCount(); // 4 추상 메서드 선언 }
abstract키워드로 추상클래스를 지정하고 생성자를 비롯해 메서드도 선언만 한다.
추상 클래스는 어떻게 동작하는지 정의가 없고 선언까지만 해주면 된다.
[추상 클래스를 구현하는 클래스 ]
// implements 키워드를 사용해 추상 클래스를 구현하는 클래스 class GirlGroup implements Idol { 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 blackPink = GirlGroup('블랙핑크', 4); blackPink.sayName(); blackPink.sayMembersCount(); }
추상 메서드는 부모 클래스를 인스턴스화할 일이 없고, 자식클래스들에 필수적으로 또는 공통적으로 정의돼야 하는 메서드가 존재할 때 사영된다.
추상 클래스는 인스턴스화가 필요 없는 공통 부모 클래스를 만들 때 사용한다.
제네릭
제네릭은 클래스나 함수의 정의를 선언할 때가 아니라 인스턴스화하거나 실행할 때로 미룬다.,
특정 변수의 타입을 하나의 타입으로 제한하고 싶지 않을 때 자주 사용한다.
// 인스턴스화할 때 입력받을 타입을 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 변수의 타입이 자동으로 유추됩니다. // reduce() 함수가 기억나지 않는다면 1.4.1절 'List 타입'을 복습하세요. print(cache.data.reduce((value, element) => value + element)); }
스태틱
지금까지 변수와 메서드 등 모든 속성은 각 '클래스의 인스턴스'에 귀속 되었다면 static키워드를 사용하면
클래스 자체에 귀속된다.
class Counter{ // 1 static 키워드를 사용해서 static 변수 선언 static int i= 0; // 2 static 키워드를 사용해서 static 변수 선언 Counter(){ i++; print(i++); } } void main() { Counter count1 = Counter(); Counter count2 = Counter(); Counter count3 = Counter(); }
변수 i를 스태틱으로 지정한다. 카운터클래스에 귀속되기 때문에 인스턴스를 호출할 때마다 1씩 증가한다.
생성자 this.i가 아니고 i로 명시해서 static변수는 클래스에 직접 귀속되기 떄문에 생성자에서 스태틱값을 지정하지 못한다.
결과적으로 스태틱키워드는 인스턴스끼리 공유해야 하는 정보에 지정하면 된다.
캐스케이드 연산자
인스턴스에서 해당 인스턴스 속성이나 멤버 함수를 연속해서 사용하는 기능이다.
캐스케이드 연산자는 .. 기호를 사용한다.
void main() { // cascade operator (..)을 사용하면 // 선언한 변수의 메서드를 연속으로 실행할 수 있습니다. Idol blackpink= Idol('블랙핑크, 4) ..sayName() ..sayMembersCount(); }
이처럼 간결한 코드를 작성할 수 있다.