class 고급 활용 바로시작!!
이전 포스팅과 이어지는 내용이다.
이번에 공부하면서 처음 알게된 부분이었다.
얼마나 코딩을 개판으로 해왔으면
맨날 보는 static인데 그 의미를
찾아볼 생각조차 안했을까...
이번에 문법 공부를 하면서 느끼는 바가 많다ㅠ
static은 한마디로 특정 constructor를
instance가 아닌 class에
귀속시키는 개념이다.
코틀린에서 쓰던 companion 이나
swift의 static과 대응되는 개념같다.
(잘 알지도 못하면서 써오긴 했다..)
다음을 보자.
class Cafe {
String name;
bool hasAmericano = true;
Cafe({required this.name});
}
void main() {
Cafe cafe1 = Cafe(name: "스타벅스");
Cafe cafe2 = Cafe(name: "이디야");
Cafe cafe3 = Cafe(name: "투썸플레이스");
cafe1.hasAmericano = false;
print(cafe1.hasAmericano); // false
print(cafe2.hasAmericano); // true
print(cafe3.hasAmericano); // true
}
위와 같이 평범한 class constructor 사용시
cafe1의 hasAmericano constructor를 바꾼다고
cafe2나 cafe3의 내용이 바뀌진 않는다.
따라서 찍어내는 모든 객체에 대해
hasAmericano constructor값을 넣어줘야 한다.
하지만 아메리카노 없는 카페는 없으니,
이를 카페 클래스 자체에 있는 기본
consructor로 만들고 싶다면?
static을 사용하면 된다.
class Cafe {
static bool hasAmericano = true;
String name;
Cafe({required this.name});
}
void main() {
print(Cafe.hasAmericano); // true
Cafe.hasAmericano = false;
print(Cafe.hasAmericano); // false
}
이처럼 객체 하나하나의 constructor와 구분되는
클래스명.static명 과 같은 형식의
클래스 버전 전역변수를 사용할 수 있게 된다.
기본적으로 한 클래스는 한 클래스만
상속이 가능하지만, 복잡한 oop 설계를
하다보면 여러 클래스의 method나 constructor를
상속할 일이 많아진다.
(물론 내 레벨에서 그럴일은 없었지만..)
이에 다른 언어에서는 다중 상속을
지원하는데, 다트에서는 비슷한 기능이
없나 찾아봤다.
결론은 역시 있었고, mixin이라는 걸
사용하면 가능하다.
class Cafe {
String name;
Cafe({required this.name});
}
mixin Food {
String foodName = "음식";
void food() {
print("$foodName을(를) 팝니다");
}
}
mixin Beverage {
String beverageName = "음료";
void beverage() {
print("$beverageName을(를) 팝니다");
}
}
mixin Coffee on Cafe {
String coffeeName = "커피";
void coffee() {
print("$coffeeName을(를) 팝니다");
}
}
class BrandCafe extends Cafe with Food, Beverage, Coffee {
String brand;
BrandCafe({
required this.brand,
required super.name
});
}
void main() {
BrandCafe cafe = BrandCafe(
name: "투썸플레이스 인천가좌점",
brand: "투썸플레이스"
);
cafe.coffeeName = "돌체라떼";
cafe.foodName = "초코케이크";
cafe.beverageName = "자몽허니블랙티";
cafe.coffee();
cafe.food();
cafe.beverage();
}
// 돌체라떼을(를) 팝니다
// 초코케이크을(를) 팝니다
// 자몽허니블랙티을(를) 팝니다
솔직히 이런 방식은 처음봐서 낯설었다.
사용 방법은 먼저 다중 상속하려는
constructor나 method를
mixin이라는 특수한 class로 정리한 뒤,
class명 뒤에 with라는 형태로
상속시키면 된다.
(꼭 extends로 다른 상속이 없어도 된다)
mixin을 상속받은 클래스에서는 해당
constructor나 method를 별다른
표기 없이 바로 사용할 수 있다.
개발자 문서를 보니 위 코드의
mixin Coffee on Cafe 처럼
on을 통해 상속될 클래스를 지정해주는 형태도
가능한 듯 싶다.
이런 고오급 문법도 사용할 수 있을만큼
내 개발실력이 늘었으면 싶다..
다른 oop 언어가 그렇듯, 다트에서도
추상클래스 / 인터페이스의 역할이 존재한다.
사용 방법은 implements를 활용하는 것이다.
abstract class Cafe {
introduceSeats();
introduce();
}
class Starbucks implements Cafe {
final String name;
final String brandName;
Starbucks({
required this.name,
required this.brandName
});
void introduceSeats() {
print("이 카페의 좌석은 총 30석 입니다.");
}
void introduce() {
print("이 카페의 이름은 $name이고, 브랜드는 $brandName입니다.");
}
}
void main() {
Starbucks cafe = Starbucks(
name: "스타벅스 신촌점",
brandName: "스타벅스"
);
cafe.introduce();
cafe.introduceSeats();
}
// 이 카페의 이름은 스타벅스 신촌점이고, 브랜드는 스타벅스입니다.
// 이 카페의 좌석은 총 30석 입니다.
보이는 것처럼 extends를 썼던 자리에
implements를 써서 상속하면 된다.
추상 클래스 / 인터페이스에서 정의해둔
모든 메소드를 반드시 오버라이딩 해야하며,
일반 클래스 상속과 다르게
한번에 여러 추상클래스 / 인터페이스를
상속받을 수 있다.
그런데 검색해본 결과, 다트에서는
추상 클래스와 인터페이스를 구분하지 않는다.
(swift에서 protocol로 통일해 쓰는것처럼)
따라서 자바에 익숙한 사람이라면
그냥 이게 인터페이스구나 하고 작명해서
쓰면 될 것 같다.
List를 써보면, 타입을 선언할 당시부터
리스트 내부 요소들의 타입을 지정해줄 수 있다.
List<int> numbers = [1, 2, 3];
이처럼 class를 선언할 당시 특정 타입을
지정하여 사용할 수 있다.
이를 서비스에 활용한 사례로는
ApiRequest 구조체를 만들때 였는데,
사례로 같이 보자.
class ApiRequest<ResponseType> {
ResponseType response;
ApiRequest({required this.response});
}
class FetchCafeResponse {
}
void main() {
ApiRequest req = ApiRequest<FetchCafeResponse>(response: FetchCafeResponse());
}
간단하게 구현해본 모습이다.
request에 response 타입을 지정해주어
어떤 형태의 반환값이 나올지 미리 알 수 있다.
참고로 나는 ResponseType이라고 명명했지만
보통 T라는 형태로 많이 쓴다.
다만 위와 같이 활용하는 경우는 없다.
(정확히 말하자면 저럴거면 안쓰는게 낫다)
좀 더 실용성 있게 하자면 다음과 같다.
abstract class ApiResponse {
final int id = 0;
}
abstract class ApiRequest {
printResponseType();
}
class FetchCafeResponse implements ApiResponse {
@override
final int id;
final String name;
final double latitude;
final double longitude;
FetchCafeResponse(this.id, this.name, this.latitude, this.longitude);
}
class FetchCafeRequest<T extends ApiResponse> implements ApiRequest {
final String accessToken;
final String endpoint;
FetchCafeRequest({
required this.accessToken,
required this.endpoint
});
@override
void printResponseType() {
print("이 요청의 반환 타입은 $T 입니다.");
}
}
void main() {
FetchCafeRequest req = FetchCafeRequest<FetchCafeResponse>(
accessToken: "access_token",
endpoint: "/cafe"
);
req.printResponseType();
}
// 이 요청의 반환 타입은 FetchCafeResponse 입니다.
위처럼 ApiResponse, ApiRequest라는
interface를 만들어 각각
FetchCafeResponse와 FetchCafeRequest에
상속 시켰다.
이때, FetchCafeRequest에는 Generic type T를
두었는데, 옆에 extends ApiResponse 라는 것을 붙였다.
이는 다음과 같은 의미이다.
"T라고 하는 타입 파라미터를 받을건데,
이 파라미터의 타입은 ApiResponse로 제한할거야."
ApiResponse를 상속받은 FetchCafeResponse 클래스는
부모와 자신의 클래스를 모두 가지므로,
FetchCafeRequest의 타입 선언자리에
들어올 수 있게 된 것.
따라서 FetchCafeRequest\<String>같은 것은
애초에 쓸 수가 없다는 것이다.
이런걸 보면 애초에 OOP가 원숭이 코더들이
자신의 코드를 망치지 못하게 하기위해
개발됐다는 우스갯소리가 정말인가 싶다..
요즘 코딩하면서 확장함수를 정말 많이쓴다.
자주 쓰는 클래스 로직이 있다면 해당 타입 클래스의
메소드로 등록해놓으면 요긴하게 쓸 수 있다.
다만 너무 남용하면 안쓰느니만 못하다ㄷㄷ
extension StringExtension on String {
String parseYear() {
return substring(0, 4);
}
}
void main() {
print("2023-05-06T13:50:49.554943".parseYear());
// 2023
}
문법 공부는 여기까지!
다트의 기본부터 클래스까지
대략적인 문법 공부는 여기까지다.
물론 각 부분들의 고급 활용까지 한다면
포스팅 열개를 더 쓰고도 남겠지만,
문법서를 만드는게 목적이 아니므로,
각 부분이 필요할 때 더 자세하게
찾아보는걸로 하자.
이제 본격적으로 서비스를 만들어볼껀데,
간략하게 기록할만한 부분만 정리하고
이번 문법 정리처럼 상세하게 쓰지는
않을 듯 하다.
상세하게 하려면 clean architecture부터
MVVM패턴 api service 설계, authentication,
디자인, thread, 권한, async설계 등..
쓸게 너무 많아 개발하는 시간보다
포스팅 쓰는 시간이 더 많아질 것이다.
그래도 컨셉이나 경과정도는
꼭 남겨놓는 습관을 들여야지.