code links added to readme

This commit is contained in:
Michał Michaluk
2018-02-18 22:28:24 +01:00
parent df64659038
commit 8e9e3a1ca9
17 changed files with 111 additions and 71 deletions

View File

@@ -5,9 +5,10 @@ Not every piece of software is equally important...
Not every piece will decide about company / product success or can cause not reversible Not every piece will decide about company / product success or can cause not reversible
negative business consequences like materialise brand risk or money loses. negative business consequences like materialise brand risk or money loses.
On the other hand scalability or non functional requirements are different for different activities in software. On the other hand scalability or non functional requirements are different for different activities in software.
To accommodate to those differences, separate architectural patterns are applied: To accommodate to those differences, separate architectural patterns are applied:
![Command Query CRUD Responsibility Segregation](https://github.com/michal-michaluk/factory/raw/master/command-query-crud.png) ![Command Query CRUD Responsibility Segregation](command-query-crud.png)
**Simple Create Read Update Delete functionality** are exposed with leverage of CRUD framework. **Simple Create Read Update Delete functionality** are exposed with leverage of CRUD framework.
@@ -16,6 +17,11 @@ Goals of that approach:
- fast respond to typical changes (ex. „please add another 2 fields on UI”), - fast respond to typical changes (ex. „please add another 2 fields on UI”),
- exposure of high quality API. - exposure of high quality API.
Examples in code:
- CRUD-able document [ProductDescription](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescription.java)
- persistence of document [ProductDescriptionEntity](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEntity.java)
- CRUD exposed as DAO and REST endpoint [ProductDescriptionDao](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionDao.java)
**Complex Commands (business processing)** expressed in Domain Model which is embedded in hexagonal architecture. **Complex Commands (business processing)** expressed in Domain Model which is embedded in hexagonal architecture.
Goals of that approach: Goals of that approach:
@@ -25,6 +31,25 @@ caused by technological choices or transport models from external services / con
- make the core business of application technology agnostic, enabling continues technology - make the core business of application technology agnostic, enabling continues technology
migration and keeping long living projects up to date with fast evolving frameworks and libraries. migration and keeping long living projects up to date with fast evolving frameworks and libraries.
Examples of Domain Model in code:
- aggregate [ProductDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemand.java)
- entity [DailyDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DailyDemand.java)
- value object [Adjustment](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Adjustment.java)
- policy [ReviewPolicy](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ReviewPolicy.java)
- domain event [DemandedLevelsChanged](shared-kernel-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandedLevelsChanged.java)
Examples of Ports in code:
- application service (primary port) [DemandService](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java)
- repository (secondary port) [ProductDemandRepository](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandRepository.java)
- domain events handling (secondary port) [DemandEvents](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEvents.java)
Examples of Adapters in code:
- REST endpoint for complex command (driving adapter)
- command resource [DemandAdjustmentDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/DemandAdjustmentDao.java)
- command handler [CommandsHandler](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java)
- repository implementation (driven adapter) [ProductDemandORMRepository](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandORMRepository.java)
- events propagation (driven adapter) [DemandEventsPropagation](app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java)
**Complex Query** implemented as direct and simple as possible by: **Complex Query** implemented as direct and simple as possible by:
- fetching persistent read model expected by consumer, the read model is a projection of past domain event, - fetching persistent read model expected by consumer, the read model is a projection of past domain event,
- read model composed at query execution time build directly from persistent form of Domain Model, - read model composed at query execution time build directly from persistent form of Domain Model,
@@ -34,15 +59,20 @@ Additional complex calculations or projections can be partially delegated to the
Goals of that approach: Goals of that approach:
- encapsulation of the Domain Model complexity by providing (simpler) consumer driven or published language API, - encapsulation of the Domain Model complexity by providing (simpler) consumer driven or published language API,
- freeing Domain the Model from exposing data for reads making the Domain Model simpler, - freeing the Domain Model from exposing data for reads making the Domain Model simpler,
- improves reads performance and enable horizontal scalability. - improves reads performance and enable horizontal scalability.
Examples in code:
- projection of domain events to persistent read model [DeliveryForecastProjection](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java)
- REST endpoint for persistent read model [DeliveryForecastDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastDao.java)
- read model composed at query execution time [StockForecastQuery](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java)
- REST resource processor for NOT persistent read model [StockForecastResourceProcessor](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java)
## Hexagonal Architecture ## Hexagonal Architecture
Only the most valuable part of that enterprise software is embedded in hexagonal architecture - Only the most valuable part of that enterprise software is embedded in hexagonal architecture -
complex business processing modeled in form of the Domain Model. complex business processing modeled in form of the Domain Model.
![Domain Model embedded in hexagonal architecture](https://github.com/michal-michaluk/factory/raw/master/hexagon.png) ![Domain Model embedded in hexagonal architecture](hexagon.png)
**Application Services** - providing entry point to Domain Model functionality, **Application Services** - providing entry point to Domain Model functionality,
Application Services are ports for Primary / Driving Adapters like RESTfull endpoints. Application Services are ports for Primary / Driving Adapters like RESTfull endpoints.
@@ -69,13 +99,13 @@ with **Model Exploration Whirlpool** and build **Ubiquitous Language** with your
Adding infrastructure and technology later is easy thanks to Hexagonal Architecture. Adding infrastructure and technology later is easy thanks to Hexagonal Architecture.
Simply starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**: Simply starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**:
![Big Picture Event Storming](https://github.com/michal-michaluk/factory/raw/master/es-big-picture-original.jpg) ![Big Picture Event Storming](es-big-picture-original.jpg)
after cleaning and trimming initial model to most valuable and needed areas: after cleaning and trimming initial model to most valuable and needed areas:
![Big Picture Event Storming](https://github.com/michal-michaluk/factory/raw/master/es-big-picture-cleaned.jpg) ![Big Picture Event Storming](es-big-picture-cleaned.jpg)
Deep dive in **Demand Forecasting** sub-domain with **Design Level Event Storming**: Deep dive in **Demand Forecasting** sub-domain with **Design Level Event Storming**:
![Design Level Event Storming - Demand Forecasting](https://github.com/michal-michaluk/factory/raw/master/es-design-demand-forecasting.jpg) ![Design Level Event Storming - Demand Forecasting](es-design-demand-forecasting.jpg)
is excellent canvas to cooperative exploration of: is excellent canvas to cooperative exploration of:
- impacted and required actors, - impacted and required actors,

View File

@@ -10,13 +10,13 @@ import pl.com.bottega.factory.delivery.planning.projection.DeliveryForecastProje
@Component @Component
@AllArgsConstructor @AllArgsConstructor
@RepositoryEventHandler @RepositoryEventHandler
public class DeliveryPlannerDefinitionEventsMapping { public class DeliveryPlannerDefinitionEventsPropagation {
private DeliveryForecastProjection projection; private DeliveryForecastProjection projection;
@HandleAfterSave @HandleAfterSave
@HandleAfterCreate @HandleAfterCreate
public void handle(DeliveryPlannerDefinitionEntity entity) { public void handleCreateAndUpdate(DeliveryPlannerDefinitionEntity entity) {
projection.handleDeliveryPlannerDefinitionChange(entity.getRefNo()); projection.applyDeliveryPlannerDefinitionChange(entity.getRefNo());
} }
} }

View File

@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
@Lazy @Lazy
@Component @Component
@AllArgsConstructor @AllArgsConstructor
class DemandEventsMapping implements DemandEvents { class DemandEventsPropagation implements DemandEvents {
private final CurrentDemandProjection demandProjection; private final CurrentDemandProjection demandProjection;
private final DeliveryForecastProjection deliveryProjection; private final DeliveryForecastProjection deliveryProjection;
@@ -26,8 +26,8 @@ class DemandEventsMapping implements DemandEvents {
@Override @Override
public void emit(DemandedLevelsChanged event) { public void emit(DemandedLevelsChanged event) {
demandProjection.persistCurrentDemands(event); demandProjection.applyDemandedLevelsChanged(event);
deliveryProjection.persistDeliveryForecasts(event); deliveryProjection.applyDemandedLevelsChanged(event);
shortagePrediction.predictShortages(event); shortagePrediction.predictShortages(event);
} }

View File

@@ -2,6 +2,7 @@ package pl.com.bottega.factory.product.management;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.data.rest.core.annotation.HandleAfterCreate; import org.springframework.data.rest.core.annotation.HandleAfterCreate;
import org.springframework.data.rest.core.annotation.HandleAfterDelete;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler; import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import pl.com.bottega.factory.demand.forecasting.DemandService; import pl.com.bottega.factory.demand.forecasting.DemandService;
@@ -11,14 +12,19 @@ import pl.com.bottega.factory.stock.forecast.ressource.StockForecastEntity;
@Component @Component
@AllArgsConstructor @AllArgsConstructor
@RepositoryEventHandler @RepositoryEventHandler
public class ProductDescriptionEventsMapping { public class ProductDescriptionEventsPropagation {
private final DemandService demandService; private final DemandService demandService;
private final StockForecastDao stockForecasts; private final StockForecastDao stockForecasts;
@HandleAfterCreate @HandleAfterCreate
public void handle(ProductDescriptionEntity entity) { public void handleCreate(ProductDescriptionEntity entity) {
demandService.init(entity.getRefNo()); demandService.init(entity.getRefNo());
stockForecasts.save(new StockForecastEntity(entity.getRefNo())); stockForecasts.save(new StockForecastEntity(entity.getRefNo()));
} }
@HandleAfterDelete
public void handleDelete(ProductDescriptionEntity entity) {
stockForecasts.delete(entity.getRefNo());
}
} }

View File

@@ -8,7 +8,7 @@ import pl.com.bottega.factory.shortages.prediction.notification.NotificationOfSh
@Lazy @Lazy
@Component @Component
@AllArgsConstructor @AllArgsConstructor
class ShortageEventsMapping implements ShortageEvents { class ShortageEventsPropagation implements ShortageEvents {
private final NotificationOfShortage notification; private final NotificationOfShortage notification;

View File

@@ -31,7 +31,7 @@ public class StockForecastQuery {
public StockForecast get(RefNoId refNo) { public StockForecast get(RefNoId refNo) {
Stock stock = stocks.forRefNo(refNo); Stock stock = stocks.forRefNo(refNo);
LocalDate today = LocalDate.now(clock); LocalDate today = LocalDate.now(clock);
return build(refNo, today, stock, return build(today, stock,
this.demands this.demands
.findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream() .findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream()
.collect(toMap( .collect(toMap(
@@ -47,7 +47,7 @@ public class StockForecastQuery {
); );
} }
private StockForecast build(RefNoId refNo, LocalDate today, private StockForecast build(LocalDate today,
Stock stock, Stock stock,
Map<LocalDate, Long> demands, Map<LocalDate, Long> demands,
Map<LocalDate, Long> outputs) { Map<LocalDate, Long> outputs) {

View File

@@ -1,20 +0,0 @@
package pl.com.bottega.factory.stock.forecast.ressource;
import org.springframework.stereotype.Component;
import pl.com.bottega.factory.product.management.RefNoId;
import pl.com.bottega.factory.stock.forecast.StockForecast;
import pl.com.bottega.factory.stock.forecast.StockForecastQuery;
@Component
class StaticAccess {
private static StockForecastQuery query;
StaticAccess(StockForecastQuery query) {
StaticAccess.query = query;
}
static StockForecast calculateQuery(String refNo) {
return query.get(new RefNoId(refNo));
}
}

View File

@@ -1,6 +1,7 @@
package pl.com.bottega.factory.stock.forecast.ressource; package pl.com.bottega.factory.stock.forecast.ressource;
import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import pl.com.bottega.tools.ProjectionRepository; import pl.com.bottega.tools.ProjectionRepository;
@@ -8,7 +9,12 @@ import pl.com.bottega.tools.ProjectionRepository;
@RepositoryRestResource(path = "stock-forecasts", @RepositoryRestResource(path = "stock-forecasts",
collectionResourceRel = "stock-forecasts", collectionResourceRel = "stock-forecasts",
itemResourceRel = "stock-forecast", itemResourceRel = "stock-forecast",
excerptProjection = StockForecastEntity.CollectionItem.class) excerptProjection = StockForecastDao.CollectionItem.class)
public interface StockForecastDao extends ProjectionRepository<StockForecastEntity, String> { public interface StockForecastDao extends ProjectionRepository<StockForecastEntity, String> {
@Projection(types = {StockForecastEntity.class})
interface CollectionItem {
String getRefNo();
}
} }

View File

@@ -2,7 +2,7 @@ package pl.com.bottega.factory.stock.forecast.ressource;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.data.rest.core.config.Projection; import lombok.Setter;
import pl.com.bottega.factory.stock.forecast.StockForecast; import pl.com.bottega.factory.stock.forecast.StockForecast;
import javax.persistence.Entity; import javax.persistence.Entity;
@@ -18,17 +18,10 @@ public class StockForecastEntity implements Serializable {
@Id @Id
private String refNo; private String refNo;
@Setter
private transient StockForecast stockForecast;
public StockForecastEntity(String refNo) { public StockForecastEntity(String refNo) {
this.refNo = refNo; this.refNo = refNo;
} }
public StockForecast getStockForecast() {
return StaticAccess.calculateQuery(refNo);
}
@Projection(types = {StockForecastEntity.class})
interface CollectionItem {
String getRefNo();
}
} }

View File

@@ -0,0 +1,23 @@
package pl.com.bottega.factory.stock.forecast.ressource;
import lombok.AllArgsConstructor;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.stereotype.Component;
import pl.com.bottega.factory.product.management.RefNoId;
import pl.com.bottega.factory.stock.forecast.StockForecastQuery;
@Component
@AllArgsConstructor
class StockForecastResourceProcessor implements ResourceProcessor<Resource<StockForecastEntity>> {
private final StockForecastQuery query;
@Override
public Resource<StockForecastEntity> process(Resource<StockForecastEntity> resource) {
resource.getContent().setStockForecast(
query.get(new RefNoId(resource.getContent().getRefNo()))
);
return resource;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -22,7 +22,7 @@ public class DeliveryForecastProjection {
private final CurrentDemandDao demandDao; private final CurrentDemandDao demandDao;
private final DeliveryAutoPlannerORMRepository planners; private final DeliveryAutoPlannerORMRepository planners;
public void persistDeliveryForecasts(DemandedLevelsChanged event) { public void applyDemandedLevelsChanged(DemandedLevelsChanged event) {
DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo()); DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo());
event.getResults().keySet() event.getResults().keySet()
.forEach(daily -> forecastDao.deleteByRefNoAndDate( .forEach(daily -> forecastDao.deleteByRefNoAndDate(
@@ -42,7 +42,7 @@ public class DeliveryForecastProjection {
); );
} }
public void handleDeliveryPlannerDefinitionChange(String refNo) { public void applyDeliveryPlannerDefinitionChange(String refNo) {
List<CurrentDemandEntity> demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock)); List<CurrentDemandEntity> demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock));
DeliveryAutoPlanner planner = planners.get(refNo); DeliveryAutoPlanner planner = planners.get(refNo);
forecastDao.deleteByRefNo(refNo); forecastDao.deleteByRefNo(refNo);

View File

@@ -17,7 +17,7 @@ import java.time.LocalDate;
@Component @Component
@AllArgsConstructor @AllArgsConstructor
@RepositoryEventHandler @RepositoryEventHandler
public class Handler { public class CommandsHandler {
private final DemandService service; private final DemandService service;
private final DemandAdjustmentDao adjustments; private final DemandAdjustmentDao adjustments;

View File

@@ -10,7 +10,7 @@ public class CurrentDemandProjection {
private final CurrentDemandDao demandDao; private final CurrentDemandDao demandDao;
public void persistCurrentDemands(DemandedLevelsChanged event) { public void applyDemandedLevelsChanged(DemandedLevelsChanged event) {
event.getResults().forEach((daily, change) -> { event.getResults().forEach((daily, change) -> {
demandDao.deleteByRefNoAndDate( demandDao.deleteByRefNoAndDate(
daily.getRefNo(), daily.getRefNo(),

View File

@@ -1,6 +1,6 @@
package pl.com.bottega.factory.demand.forecasting; package pl.com.bottega.factory.demand.forecasting;
import lombok.Value; import lombok.AllArgsConstructor;
import pl.com.bottega.factory.demand.forecasting.DailyDemand.Result; import pl.com.bottega.factory.demand.forecasting.DailyDemand.Result;
import java.time.LocalDate; import java.time.LocalDate;
@@ -11,12 +11,12 @@ import java.util.Optional;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Value @AllArgsConstructor
public class AdjustDemand { public class AdjustDemand {
String refNo; private final String refNo;
Map<LocalDate, Adjustment> adjustments; private final Map<LocalDate, Adjustment> adjustments;
public List<Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Adjustment, Result> f) { List<Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Adjustment, Result> f) {
return adjustments.entrySet().stream() return adjustments.entrySet().stream()
.filter(e -> !e.getKey().isBefore(date)) .filter(e -> !e.getKey().isBefore(date))
.map(e -> f.apply(e.getKey(), e.getValue())) .map(e -> f.apply(e.getKey(), e.getValue()))
@@ -24,6 +24,11 @@ public class AdjustDemand {
} }
public Optional<LocalDate> latestAdjustment() { public Optional<LocalDate> latestAdjustment() {
return adjustments.keySet().stream().max(Comparator.naturalOrder()); return adjustments.keySet().stream()
.max(Comparator.naturalOrder());
}
public String getRefNo() {
return refNo;
} }
} }

View File

@@ -1,33 +1,30 @@
package pl.com.bottega.factory.demand.forecasting; package pl.com.bottega.factory.demand.forecasting;
import lombok.Value; import lombok.AllArgsConstructor;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Value @AllArgsConstructor
public class Document { public class Document {
Instant created; private final Instant created;
String refNo; private final String refNo;
SortedMap<LocalDate, Demand> demands; private final SortedMap<LocalDate, Demand> demands;
public Document(Instant created, String refNo, SortedMap<LocalDate, Demand> demands) { List<DailyDemand.Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Demand, DailyDemand.Result> f) {
this.created = created;
this.refNo = refNo;
this.demands = Collections.unmodifiableSortedMap(demands);
}
public List<DailyDemand.Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Demand, DailyDemand.Result> f) {
return demands.entrySet().stream() return demands.entrySet().stream()
.filter(e -> !e.getKey().isBefore(date)) .filter(e -> !e.getKey().isBefore(date))
.map(e -> f.apply(e.getKey(), e.getValue())) .map(e -> f.apply(e.getKey(), e.getValue()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public String getRefNo() {
return refNo;
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 296 KiB