프로그래밍/플러터

다트 객체지향 프로그래밍

스루나루 2024. 2. 10. 16:10
728x90
728x90

 

객체지향 프로그래밍의 필요성

 

- 모든 코드를 main()함수에다가 정의하면 코드 조짐 : 유지보수 빡세고 가독성 최악

- 변수와 메서드를 특정 클래스에 종속되게 함으로서 객체지향 프로그래밍을 할 수 있음 

- 클래스라는 설계도를 사용해서 데이터의 속성 , 기능을 정의하여 객체지향적으로 코드 작성 가ㅡㄴㅇ 

 

 

클래스 

- 일종의 설계도 

// class 키워드를 입력 후 클래스명을 지정해 클래스를 선언합니다.
class Idol {
  // ❶ 클래스에 종속되는 변수를 지정할 수 있습니다.
  String name = '블랙핑크';

  // ❷ 클래스에 종속되는 함수를 지정할 수 있습니다.
  // 클래스에 종속되는 함수를 메서드라고 부릅니다.
  void sayName() {

    // ❸ 클래스 내부의 속성을 지칭하고 싶을 때는 this 키워드를 사용하면 됩니다.
    // 결과적으로 this.name은 Idol 클래스의 name 변수를 지칭합니다.
    print('저는 ${this.name}입니다.');
    // ➍ 스코프 안에 같은 속성 이름이 하나만 존재한다면 this를 생략할 수 있습니다.
    print('저는 $name입니다.');
  }
}

void main() {
  // 변수 타입을 Idol로 지정하고
  // Idol의 인스턴스를 생성할 수 있습니다.
  // 인스턴스를 생성할 때는 함수를 실행하는 것처럼
  // 인스턴스화하고 싶은 클래스에 괄호를 열고 닫아줍니다.
  Idol blackPink = Idol();  // ➊ Idol 인스턴스 생성

  // 메서드를 실행합니다.
  blackPink.sayName();
}

 

 

1. 생성자 

- 클래스의 인스턴스를 만드는 메서드 

- 파라미터를 받아서 클래스의 멤버 변수를 초기화 할 수 있음 

- 보통 멤버 변수는 final로 정의하여 변수의 변경을 방지함 

- 클래스 하나로 인스턴스를 여러개 만들어 코드 중복없이 활용이 가능해짐 - 유지보수가 편해짐 

class Idol {
  // ❶ 생성자에서 입력받는 변수들은 일반적으로 final 키워드 사용
  final String name;

  // ❷ 생성자 선언
  // 클래스와 같은 이름이어야 합니다.
  // 함수의 매개변수를 선언하는 것처럼 매개변수를 지정해줍니다.
  Idol(String name) : this.name = name;

  void sayName() {
    print('저는 ${this.name}입니다.');
  }
}

void main() {
  // name에 '블랙핑크' 저장
  Idol blackPink = Idol('블랙핑크');
  blackPink.sayName();

  // name에 'BTS' 저장
  Idol bts = Idol('BTS');
  bts.sayName();
}

 

 

2. 네임드 생성자 

- 클래스를 생성하는 방법을 여러가지 정의하고 싶을 때 사용 

- 딱히 기본생성자랑 큰 틀에 차이는 없는 듯 

class Idol {
  final String name;
  final int membersCount;

  // ❶ 생성자
  Idol(String name, int membersCount)
  // 1개 이상의 변수를 저장하고 싶을 때는 , 기호로 연결해주면 됩니다.
      : this.name = name,
        this.membersCount = membersCount;

  // ❷ 네임드 생성자
  // {클래스명.네임드 생성자명} 형식
  // 나머지 과정은 기본 생성자와 같습니다.
  Idol.fromMap(Map<String, dynamic> map)
      : this.name = map['name'],
        this.membersCount = map['membersCount'];

