Swift経験者からみたDart言語の不思議 〜暗黙的インターフェースから理解するクラス修飾子編〜

こんにちは!決済認証システム開発事業部の冨永です。
普段はiOSアプリ、iPadアプリを開発しているSwift愛好家です。

iOS・Androidアプリ両アプリを開発できるフレームワークFlutterで使用される言語にDartがあります。
Swift経験者が初見でDartに触れた際にクラスの修飾子の多さに驚くことかと思います。  

base, abstract, interface, mixin, final, sealed...

ざっと6種類あります。
(例: base classなどclassの修飾子として使われる)

ぶっちゃけそんなあんの!?というのが最初の感想でした。

ただ、これはDart言語の特有の暗黙的インターフェイスを理解すると、クラス修飾子、Dart特有の実装方法について理解が深まることに気づきました。

本ブログ記事を通してクラス修飾子への理解が深まれば幸いです。

※Dart 3.0にて新たに追加されたクラスの修飾子があるため、本ブログ記事はDart3.0をベースに執筆しています。

目次

ん?Dartではクラスがインターフェースにもなる。。?

まず、DartのクラスにはSwiftにはない暗黙的インターフェース(Implicit interfaces)という特徴があります。 公式資料の説明は以下になります。

すべてのクラスは、クラスのすべてのインスタンス メンバーと、クラスが実装するすべてのインターフェイスを含むインターフェイスを暗黙的に定義します。クラス B の実装を継承せずにクラス B の API をサポートするクラス A を作成する場合、クラス A はクラス B インターフェイスを実装する必要があります。

参考:https://dart.dev/language/classes#implicit-interfaces

つまり、どんなクラスもインターフェースとして使用できるという点です。
通常、クラスが継承できることはどの言語においてもよくあることかと思います。
例を見て考えてみます。
以下が継承と暗黙的インターフェースを使った例になります。
親クラスであるBirdクラスに対して、Sparrowクラスは継承しているのに対して、Eagleクラスはimplementsで実装をしています。
Dartのクラスには暗黙的インターフェースがあるというのが一つの特徴になります。

// Base class
class Bird {
  void fly() {
    print("The bird flies.");
  }
}

// classを継承
class Sparrow extends Bird {}

// classの暗黙的インターフェースを実装
class Eagle implements Bird {
  @override
  void fly() { // インターフェースの全てのメソッドの実装が必要
    print("The eagle flies.");
  }
}

// 使用例
void main() {
  Sparrow mySparrow = Sparrow();
  mySparrow.fly();  // The bird flies.
  Eagle myEagle = Eagle();
  myEagle.fly();   // The eagle flies.
}

この暗黙的インターフェイスを活用することによって、
例えばダミーオブジェクトへの置き換えが便利になったり、
あとは、依存性逆転の原則を意識しなくても、
ドメイン側からの実装がしやすくなるのではないかと感じました。 ※ 別の開発者からのアドバイスで、ロックインしてしまうようなデフォルト実装にfirebaseなどを記載すると、 supabaseなどでoverrideしてもfirebaseのimportが付き纏うので注意とのことでした。なるほど。。。。

ダミーオブジェクトへの置き換えの例

// Real class.
class Cat {
  String meow(String suffix) => 'Meow$suffix';
  String hiss(String suffix) => 'Hiss$suffix';
}

// Fake class.
class FakeCat extends Fake implements Cat {
  @override
  String meow(String suffix) => 'FakeMeow$suffix';
}

void main() {
  // Create a new fake Cat at runtime.
  var cat = new FakeCat();

  // Try making a Cat sound...
  print(cat.meow('foo')); // Prints 'FakeMeowfoo'
  print(cat.hiss('foo')); // Throws
}

参考:https://api.flutter.dev/flutter/test_api.fake/Fake-class.html

参考:https://medium.com/flutter-jp/architecture-240d3c56b597

クラスを図にしてみる

ここでclassを図にしてみるとこのような図になります。 classの特徴として、親はインスタンス化、継承、実装が可能です。

classの図

この図でいうインスタンス化・継承・実装に関係するグループとその他に分けることができます。

インスタンス化・継承・実装に関係するグループ

abstract

abstractを図にしてみると以下になります。   abstractの特徴として、内部に実装を持たず、インスタンス化ができないという特徴になります。

abstractの図

final

abstractとは対照的にfinalの図を見てみます。 finalの特徴として、内部に実装をもち、インスタンス化ができるが、継承と実装ができません。 abstractとは対照的な関係に見えます。  

finalの図

interfaceとbase

interfaceはざっくりいうと実装しかできません。 ※厳密には自身が宣言されたライブラリ以外では継承ができないという特徴。

対して、baseはざっくりいうと継承しかできません。 ※厳密には自身が宣言されたライブラリ以外では実装ができないという特徴。

interface / base の図

abstract interface

abstractのインスタンス化できないという特徴とinterfaceの実装のみしかできないという特徴を合わせたのがabstract interfaceになります。
他の言語でいうインターフェースのイメージに一番近いのはこのabstract interfaceの印象を受けました。

abstract interfaceの図

その他のグループ

その他のクラス修飾子としては2種類のみになります。

sealed

SwiftでいうとEnumに異なる型のデータを関連付けることができるAssociated Valueのようなイメージが自分の中で近いイメージです。

SwiftのAssociated Valueの例

enum NetworkError {
    case badURL
    case timeout(seconds: Int) // int型を関連づける
    case serverError(code: Int, message: String)
}

Dartのsealedクラスの例

sealed class NetworkError {}

class BadURL extends NetworkError {
  const BadURL();
}

class Timeout extends NetworkError {
  final int seconds;
  const Timeout(this.seconds);
}

class ServerError extends NetworkError {
  final int code;
  final String message;
  const ServerError(this.code, this.message);
}

sealedも図にするとこのようなイメージになります。

sealedの図

mixin

Swiftでデフォルト実装付きのプロトコルのイメージに一番近いのがこの修飾子になります。

mixinを図にすると以下になります。

mixinの図

まとめ

いかがだったでしょうか。 本ブログ記事を通してアクセス修飾子のイメージに少しでも役に立てば幸いです。 ではでは!

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp