Dart 객체지향 프로그래밍

SeonHo·2024년 8월 3일

Dart

목록 보기
2/13
post-thumbnail

Dart 에서의 객체지향 개념

  • Dart는 강력한 객체지향 프로그래밍(OOP) 기능을 지원하는 언어로,
    Flutter 프레임워크와 함께 모바일, 앱 개발에서 사용합니다.
  • Dart의 OOP는 현실 세계의 모든것을 코드로 모델링하여
    코드의 구조화하고 재사용성을 증가 시킵니다.

1. 클래스 (Class)

Dart에서 클래스는 객체(Object)를 만들기 위한 객체의 설계도 입니다.
클래스는 객체가 가져야 할 속성(attribute)메서드(method)를 정의합니다.

클래스의 역할

  • 동일한 종류의 객체를 여러 번 생성할 때
    클래스를 사용하면 코드를 효율적으로 재사용 할 수 있습니다.
  • 복잡한 프로그램을 작은 단위로 나누어 관리하게 쉽게 만들어줍니다.
  • 객체 지향 프로그래밍에서 클래스는 가장 기본적인 개념입니다.

ex) 요리를 만들 요리 레시피

생성자 (Constructor)

생성자는 객체를 생성하고 초기화 하는 역할을 합니다.
생성자는 객체가 생성될 때 자동으로 호출됩니다.
생성자는 객체의 인스턴스 변수에 초기값을 할당합니다.

특징

  • 클래스와 같은 이름을 가집니다.
  • 반환형을 명시하지 않습니다.
  • this 키워드를 사용하여 인스턴스 변수에 접근합니다.

ex) 요리자

메서드 (Method)

메서드는 객체(instance)가 수행할 행동을 정의합니다.
메서드는 객체가 생성된 후, 필요에 따라 호출합니다.
메서드는 객체의 상태를 변경하거나, 특정값을 반환합니다.

특징

  • 클래스 내부에 정의된 함수입니다.
  • 반환형을 명시할 수 있습니다.
  • this 키워드를 사용하여 인스턴스 변수에 접근합니다.

ex) 요리 하는 과정 / 행동

객체 (Object)

객체는 클래스를 바탕으로 생성된 실제 인스턴스(instance) 입니다.
각 객체는 고유한 상태 (속성 값)을 가질 수 있으며,
객체를 생성하면 클래스에 정의된 속성메서드를 가진
실제 데이터가 메모리에 할당됩니다.

ex) 실제로 만들어진 음식

1. 요리 재료 클래스 정의

// 요리 재료 클래스
class Ingredient {
	String name; // class 인스턴스 (name,quantity,unit)
    double quantity; // quantity(양)
    String unit; // unit(단위)
    
    // 생성자 : 객체 값을 초기화
    Ingredinet(this.name, this.quantity, this.unit);
    
    // 메서드 
    String toString() {
    	return '$quantity $unit의 $name';
    }
}

2. 요리 레시피 클래스 정의

class Recipe {
	String name;
    List<Ingredient> ingredients; // ingredients(요리 재료)
    String instructions; // instructions(만드는 법)
    
    // 생성자 객체 값을 초기화
    Recipe(this.name, this.ingredients, this.instructions);
    
    // 메서드
    void printRecipe() {
    	print('--- $name 레시피 ---');
        print('재료');
        for (var ingredient in ingredients) {
        	print('- ingredient');
    	}
    print('만드는 법');
    print('instructions');
	}
}

3. 요리 만들기 예시

// 메서드
void main() {
	// 재료 생성
    Ingredient egg = Ingredient('달걀', 2, '개');
    Ingredient flour = Ingredient('밀가루', 100, 'g');
    Ingredient suger = Ingredient('설탕', 50, 'g');
    
    List<Ingredient> pancakeIngredients = [egg, flour, sugar];
    Recipe pancakeRecipe = Recipe('팬케이크', pancakeIngredients, '달걀, 밀가루, 설탕을 섞고 팬에 부쳐줍니다.');
    
    pancakeRecipe.printRecipe();
}

