This serves as a simple explanation of what should be done versus what we should avoid doing to follow best practices. To also follow the humble object approach.
DO β :
Domain Layer
class UserModel {
final String id;
final int age;
final String name;
factory UserModel.fromJson(final Map<String, Object?> json) =>
UserModelFromJson(json);
factory UserModel.toJson(final Map<String, Object?> json) =>
_$UserModelFromJson(json);
UserModel(this.id, this.age, this.name);
}
Data Layer
class APIPath {
APIPath();
static String company(final String uid, final String companyId) =>
'users/$uid/company/$companyId';
static String user(final String uid, final String companyId) => 'users/$uid';
}
Here the repository level implementation does not know anything about the domain layer. We have single line baring as little logic as possible. Each of the implementation is a single line of code. It remains testable, readable and scalable, making the code less bug prone.
This file should not bear specific functions but rather. The idea of having generic method is that we do not have to test these but rather only test the implementation level.If there are break in data or issue, this layer will be out of cause.
typedef QueryBuilder<T> = T Function(Map<String, dynamic> data);
class ApiService {
ApiService._();
static final instance = ApiService._();
Future<T> setData({
required final String path,
required final Map<String, dynamic> data,
}) async {
final reference = Http.instance.doc(path);
await reference.set(data);
}
Future<T> updateData({
required final String path,
required final Map<String, dynamic> data,
}) async {
final reference = Http.instance.doc(path);
await reference.update(data);
}
Future<T> deleteData(
{required final String path, final String? image}) async {
final reference = ApiService.instance.doc(path);
if (image != null) {
final storageReference = Http.instance.refFromURL(image);
await storageReference.delete();
await reference.delete();
}
await reference.delete();
}
Stream<List<T>> collectionStream<T>({
required final String path,
required final QueryBuilder<T> builder,
final Function(Query query)? queryBuilder,
final int Function(T lhs, T rhs)? sort,
}) {
var query = ApiService.instance.collection(path);
if (queryBuilder != null) {
query = queryBuilder(query) as CollectionReference<Map<String, dynamic>>;
}
final snapshots = query.snapshots();
return snapshots.map((final snapshot) {
final result = snapshot.docs
.map((final snapshot) => builder(snapshot.data()))
.where((final value) => value != null)
.toList();
if (sort != null) {
result.sort(sort);
}
return result;
});
}
Stream<T> documentStream<T>({
required final String path,
required final T Function(Map<String, dynamic> data, String documentID)
builder,
}) {
final reference = Http.instance.doc(path);
final snapshots = reference.snapshots();
return snapshots
.map((final snapshot) => builder(snapshot.data()!, snapshot.id));
}
}
DO NOT DO: β
Domain layer
class UserModel {
final String id;
final int age;
final String name;
//β 1- Domain logic mixed with data parsing logic
factory UserModel.fromJson(final Map<String, Object?> json) {
// Domain logic (business rules)
if (json['age'] == null || json['name'] == null) {
throw Exception("Invalid user data");
}
// β2- Data parsing logic mixed with domain logic
return UserModel(
json['id'] as String,
json['age'] as int,
json['name'] as String,
);
}
UserModel(this.id, this.age, this.name);
}
Data Layer
In the following case a common mistake is to duplicate code, mix layers and have unhandled edge case.
class ApiService {
ApiService._();
static final instance = ApiService._();
// β - 1 Seems great but we confuse layers and throw exception
Future<UserModel> getUser({required final String userId}) async {
final response = await http.get('https://api.example.com/user/$userId');
if (response.statusCode == 200) {
// Data parsing logic mixed with domain logic
final userData = json.decode(response.body);
return UserModel.fromJson(userData); // <----Dependency on UserModel domain class
} else {
throw Exception('Failed to load user');
}
}
// β - 2 We mix layers and repeat the same code with also handling logic.
Future<UserModel?> deleteUser({required final String userId}) async {
final response = await http.get('https://api.example.com/user/$userId');
if (response.statusCode == 200) {
// Data parsing logic mixed with domain logic
final userData = json.decode(response.body);
return UserModel.fromJson(userData); // <----Dependency on UserModel domain class
} else {
throw Exception('Failed to load user');
}
}
}
// β - 2 Using notify listener in wrong layer and method holds to much business logic. In
//addition the value returned could be null, this will cause us to cover this edge case. By
//trying to cover it we might cause more issues and testing that will be a nightmanre.
Future<UserModel?> updateUser({required final String userId}) async {
final response = await http.get('https://api.example.com/user/$userId');
if (response.statusCode == 200) {
// Data parsing logic mixed with domain logic
final userData = json.decode(response.body);
return UserModel.fromJson(userData); //
notifyListeners(); <----Dependency on UserModel domain class
} else if(await authService.token != null) { //<- referencing a value of different layer
//... do some garbage here.
throw Exception('Failed to load user');
}
}
This serves as a simple explanation of what should be done versus what we should avoid doing to follow best practices. To also follow the humble object approach.
DO β :
Domain Layer
Data Layer
Description:
Here the repository level implementation does not know anything about the domain layer. We have single line baring as little logic as possible. Each of the implementation is a single line of code. It remains testable, readable and scalable, making the code less bug prone.
Service:
This file should not bear specific functions but rather. The idea of having generic method is that we do not have to test these but rather only test the implementation level.If there are break in data or issue, this layer will be out of cause.
DO NOT DO: β
Domain layer
Data Layer
In the following case a common mistake is to duplicate code, mix layers and have unhandled edge case.