Nomadcoders - Dart 시작하기 #4

Nomadcoders - Dart 시작하기 #4

Class의 constructor, named constructor, cascade notation, enum, abstract class, inheritance, mixin에 대해 정리했습니다.

알게된 사실 👨🏻‍💻

Class에서는 타입을 꼭 명시하는 것이 코드의 안정성이나 유지보수 면에서 좋고, 만약 속성 값을 바꾸지 못하게 하려면 final을 사용한다.

또한 Class 메서드 내에서의 this는 어쩔 수 없는 경우(메서드 내에 같은 변수명이 있는 경우)가 아니면 사용하지 않는 것을 권장한다.

constructor

class Player {
    late final String name;
    late int xp;

    Player(String name, int xp) {
        this.name = name;
        this.xp = xp;
    }

    void sayHello() {
        print("Hi my name is $name");
    }
}

class Player {
    final String name;
    int xp;

    Player(this.name, this.xp)

    void sayHello() {
        print("Hi my name is $name");
    }
}

class의 경우에도 positional constructor parameters 보다는 named constructor parameters를 사용하는 게 더 좋다. (parameter의 수가 많을수록 더 그렇다)

class Player {
    final String name;
    int xp;

    Player({
        required this.name, 
        required this.xp,
    })

    void sayHello() {
        print("Hi my name is $name");
    }
}

named constructors

class Player {
    final String name;
    int xp;

    Player({
        required this.name, 
        required this.xp,
    })

    Player.createBluePlayer({  // named syntax
        required int age,
    })  : this.name = 'blue',  // <- 콜론(:)으로 Dart에게 여기서 Player 객체를 초기화하겠다~
              this.age = age;

    Player.createRedPlayer(int age) :  // positional syntax
        this.name = 'red',
        this.age = age;

    void sayHello() {
        print("Hi my name is $name");
    }
}

void main() {
    var bluePlayer = Player.createBluePlayer(
        age: 25,
    ); 
    var redPlayer = Player.createRedPlayer(25);
}

named constructor는 syntax sugar로, 실제로 실무에서 많이 쓰인다.

콜론만 써서 argumentproperty를 일대일 초기화하는 생성자를 만든다고 생각하면 된다.

// Player 클래스 생성 및 초기화 작업
class Player {
    final String name;
    int xp;
    String team;

    // named constructor
    Player.fromJson(Map<String, dynamic> playerJson) :
        name = playerJson['name'],
        xp = playerJson['xp'],
        team = playerJson['team'];

    // class method
    void sayHello() {
        print("Hi my name is $name");
    }
}

void main() {
    var apiData = [
        {
            "name": "hardy0",
            "team": "naver",
            "xp": 0,
        },
        {
            "name": "hardy1",
            "team": "kakao",
            "xp": 1,
        },
        {
            "name": "hardy2",
            "team": "line",
            "xp": 2,
        },
        {
            "name": "hardy3",
            "team": "coupang",
            "xp": 2,
        },
        {
            "name": "hardy4",
            "team": "baemin",
            "xp": 2,
        },
    ];

    apiData.forEach((playerJson) {
        var player = Player.fromJson(playerJson);
        player.sayHello();
    });
}

cascade notation

이전에 살펴보았던 Cascade 노테이션을 클래스에서 사용한 코드이다.

class Player {
    final String name;
    int xp;
    string team;

    Player({
        required this.name, 
        required this.xp,
        required this.team,
    })

    void sayHello() {
        print("Hi my name is $name");
    }
}

void main() {
    var hardy = Player(name: 'turtley', xp: 0, team: 'toss')
    ..name = 'hardy'
    ..xp = 1
    ..team = 'apple';

    var kardy = hardy
    ..name = 'kardy'
    ..xp = 2
    ..team = 'google'
  ..sayHello();
}

enums

enum을 사용하면 선택의 폭을 좁혀 실수를 방지하고, 안전성을 높일 수 있다.

enum Team { apple, google, naver, kakao, line, coupang, baemin, toss }
enum XPLevel {0, 1, 2}

class Player {
    final String name;
    XPLevel xp;
    Team team;

    Player({
        required this.name, 
        required this.xp,
        required this.team,
    })

    void sayHello() {
        print("Hi my name is $name");
    }
}

void main() {
    var hardy = Player(name: 'turtley', xp: XPLevel.0, team: Team.toss)
    ..name = 'hardy'
    ..xp = XPLevel.1
    ..team = Team.apple;

    var kardy = hardy
    ..name = 'kardy'
    ..xp = XPLevel.2
    ..team = Team.google
  ..sayHello();
}

abstract class

추상화 클래스로는 객체를 생성할 수 없지만(인스턴스화 X), 특정 메서드를 구현하도록 강제할 수 있다.

추상화 클래스는 다른 클래스들이 직접 구현해야하는 메서드들을 모아 놓은 일종의 청사진이라고 할 수 있고, 수많은 청사진에 메서드의 이름과 반환 타입만 정해서 정의할 수 있다.

