做做Flutter-Day06:Dart in OOP

影山小麥機
16 min readJul 3, 2023

實際的聊聊Dart的物件導向(object-oriented programming)

三鐵冠軍帽

前言

今天很順利的拿到了埔鹽順澤宮的帽子,作為一個在基督信仰背景下長大的小孩,說真的有點不太習慣,最後拿帽子在宮爐上順時針還是逆時繞了三圈其實已經有點抵觸基督信仰的唯一真神價值了,不過好像也蠻有趣的,順澤宮其實就是一個蠻默默無名的小宮廟,但在伊登這個三鐵冠軍在比賽的半路上莫名奇妙的戴上帽子後奪冠,順澤宮也就大紅起來。

但從台中到埔鹽順澤宮的路上其實沒什麼樂趣,就是平路的騎乘,不過平靜的感覺其實是一種很好的放鬆。

正文

來講講Dart語言的物件導向。

在官方的使用範例上,大致上把分成這幾個章節:

  1. class
  2. method
  3. extend class
  4. mixins
  5. enums
  6. extension methods
  7. callable objects

那在我這邊的章節談Class類別這個算體建構中最重要的概念,我可能會把它稍微整理成Class的主要概念與對於Swift的角度、iOS開發者可能認為Flutter的語法在使用上可能稍微不一樣的地方。
不過這邊可能會在內容上把它截成兩半,mixins之後自成一章:)

初始化、使用建構子

就算網路上有許多的詞是關於初始化這個動作,但我還是習慣使用「初始」這個詞,其次才是「實體化」,理由是因為初始可能意味著「一開始就…」然後「…賦予」,這個動詞其實蠻能詮釋一個物件可能在創建的過程的意義,而我覺得「實體化」可能也可以用的理由是因為,我可能在編寫一個物件的時候,還沒有進到run time的時候,這個物件都是虛擬的、紙上談兵的。

比如說以前在Swift中寫初始化一個Controller,如下:

let viewController = ViewController()

我們都知道()在這個脈絡裡面的意思就是這個物件初始化了,只是可能()裡面並沒有被設定需要什麼必須初始化的屬性。所以,我才會對於「實體化」這個詞比較有一些好感。

那Dart的語法中,初始化一個物件會是怎麼樣的概念?是說Dart的Class這個章節中沒有提到過initialization這個詞,倒是在後面一直提到intializing。

var p = Point(2,2);

///然後如果你想要調Point的y屬性出來,你可能會這樣做:

print(p.y);
///應該會是等於2啦。

/// 你應該可以推測Point這個物件未初始化之前長怎樣:
class Point {
double? x;
double? y;
}

然後在Dart的物件的用法中,還有一些關鍵字的用法可以提一下:

///比如final
class ProfileMark {
final String name;
final DateTime start = DateTime.now();

ProfileMark(this.name);
profileMark.unnameed() : name = "";

}

final的意思在官方語法裡面是這樣描述:

Instance variables can be final, in which case they must be set exactly once. Initialize final, non-late instance variables at declaration, using a constructor parameter, or using a constructor’s initializer list

「只可以設定一次」,表示這個變數只能被設定一次,且不能被修改。


class Person {
final String _name;
Person(this._name);
String greet(String who) => 'Hello, $who. I am $_name.';
}

class Impostor implements Person {
String get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
print(greetBob(Person('Kathy'))); ///Hello, Bob. I am Kathy.
print(greetBob(Impostor())); ///Hi Bob. Do you know who I am?
}

所以像上面的例子,大概會印出像是這樣的東西:

Hello, Bob. I am Kathy.
Hi Bob. Do you know who I am?

我們在上面看到了標註final的屬性的只能初始化一次,所以像上面的Person物件,我們給定了Bob之後,就算Imposter會使用的Person,也都會因為設定了Bob作為一次的String設定,所以上面的console結果才會變成Bob。

不過,final這個用法我在過去使用程式語言中沒有什麼看過別人用過,這個用法或許可以開啟一些見識。

靜態變數

Use the static keyword to implement class-wide variables and methods.

Static variables aren’t initialized until they’re used.

在Dart官方語法上是這樣寫:

  1. static 關鍵字可以實作類別級別的變數和方法。

2. 描述的意義就是沒用到的時候不初始化,用到的時候才初始化。

舉個例子:

class MyClass {
// 靜態變數
static int staticVariable = 10;

// 靜態方法
static void staticMethod() {
print('This is a static method.');
}
}

void main() {
// 訪問靜態變數
print(MyClass.staticVariable); // 輸出:10

// 呼叫靜態方法
MyClass.staticMethod(); // 輸出:This is a static method.
}

