Like last time, it's a refactored article of creating a sample program in DDD using a database specialist issue.
I made a sample program using the problem of database specialist in Domain Driven Design Creating a sample program using the problem of a database specialist with DDD, improvement 1
This time, I refactored the part related to the "Deposit" endpoint.
The source code is here The version at the time of posting this article is tag 1.2.
In the first article posted
In a multi-stage lottery, determining the winning of the lottery from the outside of the entry causes the logic to leak to the Application, and above all, you will regret it later when the number of stages increases. It doesn't say that there are two stages, and if you increase it to two stages, you will be asked to increase it one more stage immediately.
I usually commented that. I tried this.
First, let's get the lottery result of the lottery participation application including the result of the multi-stage lottery, register it in the first class collection, and make it possible to get whether or not you have won from the first class collection. It was.
LotteryEntryResults.java
public class LotteryEntryResults {
private List<LotteryEntryResult> list;
public LotteryEntryResults(List<LotteryEntryResult> list) {
this.list = list;
}
/**
*Returns whether you have won.Returns true if winning.
*/
public boolean winning() {
for (LotteryEntryResult result : list) {
if (result.lotteryResult == LotteryResult.winning) {
return true;
}
}
return false;
}
}
MybatisLotteryEntryResultRepository.java
@Override
public LotteryEntryResults findLotteryEntryResults(
FestivalId festivalId,
MemberId memberId,
EntryId entryId) {
List<LotteryEntryResult> resultList = new ArrayList<>();
EntryId targetEntryId = entryId;
while (true) {
LotteryEntryResult lotteryEntryResult = lotteryEntryResultMapper.selectLotteryEntryResult(
festivalId, memberId, targetEntryId);
if (lotteryEntryResult == null) {
break;
}
resultList.add(lotteryEntryResult);
EntryDto entryDto = entryMapper.selectEntry(festivalId, targetEntryId);
EntryId followingEntryId = entryDto.followingEntryId();
if (followingEntryId == null) {
break;
}
targetEntryId = followingEntryId;
}
return new LotteryEntryResults(resultList);
}
I was asked to try using recursive processing, but I had never done recursive processing with SQL, so this time I tried it muddy: sweat_smile:
As a result, it was possible to move the winning judgment of the lottery from the application layer to the domain layer in the multi-stage lottery. In addition, it is now possible to handle multi-stage lottery with 3 or more stages.
** Before refactoring **
PaymentCommandService.java
Entry entry = entryRepository.findEntry(festivalId, application.entryId());
if (entry.isLotteryEntry()) {
//If the target entry is a lottery, check if it has won
LotteryEntryResult entryResult = lotteryEntryResultRepository.findLotteryEntryResult(
festivalId, memberId, entry.entryId());
if (entryResult.lotteryResult() == LotteryResult.failed) {
EntryId followingEntryId = ((LotteryEntry)entry).followingEntryId();
if (followingEntryId == null) {
throw new BusinessErrorException("I have not won the target tournament");
} else {
LotteryEntryResult followingEntryResult =
lotteryEntryResultRepository.findLotteryEntryResult(
festivalId, memberId, followingEntryId);
if (followingEntryResult.lotteryResult() == LotteryResult.failed) {
throw new BusinessErrorException("I have not won the target tournament");
}
}
}
}
** After refactoring **
PaymentCommandService.java
Entry entry = entryRepository.findEntry(festivalId, application.entryId());
if (entry.isLotteryEntry()) {
//If the target entry is a lottery, check if it has won
LotteryEntryResults lotteryEntryResults =
lotteryEntryResultRepository.findLotteryEntryResults(
festivalId, memberId, entry.entryId());
if (!lotteryEntryResults.winning()) {
throw new BusinessErrorException("I have not won the target tournament");
}
}
Cleaner with fewer if statements: thumbs up:
Regarding the processing of point usage
I think it's good to make MemberPoints a first-class collection, but I get the impression that the inside of for is long. I thought that it would be cleaner to have MemberPoint itself, which is a collection member, in most of the memberPoints for.
I feel that the point balance is a method that should be given to the Point class. I also hard-code the expiration date with plusYears (1), but it seems better to have this in the Point class as well.
I received comments such as, and tried refactoring with reference to the comments I received.
** Before refactoring **
MemberPoints.java
public class MemberPoints {
private List<MemberPoint> list;
/**
*Determine if the argument points can be used and use from the point near the expiration date.
*In addition, change the state of the MemberPoint object to be retained.
*/
public void usePoints(LocalDate paymentDate, PointAmount pointAmount) {
//Use points from the points given so far until this value becomes zero
BigDecimal x = pointAmount.value();
for (MemberPoint memberPoint : list) {
//Expiration date check
LocalDate expirationDate = memberPoint.givenPointDate().plusYears(1);
if (paymentDate.compareTo(expirationDate) > 0) {
continue;
}
//Check the point balance
BigDecimal availableUsePoint = memberPoint.givenPoint().value()
.subtract(memberPoint.usedPoint().value());
if (availableUsePoint.compareTo(BigDecimal.ZERO) == 0) {
continue;
}
if (availableUsePoint.compareTo(x) <= 0) {
memberPoint.use(availableUsePoint);
x = x.subtract(availableUsePoint);
} else {
memberPoint.use(x);
x = BigDecimal.ZERO;
break;
}
}
//Use points from the one with the closest expiration date, and if the number of points you want to use is not reached, an error will occur.
if (x.compareTo(BigDecimal.ZERO) > 0) {
throw new BusinessErrorException("Insufficient points");
}
}
}
** After refactoring **
MemberPoints.java
public class MemberPoints {
private List<MemberPoint> list;
/**
*Determine if the argument points can be used and use from the point near the expiration date.
*In addition, change the state of the MemberPoint object to be retained.
*/
public void usePoints(LocalDate paymentDate, PointAmount usePointAmount) {
PointAmount pointBalance = pointBalance(paymentDate);
if (pointBalance.value().compareTo(usePointAmount.value()) < 0) {
throw new BusinessErrorException("Insufficient points");
}
//Use points from the points given so far until this value becomes zero
BigDecimal x = usePointAmount.value();
for (MemberPoint memberPoint : list) {
//Expiration date check
if (memberPoint.hasPassedExpirationDate(paymentDate)) {
continue;
}
//Check the point balance
PointAmount availablePoint = memberPoint.availablePoint(paymentDate);
if (!availablePoint.isPositive()) {
continue;
}
if (availablePoint.value().compareTo(x) <= 0) {
memberPoint.use(availablePoint);
x = x.subtract(availablePoint.value());
} else {
memberPoint.use(new PointAmount(x));
break;
}
}
}
private PointAmount pointBalance(LocalDate paymentDate) {
PointAmount result = new PointAmount(BigDecimal.ZERO);
for (MemberPoint memberPoint : list) {
PointAmount availablePoint = memberPoint.availablePoint(paymentDate);
result = result.add(availablePoint);
}
return result;
}
}
MemberPoint.java
public class MemberPoint implements Entity {
private MemberId memberId;
private LocalDate givenPointDate;
private PointAmount givenPoint;
private PointAmount usedPoint;
/**
*Returns the number of points available on the target date specified by the argument.
*/
PointAmount availablePoint(LocalDate targetDate) {
if (targetDate.compareTo(expirationDate()) > 0) {
return new PointAmount(BigDecimal.ZERO);
}
BigDecimal result = givenPoint.value().subtract(usedPoint.value());
return new PointAmount(result);
}
/**
*Return expiration date.
*/
LocalDate expirationDate() {
return givenPointDate.plusYears(1);
}
/**
*Returns whether the expiration date has passed.Returns true if the expiration date has passed.
*/
boolean hasPassedExpirationDate(LocalDate paymentDate) {
return paymentDate.compareTo(expirationDate()) > 0;
}
}
By creating a method that returns the number of points available to the MemberPoint
class and a method that returns the expiration date and whether the expiration date has passed, it is possible to make the ʻusePoints method of the
MemberPoints` class cleaner. It's done.
Rather than domain-driven design, it's more like object-oriented programming. Both are really difficult: expensive: Especially in my case, I only have tactical DDD, so I don't know the "bounded context" at all ... I hope someday I can also output strategic DDD ...
Also, there was another endpoint of "registering lottery results", but I couldn't think of a good idea, so I gave up this time. (I feel that the endpoint specifications were not good in the first place)
This time, I tried to make a sample program using this database specialist's problem, and I feel that my experience has improved. Also, I would like to challenge using the problems of another year and produce an output that has grown a little more than this time.
Thank you for reading this far. Comments from everyone I am very happy and have a lot to learn, so I would appreciate it if you could comment.
Recommended Posts