Ravi de vous rencontrer. Mon nom est Kotobukiyama. Ceci est le premier message de Qiita! !!
Faites-vous du design piloté par domaine? Si vous faites de la conception basée sur le domaine, vous pouvez écrire un programme avec une maintenabilité élevée, donc je veux l'essayer! !! Ce sera. Mais c'est difficile, n'est-ce pas? Il existe plusieurs livres, mais ils sont difficiles à comprendre. De nombreuses personnes voudront peut-être voir des exemples de code plus pratiques. Cependant, ces dernières années, la conception axée sur le domaine est devenue un sujet brûlant lors des sessions d'étude et des événements, et j'estime que les opportunités de voir des exemples de programmes se multiplient. Par conséquent, je voulais rédiger moi-même un exemple de programme et acquérir un savoir-faire pratique, j'ai donc créé un exemple de programme avec les questions d'examen des spécialistes de la base de données IPA comme thème.
Le code source est ici La version au moment de la publication de cet article est la balise 1.0.
Ce n'était pas du tout une conception axée sur le domaine, donc il semblait que beaucoup de tsukkomi entreraient, mais j'ai réussi à le mettre en forme. Cette fois, j'aimerais parler de ce qui était bon et de ce qui n'a pas fonctionné lorsque je l'ai essayé.
Examen de spécialiste de base de données IPA printemps 2019 Il s'agit d'un exemple de programme créé par la conception du système de gestion de tournoi axée sur le domaine qui a été posée à la question 1 de la question de 13 h.
Page de téléchargement du PDF du problème
La conception de la table est la même que la question qui a été posée.
J'ai extrait uniquement la classe du modèle de domaine et créé un diagramme de classes. Pour ne pas encombrer les lignes, j'ai écrit quelques associations. La classe Entry étant une classe abstraite, seule la relation de généralisation de la classe concrète qui l'implémente et la multiplicité des classes MemberPoints et MemberPoint sont décrites. J'ai écrit le diagramme de classes plus tard, mais il y a peu de méthodes ... (Est-ce vraiment une conception axée sur le domaine ...)
Cette fois, j'ai créé les trois points de terminaison suivants.
POST : /applications/entry
POST : /lottery-entry-result
POST : /applications/payment
Si vous pouviez créer une méthode dans le modèle de domaine, la couche Application serait ** relativement mince et si vous pouviez difficilement créer une méthode dans le modèle de domaine, la couche Application serait gonflée. (C'est un résultat naturel ...)
Les méthodes de la couche Application des trois points de terminaison sont les suivantes
ApplicationCommandService.java
/**
*Demander un créneau d'inscription.
*/
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("C'est un membre qui n'existe pas");
}
final Application alreadyApplication =
applicationRepository.findApplication(festivalId, memberId);
if (alreadyApplication != null) {
throw new BusinessErrorException("J'ai déjà postulé pour le tournoi spécifié");
}
final Entry entry = entryRepository.findEntry(festivalId, entryId);
if (entry == null) {
throw new BusinessErrorException("C'est un cadre d'entrée qui n'existe pas");
}
final Application application = Application.createEntityForEntry(
festivalId,
memberId,
entryId,
applicationDate
);
entry.validateAndThrowBusinessErrorIfHasErrorForApplication(application);
entry.incrementApplicationNumbers();
entryRepository.saveEntry(entry);
applicationRepository.addApplication(application);
}
LotteryEntryResultCommandService.java
/**
*Enregistrer le résultat de la loterie.
*/
public void registerLotteryEntryResult(RegisterLotteryEntryResultRequest request) {
final Member member = memberRepository.findMember(request.memberId());
if (member == null) {
throw new BusinessErrorException("C'est un membre qui n'existe pas");
}
final Entry entry = entryRepository.findEntry(request.festivalId(), request.entryId());
if (entry == null) {
throw new BusinessErrorException("C'est un cadre d'entrée qui n'existe pas");
}
if (!entry.isLotteryEntry()) {
throw new BusinessErrorException("Ce n'est pas un cadre d'entrée de loterie");
}
if (entry.entryStatus() != EntryStatus.underLottery) {
throw new BusinessErrorException("La loterie n'a pas encore commencé");
}
LotteryEntryResult already = lotteryEntryResultRepository.findLotteryEntryResult(
request.festivalId(),
request.memberId(),
request.entryId()
);
if (already != null) {
throw new BusinessErrorException("Le résultat de la loterie a déjà été enregistré");
}
Application application = applicationRepository.findApplication(
request.festivalId(),
request.memberId());
if (application == null) {
throw new BusinessErrorException("Je n'ai pas postulé pour le tournoi cible");
}
//Si ce n'est pas le cadre d'entrée pour lequel vous avez postulé, s'agit-il d'un cadre de loterie à plusieurs étapes pour le cadre d'entrée que vous avez demandé?
if (!(application.entryId().equals(request.entryId()))) {
Entry firstEntry = entryRepository.findEntry(request.festivalId(), application.entryId());
if (!(((LotteryEntry)firstEntry).followingEntryId().equals(request.entryId()))) {
throw new BusinessErrorException("C'est un cadre d'entrée qui n'est pas soumis à la loterie");
}
LotteryEntryResult firstEntryResult =
lotteryEntryResultRepository.findLotteryEntryResult(
application.festivalId(),
application.memberId(),
application.entryId());
if (firstEntryResult == null) {
throw new BusinessErrorException("Le résultat de la loterie du cadre de participation pour lequel vous avez postulé n'a pas encore été enregistré");
}
if (firstEntryResult.lotteryResult() == LotteryResult.winning) {
throw new BusinessErrorException("Puisque vous avez déjà gagné, vous ne pouvez pas enregistrer le résultat de la loterie pour la fente d'inscription suivante.");
}
}
LotteryEntryResult lotteryEntryResult = new LotteryEntryResult(
request.festivalId(),
request.memberId(),
request.entryId(),
request.getLotteryResult()
);
lotteryEntryResultRepository.saveLotteryEntryResult(lotteryEntryResult);
}
PaymentCommandService.java
/**
*Faire un dépôt.
*/
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("Je n'ai pas postulé pour le tournoi cible");
}
Entry entry = entryRepository.findEntry(festivalId, application.entryId());
if (entry.isLotteryEntry()) {
//Si l'entrée cible est une loterie, vérifiez si elle a gagné
LotteryEntryResult entryResult = lotteryEntryResultRepository.findLotteryEntryResult(
festivalId, memberId, entry.entryId());
if (entryResult.lotteryResult() == LotteryResult.failed) {
EntryId followingEntryId = ((LotteryEntry)entry).followingEntryId();
if (followingEntryId == null) {
throw new BusinessErrorException("Je n'ai pas gagné le tournoi cible");
} else {
LotteryEntryResult followingEntryResult =
lotteryEntryResultRepository.findLotteryEntryResult(
festivalId, memberId, followingEntryId);
if (followingEntryResult.lotteryResult() == LotteryResult.failed) {
throw new BusinessErrorException("Je n'ai pas gagné le tournoi cible");
}
}
}
}
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);
}
Je pense que «demander le cadre de saisie» et «payer» sont toujours meilleurs, mais «enregistrer les résultats de la loterie» a entraîné la création de nombreux traitements de vérification des erreurs commerciales dans la couche Application. Je me suis demandé si je devais créer un service de domaine à de tels moments, mais j'ai arrêté cette fois-ci car il semblait que la simple création d'une classe de service de domaine changerait simplement l'endroit où écrire le code. En premier lieu, je pense que la conception de l'entité a été déplacée vers la base de données et ne fonctionne pas bien.
Pour ceux qui "postulent pour entrée" et "dépôt", je sens que j'ai pu concevoir un design un peu orienté objet, alors laissez-moi vous le présenter.
Il y a un modèle de cadre premier arrivé, premier servi et de cadre de loterie dans le cadre d'entrée, mais cette fois, j'ai créé le cadre d'entrée avec une classe abstraite et fait du cadre premier arrivé, premier servi et du cadre de loterie une classe concrète.
Par exemple, lors de l'application, le nombre de candidats est incrémenté, mais chacun a la mise en œuvre suivante.
FirstArrivalEntry.java
/**Cadre d'entrée premier arrivé, premier servi. */
/**
*Augmenter le nombre de candidats.Lorsque le nombre de candidats atteint la capacité, changez pour confirmer les participants.
*/
@Override
public void incrementApplicationNumbers() {
applicationNumbers = applicationNumbers.increment();
if (capacity.same(applicationNumbers)) {
entryStatus = EntryStatus.participantConfirmation;
}
}
LotteryEntry.java
/**Cadre d'entrée de loterie. */
@Override
public void incrementApplicationNumbers() {
applicationNumbers = applicationNumbers.increment();
}
Dans le cas du principe du premier arrivé, premier servi, il est nécessaire de clôturer l'application lorsque le nombre de candidats atteint la capacité. Côté client, la méthode est exécutée pour l'interface comme indiqué ci-dessous, donc je me demande si elle pourrait être orientée objet.
ApplicationCommandService.java
final Entry entry = entryRepository.findEntry(festivalId, entryId);
entry.incrementApplicationNumbers();
À propos, dans la partie qui acquiert Entity de DB, les données sont acquises à partir de MyBatis par DTO, et la classe à générer est divisée en trame premier arrivé, premier servi ou trame de loterie. Je me demande si c'est aussi une partie réussie.
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);
}
}
Vous pouvez utiliser des points lors du dépôt, mais ce point a une date d'expiration d'un an, et c'est une spécification que celui avec la date d'expiration la plus proche sera utilisé, donc je pense que cela a bien fonctionné.
Nous utilisons les objets de collection introduits dans Principes de conception de système utiles sur le terrain.
MemberPoint.java
public class MemberPoint implements Entity {
private MemberId memberId;
private LocalDate givenPointDate;
private PointAmount givenPoint;
private PointAmount usedPoint;
/**
*Utiliser des points pour la valeur utilisée dans l'argument.
*/
void use(BigDecimal value) {
BigDecimal totalUsedPointAmountValue = usedPoint.value().add(value);
if (totalUsedPointAmountValue.compareTo(givenPoint.value()) > 0) {
throw new IllegalArgumentException("Il y a plus de points utilisés que de points accordés");
}
usedPoint = new PointAmount(totalUsedPointAmountValue);
}
}
MemberPoints.java
public class MemberPoints {
private List<MemberPoint> list;
/**
*Déterminer si le point de l'argument peut être utilisé et l'utiliser à partir du point proche de la date d'expiration.
*Modifiez l'état de l'objet MemberPoint à conserver.
*/
public void usePoints(LocalDate paymentDate, PointAmount pointAmount) {
//Utilisez les points des points donnés jusqu'à présent jusqu'à ce que cette valeur devienne zéro
BigDecimal x = pointAmount.value();
for (MemberPoint memberPoint : list) {
//Vérification de la date d'expiration
LocalDate expirationDate = memberPoint.givenPointDate().plusYears(1);
if (paymentDate.compareTo(expirationDate) > 0) {
continue;
}
//Vérifier le solde des points
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;
}
}
//Utilisez des points de celui avec la date d'expiration la plus proche, et une erreur si le nombre de points que vous souhaitez utiliser n'est pas atteint
if (x.compareTo(BigDecimal.ZERO) > 0) {
throw new BusinessErrorException("Points insuffisants");
}
}
}
Depuis le client, j'ai pu vérifier le solde de points et modifier le statut de plusieurs points membres avec le code suivant.
MemberPoints memberPoints = memberPointRepository.findMemberPoints(memberId);
memberPoints.usePoints(request.getPaymentDate(), usePoints);
memberPointRepository.saveMemberPoints(memberPoints);
En fin de compte, c'est devenu un volume raisonnable, et je suis heureux d'avoir pu créer un échantillon relativement pratique, y compris la validation et l'enregistrement dans la base de données. Cependant, ce que j'ai réellement essayé et voulu améliorer est le suivant.
Afin de les améliorer, je pense que je vais essayer de refactoriser ou de pratiquer avec des problèmes d'années différentes (peut-être aurais-je dû choisir un problème avec plus de calculs).
Aussi, si vous lisez cet article et avez une opinion telle que "N'est-ce pas mieux si vous faites cela?", Je vous serais reconnaissant si vous pouviez commenter. Merci d'avoir lu jusqu'ici! !!
Recommended Posts