Skip to content

Commit dbcca22

Browse files
committed
refactor: apply consistently the same interface pattern for view models and use case output data
1 parent b096105 commit dbcca22

File tree

13 files changed

+58
-37
lines changed

13 files changed

+58
-37
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Having all of your infrastructure code in a single source code tree means that i
103103
- [Domain-Driven Design tactical design and Clean Architecture (video)](https://www.youtube.com/watch?v=hf_XBb5cSoA).
104104
- [Pull-based vs push-based approach managing use case output](https://softwareengineering.stackexchange.com/a/420360) or [why presenters (push-based) should be preferred to returned use case values (pull-based)](https://lukemorton.tech/articles/nuances-in-clean-architecture). TLDR; Communication from Controller to Presenter is meant to go through the application layer, making the Controller do part of the Presenters job is likely a domain/application leak.
105105
- [Single responsibility principle and mixing presenter/controller](https://stackoverflow.com/questions/64415618/clean-architecture-controller-and-presenter-should-always-be-separate-classes-o). TLDR; Not mixing them allows better flexibility later if some other ways of displaying data (i.e. presenters) should be supported (Web, CLI, JSON, ...).
106+
- [The "Real" Repository Pattern in Android](https://proandroiddev.com/the-real-repository-pattern-in-android-efba8662b754).
106107

107108
<br>
108109

modules/catalog/src/GetProducts/adapters/GetProductsPresenter.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,30 @@ export const createGetProductsPresenter: PresenterFactory<
1515
> = (onViewModelChange) => {
1616
return {
1717
display(input) {
18-
const viewModel: GetProductsViewModel = input.map((item) => {
19-
if (item instanceof Error) {
20-
return { payload: formatError(item), type: "failure" };
21-
}
18+
const viewModel: GetProductsViewModel = input.map(
19+
({ payload, type }) => {
20+
if (type === "failure") {
21+
return {
22+
payload: formatError(payload),
23+
type: "failure",
24+
};
25+
}
2226

23-
return { payload: formatProduct(item), type: "success" };
24-
});
27+
return { payload: formatProduct(payload), type: "success" };
28+
},
29+
);
2530

2631
onViewModelChange(viewModel);
2732
},
2833
};
2934
};
3035

31-
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
32-
const formatError = (input: Extract<GetProductsOutputData[number], Error>) => {
36+
type GetProductOutputData = GetProductsOutputData[number];
37+
38+
const formatError = (
39+
input: Extract<GetProductOutputData, { type: "failure" }>["payload"],
40+
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
41+
) => {
3342
return String(input);
3443
};
3544

@@ -38,7 +47,7 @@ const formatProduct = ({
3847
brand,
3948
price,
4049
thumbnail,
41-
}: Exclude<GetProductsOutputData[number], Error>) => {
50+
}: Extract<GetProductOutputData, { type: "success" }>["payload"]) => {
4251
return {
4352
title: title.toLocaleUpperCase(),
4453
brand,

modules/catalog/src/GetProducts/adapters/GetProductsViewModel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export type GetProductsViewModel = ViewModel<
88
thumbnail: string;
99
},
1010
string
11-
>;
11+
>[];

modules/catalog/src/GetProducts/useCases/GetProductsUseCase.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type GetProductsOutputData = UseCaseOutputData<
1717
thumbnail: string;
1818
},
1919
Error
20-
>;
20+
>[];
2121

2222
export type GetProductsInteractor = UseCaseInteractor<GetProductsInputData>;
2323

@@ -34,13 +34,16 @@ export const createGetProductsInteractor: UseCaseInteractorFactory<
3434

3535
results.forEach(({ payload, type }) => {
3636
if (type === "failure") {
37-
presenterInput.push(payload);
37+
presenterInput.push({ payload, type: "failure" });
3838
} else {
3939
presenterInput.push({
40-
title: payload.title,
41-
brand: payload.brand,
42-
price: payload.price.value,
43-
thumbnail: payload.thumbnail,
40+
payload: {
41+
title: payload.title,
42+
brand: payload.brand,
43+
price: payload.price.value,
44+
thumbnail: payload.thumbnail,
45+
},
46+
type: "success",
4447
});
4548
}
4649
});

modules/catalog/src/Product/entities/ProductEntity.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
success,
66
} from "@clean-architecture/shared-kernel";
77
import type {
8-
DataTransferObject,
98
Entity,
109
EntityGatewayBoundary,
1110
IdValueObject,
@@ -34,15 +33,15 @@ export type ProductEntityGatewayBoundary = EntityGatewayBoundary<{
3433
toEntity: (input: ProductDataSourceBoundaryDto) => Result<ProductEntity>;
3534
}>;
3635

37-
export type ProductEntityFactoryInput = DataTransferObject<{
36+
export type ProductEntityFactoryInput = {
3837
id: string;
3938
title: string;
4039
brand: string;
4140
category: string;
4241
createdAt: string;
4342
price: number;
4443
thumbnail: string;
45-
}>;
44+
};
4645

4746
export const createProductEntity = createEntityFactory<
4847
ProductEntity,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import type { AnyRecord } from "./AnyRecord";
2-
3-
export type DataTransferObject<Input extends AnyRecord = AnyRecord> = Input;
1+
export type DataTransferObject<SuccessInput, FailureInput> =
2+
| { payload: FailureInput; type: "failure" }
3+
| { payload: SuccessInput; type: "success" };

modules/shared-kernel/src/adapters/Presenter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import type { ViewModel } from "./ViewModel";
77
* The presenter implements the `UseCaseOutputBoundary` following the clean architecture [diagram](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).
88
*/
99
export type Presenter<
10-
OutputData extends UseCaseOutputData = UseCaseOutputData,
10+
OutputData extends UseCaseOutputData | UseCaseOutputData[],
1111
> = UseCaseOutputBoundary<OutputData>;
1212

1313
export type PresenterFactory<
1414
Output extends Presenter<OutputData>,
15-
OutputData extends UseCaseOutputData,
16-
Model extends ViewModel,
15+
OutputData extends UseCaseOutputData | UseCaseOutputData[],
16+
Model extends ViewModel | ViewModel[],
1717
> = (onViewModelChange: (input: Model) => void) => Output;
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export type ViewModel<SuccessInput = unknown, FailureInput = unknown> = (
2-
| { payload: FailureInput; type: "failure" }
3-
| { payload: SuccessInput; type: "success" }
4-
)[];
1+
import type { DataTransferObject } from "../DataTransferObject";
2+
3+
type FailureInputConstraint = string;
4+
5+
export type ViewModel<
6+
SuccessInput = unknown,
7+
FailureInput extends FailureInputConstraint = FailureInputConstraint,
8+
> = DataTransferObject<SuccessInput, FailureInput>;

modules/shared-kernel/src/frameworks/Hook.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import type { ViewModel } from "../adapters/ViewModel";
22
import type { Controller } from "../adapters/Controller";
33
import type { AnyInput } from "../AnyInput";
44

5-
export type Hook<C extends Controller<AnyInput>, VM extends ViewModel> = () => {
5+
export type Hook<
6+
C extends Controller<AnyInput>,
7+
VM extends ViewModel | ViewModel[],
8+
> = () => {
69
controller: C;
710
viewModel: VM;
811
};
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { DataTransferObject } from "../DataTransferObject";
1+
import type { AnyRecord } from "../AnyRecord";
22

3-
export type UseCaseInputData<
4-
Input extends DataTransferObject = DataTransferObject,
5-
> = DataTransferObject<Input>;
3+
export type UseCaseInputData<Input extends AnyRecord = AnyRecord> = Input;

0 commit comments

Comments
 (0)