天天看點

★Dart-5-了解庫和隐私模型

目錄

          • 1.定位和導入庫
          • 2.隐藏具有庫隐私的功能
          • 3.組織庫源檔案
          • 4.打包庫
          • 5.腳本是可運作的庫
          • 總結

在本章中,您将學習如何在Dart中建立和使用代碼庫,以及這些庫如何與Dart的隐私模型相關,您可以使用該模型隐藏庫的内部工作。庫是可部署代碼的最小單元,可以小到單個類或函數,也可以大到整個應用程式。在現實世界中,除了最普通的應用程式外,所有應用程式都應該将其代碼拆分為多個庫,因為這種設計促進了良好的松散耦合體系結構、可重用性和可測試性。通過建構一個簡單的記錄器架構,您可以将其導入到自己的代碼中,在閱讀本章時,您将了解這些特性。

當您建構代碼包以供重用時,通常存在一些内部工作,您不希望第三方使用者能夠通路這些工作,除非通過已釋出并商定的接口(例如某些類資料的内部狀态)進行通路。在Dart中,您可以向您的團隊成員或web使用者釋出代碼庫,其中隻包含您希望使這些最終使用者可見的部分。此設定允許在不影響最終使用者的情況下更改庫的内部結構。它與Java和C#中的不同,Java和C#具有不同的、以類為中心的隐私模型。在這些語言中,類内部可以更改,而不會影響最終使用者。

為什麼Dart沒有以類為中心的隐私模型?

這是Dart特别受JavaScript和web開發影響的領域之一。在JavaScript中,沒有隐私的概念,除非遵循某些約定,例如從其他函數傳回閉包。是以,Dart隐私模型應該被認為是對JavaScript的改進,而不是将其與更傳統的基于類的語言(如Java和C)進行比較。

Dart的可選類型允許您在使用者與庫互動時在代碼中提供文檔類型資訊(documentary type),例如函數參數和傳回類型或類屬性,同時允許您僅使用庫中您認為必要的類型資訊。正如本書前面提到的,類型資訊不會改變應用程式的運作方式,但它會為工具和其他開發人員提供文檔。

1.定位和導入庫

我們将建立一個名叫loglib的日志庫,如下圖:

★Dart-5-了解庫和隐私模型

庫名稱的目的是辨別可重用的代碼塊。庫名稱必須為小寫,多個單詞之間用下劃線分隔。庫名稱不需要與檔案名相同,盡管按照慣例Dart檔案也都是小寫的,多個單詞之間用下劃線分隔。與Java不同,Dart中的檔案名和檔案夾結構之間沒有關系。下圖顯示了一些可以和不能用作庫名稱的值。

★Dart-5-了解庫和隐私模型
library loglib;

debug(msg) => print("DEBUG:$msg");
warn(msg) => print("WARN:$msg");
info(msg) => print("INFO:$msg");

class Logger {
	log(msg) => print("LOG:$msg");
}
           
★Dart-5-了解庫和隐私模型

導入的順序并不重要,因為所有導入都是在代碼開始執行之前加載的,但它們必須出現在任何代碼語句之前。

如果loglib要導入dart:html庫,那麼dart:html庫将僅對loglib庫可用。如果應用程式的其餘部分也希望使用dart:html庫,那麼還需要在應用程式檔案的其他位置指定另一條導入語句。對庫而言導入語句是本地的。

庫導入之間也允許循環引用。例如,庫A可以導入庫B,庫B也可以導入庫A,因為Dart庫在應用程式開始運作之前已完全加載,是以Dart工具可以了解這種依賴關系。

Dart被設計為轉換為JavaScript,這樣它就可以在本機不支援Dart的浏覽器中運作。JavaScript本身并不支援庫:一個多庫Dart應用程式在轉換時變成一個單獨的JavaScript檔案,每個導入的庫都在其自己的注釋部分,并用庫名稱進行注釋,如下所示。

★Dart-5-了解庫和隐私模型