我們可以實際的直接用MyClass.staticMethod或MyClass.staticVariable去呼叫這個屬性或是方法的功能或值,可以直觀的看到,其實static的作用就是讓程式設計者可以直接不初始化物件,就調用物件裡的屬性或方法。

建構子Constructor

class Point {
double x = 0;
double y = 0;

Point(double x, double y) {
this.x = x;
this.y = y;
}
}

在上述的物件導向的描述裡面,其實可以把建構子的部分寫出來,就可以很直覺的知道,在初始化物件的時候會針對哪幾個屬性進行初始化。像上面的物件就是初始值就必須賦值,不然就要給定?讓編譯可以確定你可能有或沒有要賦予它值。

這邊倒有另外一個例子,就是給予建構子名稱,比如下面這樣

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
final double x;
final double y;

Point(this.x, this.y);

Point.origin()
: x = xOrigin,
y = yOrigin;
}

我們給定了原始的建構子用origin()的方法直接賦給內部屬性值,這樣直接使用會像下面這樣:

void main() {
var point = Point(2, 3);
print('Point: (${point.x}, ${point.y})'); // 輸出:Point: (2.0, 3.0)

var originPoint = Point.origin();
print('Origin: (${originPoint.x}, ${originPoint.y})'); // 輸出:Origin: (0.0, 0.0)
}

使用建構子中的super特性

super關鍵字的特性,就是可以取用父類別內的屬性或是方法,下面先舉個方法的例子:

///我們先寫一個Animal物件
class Animal {
String? name;

Animal.fromJson(Map data) {
print('Creating an animal...');
name = data['name'];
}
}


/// 讓狗這個物件繼承Animal這個底層物件
class Dog extends Animal {
String? breed;

/// 所以這邊的dog物件就可以使用breed這個物件
Dog.fromJson(Map data) : super.fromJson(data) {
print('Creating a dog...');
breed = data['breed'];
}
}

/// 這邊的function在執行的時候就可以直接取用父類別的fromJson方法,去取用dogData的資料。
void main() {
var dogData = {
'name': 'Max',
'breed': 'Labrador Retriever',
};

var dog = Dog.fromJson(dogData);
print(dog);
// Prints:
// Creating an animal...
// Creating a dog...
// Instance of 'Dog' (name: Max, breed: Labrador Retriever)
}

上面描述完了方法的舉例,我們可以看看屬性的舉例:

class Shape {
final double width;
final double height;

Shape(this.width, this.height);
}

class Rectangle extends Shape {
final double depth;

Rectangle(double width, double height, this.depth) : super(width, height);
}

void main() {
var rectangle = Rectangle(5, 10, 3);
print(rectangle);
// Prints: Instance of 'Rectangle' (width: 5.0, height: 10.0, depth: 3.0)
}

總之,這邊在做的例子就是讓Rectangle這個物件直接繼承Shape這個物件,所以你可以看到rectangle這個物件在初始化運作的時候是直接繼承Shape這個物件的屬性的。

Factory constructors

接下來會講講Factory這類型的建構子,在下面的範例看起來,Factory這個keyword看起來就是要讓物件在初始化的時候給予更彈性、靈活的空間去初始化:

class Shape {
final String name;

factory Shape(String type) {
if (type == 'circle') {
return Circle();
} else if (type == 'rectangle') {
return Rectangle();
} else {
throw ArgumentError('Invalid shape type: $type');
}
}

Shape._(this.name);
}

class Circle extends Shape {
Circle() : super._('Circle');
}

class Rectangle extends Shape {
Rectangle() : super._('Rectangle');
}

void main() {
final shape1 = Shape('circle');
final shape2 = Shape('rectangle');

print(shape1.name); // 輸出: Circle
print(shape2.name); // 輸出: Rectangle
}

這邊稍微比較一下factory建構子跟一般的建構子的差異,我覺得應該可以稍微整理一下差別:

  1. 返回實例的方式:普通建構子總是返回新的實例,而工廠建構子可以根據特定的邏輯返回不同的實例。這使得工廠建構子可以從快取中返回實例,返回子類別的實例,或根據其他邏輯來決定要返回的實例。
  2. 架構靈活性:工廠建構子可以根據不同的情況返回不同的實例,這提供了更大的彈性和多態性。這對於根據不同的參數值或條件返回不同的實例非常有用。
  3. 初始化 final 變數:工廠建構子可以在初始化 final 變數時執行複雜的邏輯,而初始化列表無法處理這種情況。這允許使用工廠建構子根據需要計算和設定 final 變數的值。

不過,這種類型的建構子到底是不是會讓寫程式這件事情變得複雜,我其實蠻困惑的(笑),大概用不用也是看設計物件的人還有需求是什麼啦..

屬性方法