추상화 클래스는 다른 클래스들이 어떤 청사진을 가지고 있어야 하는지 정의해주기 때문에 유용하다.

abstract class Human {
    void walk() {}
}

class Developer extends Human {
    void walk() {
        print('developer walking...');
    }
}

class Player extends Human {
    final String name;
    XPLevel xp;
    Team team;

    Player({
        required this.name, 
        required this.xp,
        required this.team,
    })

    void walk {
        print('player walking...');
    }

    void sayHello() {
        print("Hi my name is $name");
    }
}

void main() {
    var hardy = Player(name: 'turtley', xp: XPLevel.0, team: Team.toss)
    ..name = 'hardy'
    ..xp = XPLevel.1
    ..team = Team.apple;

    var kardy = hardy
    ..name = 'kardy'
    ..xp = XPLevel.2
    ..team = Team.google
  ..sayHello();
}

Dart의 추상화 클래스가 특정한 작업이나 기능의 청사진으로 작용한다는 점에서 Swift의 Protocol이 생각났다. 그러나 Dart의 abstract class는 기본 구현을 포함할 수 있으며 다중 상속이 불가능한 반면, Swift의 protocol은 기본 구현을 직접 포함할 수 없지만 extension을 통해 가능하며, 하나의 타입이 여러 protocol을 동시에 채택할 수 있다는 차이가 있다.

StatelessWidget이나 StatefulWidget은 추상화 클래스일까?`

특정 메서드를 구현하도록 강제하는 것은 맞지만, 추상화 클래스는 아니다.

💡 StatelessWidgetStatefulWidget이 직접적으로 추상화 클래스로 선언되지 않은 이유

  1. 정확한 타입 정의: StatelessWidgetStatefulWidget은 Flutter 프레임워크에서 특정 타입의 객체로 인식되어야 한다. 만약 직접적으로 추상화 클래스로 정의된다면, 다른 개발자들이 이를 직접 상속 받아 실제 객체를 생성하는 것이 불가능해진다.

  2. 프레임워크의 일관성: StatelessWidgetStatefulWidget이라는 두 개의 주요 클래스는 Flutter의 핵심적인 구성요소이다. 이 두 클래스를 추상화 클래스로 만들 경우, 개발자들이 별도의 구체적인 클래스를 정의하고, 그 클래스에서 필요한 메소드와 속성을 구현해야 하는데 이는 Flutter의 위젯 트리 구조와의 일관성을 해칠 수 있다.

  3. 간결한 API 디자인: StatelessWidgetStatefulWidget을 직접 상속받아 사용하는 것은 Flutter의 API를 간결하게 유지하는 방법이다. 만약 추상화 클래스로 선언되면, 각 위젯에 대한 추가적인 구현 클래스가 필요하게 되므로, 위젯을 정의하고 사용하는 것이 복잡해질 수 있다.

  4. 내부 메소드와 속성의 활용: 두 클래스는 build 메소드 외에도 다양한 내부 메소드와 속성을 가지고 있다. 이러한 내부 메소드와 속성은 상속받은 위젯에서 직접 사용할 수 있도록 설계되었기 때문에 위젯의 생명주기나 업데이트 과정 등을 더 잘 제어할 수 있다.

결론적으로, StatelessWidgetStatefulWidget을 직접적인 추상화 클래스로 정의하지 않은 것은 Flutter 프레임워크의 디자인 철학, 사용자 경험, 그리고 API의 간결성을 위한 결정이었을 것이라고 생각한다.

Inheritance

class Human {
    final String name;
    Human(required this.name);
    void sayHello() {
        print("Hi my name is $name");
    }
}

enum Team { blue, red }

class Player extends Human {
    final Team team;

    Player({ 
        required this.team,
        required String name,
    }) : super(name: name);  // super 클래스는 확장한 부모 클래스를 의미, name을 Human 클래스에 전달

    @override 
    void sayHello() {
        super.sayHello();  // super를 통해 부모 클래스의 메서드에 접근
        print('and I play for ${team}')
    }
}

Mixin

Mixin은 생성자가 없는 클래스를 의미하며, 다른 클래스의 프로퍼티와 메서드를 그냥 긁어와서 추가하는 경우에 유용하게 사용된다.

Mixin은 하나의 클래스에 단 한 번만 사용하는 것이 아니라 여러 클래스에 재사용을 하는 경우에 사용하는 것이 유의미하다.

class String {
    final double strengthLevel = 1500.99;
}

class QuickRunner {
    void runQuick() {
        print("runnnn!");
    }
}

class Player with Strong, QuickRunner {
    final Team team;

    Player({ 
        required this.team,
        required String name,
    }) : super(name: name); 
    @override
    void sayHello() {
        super.sayHello();
        print('and I play for ${team}')
    }
}