2. 상속

Dart에서 상속은 클래스 간의 계층 구조를 형성하여
코드의 재사용성을 높이고, 유지보수성을 향상 시키는 중요한 개념입니다.
부모 클래스(superclass)의 속성과 메서드를 정의하고
자식 클래스(subclass)는 필요한 부분은 물려받고,

필요에 따라 수정하거나 추가하여 활용할 수 있습니다. <- 다형성 부분

// 부모 클래스(superclass)
class Animal {
	String name;
    
    // 생성자 : 변수 초기화
    Animal(this.name);
    
    // 메서드
    void eat() {
    	print('$name이 먹습니다.');
    }
}

// 자식 클래스(subclass) 상속
class Dog extends Animal {
	@override // 오버라이드 (다형성 부분)
    void bark() {
    	print('$name'이 짖습니다.');
    }
}

void main() {
	Dog mydog = Dog('치와와');
    myDog.eat(); // 부모 클래스 메서드
    myDog.bark(); // 자식 클래스 메서드
}

상속의 종류

  • 단일 상속 : 하나의 클래스가 무조건 하나의 부모 클래스만 가질수 있습니다.
  • 다중 상속 :
    하나의 클래스에 여러 개의 부모 클래스를 가질수 있습니다.
    Dart는 직접적인 다중 상속은 지원하지 않지만,
    Mixin을 통해 유사한 기능을 제공합니다.

3. 믹스인 (Mixin)

믹스인은 클래스에 기능을 추가하는 방법입니다.
믹스인 자체는 인스턴스를 생성하지 않습니다.
믹스인을 사용할려면 믹스인을 사용하는 클래스를 인스턴스화 해야합니다.
믹스인을 다른 클래스에 추가할 때 with 키워드를 사용해야 합니다.

// 믹스인 정의 기능 추가
mixin Flyable {
  void fly() {
    print('날아다닙니다.');
  }
}

class Animal {
  void move() {
    print('움직입니다.');
  }
}

// 상속 및 믹스인
class Bird extends Animal with Flyable {
  void sing() {
    print('지저귀다.');
  }
}

class Fish extends Animal {
  void swim() {
    print('헤엄칩니다.');
  }
}

void main() {
  Bird bird = Bird(); // 클래스 인스턴스화
  bird.move(); // 움직입니다.
  bird.fly();  // 날아다닙니다.
  bird.sing(); // 지저귀다.

  Fish fish = Fish();
  fish.move(); // 움직입니다.
  fish.swim(); // 헤엄칩니다.
}

상속 시 주의할 점

  • 상속의 오남용 : 모든 클래스를 상속 관계로 만들 필요는 없습니다.
  • is-a 관계를 나태내야 합니다.
    ex) 개는 동물이다. (o) 자동차는 엔진이다 (x)

상속과 믹스인 차이점

특징상속믹스인
관계is-a 관계 (A 는 B 의 종류이다)has-a (A 는 B 를 가지고 있다)
목적계층 구조 형성(자식 extends 부모)메서드 추가
제약하나의 부모 클래스만 상속 가능여러 믹스인을 혼합가능

Dart의 다형성 (Polymorphism)

Dart의 다형성은 다양한 객체가 하나의 타입으로 다루는 방법입니다.
같은 메서드를 통해 다양한 동작을 수행할 수 있도록 하는 것입니다.

다형성 예시)
부모 클래스: 조미료

  • 속성: name, type
  • 메서드: made()

자식 클래스 : 소금, 설탕

  • 소금 made() 메서드를 호출하면 "바다물을 증발하여 만듭니다."
  • 설탕 made() 메서드를 호출하면 "사탕수수를 정제하여 만듭니다."

"만드는 방법" 공통적인 인터페이스(메서드)를 가지고 있지만,
각 조미료에 제조 방법 과정이 다릅니다.
다형성은 같은 이름의 메서드를 통해 다양한 객체의 고유한 특징을
나타낼 수 있도록 하는 것입니다.

