実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

実践Dart:継承

はじめに

本記事ではDartにおけるクラスの継承に関連する項目のポイントをまとめていきます。

クラスの継承とは、親のクラスの性質を子のクラスに受け継がせる仕組みです。

ソフトウェアを作るとき、同じような性質や役割を持つパーツがあるものの、共通の部分とそれぞれ異なる部分があるという場合に、継承の仕組みを使って共通部分を親クラスとすると開発効率が非常に高まります。

また単純な継承に加えて多重継承という複数の親から共通部分を受け継ぐ仕組みがC++に導入され、これが長年の議論を呼びました。便利なのですが、あまりに複雑な事象を引き起こし、逆に開発効率が下がってしまう場合もあるんですよね。

それらの議論から生まれたのがインターフェイスミックスインの仕組みです。これらも含めてみていきます。

一点注意ですが、抽象クラスって何で存在するの?と思われる方は一旦最初のクラスの継承の形のみをみたあとスキップして次に進んで下さい。この記事では抽象クラスやインターフェイスの記載のDartでの書き方を中心に書いていきますが、これらの仕組みがなぜ有用なのかを書くスペースがありませんので別の機会にします。かなり深いですので、Flutterのベーシックな部分をとりあえず学ぶためには一旦スキップした方がスムーズかも知れません。




継承

様々な継承の形についてみていきます。

基本の形

継承は親クラスの必要な部分を引き継ぎ、また必要な部分を改変することを可能にする仕組みです。

重複した実装は避ける必要があるため、類似の性質をもつオブジェクトはうまく継承の仕組みを活用し、実装を最小化していくことが重要です:

class Car{
  String _name;
  String _engine;
  
  //引数なしのコンストラクタ
  Car():_name="",_engine="";
  Car.setting(this._name, this._engine);
   
  //Getter
  String get name => _name;
  String get engine => _engine;
  
  void printCar(){
    print(_name+" "+_engine);
  }  
}

//Carクラスを継承
class HybridCar extends Car{
  HybridCar.setting(String n){
    super._name = n;
    super._engine = "Hybrid";
  }  
}

void main(){
  var y = Car.setting("Yaris", "Normal");
  var p = HybridCar.setting("Prius");
  var yh = HybridCar.setting("Yaris Hb");

  y.printCar();    //Yaris Normal
  p.printCar();   //Prius Hybrid
  yh.printCar(); //Yaris Hb Hybrid
}

上記の例ではCarクラスを親クラスとしてHybridCarクラスを定義しています。「extends Car」としているのが継承の宣言です。

継承した子クラスHybridCarは、そのコンストラクタ内で親クラスCarのコンストラクタを必ず呼び出します。これによって親クラス部分のインスタンスの雛形を作り、加えて子クラス部分のインスタンスを作ることで新しいインスタンスを作るわけですね。

上記の例では引数のないコンストラクタ(デフォルトコンストラクタと言います)をCarクラスに用意しています。

この場合は子クラスであるHybridCarクラスのコンストラクタでは何も意識しなくても、親クラスのデフォルトコンストラクタが自動的に呼び出されて親クラスの部分のインスタンスを形成します。

自然な流れでデフォルトコンストラクタを用意できる場合は、用意しておいたほうがプログラムの見た目として自然ですしおすすめです。


デフォルトコンストラクタがない場合

親クラス側でデフォルトコンストラクタを作っていない場合は、子クラスのコンストラクタ側ではなんらかの親クラスのコンストラクタを明示的に呼び出す必要があります。

継承したクラスは基本的にこれを呼び出してベースとなるインスタンスを形成します。

しかしデフォルトコンストラクタがない場合は、親クラスに存在するコンストラクタを初期化リストの中で明示的に呼び出してベースを形成するよう指定します:

class Car{
  String _name;
  String _engine;
  
  //Car():_name="",_engine="";
  Car.setting(this._name, this._engine);
   
  //Getter
  String get name => _name;
  String get engine => _engine;
  
  void printCar(){
    print(_name+" "+_engine);
  }  
}

class HybridCar extends Car{
  HybridCar.setting(String n):super.setting(n, "Hybrid"){
    //ここにsuper.setting(n, "Hybrid");としてはいけない
    print("HybridCar was made");
  } 
}

このとき注意しなくてはいけないのは、上記のようにsuperのコンストラクタを呼び出すのは「初期化リストの中」ということです。HybridCarのコンストラクタのボディ(本文)の方に入れてはいけません。

子クラスのコンストラクションが始まる前にかならず親クラスのコンストラクションが終わっていないといけないというわけですね。


抽象クラス

抽象クラスとはインスタンスを生成しないクラスの雛形のようなものです。

例えば「乗り物」という抽象クラスがあり、その子クラスに「自動車」や「自転車」があるという状況で使います。

「乗り物」のなかで運転者がいるなどの共通点をくくりだして、これを表現するインスタンス変数やメソッドを実装します。しかし「乗り物」自体は抽象概念で実体を持ちえない、実体をもたせるとおかしなことになるという作者の意図を表すために用いるのが抽象クラスです。

抽象クラスを定義するためにはabstractキーワードをclass定義の前に宣言します。

abstract class Vehicle{
  //抽象メソッド
  void getStatus();
}

上記のようにabstract宣言したクラスの内部で抽象メソッドを定義することができます。

抽象メソッドは、実装をもたないメソッドで、このクラスを継承する具象クラスに実装を強制するための仕組みです。継承するクラスがまた抽象クラスであれば、必ずしもその時点で実装する必要はありません。

