★Dart-6-构造类和接口

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了★Dart-6-构造类和接口相关的知识,希望对你有一定的参考价值。

1.定义一个简单的类
class User {
	String _forename;
	String get forname => _forename;
	set forname(value) => _forename = value;
	
	String surname;
	String getFullName() {
		return "$forename $surname";
	}
}
main() {
	User user = new User();
	user.forename = "Alice";
	user.surname = "Smith";
	var fullName = user.getFullName();
}

类接口编程:
设想一个使用用户类的登录库。User类和身份验证服务类的实现细节都包含在一个外部库中,该库由名为LogonLibrary的库中的另一个开发团队提供。您可以编写自己的代码来使用AuthService类和User类,如下面的清单所示,而无需了解具体的实现细节。

import "logon_library.dart";
User doLogon(AuthService authSvc, String username, String password) {
	User user = authSvc.auth(username, password);
	print("User is authenticated:${user==null}");
	return user;
}
buttonClickHandler(event) {
	AuthService authSvc = new AuthService();
	User user = doLogon(authSvc, querySelector("#username").value, querySelector("#password").value);
}

只要AuthService实例具有返回用户实例的auth()方法,doLogon()函数就可以正常运行。

依赖注入还是控制反转?
函数的参数之一是authSvc对象。这是一个依赖注入的示例,它允许您切换实现。例如,在测试doLogon()函数时,可以为authSvc参数提供一个模拟对象。在本地测试时,可以使用简单的authSvc对象,在部署生产系统时,可以提供企业authSvc对象。

如果编写doLogon()函数时没有authSvc参数,则必须在函数中创建一个具体实例。这意味着您无法切换AuthService的实现,这使得单元测试和提供不同的部署场景变得更加困难。

假设您想测试doLogon()函数。通过使用依赖注入,您可以决定使用哪个AuthService实现。您可以提供一个实现AuthService隐含接口的模拟实现,如下图所示,而不是使用命中某些真实服务器的AuthService来提供身份验证。

AuthService的公共接口包含一个方法,该方法接受两个字符串参数并返回名为User的类的实例。使用implements关键字,可以创建一个名为MockAuthService的模拟版本,用于测试doLogon()。下面的清单显示了mock类和简单测试,它们被包装在main()函数中。

import "logon_library.dart";
class MockAuthService implements AuthService {// 实现另一个类的隐含接口
	User auth(String username, String password) {
		var user = new User();
		user.forename = "testForename";
		user.surname = "testSurname";
		return user;
	}
}
User doLogon(AuthService authSvc, String username, String password) {
	User user = authSvc.auth(username, password);
	print("User is authenticated:${user==null}");
	return user;
}
main() {
	AuthService authService = new MockAuthService();
	var user = doLogon(authService, "Alice", "password");
	print(user.forename);
	print(user.surname);
}

显式接口定义:
到目前为止,您一直在使用类定义所隐含的接口。Dart允许您显式定义接口,独立于实际提供实现的类。显式定义接口类似于定义一个类,即在没有实现的情况下定义公共类成员(称为抽象类)。让我们将AuthService更改为一个接口,并创建一个名为EnterpriseAuthService的实现,如下所示。您的MockAuthService类保持不变。

abstract class AuthService {
	User auth(String username, String password);
}
class EnterpriseAuthService implements AuthService {
	User auth(String username, String password) {
		// some enterprise implementation
	}
}

abstract关键字很重要,因为它让Dart知道这是一个包含没有实现的方法的类。这使得无法直接创建AuthService实例,因为没有可使用的AuthService实现。

在Dart中,当我们谈论接口时,实际上是指类的隐含接口。该接口是在抽象类上还是在包含方法实现的实际类上并不重要。在文章的其余部分,术语接口将以这种方式使用。

使用多个接口:

abstract class RolesService {
	List getRoles(User user);
}
abstract class AuthService {
	User auth(String username, String password);
}
class EnterpriseAuthService implements AuthService, RolesService {
	User auth(String username, String password) {
		// some enterprise implementation
	}
	List getRoles(User user) {
		// some enterprise implementation
	}
}

现在,您可以在需要使用RoleService实例的任何位置使用EnterpriseAuthService。记住这种关系的简单方法是类共享一个“is an”与它实现的任何接口类的关系。你可以说EnterpriseAuthService “is-an” RolesService,EnterpriseAuthService “is-an” AuthService。因此,下面清单中的代码是有效的,它将EnterpriseAuthService实例传递到期望接收AuthService或RoleService实例的不同函数中。

