Class Modifier에 대한 다음 설명은, Dart 공식 사이트의 내용(아래의 출처 참조)을 기반으로, 독자의 이해를 돕기 위한 추가적인 글을 포함하거나, 출처의 글을 수정하는 방식으로 작성하였습니다.
Class modifier는 class 혹은 mixin에 대한 보다 명확한 정의를 가능하게 합니다. Dart 3.0부터는 다음의 클래스 수정자(class modifier)들을 지원합니다.
- abstract
- base
- final
- interface
- sealed
- mixin
일반적으로 클래스 수정자는 class 혹은 mixin 키워드 앞에 위치합니다. 즉 abstract class와 같이 사용합니다. 클래스 수정자의 역할이 “제한”을 두는데 목적을 두고 있으니, 일반적인 Class를 만들고, extend 혹은 implement를 하는 경우, 그리고 일반적인 Mixin을 만들고 싶다면 클래스 수정자를 사용하지 않도록 합니다.
많은 modifier들이 지원되기에, 각각의 기능에 대해서 헷갈릴 수 있습니다. 이를 위해서 Dart 공식 사이트의 Class modifiers reference에서는 각 modifier별 특징을 표로 만들어서 이해를 돕고 있습니다.
abstract
추상 클래스 라고도 불리우는 abstract class는 클래스 내의 모든 기능을 채우지 않는 클래스를 만들기 위하여 이전의 Dart 언어에서도 제공하는 문법입니다. 이전의 기능과 동일한 의미로, abstract class를 parent class로 활용하는 child class에서, abstract class에서 채워지지 않은 기능(메소드 등)을 작성하도록 하는 용도로 사용합니다.
다음의 예제는 abstract class인 Vehicle과, 이를 parent class로 사용하여 만드는 Car class와 MockVehicle class를 예시로 보여줍니다. Vehicle class에서 declare만 된, 즉 메소드의 이름과 입력 파라메터, 그리고 리턴 값의 타입만 정의한 moveForward() 메소드를 child class인 MockVehicle class에서 override하여 define(메소드 내부 기능을 채우는 작업)하는 것을 볼 수 있습니다.
// Library a.dart
abstract class Vehicle {
void moveForward(int meters);
}
// Library b.dart
import 'a.dart';
// Error: Cannot be constructed
Vehicle myVehicle = Vehicle();
// Can be extended
class Car extends Vehicle {
int passengers = 4;
// ···
}
// Can be implemented
class MockVehicle implements Vehicle {
@override
void moveForward(int meters) {
// ...
}
}
base
간단하게 이야기 하면, base 클래스 수정자로 정의한 클래스는, 해당 클래스가 속한 라이브러리의 외부에서 implement 하는 것을 금지합니다. 하지만 extent 하는 것은 가능합니다. 따라서 parent class에 정의된 생성자가, child class들에서 반드시 동일하게 실행되어야 하는 등의 제약을 만들고자 하는 경우에 사용합니다. 생성자 외에도, parent class에 이미 정의한 메소드들이 있다면, child class들에서도 반드시 같은 메소드가 호출되도록 합니다.
다음의 예제는 base 클래스 수정자로 정의한 Vehicle 클래스를 extend하여 Car 클래스를 만드는 것은 가능한 것을 보여줍니다. 하지만 Vehicle 클래스를 implements 하는 것은 불가능 한 것을 보여줍니다.
// Library a.dart
base class Vehicle {
void moveForward(int meters) {
// ...
}
}
// Library b.dart
import 'a.dart';
// Can be constructed
Vehicle myVehicle = Vehicle();
// Can be extended
base class Car extends Vehicle {
int passengers = 4;
// ...
}
// ERROR: Cannot be implemented
base class MockVehicle implements Vehicle {
@override
void moveForward() {
// ...
}
}
위의 예제에서 한가지 사항을 더 확인할 수 있습니다. 즉 child class인 Car 클래스도 base 클래스 수정자를 적용한 것 입니다. 이렇게 하면, 마찬가지로 Car 클래스를 inherit하는 클래스는 허용하지만, implements하는 클래스는 제한할 수 있습니다.
interface
interface class로 정의한 클래스에 대해서는 implements 문법을 통한 새로운 클래스 정의만 가능하고, extends 문법은 불가능합니다. 이를 통해서, 특정 클래스를 implements로 확장한 클래스들이 존재하더라도, 같은 메소드를 지원하도록 합니다. 이를 우아한 표현으로 fragile base class problem을 해소한다고 합니다.
다음의 예제는 interface class인 Vehicle을 implements하여 MockVehicle 클래스를 만드는 것은 가능하지만, extends를 통한 Car 클래스를 만드는 것을 불가능한 것을 보여줍니다.
// Library a.dart
interface class Vehicle {
void moveForward(int meters) {
// ...
}
}
// Library b.dart
import 'a.dart';
// Can be constructed
Vehicle myVehicle = Vehicle();
// ERROR: Cannot be inherited
class Car extends Vehicle {
int passengers = 4;
// ...
}
// Can be implemented
class MockVehicle implements Vehicle {
@override
void moveForward(int meters) {
// ...
}
}
abstract interface
abstract 클래스 수정자와 interface 클래스 수정자를 함께 사용하는 경우입니다. 통상 pure interface라는 별칭으로도 불립니다. 이렇게 정의한 클래스는 스스로 객체화 될 수 없으며, 오로지 다른 클래스에서 implements하여 만들어지는 재료의 역할만 수행합니다.
final
final 클래스 수정자로 정의한 클래스는 사용만 가능하고 extends 혹은 implements를 금지합니다. 다음의 예제는 Vehicle 클래스로 객체를 생성할 수 있지만, 이를 사용하여 Car 혹은 MockVehicle 클래스를 만드는 것은 불가능 할 것을 보여줍니다.
// Library a.dart
final class Vehicle {
void moveForward(int meters) {
// ...
}
}
// Library b.dart
import 'a.dart';
// Can be constructed
Vehicle myVehicle = Vehicle();
// ERROR: Cannot be inherited
class Car extends Vehicle {
int passengers = 4;
// ...
}
class MockVehicle implements Vehicle {
// ERROR: Cannot be implemented
@override
void moveForward(int meters) {
// ...
}
}
sealed
sealed 클래스 수정자로 정의한 클래스는 다음의 제약 조건을 갖습니다.
첫째로 sealed class로 정의한 클래스와 같은 라이브러리에 위치 하지 않은, 라이브러리 밖에서의 extend 혹은 implement는 불가능 합니다.
둘째로 sealed class로 정의한 클래스와 같은 라이브러리 안에서는 extend 혹은 implement가 가능 합니다.
셋째로 sealed class로 정의한 클래스 만으로는 객체를 만들수 없습니다.
넷째로 sealed class가 switch 구문에 적용되는 경우는, sealed class에서 extends/implements된 모든 클래스에 대한 처리가 있어야 합니다.
다음의 예제는 위의 조건들을 보여주기 위해서 만들어 졌습니다.
sealed class Vehicle {}
class Car extends Vehicle {}
class Truck implements Vehicle {}
class Bicycle extends Vehicle {}
String getVehicleSound(Vehicle vehicle) {
// ERROR: The switch is missing the Bicycle subtype or a default case.
return switch (vehicle) {
Car() => 'vroom',
Truck() => 'VROOOOMM',
// TO VOID ERROR: all possible cases should be covered
// _ => '..'
};
}
void main() {
// ERROR: Cannot be instantiated
Vehicle myVehicle = Vehicle();
// Subclasses can be instantiated
Vehicle myCar = Car();
}
먼저 Vehicle 클래스가 sealed class 입니다. 하나의 소스 코드이기에, Car, Truck, Bicycle 클래스를 extends 혹은 implements 하는 것은 가능합니다. (둘째 규칙에 대한 예시)
main()를 보면 Vehicle 클래스 만으로 객체를 만드는 것은 ERROR로 명시되어 있습니다. (셋째 규칙에 대한 예시)
getVehicleSound() 함수에는 switch 구문이 있는데, 부합하는 조건에 대한 코드에는 sealed class를 extends한 Bicycle 클래스에 대한 처리가 없는 것을 볼 수 있습니다. 따라서 이대로 프로그램을 수행하면 에러가 납니다. 이를 해결하고자 _ => ‘..’ 코드를 주석 해제 해주어야 합니다. (넷째 규칙에 대한 예시)
만약 sealed class로 정의한 클래스를 extends/implements할 필요가 없다면, final 클래스 수정자를 사용하도록 합니다.