はじめに
Flutterを網羅的に学習するにあたってRoadmapを使って学習を進めることにしました。
この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、オブジェクト指向プログラミングについてまとめています。
RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひRoadmapを利用して学習してみてください。
Roadmapとは
簡潔に言えば、Roadmap.shは学習者にとってのガイドブックであり、学習の方向性を提供する学習ロードマップサイトです。
初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。
学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。
OOP
FlutterロードマップOOPでは以下の2つのサイトが紹介されています。興味のある方はぜひお読みください。
- Discover Object Oriented Programming: https://blog.hubspot.com/website/object-oriented-programming
- Software Development Tutorial: https://www.youtube.com/watch?app=desktop&v=SS-9y0H3Si8
設計原則とは
設計原則(Design Principles)とは、設計や開発の際に遵守すべき基本的なガイドラインやルールのことを指します。
設計原則を遵守することで、システムや製品を設計する際に品質、効率、信頼性、保守性を向上させることができます。
一般的に設計原則は設計段階から、開発、保守、運用の各段階にわたって適用されるため、プロジェクト全体の品質を上げることにつながるのです。
オブジェクト指向プログラミング(Object-Oriented Programming / OOP)
Dartは、オブジェクト指向プログラミング(Object-Oriented Programming / OOP)の原則に基づいて設計されたプログラミング言語です。
オブジェクト指向(OO / Object-Oriented)とは簡単に言うとモノに注目した考え方のことを言います。役割ごとに分割されたモノ(オブジェクト)を一つ一つ組み合わせることで、複雑で大きな処理ができるモノを作り上げるという考えのことを指しています。
現実世界でのことを例にするならば、車の組み立てについてイメージするとわかりやすいです。車はエンジン、ブレーキ、タイヤ、ハンドル、シートなど様々なパーツが組み合わさって構成されています。この一つ一つの部品がオブジェクト(モノ)なのです。部品を一箇所で作るのではなく、役割ごとに分割して作成し、最終的に組み合わせるという手法がオブジェクト指向の考え方になります。
データと処理をまとめて一つのオブジェクト(モノ)としてとらえることによって、コード量を抑え、保守性を高めることができるのです。オブジェクト指向についてもっと詳しく知りたい方はこちらにわかりやすい記事があったので、是非読んでみてください。
1. クラスとオブジェクト
クラスとはオブジェクトを生成するためにデータと処理を定義したものです。データのことをフィールドといい、処理のことをメソッドと言います。
class Car {
// フィールド: 車のデータ
String brand;
double price;
// メソッド: 走る
void drive() {
print('drive');
}
}
Flutterで使用する、ウィジェット(Widget)もクラスとして実装され、UI要素や機能を提供しています。例えば、Textウィジェットはテキストを表示するためのクラスであり、Imageウィジェットは画像を表示するためのクラスです。これらのクラスからオブジェクトを作成し、アプリケーションのUIを構築しています。
そして、クラスを実際にオブジェクトにして使うことをインスタンス化と言い、生成されたものをインスタンスと呼びます。
また、インスタンス化するときに呼ばれるメソッドをコンストラクタと言います。コンストラクタを呼び出すことでオブジェクトの初期化が行えるのです。クラス内に定義しない場合、自動的に作成されたデフォルトコンストラクタが呼ばれます。
// Carクラスの定義
class Car {
// フィールド: 車のデータ
String brand;
double price;
// パラメータ付きコンストラクタ
Car(this.brand, this.price);
// メソッド: 走る
void drive() {
print('$brandの車がdriveする');
}
}
void main() {
// コンストラクタを使用してCarオブジェクトを作成
Car car1 = Car('Toyota', 25000.0);
Car car2 = Car('Honda', 22000.0);
// オブジェクトのメソッドを呼び出して情報を表示
print('Car 1 Information:'); // Car 1 Information: Toyotaの車がdriveする
car1.drive();
print('Car 2 Information:'); // Car 2 Information: Hondaの車がdriveする
car2.drive();
}
上記のコードではCar
クラスにパラメータ付きコンストラクタが使用されています。コンストラクタはthis
を使用して、クラス(Carクラス)のフィールド(ブランド、価格)を初期化することができるのです。
また、コンストラクタを呼び出す際に、引数を渡すことで、オブジェクトを初期化できます。
上記の例でいくとCar
クラスのコンストラクタを使用して2つの車のオブジェクトcar1
とcar2
を作成しています。それぞれのオブジェクトは異なる車の情報を持っており、コンストラクタによって初期化されています。
つまり、オブジェクトを作成する際に、コンストラクタが呼び出されて、オブジェクトの状態を設定できるのです。
2. カプセル化
データとデータを操作するメソッドをクラス内でカプセル化する概念です。Flutterでは内部でデータとロジックをカプセル化し、外部からアクセスするためにプロパティやメソッドを指定することができます。これにより、内部の実装を隠し、簡潔なコードを書くことができるようになります。
カプセル化とは簡単にいうと、干渉しないことを言います。例えば、車の内部には様々な部品がありますが、運転手はその部品について詳細を知らなくても運転することができます。
運転手が知るべきものは
・アクセルを踏む=速度を上げる
・ブレーキを踏む=速度を下げる
・ハンドルを操作する => 左・右に曲がる
などの「運転方法」だけです。
つまり中の複雑な動作は内部に隠し、簡単な使い方さえ知れば良いことになります。
ここで重要なことは各部品がそれぞれの役割に徹底するということです。役割が不明瞭だと、極端な話、ドアでハンドルを操作したり、ブレーキがワイパーを動かしたりができるようになり、車として成り立たなくなるのです。
これをプログラミングに置き換えるとクラス(部品)は一つの独立した機能を持ち、その中身を理解しなくても簡単に使えるべきということになります。
例えば、Textウィジェットの中身がどういう実装になっているか知らなくても、Text('ウィジェット')
のように簡単に使えることができることがカプセル化の利点なのです。
3. 継承
クラス間でコードの再利用を可能にする概念です。Flutterでは、継承1を利用して、既存の機能を拡張し、カスタムした機能を作成することができます。例えば、Containerウィジェットを継承してカスタムしたコンテナウィジェットを作成したりすることができるのです。
継承元のクラスをスーパークラス、継承してできたクラスをサブクラスといいます。
以下のように継承を使用することで、親クラスの機能を活用しつつ、独自の機能やカスタマイズを追加できます。
import 'package:flutter/material.dart';
// Containerを継承してカスタムコンテナウィジェットを作成 CustomContainerWidget(サブクラス) Container(スーパークラス)
class CustomContainerWidget extends Container {
// カスタムコンテナウィジェットのコンストラクタ
CustomContainerWidget({
Key? key,
double width = 100.0,
double height = 100.0,
Color color = Colors.blue,
Widget? child,
}) : super(
key: key,
width: width,
height: height,
color: color,
child: child,
);
}
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Custom Container Example'),
),
body: Center(
child: CustomContainerWidget(
width: 150.0,
height: 150.0,
color: Colors.green,
child: Text('Custom Container'),
),
),
),
));
}
継承とは、特定の要素だけを受け継いで利用していくことを言います。例えば、車にはトラックや、軽自動車、セダンなど様々な種類が存在します。これらは車というクラスの特定の要素(人を乗せる、四輪で走るなど)を受け継いでいるのです。また、乗り物には車や飛行機、船など様々な種類が存在します。これらも乗り物というクラスの特定の要素(人を乗せる、走るなど)を受け継いでいます。
一から作成するよりも元々ある機能(クラス)を一部使いながら改変していくことで、機能や改修の追加が少なくなるのです。
4. ポリモーフィズム (多態性)
ポリモーフィズムとは、クラスを継承して、メソッドをオーバライド2した際に、関数(メソッド)の呼び出しに対し、オブジェクト毎に異なる動作をすることを言います。
// 乗り物クラス
class Vehicle {
void moveOn() {
print('Moving...');
}
}
// 飛行機クラス
class Airplane extends Vehicle {
@override
void moveOn() {
print('Flying');
}
}
// 船クラス
class Ship extends Vehicle {
@override
void moveOn() {
print('Sailing');
}
}
// 車クラス
class Car extends Vehicle {
@override
void moveOn() {
print('Driving');
}
}
void main() {
final airplane = Airplane();
final ship = Ship();
final car = Car();
airplane.moveOn(); // Flying
ship.moveOn(); // Sailing
car.moveOn(); // Driving
}
上記のコードでは乗り物クラスに「moveOn」(進む)という関数(メソッド)があります。そして、乗り物クラスのサブクラスには 飛行機クラス、船クラス、車クラスがあり、それぞれ「moveOn」(進む)という関数(メソッド)の中身を独自で実装しています。
乗り物クラスの「moveOn」(進む)を実行すると、飛行機クラスのオブジェクトは「Flying」、船クラスのオブジェクトは「Sailing」、車クラスのオブジェクトは「Driving」と出力します。
このように、呼び出し側は同じ「moveOn」(進む)という処理を実行していますが、命令を受け取ったオブジェクト側でそれぞれ異なる動作をするのがポリモーフィズムです。
5. 抽象化
抽象化は、簡単にいうと役割分担のことを言います。重要な概念や動作を抽出し、それを簡潔に表現することが抽象化の考え方になります。
車の部品をイメージしてください。アクセル、ブレーキ、ハンドル、ワイパーなど様々な部品がありますが、これらは個々がそれぞれの役割を全うすることで、車として機能するようになります。また、役割分担をしているおかげで、車が故障した際、どの部品を直せばいいのか原因がどこにあるのかを特定しやすくなるのです。
この時、それぞれの部品はお互いの機能について細かく理解する必要はありません。例えば、「ブレーキ」は「ハンドルで進行方向を変えることができる」ということ理解していれば、ハンドル内部の複雑な処理について知らなくても良いのです。
つまり、抽象化を取り入れることで、それぞれの部品が自身の役割に専念することができます。これをプログラミングに当てはめると、一つのクラスに多くの複雑な処理を入れるのではなく、機能ごとに別々のクラス切り出して、特定の機能を使用する際にその機能(クラス)のメソッドや変数を呼び出せば良いのです。
Flutterには抽象クラスという継承されることを前提としているクラスがあります。抽象クラスはclass
キーワードの前にabstract
というキーワードを付与したクラスのことで、処理の再利用をしたい場合に使用します。
// カーパーツの抽象クラス
abstract class CarPart {
// カーパーツの動作を表す抽象メソッド
void operate();
}
// アクセルクラス
class Accelerator extends CarPart {
@override
void operate() {
print('アクセルを踏んで加速します。');
}
}
// ブレーキクラス
class Brake extends CarPart {
@override
void operate() {
print('ブレーキを踏んで停止します。');
}
}
// ワイパークラス
class Wiper extends CarPart {
@override
void operate() {
print('ワイパーを動かして窓を拭きます。');
}
}
上記のコードのように抽象クラスを使用することで、パーツごとに独立して操作できるようになります。またそのおかげで、パーツごとの役割を全うでき、責務が明確になるのです。
参考資料