User doLogon(AuthService authService, String username, String password) {
	return authService.auth(username, password);
}
showRoles(RolesService rolesService, User user) {
	List roles = rolesService.getRoles(user);
	print(roles);
}
main() {
	var entService = new EnterpriseAuthService();
	var user = doLogon(entService, "Alice", "password");
	showRoles(entService, user);
}

声明属性的getter和setter:
回到第3章,您看到了使用getter和setter定义的属性是通过调用代码访问的,访问方式与简单字段相同:

// 你是调用setter方法还是写入字段?Dart语法是相同的,因此对于调用方来说,这并不重要!
authService.isConnected = true;

幸运的是,因为调用类使用属性getter和setter的方式与字段相同,所以您也可以在接口中以任何方式声明它。将isConnected属性getter和setter(使用get和set关键字)添加到AuthService接口,并将简单的isConnected字段添加到RoleService接口,如下清单所示:

library logonlib;
abstract class AuthService {
	User auth(String username, String password);
	bool get isConnected;
	void set isConnected(bool value);
}
abstract class RoleService {
	List getRoles(User user);
	bool isConnected;
}
class EnterpriseAuthService implements AuthService, RoleService {
	bool _isConnected;
	bool get isConnected => _isConnected;
	void set isConnected(bool value) => _isConnected = value;
	// snip auth() and getRoles()
}

因为无论您使用的是getter和setter还是属性,读取和写入属性的语法都是相同的,所以可以使用这两种方法实现属性的接口定义。它们都满足接口定义的要求,您可能还记得,接口定义是您可以调用如下代码:

authService.isConnected = true;

只要您能够以这种方式使用实现类,它就满足接口的要求,无论是使用getter和setter对还是字段。

记住
■ 类定义也隐含着接口定义。
■ abstract关键字用于声明没有实现方法的类,并可用于显式接口定义。
■ implements关键字表示一个类正在提供特定方法的实现。
■ 类与接口具有“is-a”关系。

定义接口时,提供了一种机制,允许扩展和重用自己的代码,这就是Dart库的构建方式。核心Dart中的“类”,如String、int和List,实际上是接口。在本章前面,我们注意到,尽管您需要创建一个类的新实例,但有时Dart代码看起来好像您正在创建一个新接口。这方面的一个很好的例子是:var myList = new List();。这段代码似乎不应该工作,因为您不能仅创建一个类的接口实例。这是真的,但下面将展示这样的代码是如何工作的,以及如何使用多种不同的方法来创建类的新实例。

2.构造类和接口

在设计库时,无论是为自己的应用程序还是发布给第三方,都需要使其足够灵活,以满足该库的用例。最好尽可能地部署接口,以允许类的用户将您的实现与其他实现交换。

您还应该允许库的用户以对特定用例有意义的方式创建类的实例,就像您在第3章处理元素类时所做的那样。在该实例中,您使用html代码段或标记名创建了一个元素,但仍然返回了一个元素。用例不同,但元素类的设计允许这样做。
还有一种情况下,您可能希望重用类的实例:当有一个昂贵的操作(例如连接到企业身份验证服务器)时。您可能希望重用该连接,而不是每次都创建一个新的连接。

最后,您可能希望用户能够在特定状态下创建类的单个常量版本。例如,特定错误消息类的实例在创建后将永远不会更改,并且特定错误消息的所有实例都将相同。Dart允许通过常量构造函数实现此功能。

在本节中,我们将介绍一些创建类实例的方法。javascript、Java和C#中用于创建对象新实例的new关键字在Dart中几乎总是具有相同的效果。当您使用工厂构造函数来提供对象缓存时,您将看到为什么它几乎是,而不是总是。您还将了解const关键字,这是创建新的、不变的类实例的另一种方法。

让我们来研究一个类构造函数方法,这是我们在第3章中首先看到的。您将看到如何扩展构造函数的功能以允许字段初始化,以及如何添加多个命名构造函数。

构造类实例:

class EnterpriseAuthService {
	String connection;// 初始化为null
	EnterpriseAuthService() {// 构造函数,如果没提供则会有一个默认构造函数
		print("in the constructor");
	}
}
main() {
	var entSvc = new EnterpriseAuthService();
}
class EnterpriseAuthService {
	String connection;
	EnterpriseAuthService(String connection) {// 添加一个参数到构造函数
		this.connection = connection;// this关键字,含义同Java/C#
	}
}

