Nice to meet you. My name is Kotobukiyama. This is Qiita's first post! !!
Do you guys do Domain Driven Design? If you do Domain Driven Design, you can write a program with high maintainability, so I want to try it! !! It will be. But it's difficult, isn't it? Domain Driven Design ... There are several books, but they are difficult to understand. Many people may want to see more practical code samples. However, in recent years, domain-driven design has become a hot topic at study sessions and events, and I feel that the opportunities to see sample programs are increasing. Therefore, I wanted to write a sample program myself and acquire practical know-how, so I made a sample program with the exam questions of IPA database specialists as the theme.
The source code is here The version at the time of posting this article is tag 1.0.
It wasn't a domain-driven design at all, so it seemed like a lot of tsukkomi would come in, but I managed to get it into shape. This time, I would like to talk about what was good and what didn't work when I actually tried it.
IPA 2019 Spring Database Specialist Exam This is a sample program created by domain-driven design of the tournament management system that was asked in question 1 of 1 pm question.
The table design is the same as the question that was asked.
I extracted only the classes of the domain model and created a class diagram. To avoid cluttering the lines, I've written few associations. Since the Entry class is an abstract class, only the relation of generalization of the concrete class that implements it and the multiplicity of the MemberPoints class and the MemberPoint class are described. I wrote the class diagram later, but there are few methods ... (Is this really what we call Domain Driven Design ...)
This time, I created the following three endpoints.
POST : /applications/entry
POST : /lottery-entry-result
POST : /applications/payment
If you could create a method in the domain model, the Application layer would be ** relatively thin, and if you could hardly create a method in the domain model, the Application layer would be bloated. (It is a natural result ...)
The methods of the Application layer of the three endpoints are as follows
ApplicationCommandService.java
/**
*Apply for entry slot.
*/
public void applyForEntry(ApplyForEntryRequest request) {
final FestivalId festivalId = request.festivalId();
final MemberId memberId = request.memberId();
final EntryId entryId = request.entryId();
final LocalDate applicationDate = request.getApplicationDate();
final Member member = memberRepository.findMember(request.memberId());
if (member == null) {
throw new BusinessErrorException("It is a member that does not exist");
}
final Application alreadyApplication =
applicationRepository.findApplication(festivalId, memberId);
if (alreadyApplication != null) {
throw new BusinessErrorException("I have already applied for the specified tournament");
}
final Entry entry = entryRepository.findEntry(festivalId, entryId);
if (entry == null) {
throw new BusinessErrorException("It is an entry frame that does not exist");
}
final Application application = Application.createEntityForEntry(
festivalId,
memberId,
entryId,
applicationDate
);
entry.validateAndThrowBusinessErrorIfHasErrorForApplication(application);
entry.incrementApplicationNumbers();
entryRepository.saveEntry(entry);
applicationRepository.addApplication(application);
}
LotteryEntryResultCommandService.java
/**
*Register the lottery result.
*/
public void registerLotteryEntryResult(RegisterLotteryEntryResultRequest request) {
final Member member = memberRepository.findMember(request.memberId());
if (member == null) {
throw new BusinessErrorException("It is a member that does not exist");
}
final Entry entry = entryRepository.findEntry(request.festivalId(), request.entryId());
if (entry == null) {
throw new BusinessErrorException("It is an entry frame that does not exist");
}
if (!entry.isLotteryEntry()) {
throw new BusinessErrorException("It is not a lottery entry frame");
}
if (entry.entryStatus() != EntryStatus.underLottery) {
throw new BusinessErrorException("The lottery has not started yet");
}
LotteryEntryResult already = lotteryEntryResultRepository.findLotteryEntryResult(
request.festivalId(),
request.memberId(),
request.entryId()
);
if (already != null) {
throw new BusinessErrorException("The lottery result has already been registered");
}
Application application = applicationRepository.findApplication(
request.festivalId(),
request.memberId());
if (application == null) {
throw new BusinessErrorException("I have not applied for the target tournament");
}
//If it is not the entry slot for which you applied, is it a multi-stage lottery slot for the entry slot you applied for?
if (!(application.entryId().equals(request.entryId()))) {
Entry firstEntry = entryRepository.findEntry(request.festivalId(), application.entryId());
if (!(((LotteryEntry)firstEntry).followingEntryId().equals(request.entryId()))) {
throw new BusinessErrorException("It is an entry frame that is not subject to the lottery");
}
LotteryEntryResult firstEntryResult =
lotteryEntryResultRepository.findLotteryEntryResult(
application.festivalId(),
application.memberId(),
application.entryId());
if (firstEntryResult == null) {
throw new BusinessErrorException("The lottery result of the entry frame you applied for has not been registered yet");
}
if (firstEntryResult.lotteryResult() == LotteryResult.winning) {
throw new BusinessErrorException("Since you have already won, you cannot register the lottery result for the subsequent entry slot.");
}
}
LotteryEntryResult lotteryEntryResult = new LotteryEntryResult(
request.festivalId(),
request.memberId(),
request.entryId(),
request.getLotteryResult()
);
lotteryEntryResultRepository.saveLotteryEntryResult(lotteryEntryResult);
}
PaymentCommandService.java
/**
*Make a deposit.
*/
public void payToApplication(PaymentRequest request) {
final FestivalId festivalId = request.festivalId();
final MemberId memberId = request.memberId();
Application application = applicationRepository.findApplication(festivalId, memberId);
if (application == null) {
throw new BusinessErrorException("I have not applied for the target tournament");
}
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");
}
}
}
}
PointAmount usePoints = new PointAmount(request.getUsePoints());
if (usePoints.isPositive()) {
MemberPoints memberPoints = memberPointRepository.findMemberPoints(memberId);
memberPoints.usePoints(request.getPaymentDate(), usePoints);
memberPointRepository.saveMemberPoints(memberPoints);
}
application.pay(request.getPaymentDate(), usePoints);
applicationRepository.saveApplication(application);
}
I think that "applying for entry frame" and "paying" are still better, but "registering lottery results" has resulted in creating a lot of business error check processing in the Application layer. I wondered if I should create a domain service at such times, but I stopped this time because it seemed that just creating a domain service class would just change the place to write the code. In the first place, I feel that the design of Entity is not working well due to the DB.
For those who "apply for entry" and "deposit", I feel that I was able to design a little object-oriented, so let me introduce you.
There are patterns of first-come-first-served frame and lottery frame in the entry frame, but this time, I created the entry frame with an abstract class and made the first-come-first-served frame and the lottery frame a concrete class.
For example, when applying, the number of applicants is incremented, but each has the following implementation.
FirstArrivalEntry.java
/**First-come-first-served entry frame. */
/**
*Increment the number of applicants.When the number of applicants reaches the capacity, change to confirm participants.
*/
@Override
public void incrementApplicationNumbers() {
applicationNumbers = applicationNumbers.increment();
if (capacity.same(applicationNumbers)) {
entryStatus = EntryStatus.participantConfirmation;
}
}
LotteryEntry.java
/**Lottery entry frame. */
@Override
public void incrementApplicationNumbers() {
applicationNumbers = applicationNumbers.increment();
}
In the case of the first-come-first-served basis, it is necessary to close the application when the number of applicants reaches the capacity. On the client side, the method is executed for the interface as shown below, so I wonder if it could be object-oriented.
ApplicationCommandService.java
final Entry entry = entryRepository.findEntry(festivalId, entryId);
entry.incrementApplicationNumbers();
By the way, in the part that acquires Entity from DB, data is acquired from MyBatis by DTO, and the classes to be generated are divided according to the first-come-first-served frame or the lottery frame. I wonder if this is also the part that worked.
MybatisEntryRepository.java
public Entry findEntry(FestivalId festivalId, EntryId entryId) {
EntryDto dto = entryMapper.selectEntry(festivalId, entryId);
if (dto == null) {
return null;
}
if (dto.firstArrivalLotteryType == FirstArrivalLotteryType.firstArrival) {
return new FirstArrivalEntry(
dto.festivalId,
dto.entryId,
dto.entryName,
dto.entryDescription,
dto.eventCode,
dto.capacity,
dto.participationFees,
dto.applicationNumbers,
dto.applicationStartDate,
dto.applicationEndDate,
dto.entryStatus);
} else {
return new LotteryEntry(
dto.festivalId,
dto.entryId,
dto.entryName,
dto.entryDescription,
dto.eventCode,
dto.capacity,
dto.participationFees,
dto.applicationNumbers,
dto.applicationStartDate,
dto.applicationEndDate,
dto.entryStatus,
dto.lotteryDate,
dto.followingEntryId);
}
}
You can use points when depositing, but this point has an expiration date of one year, and it is a specification that the one with the closest expiration date will be used, so I think that this worked well.
I am using the collection objects introduced in Principles of system design useful in the field.
MemberPoint.java
public class MemberPoint implements Entity {
private MemberId memberId;
private LocalDate givenPointDate;
private PointAmount givenPoint;
private PointAmount usedPoint;
/**
*Use points for the value used in the argument.
*/
void use(BigDecimal value) {
BigDecimal totalUsedPointAmountValue = usedPoint.value().add(value);
if (totalUsedPointAmountValue.compareTo(givenPoint.value()) > 0) {
throw new IllegalArgumentException("There are more used points than granted points");
}
usedPoint = new PointAmount(totalUsedPointAmountValue);
}
}
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");
}
}
}
From the client, I was able to check the point balance and change the status of multiple Member Points with the following code.
MemberPoints memberPoints = memberPointRepository.findMemberPoints(memberId);
memberPoints.usePoints(request.getPaymentDate(), usePoints);
memberPointRepository.saveMemberPoints(memberPoints);
In the end, it became a reasonable volume, and I'm glad that I was able to create a relatively practical sample, including validation and saving to the DB. However, what I actually tried and wanted to improve is as follows.
In order to improve these, I think I'll try refactoring or practicing with problems of different years (maybe I should have chosen a problem with more calculations).
Also, if you read this article and have an opinion such as "Isn't it better if you do this?", I would appreciate it if you could comment. Thank you for reading this far! !!
Recommended Posts