  void sayName() {
    print('저는 ${this.name}입니다. ${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

void main() {
  // 기본 생성자 사용
  Idol blackPink = Idol('블랙핑크', 4);
  blackPink.sayName();

  // fromMap이라는 네임드 생성자 사용
  Idol bts = Idol.fromMap({
    'name': 'BTS',
    'membersCount': 7,
  });
  bts.sayName();
}

 

3. 프라이빗 변수 

- 같은 파일에서만 접근 가능한 변수 

- '_' 로 시작하는 변수 

class Idol {
  // ❶ '_'로 변수명을 시작하면
  // 프라이빗 변수를 선언할 수 있습니다.
  String _name;

  Idol(this._name);
}

void main() {
  Idol blackPink = Idol('블랙핑크');

  // 같은 파일에서는 _name 변수에 접근할 수 있지만
  // 다른 파일에서는 _name 변수에 접근할 수 없습니다.
  print(blackPink._name);
}

 

 

4. 게터 / 세터 

- 값을 가져오고 값을 세팅하는 함수

- 보통 객체지향 프로그래밍을 할 때 변수에 불변성을 지정하기에 세터는 잘 안 씀

 

class Idol {
  String _name= '블랙핑크' ;

  // ❶ get 키워드를 사용해서 게터임을 명시합니다.
  // 게터는 메서드와 다르게 매개변수를 전혀 받지 않는다.
  String get name {
    return this._name;
  }

  // ❷ 세터는 set이라는 키워드를 사용해서 선언합니다.
  // 세터는 매개변수로 딱 하나의 변수를 받을 수 있습니다.
  set name(String name) {
    this._name = name;
  }
}

void main() {
  Idol blackPink = Idol();

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

// ❶ extends 키워드를 사용해서 상속받습니다.
// class 자식 클래스 extends 부모 클래스 순서입니다.
class BoyGroup extends Idol {

  // ❷ 상속받은 생성자
  BoyGroup(
      String name,
      int membersCount,
      ) :  super(   // super는 부모 클래스를 지칭합니다.
    name,
    membersCount,
  );

  // ❸ 상속받지 않은 기능
  void sayMale() {
    print('저는 남자 아이돌입니다.');
  }
}

void main() {

  BoyGroup bts = BoyGroup('BTS', 7);  // 생성자로 객체 생성

  bts.sayName();          // ❶ 부모한테 물려받은 메서드
  bts.sayMembersCount();  // ❷ 부모한테 물려받은 메서드
  bts.sayMale();          // ❸ 자식이 새로 추가한 메서드
}

 

 

 

오버라이드

- 부모 클래스 또는 인터페이스에 정의된 메서드를 재정의할 때 사용 

- ovveride는 생략 가능 

 

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 {
  // 2.3 상속에서처럼 super 키워드를 사용해도 되고 다음처럼 생성자의 매개변수로 직접 super 키워드를 사용해도 됩니다.
  GirlGroup(
      super.name,
      super.membersCount,
      );

  // ❶ override 키워드를 사용해 오버라이드 합니다.
  @override
  void sayName() {
    print('저는 여자 아이돌 ${this.name}입니다.');
  }
}

void main() {
  GirlGroup redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName(); // ❶ 자식 클래스의 오버라이드된 메서드 사용

  // sayMembersCount는 오버라이드하지 않았기 때문에
  // 그대로 Idol 클래스의 메서드가 실행됩니다.
  redVelvet.sayMembersCount(); // ❷ 부모 클래스의 메서드 사용
}

 

 

인터페이스 

- 공통으로 필요한 기능을 정의

- 따로 인터페이스라고 선언하는 문구는 없음

- 인터페이스는 implements를 사용해서 다수 적용이 가능

- 인터페이스로 받은 메서드를 반드시 다 재정의 해줘야함 ! - 원래 목적자체가 재정의할 필요가 있는 기능을 정의하는 용도임  

 

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

// ❶ 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 redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName();
  redVelvet.sayMembersCount();
}

 

 

믹스 인 

- 특정 클래스에 원하는 기능들만 골라 넣을 수 있는 기능 

- 특정 클래스의 속성을 정의

- 여러개의 믹스인 정의 가능 : 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{
  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);

  // 믹스인에 정의된 dance() 함수와 sing() 함수 사용 가능
  bts.sing();
}

 

 

추상

- 상속이나 인터페이스로 사용하는데 필요한 속성만 정의하고 인스턴스는 못 만드는 놈 

- 추상 메서드 선언 가능 

- 오히려 쓰는 거는 추상처럼 기능 정의할 거만 만들어두고 상속해서 기능을 재정의하는 쪽일 듯 

 

// ❶ abstract 키워드를 사용해 추상 클래스 지정
abstract class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount); // ❷ 생성자 선언

  void sayName();          // ❸ 추상 메서드 선언
  void sayMembersCount();  // ➍ 추상 메서드 선언
}

// 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 redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName();
  redVelvet.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.3.1절 List타입의 reduce() 함수를 복습해보세요.
  print(cache.data.reduce((value, element) => value + element));
}

 

 

스태틱

 

- 변수와 메서드 등 보통 클래스의 인스턴스에 귀속되어 있음

- static으로 정의하면 클래스에 귀속됨 

- 인스턴스마다 해당 값을 공유하게 됨 

class Counter{
  // ❶ static 키워드를 사용해서 static 변수 선언
  static int i= 0;


  // ❷ static 키워드를 사용해서 static 변수 선언
  Counter(){
    i++;
    print(i++);
  }
}

void main() {
  Counter count1 = Counter();
  Counter count2 = Counter();
  Counter count3 = Counter();
}

1
3
5

 

 

케스케이드 연산자

- 해당 인스턴스의 속성이나 멤버 변수를 연속해서 사용하는 기능 

- .. 라는 기호사용

- 코드가 간결해짐 

void main(){
  Idol blackpink = Idol('blackpink',4)
    ..sayName()
    ..sayMembersCount();
}

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

 


저는 blackpink입니다.
blackpink 멤버는 4명입니다.

 

 

참고

“이 글은 골든래빗 《Must Have 코드팩토리의 플러터 프로그래밍 2판》의 스터디 내용 입니다.”

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
728x90