tl;dr
https://dartpad.dev/?id=c34c85e9c3a6e873c9f29ff6229bb130 を見て。
やろうと思ったこと
DDD的(といって合っているかわからないけど)に、「ちゃんとデータクラスを作って、そこに表現力を持たせたい」と思いました。
例えば、裏側で持っているIDは16桁ぐらいあるけど、ユーザーには末尾の4桁しか出さない、みたいなことを Id
クラスのgetterに押し込めたいと思いました。
以下、この例に対して考えていきます。
Dartでかんたんに実現しようとした
Id
クラスとは言っても、基本的には文字列としての機能を持っていてほしく、別クラスとしてちゃんと宣言するのはめんどうに感じてました。
そこで、Dart 2.13 から typedef
を使って、エイリアスを作成しようと思いました。
https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067
また、その宣言したエイリアスに対して extension を利用して、機能を追加しようと思いました。
typedef Id = String;
extension on Id {
String get forUser => substring(length - 4);
}
これ自体は機能するのですが、弊害が大きすぎました。
想定外だった弊害
上記の forUser
は、あくまで Id
として宣言された文字列だけに適用したいものでした。
(他の文字列では、利用できない方が良い。)
// これはちゃんと動いてほしいし、ちゃんと動く
final Id idVar = '1234567890123456';
print('call from Id ${idVar.forUser}');
// これはコンパイルエラーになってほしい、けど動く
final String stringVar = 'hogehoge';
print('call from String ${stringVar.forUser}');
上のコード内のコメントに書いたとおり、ただの String
型(エイリアス元の型)に対しても、Functionが追加されてしまいました。
これでは、「名前」や「住所」といった文字列に対してでも forUser
を実行できてしまい、誤った利用を防げません。
また、コード補完もノイズが増えてしまい、劣化してしまいます。
どうしたか
value
として String
型のpropertyを持つクラスを定義して、その中に独自のFunctionを追加しました。
class Id {
const Id(this.value);
final String value;
String get forUser => value.substring(value.length - 4);
}
String
型が持っている多くの機能は Id
は持っていない状態となりましたが、必要になったら、必要になったFunctionだけを実装していく形で大丈夫だろう、と判断しました。
参考: https://qiita.com/tokkun5552/items/5dcb79e5283a67c2b2fe#valueobject
補足:Genericsを利用した場合
ちょっと不安になって、 List<String>
に追加したfunctionが、 List<int>
に追加されないことは確認しました。
typedef IdList = List<Id>;
extension on IdList {
String get firstItem => this[0];
}
void main() {
final List<String> stringIdList = <String>['234', '567'];
// `firstItem` がちゃんと呼び出せる
print('firstItem ${stringIdList.firstItem}');
final List<int> intList = <int>[1, 2, 3];
// ちゃんとコンパイルエラーになる
// print('firstItem ${intList.firstItem}'); // <- compile error
}
これは、大丈夫そうでした。
補足:他の言語は?
ちゃんと確かめてないですが、他の言語での状況を調べてみました。
Kotlin
Androidアプリ開発でよく利用するKotlinでの状況を調べてみました。
https://discuss.kotlinlang.org/t/extension-functions-on-type-aliases-should-only-be-accessable-from-similar-types/3722/2
https://discuss.kotlinlang.org/t/implicit-conversions-between-type-aliases/3222
こちらの、言語公式コミュニティの方では、 typealias
はあくまで"ニックネーム"としてつけるだけで、Dartと同じ動作になりそうでした。
https://kotlinlang.org/docs/inline-classes.html
https://maku77.github.io/kotlin/misc/inline-class.html
ただ、Kotlinには "inline class" というものがあり、それを利用することで実現できそうです。
("inline class"の主眼はパフォーマンス最適化のようですが。)
https://github.com/dart-lang/language/issues/2727
Dart言語でも、inline classの導入は議論されているようでした。(昨年末ぐらいの話っぽい)
Swift
こちらはiOSアプリ開発でよく利用するSwift。
https://qiita.com/mono0926/items/1b94242d4139d1982a31
ほぼこの記事で詳細に説明されてました。
やはり、 typealias
はあくまで「別の名前をつけただけ」であって、全く区別されないとのことでした。Dartと同じですね。
Swiftでは、Kotlinの"inline class"のように手軽に実現する手段はなさそうですね。
いくつかの方法が例示されていますが、 ExpressibleByXXXLiteral
を利用する方法は、「知らないとできない」ものだと思うので、この機会に知ることができてよかったです。
Go
上記のSwiftのところであげた記事で記載されていましたが、Go言語の type
では、Dartと違う動きになりそうでした。