译:Dart的不可变对象模型的built_value

1,066 阅读5分钟

Value Types

built_value包用于定义自己的value types该术语具有

,但我们非正式地使用它来表示仅基于价值的平等类型。例如,数字:我的3等于您的3。

不仅如此:我的3将永远等于您的3;它不能更改为4,null或完全不同的类型。值类型自然是不可变的。这使得它们易于交互和推理。

这听起来非常抽象。value types有哪些优点?好吧,事实证明:很多。很多。可以说-我经常争论这一点-用于模拟现实世界的任何类都应该是值类型。考虑一下:

var user1 = new User(name: "John Smith");
var user2 = new User(name: "John Smith");

print(user1 == user2);

应该打印什么?至关重要的是,两个实例都应该指代现实世界中的某个人。因为它们的值相同,所以它们必须指向同一个人。因此,必须将它们视为相等。
不变性呢?考虑:

user1.nickname = 'Joe';

更新User 的 nickname意味着什么?它可能意味着许多变化;也许我网页上的欢迎文本使用昵称,并且应该对其进行更新。我可能在某处有一些存储,因此也需要更新。我现在有两个主要问题:

  • 我不知道谁引用了“ user1”。它们之下的值刚刚改变;根据他们的使用方式,这可能会产生许多不可预测的影响。
  • 现在,持有“ user2”或类似名称的任何人都持有一个过时的值。

不变性不能解决第二个问题,但是它确实消除了第一个问题。
这意味着没有不可预测的更新,只有明确的更新:

var updatedUser = new User(name: "John Smith", nickname: "Joe");
saveToDatabase(updatedUser); //数据库将通知前端。

至关重要的是,这意味着更改是本地的,直到明确发布为止这样就产生了易于推理的简单代码,并且使代码变得正确而又快速。

Value Types的一些问题

因此,一个显而易见的问题是:如果值类型是如此有用,为什么我们不到处都看到它们呢?不幸的是,它们实施起来非常费力。在Dart和大多数其他面向对象的语言中,需要大量的样板代码在Dart开发者峰会上的演讲中,我展示了一个简单的两场课程如何需要如此多的样板,以至于填满整个幻灯片(视频)

引入built_value

我们需要一种语言功能(这很值得讨论,但不太可能很快出现)或某种形式的元编程我们发现Dart已经有了一个非常好的元编程方法:source_gen目标很明确:定义和使用值类型非常容易,以至于我们可以在任何值类型有意义的地方使用它们。
首先,我们需要看看如何使用source_gen解决此问题。source_gen工具会在您手动维护的源代码旁边的新文件中创建生成的源代码,因此我们需要为生成的实现留出空间。这意味着一个抽象类:

abstract class User {
    String get name;
    @nullable
    String get nickname;
}

那有足够的信息来生成一个实现类。按照惯例,生成的代码以“ _ $”开头,以将其标记为私有并生成。因此,生成的实现将称为“ _ $ User”。为了允许它扩展“ User”,将有一个专用的构造函数,称为“ _”:

=== user.dart ===
abstract class User {  
    String get name;  
    @nullable  
    String get nickname;
    User._();  
    factory User() = UserImpl;
}

=== user.g.dart is generated by source_gen ===
class _$User extends User {  
    String name;  
    String nickname; 
    _$User() : super._();
}

我们需要使用Dart的“ part”语句来提取生成的代码:
=== user.dart ===
library user;
part 'user.g.dart';
abstract class User {
    String get name;  
    @nullable  
    String get nickname;  
    User._();  
    factory User() = _$User;
}

=== user.g.dart is generated by source_gen ===
part of user;
class _$User extends User {  
    String name;  
    String nickname; 
    _$User() : super._();  
    
    // Generated implementation goes here.
}

我们到了某个地方!我们有一种生成代码并将其插入我们手工编写的代码中的方法。现在回到有趣的部分:您实际必须手工编写的内容以及应该生成的built_value。
我们缺少一种为字段实际指定值的方法。我们可以考虑使用命名的可选参数:

factory User({String name, String nickname}) = _$User;

但这有两个缺点:它迫使您在构造函数中重复所有字段名称,并且仅提供一种一次性设置所有字段的方法。如果您想逐步建立value怎么办?幸运的是,构建者模式得以解救。我们已经看到
了它在Dart中对集合的工作效果如何-多亏了层叠运算符。假设我们有一个构建器类型,我们可以将其用于构建器-通过请求一个以构建器为参数的函数

abstract class User {  
    String get name;  
    @nullable  
    String get nickname;  
    User._();  
    factory User([updates(UserBuilder b)]) = _$User;
}

这有点令人惊讶,但是它产生了一个非常简单的实例化语法:
var user1 = new User((b) => b    ..name = 'John Smith'    ..nickname = 'Joe');

如何基于旧值创建新值?传统的构建器模式提供了一种“ toBuilder”方法来转换为构建器。然后,您应用您的更新并调用“构建”。但是,对于大多数用例而言,更好的模式是采用“重建”方法。像构造函数一样,它采用一个函数,该函数采用一个生成器,并提供简单的内联更新

