bette use of value objects

This commit is contained in:
sbriat 2023-09-13 15:28:07 +02:00
parent 4731020e8a
commit 74fb2c120e
22 changed files with 387 additions and 96 deletions

View File

@ -14,7 +14,6 @@ import {
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
import { ExtendedMapper } from '@mobicoop/ddd-library';
import { Waypoint } from './core/domain/value-objects/waypoint.value-object';
/**
* Mapper constructs objects that are used in different layers:
@ -112,13 +111,12 @@ export class AdMapper
margin: scheduleItem.margin,
}),
),
waypoints: this.directionEncoder.decode(record.waypoints).map(
(coordinates, index) =>
new Waypoint({
position: index,
...coordinates,
}),
),
waypoints: this.directionEncoder
.decode(record.waypoints)
.map((coordinates, index) => ({
position: index,
...coordinates,
})),
fwdAzimuth: record.fwdAzimuth,
backAzimuth: record.backAzimuth,
points: [],

View File

@ -13,8 +13,10 @@ import {
PathCreator,
PathType,
TypedRoute,
} from '@modules/ad/core/domain/patch-creator.service';
} from '@modules/ad/core/domain/path-creator.service';
import { Point } from '../../types/point.type';
import { Waypoint } from '../../types/waypoint.type';
import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
@ -29,11 +31,21 @@ export class CreateAdService implements ICommandHandler {
const roles: Role[] = [];
if (command.driver) roles.push(Role.DRIVER);
if (command.passenger) roles.push(Role.PASSENGER);
const pathCreator: PathCreator = new PathCreator(roles, command.waypoints);
const pathCreator: PathCreator = new PathCreator(
roles,
command.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
let typedRoutes: TypedRoute[];
try {
typedRoutes = await Promise.all(
pathCreator.getPaths().map(async (path: Path) => ({
pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type,
route: await this.routeProvider.getBasic(path.waypoints),
})),

View File

@ -1,5 +1,5 @@
import { Route } from '@modules/geography/core/domain/route.types';
import { Waypoint } from '../types/waypoint.type';
import { Waypoint } from '../../domain/value-objects/waypoint.value-object';
export interface RouteProviderPort {
/**

View File

@ -1,5 +1,12 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract';
import { Role } from '@modules/ad/core/domain/ad.types';
import {
Waypoint as WaypointValueObject,
WaypointProps,
} from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Waypoint } from '../../../types/waypoint.type';
import { WayStepsCreator } from '@modules/ad/core/domain/waysteps-creator.service';
/**
* Complete candidates by setting driver and crew waypoints
@ -7,7 +14,53 @@ import { Completer } from './completer.abstract';
export class PassengerOrientedWaypointsCompleter extends Completer {
complete = async (
candidates: CandidateEntity[],
): Promise<CandidateEntity[]> => candidates;
): Promise<CandidateEntity[]> => {
candidates.forEach((candidate: CandidateEntity) => {
const carpoolPathCreator = new WayStepsCreator(
candidate.getProps().role == Role.DRIVER
? candidate.getProps().waypoints.map(
(waypoint: WaypointProps) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
)
: this.query.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
candidate.getProps().role == Role.PASSENGER
? candidate.getProps().waypoints.map(
(waypoint: WaypointProps) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
)
: this.query.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
candidate.setWaySteps(carpoolPathCreator.getCrewCarpoolPath());
});
// console.log(
// candidates[0]
// .getProps()
// .waySteps?.map((waystep: WayStep) => waystep.actors),
// );
return candidates;
};
}
// complete = async (candidates: Candidate[]): Promise<Candidate[]> => {

View File

@ -11,7 +11,8 @@ import {
PathCreator,
PathType,
TypedRoute,
} from '@modules/ad/core/domain/patch-creator.service';
} from '@modules/ad/core/domain/path-creator.service';
import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
export class MatchQuery extends QueryBase {
driver?: boolean;
@ -37,6 +38,7 @@ export class MatchQuery extends QueryBase {
driverRoute?: Route;
passengerRoute?: Route;
backAzimuth?: number;
private readonly originWaypoint: Waypoint;
constructor(props: MatchRequestDto) {
super();
@ -60,6 +62,9 @@ export class MatchQuery extends QueryBase {
this.maxDetourDurationRatio = props.maxDetourDurationRatio;
this.page = props.page ?? 1;
this.perPage = props.perPage ?? 10;
this.originWaypoint = this.waypoints.filter(
(waypoint: Waypoint) => waypoint.position == 0,
)[0];
}
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
@ -126,8 +131,8 @@ export class MatchQuery extends QueryBase {
date: initialFromDate,
time: this.schedule[0].time,
coordinates: {
lon: this.waypoints[0].lon,
lat: this.waypoints[0].lat,
lon: this.originWaypoint.lon,
lat: this.originWaypoint.lat,
},
},
this.frequency,
@ -138,8 +143,8 @@ export class MatchQuery extends QueryBase {
date: initialFromDate,
time: this.schedule[0].time,
coordinates: {
lon: this.waypoints[0].lon,
lat: this.waypoints[0].lat,
lon: this.originWaypoint.lon,
lat: this.originWaypoint.lat,
},
},
this.frequency,
@ -151,8 +156,8 @@ export class MatchQuery extends QueryBase {
date: this.fromDate,
time: scheduleItem.time,
coordinates: {
lon: this.waypoints[0].lon,
lat: this.waypoints[0].lat,
lon: this.originWaypoint.lon,
lat: this.originWaypoint.lat,
},
},
this.frequency,
@ -162,8 +167,8 @@ export class MatchQuery extends QueryBase {
date: this.fromDate,
time: scheduleItem.time,
coordinates: {
lon: this.waypoints[0].lon,
lat: this.waypoints[0].lat,
lon: this.originWaypoint.lon,
lat: this.originWaypoint.lat,
},
},
this.frequency,
@ -177,11 +182,21 @@ export class MatchQuery extends QueryBase {
const roles: Role[] = [];
if (this.driver) roles.push(Role.DRIVER);
if (this.passenger) roles.push(Role.PASSENGER);
const pathCreator: PathCreator = new PathCreator(roles, this.waypoints);
const pathCreator: PathCreator = new PathCreator(
roles,
this.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
try {
(
await Promise.all(
pathCreator.getPaths().map(async (path: Path) => ({
pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type,
route: await routeProvider.getBasic(path.waypoints),
})),

View File

@ -1,5 +1,6 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CandidateProps, CreateCandidateProps } from './candidate.types';
import { WayStepProps } from './value-objects/waystep.value-object';
export class CandidateEntity extends AggregateRoot<CandidateProps> {
protected readonly _id: AggregateID;
@ -9,6 +10,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
return new CandidateEntity({ id: create.id, props });
};
setWaySteps = (waySteps: WayStepProps[]): void => {
this.props.waySteps = waySteps;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}

View File

@ -1,20 +1,29 @@
import { Role } from './ad.types';
import { WaypointProps } from './value-objects/waypoint.value-object';
import { WayStepProps } from './value-objects/waystep.value-object';
// All properties that a Candidate has
export interface CandidateProps {
role: Role;
waypoints: Waypoint[];
waypoints: WaypointProps[]; // waypoints of the original Ad
waySteps?: WayStepProps[]; // carpool path for the crew (driver + passenger)
}
// Properties that are needed for a Candidate creation
export interface CreateCandidateProps {
id: string;
role: Role;
waypoints: Waypoint[];
waypoints: WaypointProps[];
}
export type Waypoint = {
lon: number;
lat: number;
position: number;
export type Spacetime = {
duration: number;
distance?: number;
};
export enum Target {
START = 'START',
INTERMEDIATE = 'INTERMEDIATE',
FINISH = 'FINISH',
NEUTRAL = 'NEUTRAL',
}

View File

@ -1,6 +1,6 @@
import { Route } from '@modules/geography/core/domain/route.types';
import { Role } from './ad.types';
import { Waypoint } from './candidate.types';
import { Waypoint } from './value-objects/waypoint.value-object';
export class PathCreator {
constructor(
@ -8,7 +8,7 @@ export class PathCreator {
private readonly waypoints: Waypoint[],
) {}
public getPaths = (): Path[] => {
public getBasePaths = (): Path[] => {
const paths: Path[] = [];
if (
this.roles.includes(Role.DRIVER) &&

View File

@ -0,0 +1,27 @@
import { ValueObject } from '@mobicoop/ddd-library';
import { Role } from '../ad.types';
import { Target } from '../candidate.types';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface ActorProps {
role: Role;
target: Target;
}
export class Actor extends ValueObject<ActorProps> {
get role(): Role {
return this.props.role;
}
get target(): Target {
return this.props.target;
}
protected validate(): void {
return;
}
}

View File

@ -1,18 +1,13 @@
import {
ArgumentInvalidException,
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { PointProps } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface WaypointProps {
export interface WaypointProps extends PointProps {
position: number;
lon: number;
lat: number;
}
export class Waypoint extends ValueObject<WaypointProps> {
@ -33,9 +28,5 @@ export class Waypoint extends ValueObject<WaypointProps> {
throw new ArgumentInvalidException(
'position must be greater than or equal to 0',
);
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
}
}

View File

@ -0,0 +1,46 @@
import {
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { WaypointProps } from './waypoint.value-object';
import { Actor } from './actor.value-object';
import { Role } from '../ad.types';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface WayStepProps extends WaypointProps {
actors: Actor[];
}
export class WayStep extends ValueObject<WayStepProps> {
get position(): number {
return this.props.position;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
get actors(): Actor[] {
return this.props.actors;
}
protected validate(props: WayStepProps): void {
if (props.actors.length <= 0)
throw new ArgumentOutOfRangeException('at least one actor is required');
if (
props.actors.filter((actor: Actor) => actor.role == Role.DRIVER).length >
1
)
throw new ArgumentOutOfRangeException(
'a waystep can contain only one driver',
);
}
}

View File

@ -0,0 +1,62 @@
import { Role } from './ad.types';
import { Target } from './candidate.types';
import { Actor } from './value-objects/actor.value-object';
import { Waypoint } from './value-objects/waypoint.value-object';
import { WayStep } from './value-objects/waystep.value-object';
export class WayStepsCreator {
constructor(
private readonly driverWaypoints: Waypoint[],
private readonly passengerWaypoints: Waypoint[],
) {}
public getCrewCarpoolPath = (): WayStep[] => this._createPassengerWaysteps();
private _createPassengerWaysteps = (): WayStep[] => {
const waysteps: WayStep[] = [];
this.passengerWaypoints.forEach((passengerWaypoint: Waypoint) => {
const waystep: WayStep = new WayStep({
lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat,
position: passengerWaypoint.position,
actors: [
new Actor({
role: Role.PASSENGER,
target: this._getTarget(
passengerWaypoint.position,
this.passengerWaypoints,
),
}),
],
});
if (
this.driverWaypoints.filter((driverWaypoint: Waypoint) =>
this._isSameWaypoint(driverWaypoint, passengerWaypoint),
).length > 0
) {
waystep.actors.push(
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
);
}
waysteps.push(waystep);
});
return waysteps;
};
private _isSameWaypoint = (
waypoint1: Waypoint,
waypoint2: Waypoint,
): boolean =>
waypoint1.lon === waypoint2.lon && waypoint1.lat === waypoint2.lat;
private _getTarget = (position: number, waypoints: Waypoint[]): Target =>
position == 0
? Target.START
: position ==
Math.max(...waypoints.map((waypoint: Waypoint) => waypoint.position))
? Target.FINISH
: Target.INTERMEDIATE;
}

View File

@ -1,9 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { RouteProviderPort } from '../core/application/ports/route-provider.port';
import { Waypoint } from '../core/application/types/waypoint.type';
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens';
import { Route } from '@modules/geography/core/domain/route.types';
import { Route, Waypoint } from '@modules/geography/core/domain/route.types';
@Injectable()
export class RouteProvider implements RouteProviderPort {

View File

@ -0,0 +1,14 @@
import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
describe('Actor value object', () => {
it('should create an actor value object', () => {
const actorVO = new Actor({
role: Role.DRIVER,
target: Target.START,
});
expect(actorVO.role).toBe(Role.DRIVER);
expect(actorVO.target).toBe(Target.START);
});
});

View File

@ -40,6 +40,16 @@ const mockAdRepository = {
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
getProps: jest.fn().mockImplementation(() => ({
role: Role.DRIVER,
waypoints: [
{
lat: 48.68787,
lon: 6.165871,
},
{
lat: 48.97878,
lon: 2.45787,
},
],
})),
},
]),

View File

@ -63,13 +63,13 @@ const candidates: CandidateEntity[] = [
waypoints: [
{
position: 0,
lat: 48.668487,
lon: 6.178457,
lat: 48.689445,
lon: 6.17651,
},
{
position: 1,
lat: 48.897457,
lon: 2.3688487,
lat: 48.8566,
lon: 2.3522,
},
],
}),

View File

@ -1,26 +1,31 @@
import { Role } from '@modules/ad/core/domain/ad.types';
import { Waypoint } from '@modules/ad/core/domain/candidate.types';
import {
Path,
PathCreator,
PathType,
} from '@modules/ad/core/domain/patch-creator.service';
} from '@modules/ad/core/domain/path-creator.service';
import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypoint: Waypoint = {
const originWaypoint: Waypoint = new Waypoint({
position: 0,
lat: 48.689445,
lon: 6.17651,
};
const destinationWaypoint: Waypoint = {
});
const destinationWaypoint: Waypoint = new Waypoint({
position: 1,
lat: 48.8566,
lon: 2.3522,
};
const intermediateWaypoint: Waypoint = {
});
const intermediateWaypoint: Waypoint = new Waypoint({
position: 1,
lat: 48.74488,
lon: 4.8972,
};
});
const destinationWaypointWithIntermediateWaypoint: Waypoint = new Waypoint({
position: 2,
lat: 48.8566,
lon: 2.3522,
});
describe('Path Creator Service', () => {
it('should create a path for a driver only', () => {
@ -28,7 +33,7 @@ describe('Path Creator Service', () => {
[Role.DRIVER],
[originWaypoint, destinationWaypoint],
);
const paths: Path[] = pathCreator.getPaths();
const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.DRIVER);
});
@ -37,7 +42,7 @@ describe('Path Creator Service', () => {
[Role.PASSENGER],
[originWaypoint, destinationWaypoint],
);
const paths: Path[] = pathCreator.getPaths();
const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.PASSENGER);
});
@ -46,7 +51,7 @@ describe('Path Creator Service', () => {
[Role.DRIVER, Role.PASSENGER],
[originWaypoint, destinationWaypoint],
);
const paths: Path[] = pathCreator.getPaths();
const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.GENERIC);
});
@ -56,10 +61,10 @@ describe('Path Creator Service', () => {
[
originWaypoint,
intermediateWaypoint,
{ ...destinationWaypoint, position: 2 },
destinationWaypointWithIntermediateWaypoint,
],
);
const paths: Path[] = pathCreator.getPaths();
const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(2);
expect(
paths.filter((path: Path) => path.type == PathType.DRIVER),

View File

@ -0,0 +1,62 @@
import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object';
describe('WayStep value object', () => {
it('should create a waystep value object', () => {
const wayStepVO = new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
});
expect(wayStepVO.position).toBe(0);
expect(wayStepVO.lon).toBe(6.17651);
expect(wayStepVO.lat).toBe(48.689445);
expect(wayStepVO.actors).toHaveLength(2);
});
it('should throw an exception if actors is empty', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [],
});
} catch (e: any) {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
});
it('should throw an exception if actors contains more than one driver', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
],
});
} catch (e: any) {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
});
});

View File

@ -1,6 +1,6 @@
import { QueryBase } from '@mobicoop/ddd-library';
import { Waypoint } from '@modules/geography/core/domain/route.types';
import { GeorouterSettings } from '../../types/georouter-settings.type';
import { Waypoint } from '@modules/geography/core/domain/route.types';
export class GetRouteQuery extends QueryBase {
readonly waypoints: Waypoint[];

View File

@ -20,6 +20,7 @@ export interface CreateRouteProps {
georouterSettings: GeorouterSettings;
}
// Types used outside the domain
export type Route = {
distance: number;
duration: number;

View File

@ -1,30 +1,17 @@
import {
ArgumentInvalidException,
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { PointProps } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface StepProps {
lon: number;
lat: number;
export interface StepProps extends PointProps {
duration: number;
distance: number;
}
export class Step extends ValueObject<StepProps> {
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
get duration(): number {
return this.props.duration;
}
@ -33,6 +20,14 @@ export class Step extends ValueObject<StepProps> {
return this.props.distance;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
protected validate(props: StepProps): void {
if (props.duration < 0)
throw new ArgumentInvalidException(
@ -42,9 +37,5 @@ export class Step extends ValueObject<StepProps> {
throw new ArgumentInvalidException(
'distance must be greater than or equal to 0',
);
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
}
}

View File

@ -1,18 +1,13 @@
import {
ArgumentInvalidException,
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { PointProps } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface WaypointProps {
export interface WaypointProps extends PointProps {
position: number;
lon: number;
lat: number;
}
export class Waypoint extends ValueObject<WaypointProps> {
@ -33,9 +28,5 @@ export class Waypoint extends ValueObject<WaypointProps> {
throw new ArgumentInvalidException(
'position must be greater than or equal to 0',
);
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
}
}