Flutter & Dart Cookbookの読書メモ(第5章)

Geting Started with Object-Oriented Dart
このセクションでは、クラスを扱うためのオブジェクト指向のテクニックを紹介し、これらをDartと一緒に使用する方法を示します。この章では、オブジェクトの宣言と拡張の両方を学びます。Dartはオブジェクト指向の言語であるため、これらのテクニックは重要です。基本を学ぶことで、自分自身のスキルレベルを上げ、他の人のコードをより簡単に取り入れるための良い参考となるでしょう。
この章では、まずDartに関連するオブジェクト指向プログラミングの主要な用語の概要を説明します。次に、クラスをプログラミングのレパートリーに取り入れる方法について説明します。さらに、以下の項目もカバーします。
オブジェクト指向プログラミングの基本
コンストラクタによるクラスの初期化の必要性
extends キーワードによる継承のサポート
インターフェースによるクラスのシグネチャの定義
mixin によるクラスの機能集約
開発が高度になると、カスタムクラスを利用して要求を実現することができるようになります。クラスを効率的に利用するためには、学習曲線が急なので、小さなステップを踏むようにしましょう。時間が経つにつれて、自然に上達し、非常に複雑な題材を一般的なソリューションに取り入れることができるようになります。

5.1 Beginning Object-Oriented Dart
- Problem
Dartでオブジェクト指向プログラミングを行い、再利用可能なコンポーネントを構築したい。
- Solution
Dartを使ったオブジェクト指向のテクニックを使って、モデル化されている目的を反映したコンポーザブルなオブジェクトを反映したコードを開発することができます。Dartはオブジェクト指向のテクニックをサポートしており、アプリケーションの作成に必要なアルゴリズムやデータ構造を開発する際に、その使用を推奨しています。
- Discussion
オブジェクト指向開発の知識を身につけることで、DartやFlutterのスキルが飛躍的に向上します。オブジェクトの作成方法を学ぶことは、基礎的なスキルであり、アルゴリズムとデータ構造を結びつける方法を理解するのに役立ちます。一般的に、オブジェクトはコンストラクタと呼ばれる特別なメソッドで初期化する必要があります。コンストラクタは、初期化時にオブジェクトのプロパティを設定する役割を担っています。
オブジェクトは、コードとデータタイプを代表的な構造にグループ化したものです。例えば、本のクラスと初期化された(インスタンス化された)オブジェクトは、タイトル、著者、および出版社をプロパティとして持つことができます。
bookクラスは定義を提供し、bookオブジェクトはそのクラスのランタイム版である。book クラスは、他のプロパティ(例えば isbn)や、関連するプロパティを取得(get)および更新(set)するメソッドを持つこともあります。クラス定義でデータやコードをまとめておくと便利なことは、Dart 言語を使ううちに慣れてくるはずです。
Dartでオブジェクト指向プログラミングを始めるために、継承、実装、拡張のユースケースを学び、実装シナリオに最も適したデザインパターンを選択できるようにしましょう。

5.2 Creating a Class
- Problem
データと機能の両方を表現するクラスオブジェクトを作成したい。
- Soluton
クラスを使って、情報を新しいオブジェクトに照合し、可変ストレージと情報処理機能の両方を提供する。以下は、Dartでクラスを宣言する方法の例です。
const numDays = 7;
class DaysLeftInWeek {
int currentDay = 0;
DaysLeftInWeek() {
currentDay = DateTime.now().weekday.toInt();
}
int howManyDaysLeft() {
return numDays - currentDay;
}
}
- Discussion
Dartはオブジェクト指向言語であり、NULLを除くすべてのDartオブジェクトに対してObjectクラスが用意されています。その結果、NULLでないオブジェクトはObjectのサブクラスとなります。Dartのコードを書くのに慣れてくると、クラスを作成することが自然にできるようになります。オブジェクト指向プログラミングは、アイデアをモデル化し、動作を関連付けるための手段を提供します。クラスは、データと、モデル内に組み込まれたデータにアクセスするための機能の両方を定義することができる、モデルを提供します。
このクラスは、1週間のうち何日残っているかを判断するために使用されます。
クラスの定義では、currentDayプロパティとhowManyDaysLeftメソッドを持っていることがわかります。
この宣言では、変数と関数の両方の要素を含む、この後の定義を示すために "class "を使用しています。クラスのコンストラクタであるDaysLeftInWeekは、クラスと同じ名前が与えられ、オブジェクトのインスタンス化時に呼び出されます。クラスが作成されるときに一回限りの活動を行うには、クラス・コンストラクタを使用します。
クラス内では、currentDay変数がfinal intとして宣言されており、その値は実行時に決定され、整数を表します。さらに、howManyDaysLeftと宣言されたメソッドがあり、このメソッドを使って計算を行います。