4. 오버라이드 (Override)

Dart에서의 오버라이드는 자식 클래스가 부모 클래스의 메서드를 재정의하는 것을 의미합니다.
부모 클래스에서 이미 정의된 메서드를 자식 클래스에서
다시 구현하여 다른 동작을 수행하도록 만드는 것을 말합니다.
오버라이드 하는 메서드에 @override 어노테이션을 붙여
오버라이드의 의도를 명확하게 해주는 것이 좋습니다.

// 부모 클래스
class Animal {
  void makeSound() {
    print('makeSound');
  }
}
// 자식 클래스
class Dog extends Animal {
  @override
  void makeSound() {
    print('Wolf');
  }
}
// 자식 클래스
class Cat extends Animal {
  @override
  void makeSound() {
    print('Meow');
  }
}

5. 인터페이스 (Interface)

Dart에서 인터페이스는 계약서(Contract)라고 생각하시면 편합니다.

  • 인터페이스는 규칙입니다.
  • 클래스는 그 규칙을 따르는 것입니다.

implements 키워드는

  • Dart에서 클래스가 특정 인터페이스를 구현한다는 것을 나타내는 데 사용됩니다.
    해당 클래스가 인터페이스에서 정의된 모든 메서드를 구현해야 함을 의미합니다.

인터페이스는 클래스가 가져야 할 메서드의 형식만 정의하고,
실제 구현은 각 클래스에서 자유롭게 할 수 있도록 하는 것입니다.
Dart에서 인터페이스는 모든 메서드가 추상 메서드로 구성됩니다.

abstract class Animal {
    void makeSound(); // 모든 동물은 소리를 낼 수 있어야 한다는 계약
}

class Dog implements Animal {
    @override
    void makeSound() {
    	print('bark');
    }
}

6. 추상 (Abstarct)

추상 클래스란 직접 인스턴스를 생성할 수 없는 클래스 입니다.
추상 메서드는 구현체 없이 메서드의 선언으로만 존재하는 메서드 입니다.
추상 클래스는 완성된 클래스가 아니라,
다른 클래스들이 상속받아 완성할 수 있는 기본적인 구조를 제공합니다.
추상 클래스를 선언할 때 abstract 키워드는 반드시 필요합니다.

// 추상 클래스
abstract class Animal {
	String name;
    
    Animal(this.name);
    
    void makeSound();
}

class Dog extends Animal {
	Dog(String name) : super(name);
    
    @override
    void makeSound() {
    	print('$name'이 짖습니다.');
    }
}

class Cat extends Animal {
	Cat(String name) : super(name);
    
    @override
    void makeSound() {
    	print('$name'이 소리냅니다');
	}
}

인터페이스와 추상 클래스의 차이점

특징인터페이스추상 클래스
관계완전한 추상화 (모든 메서드가 추상 메서드)부분적인 구현 가능(일반 메서드 포함)
목적다중 상속 가능단일 상속
제약계약(contract) 정의, 다양한 클래스 간의 관계 설정공통적인 특징 정의, 상속 구조 형성

7. 제네릭 (Generic)

제네릭은 컴파일 시 타입 안정성을 보장하기 위해 사용되는 강력한 도구입니다.
특정 자료형에 종속되지 않고, 다양한 자료형을 다룰 수 있도록 하는
매개변수화된 타입이라고 할 수 있습니다.

class Pair<K,V> {
	K key;
    V value;
    
    Pair(this.key, this.value);
}

7-1. 제네릭 클래스와 메서드

class Box<T> {
	T value;
    
    Box(this.value);
    
    T getValue() {
    	return value;
    }
}

Box<int> numberBox = Box(42);
Box<String> stringBox = Box('Hello');

print(numberBox.getValue());
print(stringBox.getValue());

7-2. 제네릭 함수

T identity<T>(T value) {
	return value;
}

print(identity<int>(42));
print(identity<String>('Hello'));

7-3. 제네릭 확장

extension InterableExtension<T> on Iterable<T> {
	T? firstOrNull() {
    	return isEmpty ? null : first;
    }
}

List<int> numbers = [1,2,3];
int? firstNumber = numbers.firstOrNull();
print(firstNumber);

7-4. 제네릭 인터페이스

abstract class Comparable<T> {
	int compareTo(T other);
}

class Person implements Comparable<Person> {
	final String name;
    final int age;
    
