[JAVA] J'ai essayé de créer un exemple de programme en utilisant le problème du spécialiste des bases de données dans la conception pilotée par domaine

introduction

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é.

Spécifications d'application, etc.

problème

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

Limites

Diagramme ER

La conception de la table est la même que la question qui a été posée.

er diagram.png

Diagramme de classe

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 ...)

class diagram.png

Point de terminaison de l'API

Cette fois, j'ai créé les trois points de terminaison suivants.

Demander un créneau d'inscription

POST : /applications/entry

Enregistrer le résultat de la loterie

POST : /lottery-entry-result

Faire un dépôt

POST : /applications/payment

Ce qui était bon et ce qui ne l'était pas

La couche d'application est gonflée

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.

Demander un créneau d'inscription

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);
    }
  }

Faire un dépôt

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);

à la fin

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

J'ai essayé de créer un exemple de programme en utilisant le problème du spécialiste des bases de données dans la conception pilotée par domaine
Création d'un exemple de programme en utilisant le problème d'un spécialiste des bases de données dans DDD Improvement 2
Création d'un exemple de programme en utilisant le problème d'un spécialiste des bases de données avec DDD Improvement 1
J'ai créé un client RESAS-API en Java
J'ai essayé de convertir l'exemple d'application en microservice selon l'idée du livre "Microservice Architecture".
J'ai essayé de créer une classe parent d'objet de valeur dans Ruby
J'ai essayé d'utiliser pleinement le cœur du processeur avec Ruby
J'ai essayé de créer une application de conversation en Java à l'aide de l'IA «A3RT»
J'ai fait un exemple de la façon d'écrire un délégué dans Swift UI 2.0 à l'aide de MapKit
J'ai essayé d'utiliser une connexion à une base de données dans le développement Android
J'ai trébuché lorsque j'ai essayé d'utiliser neo4j dans l'environnement jenv, alors prenez note
J'ai essayé de résoudre le problème de la "sélection multi-étapes" avec Ruby
J'ai essayé d'illuminer le sapin de Noël dans un jeu de la vie
[Unity] J'ai essayé de créer un plug-in natif UniNWPathMonitor en utilisant NWPathMonitor
[Java] J'ai essayé de faire un labyrinthe par la méthode de creusage ♪
J'ai essayé de résoudre le problème de Google Tech Dev Guide
[Rails] Implémentation de la fonction de catégorie multicouche en utilisant l'ascendance "J'ai essayé de créer une fenêtre avec Bootstrap 3"
Après avoir appris Progate, j'ai essayé de créer une application SNS en utilisant Rails dans l'environnement local
J'ai essayé un problème de calendrier avec Ruby
J'ai essayé de créer une simple application Android de reconnaissance faciale en utilisant OpenCV
J'ai essayé de résumer les points clés de la conception et du développement de gRPC
J'ai essayé de résoudre le problème de la séquence Tribonacci en Ruby, avec récurrence.
Exemple de programme qui renvoie la valeur de hachage d'un fichier en Java
Faire une marge à gauche du TextField
J'ai essayé d'organiser la session en Rails
Ce que j'ai essayé quand je voulais obtenir tous les champs d'un haricot
J'ai essayé de résoudre le problème de la séquence Tribonacci en Ruby (temps limite 10 minutes)
Je voulais que (a == 1 && a == 2 && a == 3) vrai en Java
J'ai créé un programme qui recherche la classe cible à partir du processus surchargé avec Java
Comment créer une combinaison unique de données dans la table intermédiaire des rails
[Petite histoire] J'ai essayé de rendre java ArrayList un peu plus pratique
J'ai essayé d'implémenter un serveur en utilisant Netty
J'ai essayé d'utiliser le profileur d'IntelliJ IDEA
J'ai essayé de créer une application Web qui recherche les tweets avec vue-word cloud et examine la tendance de ce qui est écrit dans le profil associé
[Swift] J'ai déjà beaucoup d'informations, mais j'ai essayé de résumer le casting (comme, comme!, Comme?) À ma manière.
J'ai essayé de créer un outil de comparaison des prix des produits Amazon dans le monde entier avec Java, l'API Amazon Product Advertising, l'API Currency (29/01/2017)
J'ai essayé de créer une fonction de message de l'extension Rails Tutorial (Partie 1): Créer un modèle
[Rails] Je souhaite afficher la destination du lien de link_to dans un onglet séparé
J'ai essayé d'étudier le mécanisme d'Emscripten en l'utilisant avec un solveur allemand
Exemple de code pour attribuer une valeur dans le fichier de propriétés à un champ du type attendu
J'ai essayé d'utiliser la fonction Server Push de Servlet 4.0
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de réduire la capacité de Spring Boot
J'ai essayé de créer une compétence Clova en Java
J'ai écrit un diagramme de séquence de l'exemple j.u.c.Flow
Utilisation de la base de données (SQL Server 2014) à partir d'un programme Java 04/01/2018
J'ai essayé d'implémenter la méthode de division mutuelle d'Eugrid en Java
Nous avons créé un environnement pour exécuter des tests unitaires à l'aide de la base de données Oracle (oracle12c) sur l'image Docker in Docker (dind) de GitLab-CI
[VBA] J'ai créé un outil pour convertir le type primitif de la classe Entity générée par Hibernate Tools en type de référence correspondant.
J'ai fini de regarder les roses de Versailles, alors j'ai essayé de reproduire la chanson de fin en Java
J'ai essayé de faire une fonction de réponse de l'extension Rails Tutorial (Partie 3): Correction d'un malentendu des spécifications
Exemple de code pour obtenir les valeurs des principaux types SQL dans Java + Oracle Database 12c
J'ai essayé de résoudre le problème de la boisson bonus Ruby (il y a un exemple de réponse)
J'ai essayé de toucher l'application de gestion d'actifs en utilisant l'émulateur du grand livre distribué Scalar DLT