5.3 Initializing a Class Using a Constructor
- Problem
クラスに基づいて新しいオブジェクトが作成されるたびに、一連の命令を実行したいとします。
- Solution
オブジェクトのインスタンスの初期化を行うには、クラスのコンストラクタを使用します。この初期化により、クラスの値に感覚的なデフォルトを設定することができます。
以下は、クラス・コンストラクタの宣言と使用方法の例です。
const numDays = 7;
class DaysLeftInWeek {
int currentDay = 0;
DaysLeftInWeek() {
currentDay = DateTime.now().weekday.toInt();
}
int howManyDaysLeft() {
return numDays - currentDay;
}
}
void main() {
DaysLeftInWeek deyCalculator = DaysLeftInWeek();
print('Today is day ${deyCalculator.currentDay}');
print('${deyCalculator.howManyDaysLeft()} day(s) left in the week');
}
- Discussion
この例では、週に何日残っているかを判断するために、このクラスを使用します。この宣言では、変数と関数の両方の要素を含む、後続の定義を示すために "class" を使用しています。
クラスは、クラスを呼び出すことによって宣言されることに注意してください。
コンストラクタはクラスと同じ名前を持ち、クラスのインスタンス化時に呼び出されます。この例では、コンストラクタはクラス変数 currentDay に今日の日付の値をセットしています。DaysLeftInWeekというクラスの中に、クラスと同じ名前の関数が定義されていることに注意してください。
このクラスを使うには、DaysLeftInWeekクラスをインスタンス化するための変数(weekClassなど)を宣言します。この宣言から、変数weekClassは、クラスに関連する変数と関数の両方にアクセスできるようになります。ここでもfinalキーワードを使用してweekClassを宣言し、実行時に値が決定されることを示しています。
最後に、print文は、変数と関数にアクセスして、基礎データにアクセスする方法を示しています。どちらの場合も、クラスの値は変数weekClassから派生しています。currentDayとメソッドhowManyDaysLeftは、どちらもクラスに関連するデータを取得することができます。
他のオブジェクト指向言語に慣れている方は、thisキーワードが省略されていることに驚かれるかもしれません。Dartでは、使用する変数の明示的なガイダンスとしてのみthisを使用する必要があります(つまり、変数のシャドウイング)。この状況を回避する方法については、変数シャドウイングに関するDartのドキュメントを参照してください。

