Dart 的類別建構子(Constructor)與之前我所接觸過的物件導向語言會有一些不一樣,比較會讓人介意的事情是,不可以做 constructor overloading !!!!
這時可能有人表情出現三條線,Google 是在幹甚麼,沒有了這個特性,那過去所接受過的物件導向觀念及設計方式不就要換個方式了嗎?
是的,Dart 給了我們兩種建構子的實作方法,叫 Named Constructor 及 Factory Constructor,以下原文一開始有詳細的介紹 Constructor:
https://www.dartlang.org/articles/idiomatic-dart/
在初期使用的時候會有些不習慣,可是一但習慣之後,反而會覺得 Dart 的建構子的實作方式是比較接近人類的思維的。我就以官方提供的文章範例來說說為什麼 Dart 的方式比較直覺
Named Constructor
由於 Dart 不支援 Constuctor 的 Overloading 所以不能如 Java 一樣可以多種不同組合的參數的 Constructor 幹以下事情:
class File { File(File parent, String child); File(String pathname); File(String parent,String child); File(URI uri); }
這段 JAVA 代碼可以用不同參數的建構子依照不同的參數來初始化一個File物件,但 Dart 不行,不過 Dart 有兩種方式可以來達成相同目的
- Optional positional parameters
- Named Constructor
Optional positional parameters 的方式就是在建構子中宣告那些是可選擇的傳入參數,這種做法其實類似 PHP 了,其實比較難做到型別上的判斷。例如以下宣告方式 :
class File { File(var param1 , [var param2]) { if(param1 is String) { // ... do something } else if(param1 is Uri) { // ... do something } } } void main(){ var a = new File("/abc.txt"); var b = new File("/","abc.txt"); }
另一種就是 Dart 的 Named Constructor 了,看看以下官方文件上提供的範例
import 'dart:math'; class Point { num x, y; Point(this.x, this.y); Point.zero() : x = 0, y = 0; Point.polar(num theta, num radius) { x = cos(theta) * radius; y = sin(theta) * radius; } }
這個範例很簡單,就是實作座標位置的類別,但是有三個 Constructor:
- Point(num x , num y) : 給予 X,Y 軸就可以建立一個座標點
- Point.zero() : 不用給任何參數,該座標會是 (0 , 0)
- Point.polar(num theta , num radius) : polar 是南北極的極,實際上這要幹什我看不懂,懂得人可以補充解釋一下嗎,哈哈。
使用方法一樣用 new 來取得物件,如下:
import 'dart:math'; main() { var a = new Point(1, 2); var b = new Point.zero(); var c = new Point.polar(PI, 4.0); }
Dart 的 Named Constructor 其實比 Constructor overloading 有更大的彈性,看上述範例就知道,在 Dart 中可以有各種不同的 Named Constructor 來組成不同目的的物件初始化,這種 Named Constructor 其實在可讀性上也有增加,因為我們會知道現在要宣告的物件是用甚麼方式來宣告的並且預期將會得到甚麼結果不是嗎。最重要的是他仍具備強型的檢查,這樣寫出來的 Code 比較不會出錯呀 ~
當然其他的語言也可以做到類似的 Named Constructor ,不過 Dart 是直接讓撰寫 Named Constructor 的方式簡化了,如果用 PHP 寫寫看會是這樣,可以比較看看,那種精簡,那種直覺
<?php class Point { public $x; public $y; private function __construct($x, $y) { $this->x = $x; $this->y = $y; } public static function create($x, $y) { return new self($x, $y); } public static function createZero() { return new self(0, 0); } public static function createPolar($theta , $radius) { $x = cos($theta) * $radius; $y = sin($theta) * $radius; return new self($x,$y); } } $a = Point::create(1,2); $b = Point::createZero(); $c = Point::createPolar(M_PI, 4.0);
Factory Constructor
物件導向常常有 Factory 的 Design Pattern 的技巧,其目的通常都是要取得一個相同的物件的實例(Instance)。在 Dart 中是直接提供 factory 的關鍵字來簡化達到更直覺的目的。
如以下範例:
class Symbol { final String name; static Map<String, Symbol> _cache; factory Symbol(String name) { if (_cache == null) { _cache = {}; } if (_cache.containsKey(name)) { return _cache[name]; } else { final symbol = new Symbol._internal(name); _cache[name] = symbol; return symbol; } } Symbol._internal(this.name); }
使用方式如下:
var a = new Symbol('something'); var b = new Symbol('something'); assert(identical(a, b)); // true!
這段範例代碼是這樣的,我們宣告 a,b 兩個變數是以 new Symbol('something'); 來得到物件,其實 a,b 兩個經過 assert 檢查是相同的實例(Instance)的。過去若用 Java 或 PHP 寫的時候,就必須用 Factory Design Pattern 的寫法才能達成,例如以下 PHP 範例:
class Symbol { public $name; private static $_cache = array (); private function __construct($name) { $this->name = $name; } public static function getInstance($name) { if (! isset ( self::$_cache [$name] )) { $instance = new self ( $name ); self::$_cache[$name] = $instance; } return self::$_cache [$name]; } } $a = Symbol::getInstance('something'); $b = Symbol::getInstance('something');
上述 Dart 和 PHP 範例在使用上的時候,Dart 仍是使用 new 來取得一個物件,但是它隱藏了內部的運作,因此我們可能得到了一個快取的物件,使用者並不清楚,也無須了解,而 PHP 這種語言就如上面的方式用 getInstance 的方式來得到一樣的效果了,但是就必須把建構子設定成 private 屬性讓撰寫者無法直接使用,其實若依可讀性來講,是不是用 new 來得直接呢?這純粹只是我幫 Dart 說好話的一個理由之一,因為人的記憶力有限啊,若能直接 new 最好,還搞一個 getInstance 幹甚麼對吧,那PHP能不能用 Dart 的方式也用 new 來取得一個快取呢?自己試試看吧,哈。
總結
Dart 的建構子不支援 overloading ,但是用了 Named & Factory 的方式,意外的讓程式碼的可讀性增加,其實對我來說,也不過是個習慣上的改變,一改過來之後,會發覺怎麼其他語言都是屎了啊,哈哈,不過 Dart 還有很多地方要改進是真的,畢竟它還是個很新的 Language 還有很多東西欠缺而難以架構一個比較大型的專案。