目录
- 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文件也都是小写的,多个单词之间用下划线分隔。与Java不同,Dart中的文件名和文件夹结构之间没有关系。下图显示了一些可以和不能用作库名称的值。
library loglib;
debug(msg) => print("DEBUG:$msg");
warn(msg) => print("WARN:$msg");
info(msg) => print("INFO:$msg");
class Logger {
log(msg) => print("LOG:$msg");
}
导入的顺序并不重要,因为所有导入都是在代码开始执行之前加载的,但它们必须出现在任何代码语句之前。
如果loglib要导入dart:html库,那么dart:html库将仅对loglib库可用。如果应用程序的其余部分也希望使用dart:html库,那么还需要在应用程序文件的其他位置指定另一条导入语句。对库而言导入语句是本地的。
库导入之间也允许循环引用。例如,库A可以导入库B,库B也可以导入库A,因为Dart库在应用程序开始运行之前已完全加载,所以Dart工具可以理解这种依赖关系。
Dart被设计为转换为JavaScript,这样它就可以在本机不支持Dart的浏览器中运行。JavaScript本身并不支持库:一个多库Dart应用程序在转换时变成一个单独的JavaScript文件,每个导入的库都在其自己的注释部分,并用库名称进行注释,如下所示。
上面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...
}
使用库前缀防止名称冲突:
这里没有什么可以阻止另一个开发人员使用另一个库,该库还包含一个Logger类和一个顶级info()方法。这是使用导入前缀的地方。
使用as
语句定义的前缀后,必须始终使用前缀引用该库中的任何类或函数。尽管可以在每个导入声明中始终使用前缀,但这样做可能会导致代码变得混乱,因为导入库中对每个类和方法的每个引用都需要使用前缀。实用的方法是最好的:仅当添加库前缀有助于可读性和/或防止命名冲突时才添加库前缀,而不是到处使用库前缀。
记住
■ library library_name; 语句必须是库中的第一个语句。
■ 库可以使用import “uri/to/lib.dart”;语句来导入其他库。
■ 库和导入语句必须出现在其他代码之前。
■ 可以使用库前缀来避免不同库之间的命名冲突。
2.隐藏具有库隐私的功能
目前,loglib日志库的所有函数和类都可供库用户使用。任何导入库的应用程序都不会隐藏任何内容。您的所有功能都是公开可用的。下面来讲解使某些项成为私有项,以便无法从库外部访问它们。
在构建功能库时,可能会有一些内部实现细节,您不想向该库的最终用户公开。
loglib库当前包含将数据输出到浏览器控制台的基本功能。假设您想向记录器库添加一个功能,将日志消息发送到某个服务器。您不希望库的外部用户直接调用此服务器日志代码;它需要由入口点函数在内部调用。如果您只是在库中声明类和函数,最终用户将可以访问它们;但幸运的是,Dart允许您通过在项目名称前加下划线(_)来声明项目为私有。
围绕命名约定构建语言功能?
下划线前缀是一种常见(但不一定是通用)的命名约定,用于表示隐私,特别是在没有内置隐私的语言(如JavaScript和Python)中。Dart将此命名约定作为一种语言特性,从而使其更进一步。
这个特性一直是Dart社区争论的话题,它可能是争论的焦点之一。一方面,您可以在开发人员开销很小的情况下获得隐私;在调用站点,您可以看到被调用的内容是私有的,这在您探索新库的内部时非常有用,因为您不需要查找声明。另一方面,它确实会影响可读性,并且可能会有如下代码:
var someValue = new _MyClass()._getValue()._doAction(_withProperty);
反对使用下划线前缀的另一个理由是,如果需要将某个内容从公共更改为私有(反之亦然),则必须在使用该前缀的任何地方重命名该前缀。该论点的另一方面是,如果您正在从私有重命名为公共,那么重命名将只在您的库中发生(如果它当前在您的库中是私有的,那么外部用户将不会使用它)。如果你正在从公有变为私有,那么还有更基本的问题(比如打破库用户的隐私)通过删除函数(而不仅仅是重命名)进行编码。
要记住的两条规则如下:
■ 库中的代码可以访问同一库中的任何其他代码。
■ 库外的代码只能访问该库中的非私有代码。
在loglib库中,您当前有一个Logger类。也许您想通过存储一个内部的_isEnabled属性来确定日志是启用还是禁用的:它的内部状态。使用同一库中的Logger类的其他类可以直接访问内部状态,但库的用户无法访问该内部状态。应用程序的其他部分应该不知道Logger类的工作原理,只知道它可以工作。下图说明了这种关系。
通过使用下划线前缀,您可以在库中构建丰富的功能,并确保只有库用户需要的功能通过定义良好且一致的类、方法和顶级函数接口公开。
通过getter和setter访问私有字段:
如果希望允许外部用户以只读方式访问_isEnabled属性,可以向类中添加公有getter。同样,当您添加公共setter时,该值变为可写。有趣的是,通过只提供一个getter或setter,拥有只读或只写值是完全有效的。
使用私有函数:
类中的私有函数也可以通过在方法名前加下划线来定义。
为了保持代码的可读性和可维护性,可以将代码块提取到同一类的私有方法中,该方法在库外部是不可见的。
私有类的一个困惑:
与在类中具有私有方法和私有属性的方式相同,也可以通过在类名前加下划线在库中创建私有类,如下而清单中的_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文件,该文件当前包含两个类和四个函数,如下图所示,并将其拆分为单独的源文件。
这些函数和类将分为两个独立的源文件,loglib.dart库文件将它们链接在一起。目标是最终得到总共三个文件,如下图所示。
这只是拆分库的一种方法。您可以将每个类和函数拆分为自己的文件,也可以将所有公共函数和类拆分为一个文件,将所有私有函数和类拆分为另一个文件。在库中,可能有多个功能单元,每个功能单元可能由几个类组成。作为最佳实践,应该将这些功能单元包装到单个源文件中。
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方面的使用方式无关。
从外部库导入的任何类和函数,如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字段)。
Pub通过使用约定而不是配置来工作,它需要为您的应用程序提供特定的布局。关键文件和文件夹如下图所示;幸运的是,当您创建新项目时,Dart编辑器会为您创建此结构。
要将各种依赖项拉入项目,需要使用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具有内置的模块性和灵活性,无论您是从构建库还是应用程序开始,在两者之间切换都非常容易。
总结
Dart提供了一个用于组织和重用源代码的库结构。使用库文件和部件文件,您可以以对团队开发和第三方重用有意义的方式构造代码。
import语句允许应用程序和库以简单的方式导入其他库,同时通过使用库前缀避免命名冲突。基于库的隐私允许您共享库中的代码库、函数库和类库,这些代码库是使用您的库的代码的私有部分。通过添加main()函数,库也可以成为独立的应用程序,main()函数是任何Dart脚本的入口点。
记住
■ 库可以导入其他库。
■ 库也可以用作可运行脚本。
■ 库的源代码可以跨多个部件文件拆分。
■ 库中声明为私有的任何代码都可以从该库的任何其他部分访问。
■ 任何未声明为私有的代码也可以由使用库的代码使用。
现在,您已经了解了Dart,可以构建一个由多个库和文件组成的结构化应用程序,并且了解了Dart的隐私机制与库而不是类的关系,现在是时候深入了解Dart的类、接口和继承机制,以及它们如何适应可选类型的动态世界了。