5.4 Adding ClassInheritance
- Problem
既存のクラスを拡張して、元のクラスにはない機能を追加したい。
- Solution
extends キーワードを使用したクラスは、親クラスからの継承を取り入れるために使用します。extends キーワードを使用すると、サブクラスはスーパークラスの機能を継承します。オブジェクト指向言語であるDartは、新しいリリースごとに広範なクラスサポートを提供しています。ここでは、extendsを使用してDartにクラス継承を追加する方法の例を示します。
class Media {
String title = "";
String type = "";
Media() {
type = "class";
}
void setMediaTitle(String mediaTitle) { title = mediaTitle; }
String getMediaTitle() { return title; }
String getMediaType() { return type; }
}
class Book extends Media {
String author = "";
String isbn = "";
Book() { type = "subclass"; }
void setBookAuthor(String bookAuthor) { author = bookAuthor; }
void setBookISBN(String bookISBN) { isbn = bookISBN; }
String getBookTitle() { return title; }
String getBookAuthor() { return author; }
String getBookISBN() { return isbn; }
}
void main() {
var myMedia = Media();
myMedia.setMediaTitle('Tron');
print('Title: ${myMedia.getMediaTitle()}');
print('Type: ${myMedia.getMediaType()}');
var myBook = Book();
myBook.setMediaTitle('Jungle Book');
myBook.setBookAuthor('R Kipling');
print('Title: ${myBook.getMediaTitle()}');
print('Author: ${myBook.getBookAuthor()}');
print('Type: ${myBook.getMediaType()}');
}
- Discussion
このコード例では、MediaクラスはBookサブクラスを通して拡張されています。親クラスとして、Mediaの機能はどの子クラスでも利用できます。
継承によって、クラスはスーパークラスのメソッドやプロパティを引き受けることができます。その結果、子クラスと親クラスは同じプロパティとメソッドをサポートすることになります。
したがって、Book クラスには、Book クラスで明示的に定義されているものに加えて、Media に関連するプロパティとメソッドが含まれます。
Mediaクラスを継承する子Bookサブクラスを作成します。つまり、そのクラス内でインスタンス化されたメソッドと変数にアクセスするために使用できます。サブクラスを使用する場合、既存のクラス機能(メソッドなど)をオーバーライドすることが可能です。
Extends は、親クラス(つまりスーパークラス)の機能を子クラス(つまりサブクラス)でも使用できる一般的なクラス継承を提供します。extends キーワードの親子関係は 1 対 1 であり、多重継承はサポートされていません。この関係を使用する場合、親クラスが子クラスで行われた変更を確実に認識するために super.method() を呼び出すことになることに注意してください。
注意:BookのsetMediaTitleメソッドを再宣言することはありません。その代わり、あたかも明示的に宣言されているかのようにBookクラスからこのメソッドを呼び出すことができます。
extendsの使用は、潜在的に少し異なったメソッドを必要とする類似のデータ構造が存在する場合に有効なアプローチです。 この例では、Media クラスは基本情報を保持するために設定された汎用的な抽象化です。Book クラスは Media クラスを特殊化したもので、 書籍固有の情報を追加する機能を提供します。

5.5 Adding a Class Interface
- Project
オブジェクトを定義する際に、宣言すべきプロパティやメソッドの概要をクラス仕様で記述したい。
- Solution
クラスインターフェースを使用して、実装者が遵守しなければならないオブジェクトの仕様を定義します。以下は、Dartでインターフェースクラスを定義する方法の例です。
abstract class Media {
late String myId;
late String myTitle;
late String myType;
void setMediaTitle(String mediaTitle);
String getMediaTitle();
void setMediaType(String mediaType);
String getMediaType();
void setMediaId(String mediaId);
String getMediaId();
}
class Book implements Media {
late String myId;
late String myTitle;
late String myType;
void setMediaTitle(String mediaTitle) {
myTitle = mediaTitle;
}
String getMediaTitle() {
return myTitle;
}
void setMediaType(String mediaType) {
myType = mediaType;
}
String getMediaType() {
return myType;
}
void setMediaId(String mediaId) {
myId = mediaId;
}
String getMediaId() {
return myId;
}
Book(String mediaTitle, String mediaType, String mediaId) {
myTitle = mediaTitle;
myType = mediaType;
myId = mediaId;
}
}
void main() {
final Book myBook = Book('Serverless Computing with Google Cloud', 'Book', 'ISBN - 1111');
print(myBook.getMediaTitle());
print(myBook.getMediaType());
print(myBook.getMediaId());
}
- Discuttion
このコード例では、MediaクラスのインターフェイスがBookサブクラスで使用されています。Mediaは親クラスなので、その定義はどの子クラスでも利用可能です。親クラスに関連する実装と初期化がないことに注意してください。その代わり、実装はインターフェイスの使用者に任されています。
インターフェースクラスを使用するには、implements キーワードを使用します。他の言語に慣れている方は、抽象クラスという言葉をご存知かもしれません。抽象クラスはクラスの定義を提供しますが、オブジェクトを初期化するために使用することはできません。
この例では、Media という名前の抽象クラスが、メディア情報のための一般的なインターフェイスを作成しています。BookクラスはMediaクラスのインターフェイスを実装しており、抽象クラスで名付けられた値を取得・設定する役割を担っていることを意味しています。プロパティもメソッドも、インターフェイスに記述された値をオーバーライドするように定義する必要があります。Bookクラスで定義された値のうち、Mediaクラスで定義されたものには、@overrideという接頭辞が付きますが、これはインターフェースが既に定義されていることを意味します。
一般に、抽象的なインタフェースは、サブクラスの開発者に実装を委ねる汎用的な型を定義するために使用される。
サブクラスは複数のインタフェースを実装することができますが、サブクラスを作成する際には、クラス階層が複雑になり過ぎないように注意する必要があります。クラスインタフェースを使用する場合、指定されたインタフェースを実装し、抽象クラスが使用するシグネチャを遵守する必要があります。
インターフェイス定義の典型的な使用例は、実装が別の問題として扱われる場合です。