在屬性方法這個區塊,比較值得提的部分應該是Getter、Setter。

Getter:

返回類型 get 屬性名稱 {
// 屬性的計算邏輯
return 值;
}

Setter:

set 屬性名稱(參數類型 參數名稱) {
// 屬性的設置邏輯
}

那我們這邊來舉個Getter跟Setter的例子:

class Person {
String _name;
int _age;

Person(this._name, this._age);

// Getter 方法用於獲取 _name 屬性的值
String get name => _name;

// Setter 方法用於設置 _name 屬性的值
set name(String value) {
_name = value;
}

// Getter 方法用於獲取 _age 屬性的值
int get age => _age;

// Setter 方法用於設置 _age 屬性的值
set age(int value) {
if (value >= 0) {
_age = value;
}
}
}

void main() {
var person = Person('Alice', 25);

print(person.name); // 輸出: Alice
print(person.age); // 輸出: 25

person.name = 'Bob';
person.age = -10;

print(person.name); // 輸出: Bob
print(person.age); // 輸出: 25
}

大概實作就知道Getter會拿取值、Setter會改變值。

抽象方法Abstract Method

抽象方法我的解讀有點像是畫一個物件藍圖的感覺,這邊的解釋是這樣:

當我們想要定義一個方法的介面,但是不希望在該類別中實現該方法的邏輯時,我們可以使用抽象方法。抽象方法只能存在於抽象類別中,它僅定義了方法的簽名,並沒有提供具體的實現內容。

要使一個方法成為抽象方法,我們需要在方法體中使用分號 (;) 代替方法的實現內容。這告訴編譯器該方法是一個抽象方法,並且需要在子類別中進行實現。

舉個例子:

abstract class Animal {
void makeSound(); // 抽象方法,沒有具體的實現內容
}

class Dog extends Animal {
void makeSound() {
print('Woof!'); // 實現抽象方法的具體邏輯
}
}

class Cat extends Animal {
void makeSound() {
print('Meow!');
}
}

void main() {
Animal dog = Dog();
Animal cat = Cat();

dog.makeSound(); // 輸出: Woof!
cat.makeSound(); // 輸出: Meow!
}

在抽象方法中,我們只實踐function的名字,但實際上讓子類別繼承的時候,才會去實踐這個function的內容。

其實在上述聊了蠻多語法,但遲遲沒有講繼承這件事情有點順序排的不好,我們接下來在這邊會實際聊聊:

Extend

這邊的內容大致上會提到的部分會是這樣:

Use extends to create a subclass, and super to refer to the superclass.

繼承這件事情在過往學Swift的語法中其實可以說就像:

class ViewController: UIViewController {

}

那只是在Dart中的設計會是以extends去標記:

class Vehicle {
void startEngine() {
print('Engine started.');
}
}

class Car extends Vehicle {
void startEngine() {
super.startEngine(); // 呼叫父類別的方法
print('Car is ready to go.');
}
}

class Truck extends Vehicle {
void startEngine() {
super.startEngine(); // 呼叫父類別的方法
print('Truck is ready to haul.');
}
}

void main() {
var car = Car();
var truck = Truck();

car.startEngine(); // 輸出: Engine started. Car is ready to go.
truck.startEngine(); // 輸出: Engine started. Truck is ready to haul.
}

剛開始的時候還以為會跟Swift的extension一樣,不過看起來是我錯了,兩個差蠻多的。

Override覆寫

那如果要overide(覆寫)一個屬性的時候應該怎麼做?下面舉個例子:

/// 父類別
class Shape {
double _area;

double get area => _area;

set area(double value) {
_area = value;
}
}

/// 子類別
class Circle extends Shape {

/// 這邊可以直接調用相關方法進行繼承父類別裡頭的方法。
@override
set area(double value) {
if (value >= 0) {
super.area = value;
} else {
throw Exception('Invalid area value for Circle');
}
}
}

void main() {
var circle = Circle();
circle.area = 25;
print(circle.area); // 輸出: 25
circle.area = -10; // 拋出異常: Invalid area value for Circle
}

後記

最近其實還買了蠻多的裝備,比如說上管包,但我發現上管包如果買的太硬可能會造成停車的時候胯下有點擠(笑),所以可能還是買一般的上管包比較好,但一般的上管包就不防水了,真是兩難呢。

下一篇會繼續講Dart語法物件的相關內容,如果結束後可以順利的進到UI的的使用就再好不過了!

Sign up to discover human stories that deepen your understanding of the world.

影山小麥機
影山小麥機

Written by 影山小麥機

本職為Mobile工程師,熱愛分享視野,也樂意站在ChatGPT的肩膀上。訂閱小麥機,收割技術、職涯、人生的難題。

No responses yet

Write a response