Value Types
The built_value package is used to define its own value types. The term has
Exact meaning
, but we use it informally to mean a type of equality based solely on value.
For example, numbers: my 3 equals your 3.
Not only that: my 3 will
forever
Is equal to your 3;
It cannot be changed to 4, NULL, or a completely different type.
Value types are naturally immutable.
This makes them easy to interact with and reason about.
forever
Is equal to your 3;
It cannot be changed to 4, NULL, or a completely different type.
Value types are naturally immutable.
This makes them easy to interact with and reason about.
That sounds very abstract. value types
What are the advantages?
Well, it turns out: a lot.
A lot of.
It can be said — and I often argue this point — that any class used to simulate the real world should be a value type. Consider:
What are the advantages?
Well, it turns out: a lot.
A lot of.
It can be said — and I often argue this point — that any class used to simulate the real world should be a value type. Consider:
var user1 = new User(name: "John Smith");
var user2 = new User(name: "John Smith");
print(user1 == user2);Copy the code
What should I print?
It is crucial that both instances refer to someone in the real world.
Because they have the same value, they have to point to
Same person.
Therefore, they must be treated as equal.
It is crucial that both instances refer to someone in the real world.
Because they have the same value, they have to point to
Same person.
Therefore, they must be treated as equal.
What about immutability?
Consideration:
Consideration:
user1.nickname = 'Joe';Copy the code
Update the User
nickname
What does it mean?
It can mean many changes;
Maybe the welcome text on my web page uses a nickname and should be updated.
I probably have some storage somewhere, so I need to update as well.
I now have two main questions:
nickname
What does it mean?
It can mean many changes;
Maybe the welcome text on my web page uses a nickname and should be updated.
I probably have some storage somewhere, so I need to update as well.
I now have two main questions:
-
I don’t know who referenced “user1”.
The values below them just changed;
Depending on how they are used, this can have a lot of unpredictable effects. -
Now, anyone with a “user2” or similar name is holding an outdated value.
Immutability does not solve the second problem, but it does eliminate the first.
This means that there are no unpredictable updates, only clear updates:
var updatedUser = new User(name: "John Smith", nickname: "Joe"); saveToDatabase(updatedUser); // The database will notify the front end.Copy the code
Crucially, this means that change is
Local, until explicitly published
.
This results in simple code that is easy to reason about, and makes it correct and fast.
Local, until explicitly published
.
This results in simple code that is easy to reason about, and makes it correct and fast.
Some problems with Value Types
So the obvious question is: if value types are so useful, why don’t we see them everywhere?
Unfortunately, they are laborious to implement.
In Dart and most other object-oriented languages, a large number of
Boilerplate code
.
In my talk at the Dart Developer Summit, HOW about I present a simple two-lesson
Need so many samples that they fill the entire slide (video)
.
Unfortunately, they are laborious to implement.
In Dart and most other object-oriented languages, a large number of
Boilerplate code
.
In my talk at the Dart Developer Summit, HOW about I present a simple two-lesson
Need so many samples that they fill the entire slide (video)
.
The introduction of built_value
We need a language feature (well worth talking about, but unlikely to come soon) or some form of one
metaprogramming
.
We found that Dart already has a very good metaprogramming approach:
source_gen
.
The goal is clear: it is so easy to define and use value types that we can use them wherever they make sense.
metaprogramming
.
We found that Dart already has a very good metaprogramming approach:
source_gen
.
The goal is clear: it is so easy to define and use value types that we can use them wherever they make sense.
First, we need to look at how to solve this problem using source_gen.
The source_gen tool creates the generated source code in a new file next to the source code you maintain manually, so we need to make room for the generated implementation.
This means an abstract class:
The source_gen tool creates the generated source code in a new file next to the source code you maintain manually, so we need to make room for the generated implementation.
This means an abstract class:
abstract class User {
String get name;
@nullable
String get nickname;
}Copy the code
That’s enough information to generate an implementation class.
By convention, generated code begins with “_ $” to mark it as private and generated.
Therefore, the generated implementation will be called “_ $User”.
To allow it to extend “User”, there will be a special constructor called “_” :
By convention, generated code begins with “_ $” to mark it as private and generated.
Therefore, the generated implementation will be called “_ $User”.
To allow it to extend “User”, there will be a special constructor called “_” :
=== 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._();
}Copy the code
We need to use the Dart “part” statement to extract the generated code:
=== 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.
}Copy the code
We’re somewhere!
We have a way to generate code and insert it into the code we write by hand.
Now back to the fun part: what you actually have to write by hand and what built_value should be generated.
We have a way to generate code and insert it into the code we write by hand.
Now back to the fun part: what you actually have to write by hand and what built_value should be generated.
We lack a way to actually specify a value for a field.
We can consider using named optional arguments:
We can consider using named optional arguments:
factory User({String name, String nickname}) = _$User;Copy the code
But this has two drawbacks: it forces you to repeat all the field names in the constructor, and provides only one way to set all the fields at once.
What if you want to build values over time?
Luckily,
Builder pattern
Saved.
We’ve already seen
What if you want to build values over time?
Luckily,
Builder pattern
Saved.
We’ve already seen
it
Dart for the collection
How well it works
– Thanks to the cascade operator.
Suppose we have a builder type that we can use for the builder – by requesting a function that takes the builder as an argument
Dart for the collection
How well it works
– Thanks to the cascade operator.
Suppose we have a builder type that we can use for the builder – by requesting a function that takes the builder as an argument
abstract class User {
String get name;
@nullable
String get nickname;
User._();
factory User([updates(UserBuilder b)]) = _$User;
}Copy the code
This is a bit surprising, but it yields a very simple instantiation syntax:
var user1 = new User((b) => b .. name ='John Smith' ..nickname = 'Joe');Copy the code
How do I create a new value based on an old value?
The traditional builder pattern provides a “toBuilder” method to transform into a builder.
Then, you apply your updates and call “Build.”
However, for most use cases, the better pattern is to adopt a “rebuild” approach.
Like constructors, it takes a function that takes a generator and provides simple inline updates
The traditional builder pattern provides a “toBuilder” method to transform into a builder.
Then, you apply your updates and call “Build.”
However, for most use cases, the better pattern is to adopt a “rebuild” approach.
Like constructors, it takes a function that takes a generator and provides simple inline updates
Abstract class Built<V, B> {// Create a new instance: this instance is Built with [updates] V rebuild(updates(B Builder)); // Convert to builder. B toBuilder(); }Copy the code
You don’t need to write an implementation for this; build_value generates it for you.
Therefore, you simply declare yourself a “built implementation” :
Therefore, you simply declare yourself a “built implementation” :
----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;
}Copy the code
That’s it!
Value types are defined, implementations are generated and are easy to use.
Of course, the generated implementation is more than just fields: it also provides “operator ==”, “hashCode”, “toString” and null checks for required fields.
However, I skipped one major detail: I said “Suppose we have a builder type.”
Of course, we’re generating code, so the answer is simple: we’ll generate it for you.
In User.g. Art, create the UserBuilder referenced from the User.
Value types are defined, implementations are generated and are easy to use.
Of course, the generated implementation is more than just fields: it also provides “operator ==”, “hashCode”, “toString” and null checks for required fields.
However, I skipped one major detail: I said “Suppose we have a builder type.”
Of course, we’re generating code, so the answer is simple: we’ll generate it for you.
In User.g. Art, create the UserBuilder referenced from the User.
…
unless
You want to write some code in the builder,
Otherwise,
That’s a reasonable thing to do.
If this is what you want, follow the same pattern for the builder.
It is declared abstract, with a private constructor and a factory that delegates to the generated implementation:
You want to write some code in the builder,
Otherwise,
That’s a reasonable thing to do.
If this is what you want, follow the same pattern for the builder.
It is declared abstract, with a private constructor and a factory that delegates to the generated implementation:
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;
}Copy the code
The “@virtual” annotation comes from “Package: Meta” and is required for the generated implementation to override the field.
Now that utility methods have been added to the builder, you can use them inline as if they were assigned to fields:
Now that utility methods have been added to the builder, you can use them inline as if they were assigned to fields:
Var user = new user ((b) => b.. ParseUser ('John Smith, "Joe"));Copy the code
Use cases for custom builders are relatively few, but they can be very powerful.
For example, you might want your builder to implement a generic interface for setting shared fields so they can be used interchangeably.
For example, you might want your builder to implement a generic interface for setting shared fields so they can be used interchangeably.
Nested Builders
You haven’t seen one of build_value’s main features yet: nested generators.
When the build_value field contains either a build_collection or another build_value, it is available in the builder by default
Nested builder
.
When the build_value field contains either a build_collection or another build_value, it is available in the builder by default
Nested builder
.
This means that
As opposed to the whole variable structure,
You can
More easily
Update deeply nested fields
:
As opposed to the whole variable structure,
You can
More easily
Update deeply nested fields
:
var structuredData = new Account((b) => b .. user.name ='John Smith'
..user.nickname = 'Joe'
..credentials.email = '[email protected]'. 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');
Copy the code
Why is it “easier” to change than structure?
First, the “update” method provided by all builders means that you can always enter new scopes, “restart” cascade operators, and make the concise and inline updates you need:
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'));Copy the code
Second, nested builders are automatically created as needed.
For example, in the base code for built_value, we define a type named Node:
For example, in the base code for built_value, we define a type named 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;
}Copy the code
By automatically creating the builder, we can create any tree structure we want inline:
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! ');Copy the code
Did I mention benchmarks?
When updating, built_value copies only the parts of the structure that need to be updated and reuses the rest.
so
Very fast –
High memory efficiency
.
When updating, built_value copies only the parts of the structure that need to be updated and reuses the rest.
so
Very fast –
High memory efficiency
.
However, you don’t need to build trees. With built_value, you can use a fully typed immutable object model… They are as fast and powerful as efficient immutable trees. You can mix and match typed data, custom structures (such as the “Node” example), and collections in 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'));Copy the code
While I think most data should be of value type,
these
That’s the type of value I’m talking about!
these
That’s the type of value I’m talking about!
Original text: medium.com/dartlang/da…