    Person(this.name, this.age);
    
    @override
    int compareTo(Person other) {
    	return name.compareTo(other.name);
    }
}

7-5. 제네릭 컬렉션

List<int> numbers = [1,2,3];
Set<String> names = {'Alice', 'Bob', 'Charlie'};
Map<String, int> ages = {'Alice': 30, 'Bob':25};

8. 스태틱 (Static)

Dart에서 static 키워드는 클래스 내에서 변수와 메서드를 선언할 때 사용됩니다.
static 으로 선언된 맴버(변수와 메서드)는 클래스 자체에 속하며,
모든 인스턴스가 공유되는 단 하나의 값을 가집니다.

static 변수 (클래스 변수)

  • 인스턴스를 생성하지 않고도 클래스 이름으로 직접 접근할 수 있습니다.
  • 클래스의 모든 인스턴스가 동일한 static 변수를 참조합니다.
  • 한 인스턴스에서 static 변수 값을 변경하면 다른 모든 인스턴스의
    값도 변경 됩니다.
class Person {
  static int population = 0;
  String name;

  Person(this.name) {
    population++; 
  }
}

void main() {
  Person person1 = Person('Alice');
  Person person2 = Person('Bob');

  print(Person.population); // 출력: 2
}

static 메서드 (클래스 메서드)

  • 인스턴스 생성 없이 클래스 이름으로 직접 호출할 수 있습니다.
  • static 메서드는 인스턴스 변수에 직접 접근 할 수 없습니다.
    static 메서드는 다른 static 변수나 메서드에서만 접근할 수 있습니다.
class MathUtils {
  static int add(int a, int b) { // `add`는 `static` 메서드 
    return a + b;
  }
}

void main() {
  int result = MathUtils.add(3, 5); // 클래스 이름 직접 호출
  print(result); // 출력: 8
}

9. final 과 const

Dart 에서 변수의 값을 변경하지 않고 고정시키고 싶을 때
사용하는 키워드가 바로 finalconst 입니다.
두 키워드 모두 변수 값을 한번 할당하면 변경할 수 없도록 만들었지만,
사용시기는 약간 다릅니다.

특징finalconst
할당 시점런타임 또는 생성자컴파일 시점
객체 변경 가능성객체 속성은 변경 가능객체 자체 및 속성 모두 변경 불가
사용 시기대부분의 상황에서 사용 가능컴파일 시점에 값이 확정될 때 사용

final 키워드

  • 변수를 선언할 때 값을 할당하거나, 생성자에서 초기화할 수 있습니다.
    한번 할당된 후에는 값을 변경할 수 없습니다.
  • fianl로 선언된 변수는 객체를 가리키는 참조는 변경될 수 없지만,
    객체의 속성은 변경될 수 있습니다.
  • 런타임에 값을 할당할 수 있습니다.
final String name = 'Alice';
final List<int> numbers = [1,2,3];

final DateTime now = DateTime.now();

const 키워드

  • 변수를 선언할 때 컴파일러가 미리 값을 알 수 있는 상수값만 할당할 수 있습니다.
  • const로 선언된 변수는 객체 자체뿐만 아니라 객체의 속성도 변경 할 수 없습니다.
  • const로 선언시 컴파일 시점에 값이 결정되어야 합니다.
const pi = 3.14159;
const List<String> colors = ['red', 'green', 'blue'];

제가 공부한 Dart 객체지향 프로그래밍을 한번 정리해 봤습니다.
Dart 공식 문서 class부분
자세한 내용은 공식 문서를 참고해주시면 좋을거 같습니다.

profile
목표를 달성하자!

0개의 댓글