本书从出版以来,已经先后印刷7次。感谢广大读者书友,友善地帮我找到了一些bug,并前后做了两次勘误。
在即将发行的印刷批次以及即将发布的电子书中,我又做了第三次勘误。本次勘误需要特别感谢周大福的庞剑锋。他认真阅读了本书的每词每句,承蒙他的慧眼,助我找出不少Bug,令我既感激又惭愧:感激锋哥的严谨与认真,帮助我提升了本书质量,惭愧则是因为这些错误委实不应该,还得怪我自己不够细心。
为了便于大家对比手上的书籍,找到错误并修订错误,我特别在这里将这三次勘误做了一次汇总,勘误顺序以本书页码为准,并在每个错误处标记勘误的批次。
勘误格式:
第一篇 开篇
第2页(第二次勘误)
我们很难给复杂系统下一个举世公认的定义。专门从事复杂系统研究的Melanie Mitchell在接受Ubiquity杂志专访时,“勉为其难”地为复杂系统给出了一个相对通俗的定义:“由大量相互作用的部分组成的系统。与整个系统比起来,这些组成部分相对简单,没有中央控制,组成部分之间也没有全局性的通信,并且组成部分的相互作用导致了复杂行为。”
第5页(第一次勘误)
文本框下第三行,“就会是的”修改为“就会使得”。
第16页(第二次勘误)
聚合(aggregate)(参见第15章)是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。聚合至少包含一个实体,且只有实体才能作为聚合根(aggregate root)。工厂(factory)和资源库(repository)(参见第1715章)负责管理聚合的生命周期。
第18页(第二次勘误)
子领域的边界明确了问题空间中领域的优先级,限界上下文的边界则确保了领域建模的最大自由度。这也是战略设计在分治上起到的效用。当我们在战略层次从问题空间映射到解空间时,子领域也将映射到限界上下文,即可根据子领域的类型为限界上下文选择不同的建模方式。例如为处于核心子领域的限界上下文选择领域模型(domain model)模式[12]116,为处于支撑子领域(supporting sub domainsubdomain)的限界上下文选择事务脚本(transaction script)模式[12]110,这样就可以灵活地平衡开发成本与开发质量。
第36页(第一次勘误)
在图3-4下方第三段:
推动领域建模完成从问题空间到解空间战术求解的核心驱动力是“领域”,在领域驱动设计统一过程中,就是通过业务服务表达领域知识,成为领域分析建模、领域设计建模和领域分析实现建模的驱动力。
第二篇 全局分析
第57页(第二次勘误)
第二段:
价值需求又好像一杆秤,每当我们挖掘到一条业务需求,就拿到这杆秤上称一称,然后根据它的质量(俗称重量)重量来确定优先级。
第75页(第二次勘误)
文本框文字的第三段:
业务服务的规格说明规约则为领域建模提供了建模依据,帮助分解任务和明确职责分配,并在通过测试驱动开发进行领域实现建模时,作为识别和编写测试用例的主要参考。
第三篇 架构映射
第151页(第三次勘误)
代码有误,修改前:
public class OrderAppService {
@Service
private PlacingOrderService placingOrderService;
// 事务管理为横切关注点
@Transactional(propagation=Propagation.REQUIRED)
public void placeOrder(Order order) {
try {
orderService.execute(order);
} catch (InvalidOrderException ex | Exception ex) {
// 日志记录为横切关注点
logger.error(ex.getMessage());
// ApplicationException派生自RuntimeException,事务会在抛出该异常时回滚
throw new ApplicationException("failed to place order", ex);
}
}
}
修改第9行代码:
public class OrderAppService {
@Service
private PlacingOrderService placingOrderService;
// 事务管理为横切关注点
@Transactional(propagation=Propagation.REQUIRED)
public void placeOrder(Order order) {
try {
placingOrderService.execute(order);
} catch (InvalidOrderException ex | Exception ex) {
// 日志记录为横切关注点
logger.error(ex.getMessage());
// ApplicationException派生自RuntimeException,事务会在抛出该异常时回滚
throw new ApplicationException("failed to place order", ex);
}
}
}
第153页(第一、二次勘误)
代码有误,修改前:
public class OrderAppService {
@Service
private PlacingOrderService placingOrderService;
// 此时将NotificationService视为基础设施服务
@Service
private NotificationService notificationService;
// 事务为横切关注点
@Transactional(propagation=Propagation.REQUIRED)
public void placeOrder(Order order) {
try {
orderService.execute(order);
notificationService.send(notificationComposer.compose(order));
} catch (InvalidOrderException ex | Exception ex) {
// 日志为横切关注点
logger.error(ex.getMessage());
// ApplicationException派生自RuntimeException,事务会在抛出该异常时回滚
throw new ApplicationException("failed to place order", ex);
}
}
}
第11行的placeOrder()方法都做了修订:
public class OrderAppService {
@Service
private PlacingOrderService placingOrderService;
// 此时将NotificationService视为基础设施服务
@Service
private NotificationService notificationService;
// 事务管理为横切关注点
@Transactional(propagation=Propagation.REQUIRED)
public void placeOrder(PlacingOrderRequest request) {
try {
Order order=request.to();
orderService.placeOrder(order);
Notification notification = notificationComposer.compose(order);
notificationService.send(notification);
} catch (InvalidOrderException Exception ex) {
// 日志记录为横切关注点
logger.error(ex.getMessage());
// ApplicationException派生自RuntimeException,事务会在抛出该异常时回滚
throw new ApplicationException("failed to place order", ex);
}
}
}
第159页(第三次勘误)
代码有误,修改前:
private void onPaymentCompleted(PaymentCompleted paymentEvent) {
if (paymentEvent.OperationResult == OperationResult.SUCCESS) {
updatingService.execute(OrderStatus.PAID);
ApplicationEvent orderPaid = composeOrderPaidEvent(paymentEvent.orderId());
eventPublisher.publishEvent("payment", orderPaid);
} else {...}
}
修改第3行代码:
private void onPaymentCompleted(PaymentCompleted paymentEvent) {
if (paymentEvent.OperationResult == OperationResult.SUCCESS) {
updatingService.execute(paymentEvent.orderId(), OrderStatus.PAID);
ApplicationEvent orderPaid = composeOrderPaidEvent(paymentEvent.orderId());
eventPublisher.publishEvent("payment", orderPaid);
} else {...}
}
第181页(第三次勘误)
图12-13下的代码模型:
修改后(调整了pl/message的位置):
第192页(第三次勘误)
代码有误,修改前:
public interface InventoryClient {
InventoryReview check(Order order);
}
增加一个lock()方法,修改后:
public interface InventoryClient {
InventoryReview check(Order order);
void lock(Order order);
}
第195页(第一次勘误)
代码有误,修改前:
public void placeOrder(Order order) {
if (!order.isValid()) {
throw new InvalidOrderException();
}
InventoryReview inventoryReview = inventoryClient.check(order);
if (!inventoryReview.isAvailable()) {
throw new NotEnoughInventoryException();
}
orderRepository.add(order);
ShoppingCartService.removeItems(order.customerId(),
Order.purchasedProducts());
inventoryClient.lock(LockingInventoryRequest.from(order));
}
第15行代码有误,应去掉对order的转换:
public void placeOrder(Order order) {
if (!order.isValid()) {
throw new InvalidOrderException();
}
InventoryReview inventoryReview = inventoryClient.check(order);
if (!inventoryReview.isAvailable()) {
throw new NotEnoughInventoryException();
}
orderRepository.add(order);
ShoppingCartService.removeItems(order.customerId(),
Order.purchasedProducts());
inventoryClient.lock(order);
}
第四篇 领域建模
第226页(第二次勘误)
我的职责就是管理世界与世界的相互关系,就是理顺事务物的顺序,就是让结果出现在原因之后,就是不使含义与含义相混淆,就是让过去出现在现在之前,就是让未来出现在现在之后。——村上春树,《海边的卡夫卡》
第231页(第三次勘误)
以下代码中的extends修改为implements:
package com.dddexplained.eas.core.domain;
public interface Identity<T> implements Serializable {
T value();
}
第245页(第三次勘误)
代码有误,修改前:
public enum LengthUnit {
MM(1), CM(10), DM(100), M(1000);
private int ratio;
Unit(int ratio) {
this.ratio = ratio;
}
int convert(Unit target, int value) {
return value * ratio / target.ratio;
}
}
LengthUnit的构造函数有误,应修改为:
public enum LengthUnit {
MM(1), CM(10), DM(100), M(1000);
private int ratio;
LengthUnit(int ratio) {
this.ratio = ratio;
}
int convert(Unit target, int value) {
return value * ratio / target.ratio;
}
}
第261页(第一次勘误)
代码中的注释“//部”改为“//client”
第261页(第三次勘误)
代码有误,AggregateRoot<T>是一个接口,应改为implements:
public class Customer implements AggregateRoot<Customer> {
private List<Order> orders;
public List<Order> getOrders() {
return this.orders;
}
}
第265页(第二次勘误)
15.5节的第5段:“从对象的角度看,生命周期代表了一个实例从创建到回收的过程,就像从出生到死亡的生命过程。而数据记录呢?生命周期的起点是指插入一条新纪记录,该记录被删除就是生命周期的终点。”
第269页(第一次勘误)
原代码为:
public static createFlight(String flightId, String ioFlag, ...)
缺少了返回值,应修改为:
public static Flight createFlight(String flightId, String ioFlag, ...)
第271页(第三次勘误)
本页下方到272页的代码有误,修改前:
public class Flight extends Entity<FlightId> implements AggregateRoot<Flight> {
private String flightNo;
private Carrier carrier;
private Airport departureAirport;
private Airport arrivalAirport;
private Gate boardingGate;
private LocalDate flightDate;
private Flight(String flightNo) {
this.flightNo = flightNo;
}
public static class Builder {
// required fields
private final String flightNo;
// optional fields
private Carrier carrier;
private Airport departureAirport;
private Airport arrivalAirport;
private Gate boardingGate;
private LocalDate flightDate;
public Builder(String flightNo) {
this.flightNo = flightNo;
}
public Builder beCarriedBy(String airlineCode) {
carrier = new Carrier(airlineCode);
return this;
}
public Builder departFrom(String airportCode) {
departureAirport = new Airport(airportCode);
return this;
}
public Builder arriveAt(String airportCode) {
arrivalAirport = new Airport(airportCode);
return this;
}
public Builder boardingOn(String gateNo) {
boardingGate = new Gate(gateNo);
return this;
}
public Builder flyingIn(LocalDate flyingInDate) {
flightDate = flyingInDate;
return this;
}
public Flight build() {
return new Flight(this);
}
}
private Flight(Builder builder) {
flightNo = builder.flightNo;
carrier = builder.carrier;
departureAirport = builder.departureAirport;
arrivalAirport = builder.arrivalAirport;
boardingGate = builder.boardingGate;
flightDate = builder.flightDate;
}
}
主要的修改是增加了第9行代码所示的prepareBuilder()方法。完整的代码修改如下所示:
public class Flight extends Entity<FlightId> implements AggregateRoot<Flight> {
private String flightNo;
private Carrier carrier;
private Airport departureAirport;
private Airport arrivalAirport;
private Gate boardingGate;
private LocalDate flightDate;
public static Builder prepareBuilder(String flightNo) {
return new Builder(flightNo);
}
public static class Builder {
// required fields
private final String flightNo;
// optional fields
private Carrier carrier;
private Airport departureAirport;
private Airport arrivalAirport;
private Gate boardingGate;
private LocalDate flightDate;
private Builder(String flightNo) {
this.flightNo = flightNo;
}
public Builder beCarriedBy(String airlineCode) {
carrier = new Carrier(airlineCode);
return this;
}
public Builder departFrom(String airportCode) {
departureAirport = new Airport(airportCode);
return this;
}
public Builder arriveAt(String airportCode) {
arrivalAirport = new Airport(airportCode);
return this;
}
public Builder boardingOn(String gateNo) {
boardingGate = new Gate(gateNo);
return this;
}
public Builder flyingIn(LocalDate flyingInDate) {
flightDate = flyingInDate;
return this;
}
public Flight build() {
return new Flight(this);
}
}
private Flight(Builder builder) {
flightNo = builder.flightNo;
carrier = builder.carrier;
departureAirport = builder.departureAirport;
arrivalAirport = builder.arrivalAirport;
boardingGate = builder.boardingGate;
flightDate = builder.flightDate;
}
}
第273页(第三次勘误)
需要将本页代码中的AirportCode修改为Airport,修改后的代码为:
public class Flight extends Entity<FlightId> implements AggregateRoot<Flight> {
private String flightNo;
private Carrier carrier;
private Airport departureAirport;
private Airport arrivalAirport;
private Gate boardingGate;
private LocalDate flightDate;
// 聚合必备的字段要在构造函数的参数中给出
private Flight(String flightNo) {
this.flightNo = flightNo;
}
public static Flight withFlightNo(String flightNo) {
return new Flight(flightNo);
}
public Flight beCarriedBy(String airlineCode) {
this.carrier = new Carrier(airlineCode);
return this;
}
public Flight departFrom(String airportCode) {
this.departureAirport = new Airport(airportCode);
return this;
}
public Flight arriveAt(String airportCode) {
this.arrivalAirport = new Airport(airportCode);
return this;
}
public Flight boardingOn(String gate) {
this.boardingGate = new Gate(gate);
return this;
}
public Flight flyingIn(LocalDate flightDate) {
this.flightDate = flightDate;
return this;
}
}
第283页(第二次勘误)
第二段出现单词拼写错误:
“例如,付款记录聚合OrdserSettlement与支付约定聚合PayAggreementPayAgreement都在支付上下文中,在计算OrderSettlement实体的支付金额时,需要PayAggreementPayAgreement实体计算获得的支付利率。因此,可在OrdserSettlement根实体的payAmountFor()方法中,传入PayAggreementPayAgreement对象:”
代码也应对应修改为:
public class OrderSettlement {
public BigDecimal payAmountFor(PayAgreement agreement) {
return orderAmount.multiply(agreement.actualPayRate());
}
}
public class PayAgreement {
public BigDecimal actualPayRate() {
return new BigDecimal(payRate * 0.01);
}
}
第286页
代码中TransferingService类的transfer()方法中,对Account的transferTo()方法的调用时,缺少了amount的参数,故而代码应修改为:
public class TransferingService {
private AccountRepository accountRepo;
private TransactionRepository transactionRepo;
public void transfer(AccountId sourceAccountId, AccountId targetAccountId, Money
amount) {
SourceAccount sourceAccount = accountRepo.accountOf(sourceAccountId);
TargetAccount targetAccount = accountRepo.accountOf(targetAccountId);
// 账户余额是否大于amount值,由Account聚合负责
Transaction transaction = sourceAccount.transferTo(targetAccount, amount);
accountRepo.save(sourceAccount);
accountRepo.save(targetAccount);
transactionRepo.save(transaction);
}
}
第292页代码与第293页的代码的transfer()方法也需要做对应的调整。
第293页(第三次勘误)
代码下的第一段内容:“通知服务也采用类似方式实现TransferingEventSubscriber接口。”
原来的TransferEventSubscriber统一修改为TransferingEventSubscriber。因此,第292页代码中也应做对应修改。第一段代码修改为:
private void publish(TransferSucceeded succeededEvent) {
for (TransferingEventSubscriber subscriber : subscribers) {
subscriber.handle(succeededEvent);
}
}
private void publish(TransferFailed failedEvent) {
for (TransferingEventSubscriber subscriber : subscribers) {
subscriber.handle(failedEvent);
}
}
代码段中的第2行与第8行,都做了修改。
第292页的第二段代码也该如此修改,如下代码中的第一行,修改为TransferingEventSubscriber:
public class TransactionService implements TransferingEventSubsriber {
private TransactionRepository transactionRepo;
第297页(第二次勘误)
图16-3中的“事件发布者”改为“发布者”。
第298页(第二次勘误)
原图16-5为:
由于应用服务也可以和端口交互,故而图中增加了端口,如下所示:
第299页(第二次勘误)
增加了一行内容(标记为红色):
第321页(第二次勘误):
该页脚注的内容:
ZenUML项目(参见ZenUML官网)的开发者是肖鹏。他曾经担任ThoughtWorks中国区持续交付Practice Lead,也是我在ThoughtWorks任职时的Buddy与Sponsor,目前在墨尔本一家咨询公司任架构师,业余时间负责ZenUML的开发。ZenUML除了提供Web版本,还提供了Chrome、ConfulenceConfluence和IntelliJ IDEA的插件。
第326页(第二次勘误)
该页中间给出的测试方法名进行了修改:
should_transfer_from_src_account_to_desttarget_account_given_correct_transfer_amount()
第327页(第三次勘误)
代码本身没有错误,但为了行文简单,没有给出setup的内容,会影响读者的理解,源代码为:
public class AccountTest {
@Test
public void should_transfer_from_src_account_to_dest_account_given_correct_transfer_amount() {
// given
Money balanceOfSrc = new Money(100_000L, Currency.RMB);
SourceAccount src = new Account(srcAccountId, balanceOfSrc);
Money balanceOfDes = new Money(0L, Currency.RMB);
TargetAccount target = new Account(targetAccountId, balanceOfDes);
Money trasferAmount = new Money(10_000L, Currency.RMB);
// when
src.transferTo(target, transferAmount);
// then
assertThat(src.getBalance()).isEqualTo(Money.of(90_000L, Currency.RMB));
assertThat(target.getBalance()).isEqualTo(Money.of(10_000L, Currency.RMB));
}
}
增加了setup和字段定义的内容:
public class AccountTest {
private AccountId srcAccountId;
private AccountId targetAccountId;
@Before
void setup() {
srcAccountId = AccountId.of("123456"); //用于演示
targetAccountId = AccountId.of("654321"); //用于演示
}
@Test
void should_transfer_from_src_account_to_target_account_given_correct_transfer_
amount() {
// given
Money balanceOfSrc = new Money(100_000L, Currency.RMB);
SourceAccount src = new Account(srcAccountId, balanceOfSrc);
Money balanceOfDes = new Money(0L, Currency.RMB);
TargetAccount target = new Account(targetAccountId, balanceOfDes);
Money trasferAmount = new Money(10_000L, Currency.RMB);
// when
src.transferTo(target, transferAmount);
// then
assertThat(src.getBalance()).isEqualTo(Money.of(90_000L, Currency.RMB));
assertThat(target.getBalance()).isEqualTo(Money.of(10_000L, Currency.RMB));
}
}
第343页(第二次勘误)
在代码下的第一段末尾,出现单词拼写错误:
在“确定是否为月末工作日”与“确定是否为间隔一星期的星期五”任务这一级,业务目标为“确定是否为正确的工作日”,故而命名为WordDayServiceWorkdayService。
第五篇 融合
第371页(第三次勘误)
该页第二段代码中的方法名有误,应修改为:
package com.dddexplained.ecommerce.ordercontext.southbound.port.client;
@FeignClient("inventory-service")
public interface InventoryClient {
@RequestMapping(value = "/inventories/order", method = RequestMethod.POST)
InventoryResponse isAvailable(@RequestBody CheckingInventoryRequest inventoryRequest);
}
第384页(第一次勘误)
18.4.1节的第二段:
由于角色构造型规定应用服务应体现业务服务的服务价值,即它作为业务服务的内外协调接口。同时,应用服务还应承担调用横切关注点的职责,事务作为一种横切关注点,将其放在应有用服务才是合情合理的。
第408页(第一次勘误)
代码中的构造函数弄错了,修改前:
@Embeddable
public class Absence {
private LocalDate leaveDate;
@Enumerated(EnumType.STRING)
private LeaveReason leaveReason;
public Absence() {
}
public Address(String country, String province, String city, String street, String zip) {
this.country = country;
this.province = province;
this.city = city;
this.street = street;
this.zip = zip;
}
}
修改后:
@Embeddable
public class Absence {
private LocalDate leaveDate;
@Enumerated(EnumType.STRING)
private LeaveReason leaveReason;
public Absence() {
}
public Absence(LocalDate leaveDate, LeaveReason leaveReason) {
this.leaveDate = leaveDate;
this.leaveReason = leaveReason;
}
}
第482页(第三次勘误)
针对该页的第二段代码,需要做适度修正。严格说来,这并非代码错误,因为本身代码使用了static import(该import并未在书中给出),从而省略了枚举的类型,但对于直接读书的读者来说,这样不太友好,故而增加了类型TicketStatus:
public class TicketServiceTest {
@Test
public void should_throw_TicketException_if_available_ticket_not_found() {
TicketId ticketId = TicketId.next();
TicketRepository mockTickRepo = mock(TicketRepository.class);
when(mockTickRepo.ticketOf(ticketId, TicketStatus.Available)).thenReturn(Optional.empty());
TicketService ticketService = new TicketService();
ticketService.setTicketRepository(mockTickRepo);
String trainingId = "111011111111";
Candidate candidate = new Candidate("200901010110", "Tom", "tom@eas.com", trainingId);
Nominator nominator = new Nominator("200901010007", "admin", "admin@eas.com",
TrainingRole.Coordinator);
assertThatThrownBy(() -> ticketService.nominate(ticketId, candidate, nominator))
.isInstanceOf(TicketException.class)
.hasMessageContaining(String.format("available ticket by id {%s} is not
found", ticketId.id()));
verify(mockTickRepo).ticketOf(ticketId, TicketStatus.Available);
}
}
同理,在第487页,也增加了枚举类型StateTransit:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/spring-mybatis.xml")
public class TicketHistoryRepositoryIT {
@Autowired
private TicketHistoryRepository ticketHistoryRepository;
private final TicketId ticketId = TicketId.from("18e38931-822e-4012-a16e-ac65dfc56f8a");
@Before
public void setup() {
ticketHistoryRepository.deleteBy(ticketId);
StateTransit availableToWaitForConfirm = StateTransit.from(Available).to(WaitForConfirm);
LocalDateTime oldTime = LocalDateTime.of(2020, 1, 1, 12, 0, 0);
TicketHistory oldHistory = createTicketHistory(availableToWaitForConfirm, oldTime);
ticketHistoryRepository.add(oldHistory);
StateTransit toConfirm = StateTransit.from(WaitForConfirm).to(Confirm);
LocalDateTime newTime = LocalDateTime.of(2020, 1, 1, 13, 0, 0);
TicketHistory newHistory = createTicketHistory(toConfirm, newTime);
ticketHistoryRepository.add(newHistory);
}
@Test
public void should_return_latest_one() {
Optional<TicketHistory> latest = ticketHistoryRepository.latest(ticketId);
assertThat(latest.isPresent()).isTrue();
assertThat(latest.get().getStateTransit()).isEqualTo(from(WaitForConfirm).to(Confirm));
}
}
第475和第476页(第三次勘误)
这两页的图20-57与20-58绘制的序列图有误。图20-57错误的部分为下图红色部分:
图20-58错误的部分为下图红色部分:
这两个图的红色部分,均修改为:
以上就是《解构领域驱动设计》的所有勘误。真可以说小错误不断,一边总结,一边脸红,真是惭愧惭愧。