Dart中的this关键字的行为与Java和C#中的相同,因为它专门指使用它的类的实例。这与JavaScript不同,JavaScript中的值在执行过程中会发生变化。

Dart通过在构造函数的参数列表中使用this关键字来隐式初始化参数,从而引入了一种速记方法。
例如,以下代码段的效果与前一个代码段完全相同:

class EnterpriseAuthService {
	String connection;
	EnterpriseAuthService(this.connection) {}
}

设计和使用有多个构造函数的类:
与Java或C#不同,Dart不支持函数、方法或构造函数重载(其中可以有相同的函数名,但参数定义不同)。如何使用多个构造函数以不同的方式创建类的实例?

在Java和C#中,您可以重用相同的构造函数名称并提供不同的参数,但Dart提供了一种不同的机制,类似于第5章中看到的库前缀。Dart将类名作为构造函数前缀,并允许您使用命名构造函数,每个构造函数都有不同的名称(以及参数,如果需要)。下图显示了Java和Dart中的等效构造函数语法。

回到第3章,您使用Element.tag()和Element.html()构建了用户界面元素。您使用命名构造函数创建元素的实例,但有两种不同的方式。现在EnterpriseAuthService已经命名了构造函数,您也可以使用这些构造函数:

var authSvc1 = new EnterpriseAuthService();// 默认构造函数
var authSvc2 = new EnterpriseAuthService.withConn("connection string");// 命名的withConn构造函数
var authSvc3 = new EnterpriseAuthService.usingServer("localhost", 8080);// 命名的usingServer构造函数

使用工厂构造器创建抽象类的实例:
在本章前面,您看到尝试创建抽象类的实例是一个错误。

在构建应用程序时,最好使用接口类型名称,而不是特定的实现类类型名称。这样做允许您在开发过程中切换实现,这对于使用模拟实现进行单元测试非常有用。这种做法的必然结果是,在设计库时,最好在类型定义中提供接口,以便为库的用户提供最大的灵活性。

但是,在大多数情况下,您通常希望库的用户使用特定的类。定义接口的AuthService抽象类就是一个很好的例子。大多数情况下,用户将AuthService接口类型与EnterpriseAuthService类结合使用,后者是同一外部库定义的一部分。因此,许多用户编写如下代码:

AuthService authSvc = new EnterpriseAuthService();

这种方法增加了库用户的复杂性,因为他们现在需要了解两种类型,一种是接口,另一种是实现类。这类代码在Java世界中非常流行,在Java世界中,庞大的库作为接口链存在,跟踪要使用的正确类实现可能是一件烦琐的事情。

幸运的是,Dart允许库设计者在抽象类上使用工厂构造函数,该类返回特定的默认实现类。在构造函数中使用factory关键字时,指定构造函数方法负责创建和返回有效对象,如下清单所示。

abstract class AuthService {
	User auth(String username, String password);
	factory AuthService() {// 定义工厂构造函数
		return new EnterpriseAuthService();// 必须返回一个对象的实例
	}
}

此方法有效地让接口用户编写如下代码:
AuthService authSvc = new AuthService();

此代码允许用户将抽象类当作实现类来处理,但仍然遵循良好的实践,针对接口进行编码。使用Dart String、List和int类型时,实际上使用的是定义了底层默认实现的接口。

工厂构造函数可以像普通构造函数一样使用多个名称。正如您将在下面看到的,工厂构造函数返回类实例的能力也可以用于标准实现类,以提供缓存机制。

用工厂构造器复用对象:
计算中的某些操作应保持在最低限度。它们要么在处理方面很昂贵,如构建大量嵌套对象,要么在时间方面很昂贵,如连接到另一台服务器。例如,当Alice登录到企业系统时,紧随其后的是Bob,您不应该让代码每次都等待一两秒钟才能连接到企业系统;该连接应缓存在应用程序中。在这些情况下,最好能够在可重用的对象池中缓存对象(如连接),并将其中一个对象返回给类的用户,而不是每次都创建一个新的对象实例。这是工厂构造函数的另一个用途,它可以存在于任何类上,而不仅仅是抽象类。

