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.
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.
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 validatedvalue objects.
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.!#$%&'*+-/=?^_`{|}~][email protected][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.
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 InvalidEmail**Failure**
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
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.
To learn about all the other things freezed can do, check out its official documentation
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>;
}
We made the class generic because we will also validate values other than
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.!#$%&'*+-/=?^_`{|}~][email protected][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');
}
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.
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);
}
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);
}
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!
#flutter #firebase #security #mobileapps