abstract class Built<V, B> {  
    // 创建一个新实例:这个实例应用了[updates]  
    V rebuild(updates(B builder));  
//转换为构建器。    B toBuilder();}

您不需要为这些编写实现,build_value会为您生成它。因此,您只需声明自己“已构建实现”即可:
----library user;
import 'package:built_value/built_value.dart';
part 'user.g.dart';

abstract class User implements Built<User, UserBuilder> {  
    String get name;  
    @nullable  
    String get nickname;  
    User._();  
    factory User([updates(UserBuilder b)]) = _$User;
}

就是这样!定义了值类型,生成了实现并且易于使用。当然,生成的实现不仅是字段:它还提供“ operator ==”,“ hashCode”,“ toString”和对必填字段的空检查。不过,我跳过了一个主要细节:我说“假设我们有一个构建器类型”。当然,我们正在生成代码,因此答案很简单:我们将为您生成它。在“ user.g.dart”中创建从“用户”引用的“ UserBuilder”。
除非您想在构建器中编写一些代码,否则这是一件很合理的事情。如果这是您想要的,则对构建器遵循相同的模式。它被声明为抽象,带有一个私有构造函数和一个委托给所生成实现的工厂:

abstract class UserBuilder extends Builder<V, B> {  
    @virtual  
    String name;  @virtual  String nickname;  
    // Parses e.g. John "Joe" Smith into username+nickname.  
    void parseUser(String user) { 
   ...  
    }  
    UserBuilder._();      
    factory UserBuilder() => _$UserBuilder;
}

“ @virtual”注释来自“ package:meta”,并且是使生成的实现覆盖字段所必需的。现在,已将实用程序方法添加到构建器中,您可以内联使用它们,就像可以分配给字段一样:
var user = new User((b)=> b..parseUser('John“ Joe” Smith'));

定制构建器的用例相对很少,但是它们可能非常强大。例如,您可能希望您的构建器实现用于设置共享字段的通用接口,以便可以互换使用它们。

Nested Builders

您尚未见到build_value的一个主要功能:嵌套生成器。当build_value字段包含一build_collection或另一个build_value时,默认情况下,它在构建器中可用作嵌套builder
这意味着与整个结构可变的相比,您可以更轻松地更新深度嵌套的字段

var structuredData = new Account((b) => b    
    ..user.name = 'John Smith'    
    ..user.nickname = 'Joe'    
    ..credentials.email = 'john.smith@example.com'    
    ..credentials.phone.country = Country.us    
    ..credentials.phone.number = '555 01234 567');
var updatedStructuredData = structuredData.rebuild((b) => b    
    ..credentials.phone.country = Country.switzerland    
    ..credentials.phone.number = '555 01234 555');

为什么比结构易变更“容易”?

首先,所有构建器提供的“更新”方法意味着您可以随时输入新的作用域,“重新启动”级联运算符,并进行简明和内联的所需更新:

var updatedStructuredData = structuredData.rebuild((b) => b    
    ..user.update((b) => b        
        ..name = 'Johnathan Smith')    
    ..credentials.phone.update((b) => b        
        ..country = Country.switzerland        
        ..number = '555 01234 555'));
其次,根据需要自动创建嵌套的构建器。例如,在built_value的基准代码中,我们定义了一个名为Node的类型:
abstract class Node implements Built<Node, NodeBuilder> {  
    @nullable  String get label;  
    @nullable  Node get left;   
    @nullable  Node get right;  
    Node._();  
    factory Node([updates(NodeBuilder b)]) = _$Node;
}

通过自动创建构建器,我们可以创建想要内联的任何树结构:
var node = new Node((b) => b  
    ..left.left.left.right.left.right.label = 'I’m a leaf!'  
    ..left.left.right.right.label = 'I’m also a leaf!');
var updatedNode = node.rebuild((b) => b  
    ..left.left.right.right.label = 'I’m not a leaf any more!'  
    ..left.left.right.right.right.label = 'I’m the leaf now!');

我提到基准了吗?更新时,built_value仅复制需要更新的结构部分,重用其余部分。因此速度很快-内存效率高

但是,您不需要构建树木。使用built_value,您可以使用完全类型化的不可变对象模型……它们与高效的不可变树一样快速,强大。您可以混合并匹配类型化的数据,自定义结构(例如“ Node”示例)以及built_collection中的集合:

var structuredData = new Account((b) => b    
    ..user.update((b) => b        
        ..name = 'John Smith')
    ..credentials.phone.update((b) => b    
        ..country = Country.us        
        ..number = '555 01234 567')    
        ..node.left.left.left.account.update((b) => b        
    ..user.name = 'John Smith II'        
    ..user.nickname = 'Is lost in a tree')    
        ..node.left.right.right.account.update((b) => b        
    ..user.name = 'John Smith III'));

当我认为大多数数据应该是值类型时,这些就是我在谈论的值类型!

原文:medium.com/dartlang/da…