5.6 Adding a Class Mixin
- problem
既存のクラスに、複数のクラス階層から機能を集約させたい。
複数のクラスから機能を必要とする場合は、Mixinを使用します。ミキシンはクラスを扱う際の強力なツールであり、複数のクラスから情報を取り込むことを可能にします。以下はMixinの使用例です。
abstract class SnickersOriginal {
bool hasHazelnut = true;
bool hasRise = false;
bool hasAlmond = false;
}
abstract class SnickersCript {
bool hasHazelnut = true;
bool hasRise = true;
bool hasAlmond = false;
}
class ChocolateBar {
bool hasChocolate = true;
}
class CandyBar extends ChocolateBar with SnickersOriginal {
List<String> ingredients = [];
List<String> getIngredients() {
return ingredients;
}
CandyBar() {
if (hasChocolate) {
ingredients.add('Chocolate');
}
if (hasHazelnut) {
ingredients.add('Hazelnut');
}
if (hasRise) {
ingredients.add('Rise');
}
if (hasAlmond) {
ingredients.add('Almond');
}
}
}
void main() {
var snickersOriginal = CandyBar();
print('Ingredients:');
snickersOriginal.getIngredients().forEach((ingredient) => print(ingredient));
}
- Discuttion
この例では、スニッカーズのチョコレートバーのバリエーションに関する情報を保持するために、2つの抽象クラスが定義されています。メインのチョコレートバークラスには必要な機能が含まれていないため、プログラムの機能を拡張するために新しいクラスを組み込みます。
with キーワードが Dart に導入されたのは最近のことですが、開発者からの要望でもありました。Flameゲームエンジンを使っている人なら、このキーワードはよく使うでしょう。他の言語を使っている方は、複数のクラスを組み合わせて機能を追加できるmixinという用語の方が馴染みがあるかもしれません。
mixin は、継承とインターフェースの両方のクラス定義で使用することができます。CandyBarクラスで抽象クラスを使うには、mixinを使います。mixinは、withキーワードを使用して、クラスオブジェクトを結合します。
このとき、参照されるスーパークラスは分離されている必要があり、使用されるクラスが重複してはいけません。CandyBar の基底クラスは、抽象クラスや親クラスで使用されているデフォルトコンストラクタをオーバーライドしてはいけません。
通常、抽象クラスは、作成するオブジェクトの青写真を定義するために使用されます。この例では、抽象クラスはキャンディバーの主要な材料を示しています。さらに、共有の詳細を保持するために使用できるチョコレートバークラスを作成します。
mixinを使用すると、特定のコードを記述することなく、サブクラスのオブジェクトがより多くの機能を組み込むことができるようになります。この例では、親クラスと抽象クラスの組み合わせにより、異なる機能を融合させることができます。このマージにより、CandyBarサブクラスはチョコレートバーとスニッカーズの種類(つまり、CrispバージョンよりもOriginal)に関連する振る舞いを持つようになります。子サブクラスが作成されると、それを使ってキャンディバーの一般的な成分にアクセスすることができるようになります。