通过将前面看到的usingServer()命名构造函数转换为工厂构造函数(通过添加factory前缀),可以在EnterpriseAuthService中使用此行为。最简单的实现是:

class EnterpriseAuthService {
	factory EnterpriseAuthService.usingServer(String server, int port) {
		return new EnterpriseAuthService();
	}
}
// elsewhere in your code
var authSvc = new EnterpriseAuthService.usingServer("localhost", 8080);

在工厂构造函数的简单实现中,使用工厂什么也得不到。但是,当您在工厂构造函数中缓存正在创建的对象时,它会变得更加强大。一种方法是使用类静态属性,您将在几页中看到。但目前,假设EnterpriseAuthService对象列表存在于应用程序的其他位置。下面的列表显示了如何获取现有对象或返回新对象。接口定义保持不变。

class EnterpriseAuthService {
	factory EnterpriseAuthService.usingServer(String server, int port) {// 【工厂】构造函数
		var authService = getFromCache(server, port);
		if (authService == null) {
			authService = new EnterpriseAuthService ();
			// snip: set values on authService and connect
			addToCache(authService, server, port);
		}
		return authService;
	}
	// snip other methods and properties
}

工厂构造函数在与存储特定类实例的机制一起使用时功能强大,但您将这些实例存储在何处?事实证明,类的静态属性是一个很好的位置,我们将在下一步介绍。

工厂构造函数使用静态方法和属性:
Dart与Java和C#一样,提供静态方法和属性作为类定义的一部分。静态成员是跨类的所有实例共享的成员;它由static关键字表示,可以应用于方法和属性。因此,您可以创建一个静态映射(存储键/值对列表),该映射在应用程序中EnterpriseAuthService类的所有实例之间共享。您只需查看该映射,并查看是否存在基于服务器和端口值的现有匹配密钥。如果存在,您可以在工厂构造函数中检索EnterpriseAuthService的现有实例。下图展示了这是如何工作的。

访问静态方法或属性时,只能通过类名而不是变量名引用它们:静态成员在类的所有实例中共享,因此不允许通过特定实例访问它们。下面清单显示了静态缓存属性和第一次访问时初始化它的getter。getFromCache()和addToCache()方法也是静态的(这意味着它们不能通过类的特定实例访问),并使用缓存向缓存中添加实例和从缓存中检索实例。

class EnterpriseAuthService {
	//... snip other methods and properties
	static Map _cache;
	static Map get cache {
		if (_cache == null) {
			_cache = new Map();
		}
		return _cache;
	}
	static EnterpriseAuthService getFromCache(String server, int port) {// get instance from cache
		var key = "$server:$port";
		return cache[key];
	}
	static addToCache(EnterpriseAuthService authService, String server, int port) {// add instance to cache
		var key = "$server:$port";
		cache[key] = authService;
	}
	factory EnterpriseAuthService.usingServer(String server, int port) {
		var authService = getFromCache(server,port);
		if (authService == null) {
			authService = new EnterpriseAuthService();
			// snip: set values on authService and connect
			addToCache(authService,server,port);
		}
		return authService;
	}
}

当您希望跨类的所有实例存储值时,静态属性和方法非常有用。Java和C#中静态值的一个问题是多个线程可能正在访问一个静态值,但由于Dart是单线程的,所以这是不可能的。它使所有操作线程安全。在第15章讨论隔离时,我们将了解如何在不使用线程的情况下实现并发。

记住
■ 抽象类可以使用工厂构造函数来实例化默认的实现类,这允许用户通过专门处理更高级别的接口来维护良好的应用程序设计。
■ 类可以有多个以类名为前缀的命名构造函数。
■ factory关键字允许您创建看起来可以返回新对象但却可以从其他地方获取对象实例的类。

3.使用final创建常量类、不可更改的变量

EnterpriseAuthService可能返回一个错误对象,表明验证用户时出现问题。如果有很多请求,而服务器没有响应,那么可能会创建并返回数百个相同的错误对象,这些对象都将使用客户端系统上的内存。但如果创建const对象,则会得到单个错误对象,每次都会重复使用。定义常量对象有两个部分:➀属性值必须是final;➁必须使用const关键字定义构造函数并创建实例。

当您知道用户不应更改任何属性时,常量类可能非常有用。这些值在编译时已知,例如定义状态代码或错误消息。
为了实现此功能,Dart对该类设置了另一个限制:该类上的所有属性都必须是final。

