Repository navigation
Architecture
The architecture used in this project is Clean Architecture
The principal goals of clean Architecture are:
- Independent of Frameworks - The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
- Testable - The business rules can be tested without the UI, Database, Web Server, or any other external element.
- Independent of UI - The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
- Independent of Database of Servers - You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
- Independent of any external agency - In fact, your business rules simply don’t know anything at all about the outside world.
The concentric circles represent different areas of software. In general, the further you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.
The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes functions, classes, variables, or any other named software entity.
By the same token, data formats used in an outer circle should not be used by an inner circle, especially if those formats are generated by a framework in an outer circle. We don’t want anything in an outer circle to impact the inner circles.
Entities encapsulate Enterprise-wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.
In this project for the moment, entities are a set of data structures. Example:
import { Study } from "./Study";
export interface DiagnosisStudy extends Study {
SITE_NAME: string;
HRP2_TESTED: string;
HRP2_PROPORTION_DELETION: string;
HRP3_TESTED: string;
HRP3_PROPORTION_DELETION: string;
HRP2_HRP3_TESTED: string;
HRP2_HRP3_PROPORTION_DELETION: string;
PATIENT_TYPE: string;
SURVEY_TYPE: string;
Code: string;
}The software in this layer contains application-specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities and direct those entities to use their enterprise-wide business rules to achieve the goals of the use case.
One of the goals is to abstract the domain layer and the data layer. Example:
export class GetDiagnosisStudiesUseCase {
constructor(private diagnosisRepository: DiagnosisRepository) {}
execute(): FutureData<DiagnosisStudy[]> {
return this.diagnosisRepository.getStudies();
}
}The software in this layer is a set of adapters that convert data from the format most convenient for the use cases and entities to the format most convenient for some external agency such as the Database or the Web.
In this project, the repositories are the adapters. The interfaces of repositories are defined in the domain layer. Example:
export interface DiagnosisRepository {
getStudies(): FutureData<DiagnosisStudy[]>;
}And the implementations of the interfaces are defined in the data layer. Example:
export class DiagnosisApiRepository implements DiagnosisRepository {
constructor(private baseUrl: string) {}
getStudies(): FutureData<DiagnosisStudy[]> {
const params: ApiParams = {
f: "json",
where: `1=1`,
outFields: "*",
};
return request<ApiResponse<DiagnosisStudy>>({ url: `${this.baseUrl}/7/query`, params }).map(response =>
response.features.map(feature => feature.attributes)
);
}
}Clean Architecture is used to define and isolate the domain of the app and Redux to manage the state of the map at the presentation layer.
For home page, dashboards page, stories, about and contact the state is manages using custom hooks and API context.
The diagram to represents the final architecture for this project:

This project use Futures. It is control structures similar to Promises, Tasks, or Deferreds. Futures are cancelable, Cancellation is a system whereby running Futures get an opportunity to stop what they're doing and release resources that they were holding when the consumer indicates it is no longer interested in the result.
We have implemented an anticorruption layer object called future to abstract the fluture objects of this library.
The use cases return futures and these futures are mapped to observables on the EPICs. Example:
export const getDiagnosisStudiesEpic = (
action$: ActionsObservable<ActionType<typeof fetchDiagnosisStudiesRequest>>,
state$: StateObservable<State>,
{ compositionRoot }: EpicDependencies
) =>
action$.ofType(ActionTypeEnum.FetchDiagnosisStudiesRequest).pipe(
withLatestFrom(state$),
switchMap(([, state]) => {
if (state.diagnosis.studies.length === 0 && !state.diagnosis.error) {
return fromFuture(compositionRoot.diagnosis.getStudies()).pipe(
mergeMap((studies: DiagnosisStudy[]) => {
return of(fetchDiagnosisStudiesSuccess(studies));
}),
catchError((error: Error) => of(addNotificationAction(error.message), fetchDiagnosisStudiesError()))
);
} else {
return of(fetchDiagnosisStudiesSuccess(state.diagnosis.studies));
}
})
);The Composition Root is the part of the project in charge of solving the dependencies. It's a static service locator. Example:
export class CompositionRoot {
private diagnosisRepository = new DiagnosisApiRepository(config.mapServerUrl);
public get diagnosis() {
return getExecute({
getStudies: new GetDiagnosisStudiesUseCase(this.diagnosisRepository),
});
}
}
function getExecute<UseCases extends Record<Key, UseCase>, Key extends keyof UseCases>(
useCases: UseCases
): { [K in Key]: UseCases[K]["execute"] } {
const keys = Object.keys(useCases) as Key[];
const initialOutput = {} as { [K in Key]: UseCases[K]["execute"] };
return keys.reduce((output, key) => {
const useCase = useCases[key];
const execute = useCase.execute.bind(useCase) as UseCases[typeof key]["execute"];
output[key] = execute;
return output;
}, initialOutput);
}The composition root is passed as dependencies to the epics.
export type EpicDependencies = {
compositionRoot: CompositionRoot;
};
const dependencies = {
compositionRoot: new CompositionRoot(),
};
const epicMiddleware = createEpicMiddleware({ dependencies });
const middleware = [epicMiddleware];
export default function configureStore() {
const composeEnhancer: typeof compose = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(createRootReducer(), composeEnhancer(applyMiddleware(...middleware)));
epicMiddleware.run(rootEpic as any);
return { store };
}From the epics request the use case to the composition root:
export const getDiagnosisStudiesEpic = (
action$: ActionsObservable<ActionType<typeof fetchDiagnosisStudiesRequest>>,
state$: StateObservable<State>,
{ compositionRoot }: EpicDependencies
) => ...- src
- data
- {Interface}{Implementation}Repository.ts
- domain
- entities
- {Entity}.ts
- repositories
- {Interface}Repository.ts
- usecases
- {Action}UseCase.ts
- webapp
- components
- pages