抽象メソッドにはabstractキーワードを付ける必要はなく、本体の実装がないだけの記述になります。

abstract class Vehicle{
  //抽象メソッド
  void showStatus();
}

class Car extends Vehicle{
  //全ての抽象メソッドに実装を与える
  @override
  void showStatus(){
    print("....");
  }
}  

抽象クラスを継承した具体的なクラスでは、全ての親に含まれる抽象メソッドの実装を与える必要があります。

抽象クラスの中では、実装は特定されていないもののメソッドの形は特定されていて、それを用いて他の実装が記述されていたりします。子クラス側はこの仕組みをあとから具体的に完成させるイメージになります。


インターフェイス

他のOOP言語で用意されているinterfaceキーワードはDartにはありません。ただimplementsは用意されています。

このimplementsは複数のクラスを指定可能で、それらのクラスが示すメソッドの実装を与える、ということを意味します。

ここでimplementsするクラスに含まれる全てのメソッドをオーバーライドしなければならないことに注意です。すでに実装があっても必ずオーバーライドする必要があります。

abstract class Vehicle{
  void method1();
  
  void method2(){
    print("I'm 2 at vehicle");
  }
}

class Tire{
  void method3(){
    print("I'm 3 at tire");
  }
}

class Car implements Vehicle, Tire{
  @override
  void method1(){
    print("hi 1");
  }
  
  @override
  void method2(){
    print("hi 2");
  }
  
  @override
  void method3(){
    print("hi 3");
  }
}

VehicleとTireにはmethod2とmethod3の実装がありますが、これらについても全部Carでは実装を与える必要があります。

これを実装しなくてよいとなってしまうと、多重継承と同じどっちのメソッド?という闇が出てきてしまいますからね。

複数クラスを指定できるimplementsキーワード以外の基本的な機能は抽象クラスに含まれていますので、interfaceのキーワードはなくても抽象クラスの活用で十分代用可能ということがわかるかと思います。


ミックスイン

ミックスインはコンストラクタのないクラスで、他のクラスに実装を横から加える仕組みです。

classキーワードがmixinキーワードになり、コンストラクタが存在しない以外は通常のクラスと同様の記述になります。

mixinの機能を備えるにはwithキーワードでmixinを指定します。複数指定可能です。

mixin Accelerator {
  void accelerate(){
    print("go");
  }
}

mixin Brake{
  void brake(){
    print("stop");
  }
}

//withキーワードでmixinを複数アタッチする
class Car with Accelerator, Brake{
  void drive(){
    print("drive");
  }

  //mixinのメソッドのオーバーライドOK
  @override
  void brake(){
    print("stop stop");
  }
}

void main(){
  var c = Car();
  c.drive();
  c.accelerate();
  c.brake();

  //Accelerator型としてCarのインスタンスを代入可能
  Accelerator a = Car();
  a.accelerate();
  //↓NG。aはCarのインスタンスだがAcceleratorとしては呼べない
  a.drive(); 
}

上記例ではCarクラスに AcceleratorとBrakeミックスインで定義されたメソッドをくっつけています。共通の処理があるが、クラスの継承の流れにうまく乗らないときに使います。implementsも含めて多重継承の利点の部分を分解して問題が起こるのを避けつつ、必要な部分は使えるようにしている形です。

mixinにはonキーワードがあり、onでクラスを指定すると、そのサブクラスでしかwithによる実装の付加を行えないという制約を課すことができます。

mixinは部分的に機能を付加するものですので、あまりみだらに乱用されても困るという場合に、作者の意図を表す意味でonの制約を貸しておきます。

//mixinにon~をつけることで、~のサブクラスにのみmixin可になる
mixin Accelerator on Car{
  void accelerate(){
    print("go");
  }
}

mixin Brake{
  void brake(){
    print("stop");
  }
}

//AcceleratorはCarの"サブクラスのみ"に付けられる
//ここでwith Acceleratorとするとコンパイルエラー
class Car with Brake{
  void drive(){
    print("drive");
  }
  
  @override
  void brake(){
    print("stop stop");
  }
}

//CarのサブクラスなのでOK
class HybridCar extends Car with Accelerator{
  
}

//Carのサブクラスではないのでコンパイルエラー
class Other with Accelerator{
  
}





おわりに

今回は様々な継承の形について述べていきました。

冒頭ではこの仕組は共通部分のくくりだしと書きましたが、それは形上の話しでありまして、実際にはこの親子クラスは抽象化と具象化の関係であるととらえるとより良い理解になります。

抽象というと、ぼやっとした曖昧なもの、あまりよくないもののイメージで捉えられている方も多いかもしれませんが、コンピュータ・サイエンスの世界(あるいは哲学・論理学などおよそ思考を扱う学問)ではそんなことはありません。

情報の一部を切り出すこと、が抽象化です。ここに曖昧さはありません。ただ難しいのは、その抽象化という行為は、目的によって異なるということです。

例えば自動車を抽象化するとき、「運転者が操るもの」という切り出し方もありますし「タイヤが4つついているもの」という切り出し方もあります。ここが目的によって変わってきます

うまい抽象化は、その目的に沿って本質的な部分を抽出できているかどうか、できまります。変に共通項をくくりだした結果、わけのわからない概念が抽象概念として切り出され、親クラスとなったとき、その親クラスは非常に使い勝手の悪いものになるわけです。

その目的に沿った、本質的な概念の切り出し、を意識してクラス階層を設計することが、よい設計につながります。

f:id:linkedsort:20211106220634j:plain