不能这样使用:final class Box { ... },final关键字不能修饰类,要想类是final的,遵循以上➀➁点。

final是另一个新关键字,表示变量不能更改实例;它用于替代var或与强类型结合使用:

final myObject = new Object();// 用final代替var
final Object anotherObject = new Object();// final+强类型

myObject = new Object();// error!
anotherObject = new Object();// error!

如果不为final变量提供值,就不可能定义它,因为编译器需要能够在编译时计算该值。但是类中的属性可以声明为final,而无需事先初始化值。相反,您可以在构造函数初始化块中初始化它们。

构造函数初始化块:
类构造函数允许在构造函数代码开始执行之前初始化final属性。构造函数初始化块出现在构造函数参数列表和构造函数主体之间;这是一个逗号分隔的命令列表,构造函数必须执行这些命令才能正确初始化类。
初始化块与构造函数体一起使用,构造函数体还可以提供一些非final属性的初始化。
初始化块和主构造函数体之间的一个关键区别是,只能在主构造函数体中使用this关键字。下图显示了构造函数初始化块。

初始化初始化块中的final属性后,不能更改其值。还必须确保在构造函数主体开始之前初始化所有final属性,否则Dart将报告错误。可以使用以下代码创建AuthError类的实例:

// 很好地使所有final属性初始化了。因为prefix属性已经初始化了,_message和_code属性也通过构造函数完成了初始化
AuthError error001 = new AuthError("Server not available", 1);

创建常量构造函数:
无法更改AuthError类上的任何属性,因为它们都标记为final。如果要用构造函数中传递的相同值实例化第二个变量,那么第二个实例实际上是相同的,只是它是在内存中创建的第二个物理对象。可以通过使用const关键字将构造函数更改为常量构造函数来防止这种情况,如下面清单所示。

class AuthError {
	final prefix = "Error: ";
	final String _message;
	final int _code;
	const AuthError(String message, int code) : _message = message, _code = code;
	String get errorMessage => "$prefix [$code] $message";
}

现在您已经有了const构造函数,您可以通过创建类的实例来使用它。Dart知道每个实例都是不变的,并允许您以快速有效的方式比较两个实例是否相等。当您使用const构造函数创建实例时,还可以使用const关键字来创建实例,而不是使用new关键字:

AuthError errorA = const AuthError("Server not responding", 1);
AuthError errorB = const AuthError("Server not responding", 1);
print(errorA === errorB);// true

常量构造函数中不允许使用字符串插值。字符串值必须是编译时常量,并且不能在运行时动态创建。

仍然允许使用new关键字而不是const关键字。但Dart会将两个不同的实例视为不相等,因此应避免这种情况。

做得好!这是使用new和const关键字创建类和接口的四种方法。好的库设计需要技巧和实践,但Dart在其构造函数语法中提供了所需的灵活性。

记住
■ final关键字声明变量和属性在初始化后不能更改。
■ 初始化final属性需要构造函数初始化块。
■ const关键字允许您定义一个常量构造函数并创建一个不能更改的类的实例。

总结

在本章中,我们首先演示了如何使用接口强制类可以使用implements关键字实现的契约。当您针对接口而不是类编写代码时,可以很容易地在运行时交换实现,这对于使用真实类的模拟版本进行单元测试尤其有用。Dart支持这一概念,它允许您在使用底层默认类的同时创建接口实例,并在语言中构建所有类也都是接口的能力,因此即使没有显式定义接口,您仍然可以根据接口编写代码。

要使用类和接口,Dart允许您使用new和const关键字以及以下类型的构造函数创建实例:
■ 用于为不同用途提供构造函数的命名构造函数
■ 工厂构造函数,可用于从缓存、静态映射或其他机制中查找类的现有实例
■ 用于创建单例的常量构造函数,不变的常量类

在下一章中,我们将研究类和接口继承。Dart是一种面向对象的语言,提供了类似于Java和C#的继承机制。它没有JavaScript的原型继承。通过使用继承,您可以重用其他人编写和测试的代码,添加用例所需的额外功能。

以上是关于★Dart-6-构造类和接口的主要内容,如果未能解决你的问题,请参考以下文章

java 抽象类和接口

抽象类和接口的对比

抽象类和接口的区别

抽象类和接口的区别

kotlin学习总结——类和对象继承接口和抽象类

抽象类和接口的区别