上面Dart語言轉換為JavaScript後,如下:

...snip...
// ********** Library loglib **************
// ********** Code for top level **************
function debug(msg) {
	return print$(("DEBUG: " + msg));
}
// ********** Library C:\DartInAction\PackList **************
// ********** Code for PackItem **************
// ********** Code for top level **************
function main() {
	debug("Starting building UI");
	...snip...
           

請注意,在JavaScript輸出中,loglib部分中隻存在debug(msg)函數,這是因為您還沒有使用其他函數或類,是以它知道不要轉換它們。

下面是使用loglib庫的示例:

class PackItem {
	// ...snip...
	PackItem(this.itemText) {
		if (itemText.length == 0) {
			warn("User added an empty item");
		} else {
			info("User added an item $itemText");
		}
	}
	DivElement get uiElement {
		if (_uiElement == null) {
			_uiElement = new Element.tag("div");
			_uiElement.classes.add("item");
			_uiElement.text = this.itemText;
			_uiElement.onClick.listen( (event) => isPacked = !isPacked);
			_uiElement.onClick.listen( (event) => info("Item updated");
		}
		return _uiElement;
	}
	// ...snip...
}
           

使用loglib裡面的Logger類:

import "../loglib/loglib.dart";
main() {
	debug("Started building UI");
	// ...snip building the UI
	var logger = new Logger();
	logger.log("Finished building UI");
	// ...snip...
}
           
★Dart-5-了解庫和隐私模型

使用庫字首防止名稱沖突:

這裡沒有什麼可以阻止另一個開發人員使用另一個庫,該庫還包含一個Logger類和一個頂級info()方法。這是使用導入字首的地方。

★Dart-5-了解庫和隐私模型
★Dart-5-了解庫和隐私模型

使用

as

語句定義的字首後,必須始終使用字首引用該庫中的任何類或函數。盡管可以在每個導入聲明中始終使用字首,但這樣做可能會導緻代碼變得混亂,因為導入庫中對每個類和方法的每個引用都需要使用字首。實用的方法是最好的:僅當添加庫字首有助于可讀性和/或防止命名沖突時才添加庫字首,而不是到處使用庫字首。

記住

■ library library_name; 語句必須是庫中的第一個語句。

■ 庫可以使用import “uri/to/lib.dart”;語句來導入其他庫。

■ 庫和導入語句必須出現在其他代碼之前。

■ 可以使用庫字首來避免不同庫之間的命名沖突。

2.隐藏具有庫隐私的功能

目前,loglib日志庫的所有函數和類都可供庫使用者使用。任何導入庫的應用程式都不會隐藏任何内容。您的所有功能都是公開可用的。下面來講解使某些項成為私有項,以便無法從庫外部通路它們。

在建構功能庫時,可能會有一些内部實作細節,您不想向該庫的最終使用者公開。

loglib庫目前包含将資料輸出到浏覽器控制台的基本功能。假設您想向記錄器庫添加一個功能,将日志消息發送到某個伺服器。您不希望庫的外部使用者直接調用此伺服器日志代碼;它需要由入口點函數在内部調用。如果您隻是在庫中聲明類和函數,最終使用者将可以通路它們;但幸運的是,Dart允許您通過在項目名稱前加下劃線(_)來聲明項目為私有。

★Dart-5-了解庫和隐私模型

圍繞命名約定建構語言功能?

下劃線字首是一種常見(但不一定是通用)的命名約定,用于表示隐私,特别是在沒有内置隐私的語言(如JavaScript和Python)中。Dart将此命名約定作為一種語言特性,進而使其更進一步。

這個特性一直是Dart社群争論的話題,它可能是争論的焦點之一。一方面,您可以在開發人員開銷很小的情況下獲得隐私;在調用站點,您可以看到被調用的内容是私有的,這在您探索新庫的内部時非常有用,因為您不需要查找聲明。另一方面,它确實會影響可讀性,并且可能會有如下代碼:

var someValue = new _MyClass()._getValue()._doAction(_withProperty);

反對使用下劃線字首的另一個理由是,如果需要将某個内容從公共更改為私有(反之亦然),則必須在使用該字首的任何地方重命名該字首。該論點的另一方面是,如果您正在從私有重命名為公共,那麼重命名将隻在您的庫中發生(如果它目前在您的庫中是私有的,那麼外部使用者将不會使用它)。如果你正在從公有變為私有,那麼還有更基本的問題(比如打破庫使用者的隐私)通過删除函數(而不僅僅是重命名)進行編碼。

要記住的兩條規則如下:

■ 庫中的代碼可以通路同一庫中的任何其他代碼。

■ 庫外的代碼隻能通路該庫中的非私有代碼。

在loglib庫中,您目前有一個Logger類。也許您想通過存儲一個内部的_isEnabled屬性來确定日志是啟用還是禁用的:它的内部狀态。使用同一庫中的Logger類的其他類可以直接通路内部狀态,但庫的使用者無法通路該内部狀态。應用程式的其他部分應該不知道Logger類的工作原理,隻知道它可以工作。下圖說明了這種關系。

★Dart-5-了解庫和隐私模型

通過使用下劃線字首,您可以在庫中建構豐富的功能,并確定隻有庫使用者需要的功能通過定義良好且一緻的類、方法和頂級函數接口公開。

通過getter和setter通路私有字段:

如果希望允許外部使用者以隻讀方式通路_isEnabled屬性,可以向類中添加公有getter。同樣,當您添加公共setter時,該值變為可寫。有趣的是,通過隻提供一個getter或setter,擁有隻讀或隻寫值是完全有效的。

★Dart-5-了解庫和隐私模型

使用私有函數:

類中的私有函數也可以通過在方法名前加下劃線來定義。

為了保持代碼的可讀性和可維護性,可以将代碼塊提取到同一類的私有方法中,該方法在庫外部是不可見的。

★Dart-5-了解庫和隐私模型

私有類的一個困惑:

與在類中具有私有方法和私有屬性的方式相同,也可以通過在類名前加下劃線在庫中建立私有類,如下而清單中的_ServerLogger類所示。私有類非常有用,因為它們隻能在庫中建立。不能使用new關鍵字從庫外部建立私有類的新執行個體。

一個有趣的困惑是為什麼私有類(隻能在庫中通路)可以具有公共方法和屬性。當你在同一個圖書館時,财産是公共的還是私人的沒有差別;如果類是私有的,如何從庫外部引用它?下面的代碼顯示了如何在getServerLogger()函數中實作這一點,該函數傳回私有類_ServerLogger的一個新執行個體。

library mixed_loglib;
class Logger {// Logger類是public的,可以被外面調用直接引用
	_ServerLogger getServerLogger() {// 函數可被外部調用傳回一個_ServerLogger執行個體,但不能直接被外部引用
		return new _ServerLogger();
	}
}
// 私有的_ServerLogger類包含公共和私有屬性;這對庫沒有任何影響,因為整個類都是私有的
class _ServerLogger {
	var serverName;
	var _serverIp;
}
           

盡管您可以直接通路庫外部的私有類,但該庫中的公共方法或函數可能會傳回私有類的執行個體。應該避免這種模式,但Dart仍然通過可選的分型來處理它。

從庫中傳回公共類是有效的,但此類通常由公共隐式接口引用,而不是由其實作類名引用。将在下一章讨論這個想法。

隻能将結果存儲在動态的可選類型變量中:

Logger logger = new Logger();
var privateInstance = logger.getServerLogger();
           

即使不能按名稱引用_ServerLogger類,但一旦有了它的執行個體,就可以在該私有執行個體上通路它的公共屬性,而不會受到工具的抱怨。但是,您将無法獲得自動完成幫助,因為您無法向工具提供類型資訊。如果您試圖通路privateInstance._serverIp屬性,則會出現noSuchMethod錯誤,因為您試圖從庫外部通路私有屬性。不過,通路privateInstance.serverName可以正常工作,因為它沒有标記為private。除非與公共接口結合使用,否則以這種方式使用庫的目的編寫庫應該被視為不好的做法,因為庫的最終使用者無法了解如何使用私有類(除了看源碼)。

在庫中使用很有函數:

庫中的頂級函數也可以是公共函數和私有函數,方式與類相同。在私有函數前面加下劃線将使其成為庫的私有函數,這意味着可以從庫中的任何位置通路它。當您希望在庫中提供私有實用程式函數,但沒有關聯的資料,是以它們不保證成為類中的方法時,這可能很有用。

library loglib;

_logMsg(msg) {
	_ServerLogger serverLogger = new _ServerLogger();
	serverLogger.send(msg);
}
info(msg) => _logMsg("INFO $msg");
warn(msg) => _logMsg("DEBUG $msg");
debug(msg) => _logMsg("WARN $msg");

class _ServerLogger {
	// ...snip...
}
class Logger {
	log(msg) => _logMsg(msg);
}
           

記住

■ private “_”字首可以應用于函數、類、方法和屬性。

■ 标記為private的代碼隻能從同一庫中通路。

■ 該庫的外部使用者可以通路未标記為私有的代碼。

建構具有隐藏内部功能的可重用庫是大多數應用程式的标準做法,Dart通過采用下劃線約定并将其烘焙到語言中來實作此功能。

3.組織庫源檔案

盡管現在可以将應用程式拆分為可重用庫,但庫仍然可以由數千行代碼組成。在單個庫檔案中跟蹤所有這些代碼可能會很尴尬。幸運的是,Dart提供了一種進一步劃分庫的方法:将庫劃分為源檔案集合。

loglib庫現在包含公共和私有類和函數的混合。如果您要添加更多功能,不久庫檔案将變得難以導航,即使使用這些工具也是如此。在團隊中開發時,更大的問題是,如果您同時在同一個庫中工作,對檔案的任何重大重構都很容易給團隊中的其他開發人員帶來問題。

幸運的是,Dart允許您将庫拆分為多個源檔案。您的庫的外部使用者對此一無所知,而且無論它是由單個檔案、100個檔案(每個檔案包含一個類或函數)還是類和函數的任意組合構成,對您的庫的使用者來說都沒有差別。

在本節中,您将擷取loglib.dart檔案,該檔案目前包含兩個類和四個函數,如下圖所示,并将其拆分為單獨的源檔案。

★Dart-5-了解庫和隐私模型

這些函數和類将分為兩個獨立的源檔案,loglib.dart庫檔案将它們連結在一起。目标是最終得到總共三個檔案,如下圖所示。

★Dart-5-了解庫和隐私模型

這隻是拆分庫的一種方法。您可以将每個類和函數拆分為自己的檔案,也可以将所有公共函數和類拆分為一個檔案,将所有私有函數和類拆分為另一個檔案。在庫中,可能有多個功能單元,每個功能單元可能由幾個類組成。作為最佳實踐,應該将這些功能單元包裝到單個源檔案中。

Dart提供

part

關鍵字,允許您将代碼拆分為庫中的單獨檔案。它與

library

關鍵字在同一個檔案中使用,并且需要提供組成庫的其他源檔案的相對路徑:例如,

part "functions.dart"

;。您可以為classes.dart和functions.dart建立新的空文本檔案,并将類和函數剪切粘貼到其中。他們不需要額外的關鍵字。下面的清單顯示了完整的functions.dart檔案。

part of loglib;// 表明這個檔案是loglib庫的一部分

_logMsg(msg) {
	print(msg);
	_ServerLogger serverLogger = new _ServerLogger();
	serverLogger.send(msg);
}
info(msg) => _logMsg("INFO $msg");
warn(msg) => _logMsg("DEBUG $msg");
debug(msg) => _logMsg("WARN $msg");
           

你隻能在一個庫的上下文中使用它,因為它本身什麼也做不到。需要注意的是,part源檔案是對代碼的提取,本可以保留在原始庫檔案中,但為了友善開發人員,已被提取到單獨的檔案中。它與代碼在類和函數的公共和私有可見性或轉換為JavaScript方面的使用方式無關。

★Dart-5-了解庫和隐私模型
★Dart-5-了解庫和隐私模型

從外部庫導入的任何類和函數,如

import "dart:html";

對屬于loglib的所有part檔案可用。是以,庫中每個類和函數之間的關系保持不變,盡管它們是在源檔案中組織的。

源檔案限制

源檔案使用part關鍵字時,應注意以下限制:

■ 使用part指令導入到庫中的檔案需要被視為原始庫檔案的一部分。也就是說,它們不能包含自己的任何語句。如果他們這樣做了,就有可能打破library、import和part關鍵字的嚴格順序。

■ 源檔案隻能屬于應用程式中的單個庫。loglib和webloglib不能同時使用

part "classes.dart";

■ 類或函數必須存在于單個檔案中。無法使一個類或函數跨越多個檔案(沒有像C#中的分部類)。

如果您認為part檔案是同一邏輯庫檔案的一部分,那麼這些限制是有意義的。不能讓檔案的一部分同時也是其他檔案的一部分,也不能讓庫檔案包含另一個庫語句。在同一個檔案中将一個類或函數拆分為兩個獨立的部分也是不可能的。

重要的是要記住,在不同的part檔案中包含類和函數不會影響隐私。它們都被認為是在同一個庫中,隐私存在于庫級别。

記住

■ 單個庫檔案可以拆分為多個part檔案。

■ 庫的外部使用者不知道庫已被拆分。

■ Dart将庫檔案拆分為多個實體零件檔案,就像它是單個庫檔案一樣。

4.打包庫

除了将功能封裝到庫中并使其可供第三方代碼使用外,還可以像腳本一樣直接運作庫。

在Dart中,包是一個獨立的應用程式或一個或多個庫,放在一個具有版本号的單元中。軟體包有兩個用途:它們允許您的應用程式輕松導入其他人的軟體包,并且允許您格式化自己的檔案結構,進而允許第三方打包和導入您的代碼。

pub工具内置于Dart編輯器中,也可從指令行獲得,用于導入代碼所依賴的包。這些包可以托管在web伺服器、GitHub(或其他Git存儲庫)和pub.dartlang.org存儲庫中。如果您使用過其他包管理器,如Java的Maven或Node.js的npm,pub将執行類似的功能:它會自動下載下傳應用程式的依賴項和任何嵌套的依賴項。Pub還管理版本控制沖突,包括突出顯示嵌套依賴項中的沖突。

一個名為

pubspec.yaml

的檔案位于源檔案結構的根目錄中,它包含所有重要資訊,使pub能夠查找和下載下傳依賴項。它使用YAML檔案格式:一種人類可讀的标記語言,使用縮進定義節和子節。下面清單顯示了一個示例pubspec.yaml檔案。如果希望将包托管在pub.dartlang.org上,則名稱字段是必需的,版本和說明字段也是必需的。其他字段是可選的,但重要的是dependencies,它告訴pub您的依賴項(如果您在核心Dart SDK之外沒有依賴項,那麼您可以省略dependencies字段)。

★Dart-5-了解庫和隐私模型

Pub通過使用約定而不是配置來工作,它需要為您的應用程式提供特定的布局。關鍵檔案和檔案夾如下圖所示;幸運的是,當您建立新項目時,Dart編輯器會為您建立此結構。

★Dart-5-了解庫和隐私模型

要将各種依賴項拉入項目,需要使用pub install和pub update指令,這兩個指令都存在于Dart編輯器菜單中。這些指令将依賴項的較新版本安裝或拉入應用程式的結構中,并建立pubspec.lock檔案。包被下載下傳到緩存中,通常是主目錄中的.pub cache/檔案夾。pubspec.lock檔案包含pub安裝的不同依賴項的實際版本(在指定版本範圍時非常有用)。此檔案可以送出到源代碼管理中,進而確定團隊中的其他開發人員使用相同版本的任何依賴項。

安裝依賴項後,可以在庫代碼中使用import關鍵字引用它們。例如,PackList應用程式可以使用loglib包,如這行代碼所示:

import "package:loglib/loglib.dart";

5.腳本是可運作的庫

loglib庫通過提供大量外部代碼可以使用的類和函數,向外部使用者提供日志函數。也可以直接運作庫——記住Dart腳本隻不過是一個包含頂級main()函數的.dart檔案。

使用loglib庫的一個示例是允許它replay從web伺服器加載回開發人員控制台的一系列日志消息。您可以提供一個可公開通路的函數,如replay(url),該函數調用伺服器并将每個傳回的日志消息發送到現有的私有_logMsg()函數。

運作此新replay功能的一種方法是編寫一個單獨的應用程式,導入loglib庫,然後調用replay()。為了調用單個函數,這似乎需要做很多工作。幸運的是,Dart提供了另一種選擇。庫還可以包含main()函數,而main()函數是Dart使庫可運作所需的全部函數(main()函數是所有Dart腳本的入口點函數)。下面的清單顯示了添加到loglib庫的main()。

library loglib;
import "dart:html";
part "classes.dart";
part "functions.dart";
main() {
	replay("http://www.someserver.com/logMessages");
}
replay(url) {
	//snip... load msgsFromUrl list
	for (msg in msgsFromUrl) {
		_logMsg(msg);
	}
}
           

現在,您可以通過在關聯的HTML中包含腳本标記,從HTML檔案中使用此功能,例如:

<script type="application/dart" src="loglib.dart"></script>

這将在代碼完全加載并準備運作後調用main()函數。

盡管将main()保留在庫檔案(即包含庫語句的檔案)中是最佳做法,但您可以将main()放在不同的部件檔案中。請記住,部件檔案中的函數或類與主庫檔案中的函數或類的性能完全相同,main()函數也完全相同。

這意味着,通過在頂部添加庫聲明,您建立的每個Dart應用程式也可以成為庫。通過這種方式,将現有的Dart應用程式轉換為庫變得非常簡單,通過使現有的Dart應用程式也可以作為庫來運作,該庫可以嵌入到其他應用程式中。通過向PackList應用程式添加庫語句,您可以将其包含在其他應用程式的混搭中,每個應用程式都提供獨立的功能,由一個共同的主題組合在一起(見下圖)。

★Dart-5-了解庫和隐私模型

Dart具有内置的子產品性和靈活性,無論您是從建構庫還是應用程式開始,在兩者之間切換都非常容易。

總結

Dart提供了一個用于組織和重用源代碼的庫結構。使用庫檔案和部件檔案,您可以以對團隊開發和第三方重用有意義的方式構造代碼。

import語句允許應用程式和庫以簡單的方式導入其他庫,同時通過使用庫字首避免命名沖突。基于庫的隐私允許您共享庫中的代碼庫、函數庫和類庫,這些代碼庫是使用您的庫的代碼的私有部分。通過添加main()函數,庫也可以成為獨立的應用程式,main()函數是任何Dart腳本的入口點。

記住

■ 庫可以導入其他庫。

■ 庫也可以用作可運作腳本。

■ 庫的源代碼可以跨多個部件檔案拆分。

■ 庫中聲明為私有的任何代碼都可以從該庫的任何其他部分通路。

■ 任何未聲明為私有的代碼也可以由使用庫的代碼使用。

現在,您已經了解了Dart,可以建構一個由多個庫和檔案組成的結構化應用程式,并且了解了Dart的隐私機制與庫而不是類的關系,現在是時候深入了解Dart的類、接口和繼承機制,以及它們如何适應可選類型的動态世界了。