We have the big picture of Domain-Driven Design already in our minds so now it's time to get coding. You might think that since we are building a Firebase app, we will need to worry about using the Firestore
and FirebaseAuth
classes right from the start. That's not true at all with DDD. Let's start in the most important layer of them all - the domain layer. Namely, we are going to tackle authentication.
You are about to witness high amounts of abstraction throughout this tutorial series. Like with anything, pick and choose what you need and dismiss the rest.
For example, you may want to separate your app into layers and use Failure
s in place of Exception
s but instead of utilizing validated ValueObject
s (we'll get to them in this part), you might want to validate data the usual way - only in the presentation layer as it's being inputted by the user and then pass around String
s and int
s as you're used to.
This tutorial series will follow Domain-Driven Design religiously to show you what's possible. It's all up to you to choose what you deem to be an overkill. No matter how you decide to build apps in the end, knowing Domain-Driven Design will make you a better programmer who can look at problems from a new angle.
Email & password
How can we sign in using email and password? The usual way would be to have a sign in form that would validate the inputted String
s. You know, email addresses must have the '@' sign and passwords must be at least six characters long. We would then pass these String
s to the authentication service, in our case, Firebase Auth.
Sure, this is perfectly doable but we have to realize one important fact! Let's imagine we have a function which accepts two parameters.
unsuspecting_function.dart
Future<void> signIn({
@required String email,
@required String password,
}) async {
// Sign in the user
}
Is it reasonable to call this function with the following arguments?
function_call.dart
signIn(email: 'pazzwrd', password: '[email protected]');
Of course, it isn't. But what stops us from passing an email address to a parameter expecting a password? They're all String
s, after all.
Type-safety evolution
The first thing we can do is to create simple classes for EmailAddress
and Password
. Let's focus on the former, so that we don't have to deal with two classes for now. By the way, we are going to be mostly inside the domain/auth folder. Check out the GitHub repository whenever you're unsure.
auth/email_address.dart
import 'package:meta/meta.dart';
@immutable
class EmailAddress {
final String value;
const EmailAddress(this.value) : assert(value != null);
@override
String toString() => 'EmailAddress($value)';
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is EmailAddress && o.value == value;
}
@override
int get hashCode => value.hashCode;
}
This is much more expressive than a plain String
plus we get an immediate non-null check. We also override the equality operator to perform value equality and also the toString()
method to have a reasonable output.
A class like this is surely not ideal though. Yes, as soon as we have an EmailAddress
instance, we cannot mistakenly pass it to a function expecting a Password
. They're two different types. What we can do now though is the following.
instantiation.dart
void f() {
const email = EmailAddress('pazzwrd');
// Happily use the email address
}
As you can see, we've escaped one problem only to get another one. Instances of EmailAddress
happily accept any String
into its constructor and then pretend like nothing happened if it doesn't fulfill the "contract" of what the EmailAddress
represents. That's why we're going to create validated value objects.
Validating at instantiation
You are probably used to validating String
s in a TextFormField
. (If not and you're still here, this series is not for you. Please, come back after you learn the basics.) Unless the TextFormField
holds a valid value, you're not going to be able to save the Form
and proceed with the invalid value.
We will take this principle and take it to a whole another level. You see, not all validation is equal. We're about to perform the safest validation of them all - we're going to make illegal states unrepresentable. In other words, we will make it impossible for a class like EmailAddress
to hold an invalid value not just while it's in the TextFormField
but throughout its whole lifespan.
The most straightforward way of validating at instantiation is to create a factory
constructor which will perform the validation logic by throwing Exception
s if something doesn't play right and then finally instantiate an EmailAddress
by calling a private constructor.
auth/email_address.dart
@immutable
class EmailAddress {
final String value;
factory EmailAddress(String input) {
assert(input != null);
return EmailAddress._(
validateEmailAddress(input),
);
}
const EmailAddress._(this.value);
// toString, equals, hashCode...
}
String validateEmailAddress(String input) {
// Maybe not the most robust way of email validation but it's good enough
const emailRegex =
r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+""";
if (RegExp(emailRegex).hasMatch(input)) {
return input;
} else {
throw InvalidEmailException(failedValue: input);
}
}
class InvalidEmailException implements Exception {
final String failedValue;
InvalidEmailException({@required this.failedValue});
}
We're definitely getting somewhere. Passing an invalid email string to the EmailAddress
public factory will result in an InvalidEmailException
being thrown. So yes, we do make illegal states unrepresentable.
To be honest though, if throwing exceptions were the only way we could prevent invalid values from being held inside validated value objects, you wouldn't be even reading this post because this series would never have happened. Why? Let's see what we have to do to instantiate just one EmailAddress
instantiation.dart
void f() {
try {
final email = EmailAddress('pazzwrd');
} on InvalidEmailException catch (e) {
// Do some exception handling here
}
// If you have multiple validators, remember to catch their exceptions too
}
Yeah, this is not the way to go. Creating this monstrosity everywhere you instantiate a validated value object would quickly become a painful and unmaintainable experience.
Either a failure or a value
Our current troubles stem from the fact that the EmailAddress
class holds only a single field of type String
. What if, instead of throwing an InvalidEmailException
, we would instead somehow store it inside the class? And because we don't want to use Exception
s in an unconventional way, we'd create a plain old InvalidEmailFailure
class.
This will allow us to not litter our codebase with try
and catch
statements at the time of instantiation. We will still need to handle the invalid value at the time of using the EmailAddress
. We have to handle it somewhere, right?
However, we don't want to create a second class field called, for example, failure
. I mean, would you remember to write the following everywhere you used an EmailAddress
? And more importantly, would you even bother writing this code if it wasn't enforced on you?
usage_of_email_address.dart
void insideTheUI() {
EmailAddress emailAddress;
// ...
if (emailAddress.failure == null) {
// Display the valid email address
} else {
// Show an error Snackbar
}
}
The code above is frankly horrible. It relies on nulls to represent missing values - this is a recipe for a disaster. What if we joined the value
and failure
fields into one by using a union type? And not just any sort of a union - we're going to use Either
.
Either
is a union type from the dartz package specifically suited to handle what we call "failures". It is a union of two values, commonly called Left
and Right
. The left side holds Failure
s and the right side holds the correct values, for example, String
s.Additionally, we'll want to introduce a union type even for Failure
s. Although we currently have only one "ValueFailure
" representing an invalid email address, we are going to have a bunch more of them throughout this series. Even here, unions will help us not to forget about any possible "case" of a ValueFailure
.
So, we're going to use dartz for Either
but what about the regular unions? There are multiple options to choose from until Dart introduces algebraic data types into the language itself. The best option is to use the freezed package. Let's add them to pubspec.yaml and since freezed uses code generation, we'll also add a bunch of other dependencies.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
dartz: ^0.9.0-dev.6
freezed_annotation: ^0.7.1
dev_dependencies:
build_runner:
freezed: ^0.9.2
ValueFailure union
Before jumping back into the EmailAddress
class, let's first ditch the InvalidEmailException
in favor of the aforementioned union. We'll group all failures from validated value objects into one such union - ValueFailure
. Since this is something common across features, we'll create the failures.dart file inside the domain/core folder. While we're at it, let's also create a "short password" failure.
core/failures.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'failures.freezed.dart';
@freezed
abstract class ValueFailure<T> with _$ValueFailure<T> {
const factory ValueFailure.invalidEmail({
@required T failedValue,
}) = InvalidEmail<T>;
const factory ValueFailure.shortPassword({
@required T failedValue,
}) = ShortPassword<T>;
}
String
s later on in this series.The value which can be held inside an EmailAddress
will no longer be just a String
. Instead, it will be Either<ValueFailure<String>, String>
. The same will also be the return type of the validateEmailAddress
function. Then, instead of throwing an exception, we're going to return
the left
side of Either
.
auth/email_address.dart
@immutable
class EmailAddress {
final Either<ValueFailure<String>, String> value;
factory EmailAddress(String input) {
assert(input != null);
return EmailAddress._(
validateEmailAddress(input),
);
}
const EmailAddress._(this.value);
// toString, equals, hashCode...
}
Either<ValueFailure<String>, String> validateEmailAddress(String input) {
const emailRegex =
r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+""";
if (RegExp(emailRegex).hasMatch(input)) {
return right(input);
} else {
return left(ValueFailure.invalidEmail(failedValue: input));
}
}
Displaying the value held inside an EmailAddress
object now doesn't leave any room for doubts. We simply have to handle the possible ValueFailure
whether we feel like it or not.
some_widget.dart
void showingTheEmailAddressOrFailure(EmailAddress emailAddress) {
// Longer to write but we can get the failure instance
final emailText1 = emailAddress.value.fold(
(left) => 'Failure happened, more precisely: $left',
(right) => right,
);
// Shorter to write but we cannot get the failure instance
final emailText2 =
emailAddress.value.getOrElse(() => 'Some failure happened');
}
Password
EmailAddress
is implemented and it contains a lot of boilerplate code for toString
, ==
, and hashCode
overrides. We surely don't want to duplicate all of this into a Password
class. This is a perfect opportunity to create a super class.
Abstract ValueObject
This abstract class will extend specific value objects across multiple features. We're going to create it under domain/core. All it does is just extracting boilerplate into one place. Of course, we heavily rely on generics to allow the value
to be of any type.
core/value_objects.dart
@immutable
abstract class ValueObject<T> {
const ValueObject();
Either<ValueFailure<T>, T> get value;
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is ValueObject<T> && o.value == value;
}
@override
int get hashCode => value.hashCode;
@override
String toString() => 'Value($value)';
}
We can now extend this class from EmailAddress
. Not so bad now, huh?
auth/email_address.dart
class EmailAddress extends ValueObject<String> {
@override
final Either<ValueFailure<String>, String> value;
factory EmailAddress(String input) {
assert(input != null);
return EmailAddress._(
validateEmailAddress(input),
);
}
const EmailAddress._(this.value);
}
Creating and organizing
Let's first bring order to our files before we go ahead to create yet another class and validation function. Feature-specific value objects will live inside their domain feature folders. In case of EmailAddress
and Password
, that's domain/auth.
As for the validation functions, I like to put all of them into a single file under domain/core.
The validation logic for a Password
is extremely simple in our case. Just check the length of the input.
core/value_validators.dart
Either<ValueFailure<String>, String> validateEmailAddress(String input) {
// Already implemented
}
Either<ValueFailure<String>, String> validatePassword(String input) {
// You can also add some advanced password checks (uppercase/lowercase, at least 1 number, ...)
if (input.length >= 6) {
return right(input);
} else {
return left(ValueFailure.shortPassword(failedValue: input));
}
}
The Password
class will be almost identical to EmailAddress
- except for the validation.
auth/value_objects.dart
class EmailAddress extends ValueObject<String> {
// Already implemented
}
class Password extends ValueObject<String> {
@override
final Either<ValueFailure<String>, String> value;
factory Password(String input) {
assert(input != null);
return Password._(
validatePassword(input),
);
}
const Password._(this.value);
}
Fundamentals are important
It took quite a long time to validate just two value objects, didn't it? Not quite because I actually took you through the whole process of coming up with the best solution of "making illegal states unrepresentable" in Dart. Once you have the ValueObject
super class in place and you know what you're doing, creating something like a validated TodoName
won't take more than a couple of minutes.
The best thing about having these specific value objects in places that would otherwise be just plain String
s is that you cannot possibly mess up, no matter how hard you try. We're using the Dart type system to guide us.
In the next part, we're going to write code in the application layer responsible for gluing together the UI with the authentication backend. Why didn't I say Firebase Auth? As you can imagine, we're going to use abstractions!
Hi Matt, I don’t like much putting all the feature-specific failures all inside a core-file since it will become a mess very quickly if the project is not too simple.
How do you see to put them into specific files into the related feature directories?
For example an “InvalidEmailFailure” and a “ShortPasswordFailure” classes in a “auth/failures.dart” file. Or maybe, to follow your structure, putting them into an “AuthFailure” as factory constructors as you did.
Maybe the email one could remain in the core because an email is an email everywhere, but the password one looks strongly auth related to me. What if we have to handle two different kinds of passwords in our apps?
I understand that your case is very simple and you won’t have these kinds of problems, I pointed out just to reason about it.
Hey Fabrizio!
I totally understand your concerns – large apps need more organization.
What I do is that I create feature-specific unions (AuthValueFailure) that contain feature-specific “case factories” (ShortPassword, InvalidEmail). This is all fine, but what if you also need to deal with some sort of a possible SettingsValueFailure being returned?
One way is to implement a completely blank abstract class called ValueFailure by both AuthValueFailure and SettingsValueFailure. But that’s not great because you lose out on the code completion you get with unions ?
That’s why instead of implementing a common interface, I create a ValueFailure union which has Settings and Auth “case factories”. This way, you create a union of unions. It’s quite nice to work with.
Could you show the example of union of union?
What made you ditch Equatable for value equality?
Very nice explanation. This thus structure the code and brings in a better understanding of how to correctly validate objects. Thanks
Let’s say i want to test the instance of EmailAddress, is this test correct ?
test(
‘EmailAddress cannot accept null argument’,
() {
// assert
expect(EmailAddress(null), AssertionError);
},
);
When i’m running this test i’m getting this error :-
dart:core _AssertionError._throwNew
package:flutterbackend/domain/auth/value_objects.dart 11:12 new EmailAddress
test/domain/auth/value_objects_test.dart 20:14 main.
‘package:flutterbackend/domain/auth/value_objects.dart’: Failed assertion: line 11 pos 12: ‘input != null’: is not true.
Doesn’t freezed take care of things like operator, hashcode, and toString? Why did you have to copy all that into your ValueObject class?
which extension for ptf.. snippet???
Hi Matt and thank you for this fabolous series. How would you implement a ValueObject?, for example with an Address type that beholds String street, String neighborhood, City city. Would you create a separate AddressData class and ValueObject? or which would be your approach?
Hi , I have this same question ? do you know answer?
I’ve the same question. Is there any answers ?
Hi,
How would I model something like a price? The Price has currency, unit(/hour, /day) and the actual amount ofcourse. The price here should be a value object or an Entity? As much as I have read about value objects and entities, entities have ids. So my guess is that price should be a value object but then how would currency and unit work with amout?
Wouldn’t it be better to keep validator functions inside classes they logically belong to as static methods?
Yes that would be great, but you have to make validation function “static” becouse only static members can be called from factory constructors. Nice. Validation function takes one parameter of generic type T (it’s inside class ValueObject), but static functions can’t reference type parameters (T) of the class.
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.