Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

痒いところに手を届かせるArchUnitの利用法 ~アーキテクチャーテストからアプリケーション...

Recruit
December 25, 2020

痒いところに手を届かせるArchUnitの利用法 ~アーキテクチャーテストからアプリケーション解析まで~

2020/11/07_日本Javaユーザグループ主催「JJUG CCC 2020 Fall」での、小谷野の講演資料になります
https://ccc2020fall.java-users.jp/

【動画】https://www.youtube.com/watch?v=tigesXMmTpc&list=PLy44EKO1L0eIxmapLWe6_60nJwt_EAsjU&index=2

#conference #engineering

Recruit

December 25, 2020
Tweet

More Decks by Recruit

Other Decks in Technology

Transcript

  1. 8 IUUQTXXXBSDIVOJUPSHVTFSHVJEFIUNM@*OEFYIUNM JUnitͱͷ࿈ܞ @RunWith(ArchUnitRunner.class) // JUnit5Ͱ͸ෆཁ @AnalyzeClasses(packages = "com.myapp") public

    class ArchitectureTest { @ArchTest public static final ArchRule rule1 = classes()
 .should()... @ArchTest public static final ArchRule rule2 = classes() .should()... @ArchTest public static void rule3(JavaClasses classes) { // } } w +6OJUͱ࿈ܞͤ͞Δ͜ͱͰ
 "SVD6OJUͰ࣮૷੍ͨ͠໿Λ
 ͦͷ··ςετϝιου
 ͱͯ͠ఆٛͰ͖Δ w ଞͷςετϑϨʔϜϫʔΫʹ
 ૊ΈࠐΉ͜ͱ΋Մೳ
  2.  9 classes() .that() .resideInAPackage("..foo..") .should() .onlyHaveDependentClassesThat() .resideInAnyPackage( "..source.one..", "..foo.."

    ); ੍໿ΛJavaίʔυͷUTͰ࣮૷Ͱ͖Δ IUUQTXXXBSDIVOJUPSHVTFSHVJEFIUNM@*OEFYIUNM
  3.  16 // ΫϥεϑΝΠϧ͔ΒJavaClassesΠϯελϯεΛऔಘ JavaClasses importedClasses = new ClassFileImporter() //

    ςετίʔυͷbytecode͸আ֎͢Δ .withImportOption( ImportOption.d.DO_NOT_INCLUDE_TESTS ) // Ϋϥεύε্ͷbytecodeΛಡΈࠐΉ .importClasspath(); Core APIͱbytecode w $MBTT'JMF*NQPSUFSʹΑͬͯ
 ࢦఆͨ͠ΫϥεϑΝΠϧͷ CZUFDPEFΛಡΈࠐΉ w "4.ϥΠϒϥϦΛར༻ͯ͠
 CZUFDPEFΛղੳͯ͠
 "SDI6OJUݻ༗ͷදݱʹม׵͠
 $PSF"1*Λհͯ͠։ൃऀʹ
 ৘ใΛఏڙ
  4.  20 // serviceͱ͍͏จࣈΛؚΉpackage഑ԼͷΫϥε͸
 // controllerͱ͍͏จࣈΛؚΉpackage഑ԼͷΫϥεʹ
 // ΞΫηεͯ͠͸͍͚ͳ͍
 ArchRule rule

    = ArchRuleDefinition
 .noClasses() .that()
 .resideInAPackage("..service..") .should()
 .accessClassesThat()
 .resideInAPackage("..controller.."); 
 rule.check(importedClasses);
 Lang API w $PSF"1*͔ΒऔಘͰ͖Δ৘ใΛ ݩʹґଘؔ܎ͷ੍໿Λॊೈ͔ͭ ؆ܿʹ࣮૷͢ΔͨΊͷ"1* w ੍໿ʹҧ൓͢ΔΫϥεͳͲ͕
 ଘࡏͨ͠৔߹ʹςετΛ
 ࣦഊͤ͞Δ͜ͱ͕Ͱ͖Δ w جຊతʹ͸͜ͷ"1*Λ࢖ͬͯ
 ςετΛ࣮૷͢Δ͜ͱ͕ଟ͍
  5.  21 // ݕࠪର৅ͷΫϥε͕@PayloadΞϊςʔγϣϯ͕෇༩͞Εͨ
 // ϑΟʔϧυΛ͍࣋ͬͯΔ͔ΛνΣοΫ͢ΔΧελϜϧʔϧ
 DescribedPredicate<JavaClass>
 haveAFieldAnnotatedWithPayload = new

    DescribedPredicate<>( "have a field annotated with @Payload” ) { @Override public boolean apply(JavaClass input) { return checkCustomRule(input); } }; Lang APIͱΧελϜϧʔϧ w %FTDSJCFE1SFEJDBUFͳͲͷ
 ΠϯλʔϑΣʔεΛ࣮૷ͯ͠
 ಠࣗͷΧελϜϧʔϧΛ
 ࡞੒͢Δ͜ͱ͕Մೳ w ࣮ݱ͍ͨ͠ϧʔϧʹରԠ͢Δ
 "1*͕ଘࡏ͠ͳ͍৔߹͸
 -BOH"1*ͱ$PSF"1*Λ
 ૊Έ߹Θ੍ͤͯ໿Λ࣮૷͢Δ
  6.  22 Library API w ࢦఆͨ͠ύοέʔδΛ
 άϧʔϐϯάͯ͠యܕతͳ
 ΞʔΩςΫνϟʔͷ
 ϨΠϠʔͱඥ෇͚Δ
 "1*Λఏڙ

    w ϨΠϠʔͷґଘؔ܎੍໿Λ
 ͦͷ··ΞϓϦέʔγϣϯʹ
 ద༻͢Δ͜ͱ͕Ͱ͖Δ layeredArchitecture() .layer("Controller").definedBy("..controller..") .layer("Service").definedBy("..service..") .layer("Persistence").definedBy("..persistence..") .whereLayer("Controller").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller") .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
  7. w "SDI6OJUͷಛ௃
 ઃఆϑΝΠϧ΍%4-Ͱ͸ͳ͘ϥΠϒϥϦ͕ఏڙ͢Δ"1*Λ༻͍ͨ
 +BWBίʔυͰ+6OJUͳͲͷ୯ମςετͱͯ͠هड़Ͱ͖Δ w ྨࣅͷ͜ͱ͕Ұ෦࣮ݱͰ͖Δπʔϧ
 ੍໿ݕࠪɿ"TQFDU+ $IFDLTUZMF 'JOE#VHT
 ґଘؔ܎ɿK2"TTJTUBOU

    %FQFOE %FHSBQI %FQUFDUJWF ArchUnitͷಛ௃ͱྨࣅπʔϧͱͷൺֱ 25 
 
 "SDI6OJUͷศརͦ͏ͳ͜ͱ͸Θ͔Δɻ
 ͚ͩͲɺ࣮ࡍʹͲ͏͍͏࢖͍ํΛͯ͠
 ΞϓϦέʔγϣϯʹಋೖ͍ͯ͠Δͷ͔ʁ Α͋͘Δٙ໰
  8. w ϔΞαϩϯɾϦϥΫϏϡʔςΟαϩϯͷ ݕࡧɾ༧໿αʔϏε w ϞόΠϧΞϓϦ J04"OESPJE 
 8FC 1$ɾεϚʔτϑΥϯ 


    ʹͯల։ w ϞόΠϧΞϓϦ޲͚"1*ͱ
 "OESPJEΞϓϦʹ"SDI6OJUͷಋೖ ϗοτϖούʔϏϡʔςΟʔͱ͸ 27 IUUQTCFBVUZIPUQFQQFSKQEPDHVJEFTBJTIJOEBUBIUNM
  9. ϞόΠϧΞϓϦ޲͚APIͷΞʔΩςΫνϟʔ #BDLFOE
 "1*
 GPS403 #''"1*
 GPS40& J04"OESPJE ΞϓϦ %#ݕࡧΤϯδϯ 28

    w #'' #BDLFOET'PS'SPOUFOET Λར༻ͨ͠
 ϚΠΫϩαʔϏεΞʔΩςΫνϟʔΛಋೖ w ΞϓϦέʔγϣϯن໛΍νʔϜߏ੒Λߟྀͨ͠40&ɾ403ͷ෼཭ʹΑͬͯ
 "1*։ൃεϐʔυͷߴ଎ԽΛ໨ࢦ͢
  10. ϞόΠϧΞϓϦ޲͚APIͷΞʔΩςΫνϟʔ #BDLFOE
 "1*
 GPS403 #''"1*
 GPS40& J04"OESPJE ΞϓϦ %#ݕࡧΤϯδϯ 30

    w ΞϓϦͷϢʔεέʔε୯ҐͰ࡞੒ͨ͠#''ͷ"1*͸
 σʔλͷϦιʔε୯ҐͰ࡞੒ͨ͠#BDLFOEͷ"1*͔Β
 ඞཁͳ৘ใΛू໿ͯ͠ΞϓϦʹఏڙ A C A B C B
  11.  TRYɿಛఆ৚݅ԼͰͷΠϯϙʔτΛ๷ࢭ 41 @RunWith(ArchUnitRunner.class) @AnalyzeClasses(packages = BASE_PACKAGE) public class HairKireiDependencyTest

    { @ArchTest static final ArchRule 
 hairύοέʔδͷΫϥε͕kireiύοέʔδͷΫϥεʹґଘͯ͠ͳ͍͜ͱΛ֬ೝ = classes() .that() // ύοέʔδ໊ͷύεʹhairΛؚΉΫϥεΛநग़ .resideInAPackage("..hair..") // Ұ෦ͷྫ֎Λআ͍ͯɺύοέʔδ໊ͷύεʹkireiΛؚΉΫϥε΁ͷґଘΛېࢭ .should(ArchConditions.onlyDependOnClassesThat(NOT_DEPEND_KIREI_CLASS)); }
  12.  TRYɿಛఆ৚݅ԼͰͷΠϯϙʔτΛ๷ࢭ 42 private static final DescribedPredicate<JavaClass> NOT_DEPEND_KIREI_CLASS = new

    DescribedPredicate<>("must not depend on classes in kirei package") { 
 @Override public boolean apply(JavaClass input) { if (!input.getPackageName().contains("kirei")) { return true; } // @KireiResourceUsedByHairΞϊςʔγϣϯ͕෇༩͞Ε͍ͯΔΫϥε͸ྫ֎తʹґଘΛڐՄ͢Δ if (input.isAnnotatedWith(KireiResourceUsedByHair.class)) { return true; } } };
  13.  ՝୊ɿෆ҆ఆͳίʔυ͕ϝΠϯϥΠϯʹ߹ྲྀ 48  #PPLNBSL6TFDBTF #PPLNBSL3FQPTJUPSZ υϝΠϯϞσϧ $PVQPO6TFDBTF #PPLNBSL4FSWJDF $PVQPO4FSWJDF

    $PVQPO3FQPTJUPSZ υϝΠϯϞσϧ ໼ҹ͸ґଘؔ܎ͷ޲͖ &YQFSJNFOUBM6TFDBTF &YQFSJNFOUBM4FSWJDF &YQFSJNFOUBM3FQPTJUPSZ υϝΠϯϞσϧ
  14.  ՝୊ɿෆ҆ఆͳίʔυ͕ϝΠϯϥΠϯʹ߹ྲྀ 49  #PPLNBSL6TFDBTF #PPLNBSL3FQPTJUPSZ υϝΠϯϞσϧ $PVQPO6TFDBTF &YQFSJNFOUBM6TFDBTF #PPLNBSL4FSWJDF

    $PVQPO4FSWJDF &YQFSJNFOUBM4FSWJDF $PVQPO3FQPTJUPSZ &YQFSJNFOUBM3FQPTJUPSZ υϝΠϯϞσϧ ໼ҹ͸ґଘؔ܎ͷ޲͖ υϝΠϯϞσϧ
  15.  ՝୊ɿෆ҆ఆͳίʔυ͕ϝΠϯϥΠϯʹ߹ྲྀ 50  #PPLNBSL6TFDBTF #PPLNBSL3FQPTJUPSZ υϝΠϯϞσϧ $PVQPO6TFDBTF &YQFSJNFOUBM6TFDBTF #PPLNBSL4FSWJDF

    $PVQPO4FSWJDF &YQFSJNFOUBM4FSWJDF $PVQPO3FQPTJUPSZ &YQFSJNFOUBM3FQPTJUPSZ υϝΠϯϞσϧ ໼ҹ͸ґଘؔ܎ͷ޲͖ υϝΠϯϞσϧ
  16.  ՝୊ɿෆ҆ఆͳίʔυ͕ϝΠϯϥΠϯʹ߹ྲྀ 51  #PPLNBSL6TFDBTF #PPLNBSL3FQPTJUPSZ υϝΠϯϞσϧ $PVQPO6TFDBTF &YQFSJNFOUBM6TFDBTF #PPLNBSL4FSWJDF

    $PVQPO4FSWJDF &YQFSJNFOUBM4FSWJDF $PVQPO3FQPTJUPSZ &YQFSJNFOUBM3FQPTJUPSZ υϝΠϯϞσϧ ໼ҹ͸ґଘؔ܎ͷ޲͖ υϝΠϯϞσϧ
  17.  52  #PPLNBSL6TFDBTF #PPLNBSL3FQPTJUPSZ υϝΠϯϞσϧ $PVQPO6TFDBTF &YQFSJNFOUBM6TFDBTF #PPLNBSL4FSWJDF $PVQPO4FSWJDF

    &YQFSJNFOUBM4FSWJDF $PVQPO3FQPTJUPSZ &YQFSJNFOUBM3FQPTJUPSZ υϝΠϯϞσϧ ໼ҹ͸ґଘؔ܎ͷ޲͖ υϝΠϯϞσϧ TRYɿෆ҆ఆͳίʔυ΁ґଘͰ͖ΔΫϥε΍
 ɹɹ ϝιουΛςετʹΑΓ੍ݶ͢Δ
  18.  TRYɿෆ҆ఆͳίʔυ΁ґଘͰ͖ΔΫϥε΍
 ɹɹ ϝιουΛςετʹΑΓ੍ݶ͢Δ 53 @ArchTest val `ExperimentalΞϊςʔγϣϯ͕෇༩͞ΕͨΫϥε͕
 ExperimentalΞϊςʔγϣϯ͕෇༩͞ΕͨΫϥε͔ΒͷΈґଘ͞Ε͍ͯΔ͜ͱΛ֬ೝ` =

    
 classes() .that() .areAnnotatedWith(Experimental::class.java) .should(ArchConditions.onlyHaveDependentClassesThat(ANNOTATED_CLASSES)) @ArchTest val `ExperimentalΞϊςʔγϣϯ͕෇༩͞Εͨϝιου͕
 ExperimentalΞϊςʔγϣϯ͕෇༩͞Εͨϝιου΍Ϋϥε͔ΒͷΈґଘ͞Ε͍ͯΔ͜ͱΛ֬ೝ` = 
 methods() .that() .areAnnotatedWith(TEExperimental::class.java) .should(DEPENDENT_ONLY_ON_ANNOTATED_CODE)
  19.  TRYɿෆ҆ఆͳίʔυ΁ґଘͰ͖ΔΫϥε΍
 ɹɹ ϝιουΛςετʹΑΓ੍ݶ͢Δ 54 private fun checkRecursively(input: JavaClass): Boolean

    { return when { // ExperimentalΞϊςʔγϣϯ͸਌Ϋϥεʹ͔͠෇༩͞Ε͍ͯͳ͍ͷͰ // ಗ໊Ϋϥε΍ωετͷࢠΫϥεͷ৔߹͸࠶ؼతʹ਌ΫϥεΛݟʹߦ͘ input.isAnonymousClass || input.isNestedClass -> checkRecursively( input.enclosingClass.get() ) input.isAnnotatedWith(Experimental::class.java) -> true else -> false } }
  20. 56 ΞϓϦέʔγϣϯղੳ΁ͷར༻๏ CASE 3 : PRࠩ෼ͷӨڹൣғͷݕ஌ CASE 4 : ࢓༷೺ѲϚοϓͷࣗಈ࡞੒

    CASE 5 : αʔϏεؒґଘؔ܎ͷՄࢹԽ CASE 6 : ը໘ભҠਤͷࣗಈੜ੒
  21.  w ଟ͘ͷΫϥε͔Βґଘ͞Ε͍ͯΔඃґଘ౓͕
 ߴ͍ΫϥεͷΤϯϋϯε΍ϦϑΝΫλͰ͸
 ൺֱతมߋͷӨڹൣғ͕େ͖͘
 σάϨ͕ൃੜ͢Δ֬཰͕ߴ͍ w ґଘΫϥεଆͰ͸ίʔυ্ͷࠩ෼͕Ͱ΋
 ಺෦࢓༷΁ͷ҉໧తͳґଘʹΑͬͯ
 ࢓༷ͷഁյతมߋͷӨڹΛड͚Δ৔߹͕͋Δ

    58 എܠɿࠩ෼ͷӨڹൣғͱσάϨ // Τϯϋϯεલ : ਖ਼ͷ੔਺Λฦ͢
 // Τϯϋϯεޙ : ੔਺Λฦ͢
 public int calculate(int input){ var output = ... return output; } 
 // calculate()͕ਖ਼ͷ੔਺Λฦ͢ͱ͍͏
 // લఏʹґଘͯ͠ॲཧΛ࣮ߦ͢Δϝιου public int useCalculate(){ var result = calculate(10); return ... }
  22.  ิ଍ɿݕग़Ͱ͖ͳ͍Өڹൣғʹ஫ҙ w ੩తͳґଘղܾͰ͸ݕग़Ͱ͖ͳ͍έʔεʹ஫ҙ͢Δ
 ϦϑϨΫγϣϯͳͲಈతͳґଘղܾ
 ΞϓϦέʔγϣϯϑϨʔϜϫʔΫʹؔΘΔมߋ
 ઃఆϑΝΠϧͷมߋ w ͋͘·ͰӨڹൣғͷ೺Ѳͷαϙʔτͱͯ͠࢖͏ w

    σάϨογϣϯ๷ࢭ͕໨తͳΒ͹
 ΑΓྑ͍ઃܭ΍ςετͰͦ΋ͦ΋σάϨογϣϯ͕
 ൃੜ͠ʹ͘͘͢Δͷʹӽͨ͜͠ͱ͸ͳ͍ 62 03FJMMZ.FEJB *OD
 ʮ'VOEBNFOUBMTPG4PGUXBSF"SDIJUFDUVSFʯ
  23.  w ύοέʔδ΍Ϋϥεͷ
 ϨΠϠʔຖͷ໋໊نଇΛ
 ߟྀ͢Ε͹ΑΓޮ཰తʹ
 γʔέϯεΛ࡞੒Մೳ w ϥϜμؔ਺΍,PUMJO+7.͕
 ͲΜͳόΠτίʔυʹͳΔ͔͸
 एׯߟྀ͢Δඞཁ͕͋Δ৔߹΋


    ͋Δ͜ͱʹ஫ҙ 79 HowɿΤϯυϙΠϯτͱ֎෦σʔλͷରԠΛ
 ϝιουͷґଘؔ܎͔Βऔಘ  $POUSPMMFSΫϥε૚ 3FQPTJUPSZΫϥε૚ 6TF$BTFΫϥε૚ υϝΠϯϞσϧ૚
  24.  80 @RestController @RequestMapping(value = "/hair/hair-colors") @AllArgsConstructor public class HairColorController

    { private final HairColorUsecase usecase; @GetMapping public ResponseEntity<GetHairColorResponse> getHairColor() { final var body = usecase.getHairColors(); return ResponseEntity.body(body); } } w "1*ΤϯυϙΠϯτ
 IBJSIBJSDPMPST(&5͸ )BJS$PMPS6TFDBTFΫϥεͷ
 HFU)BJS$PMPSTϝιουʹ
 ґଘ͍ͯ͠Δ w Ξϊςʔγϣϯͷ஋͔Β
 "1*ΤϯυϙΠϯτͷύε΍
 ϝιου΋औಘͰ͖Δ HowɿΤϯυϙΠϯτͱ֎෦σʔλͷରԠΛ
 ϝιουͷґଘؔ܎͔Βऔಘ
  25.  81 @RestController @RequestMapping(value = "/hair/hair-colors") @AllArgsConstructor public class HairColorController

    { private final HairColorUsecase usecase; @GetMapping public ResponseEntity<GetHairColorResponse> getHairColor() { final var body = usecase.getHairColors(); return ResponseEntity.body(body); } } w "1*ΤϯυϙΠϯτ
 IBJSIBJSDPMPST(&5͸ )BJS$PMPS6TFDBTFΫϥεͷ
 HFU)BJS$PMPSTϝιουʹ
 ґଘ͍ͯ͠Δ HowɿΤϯυϙΠϯτͱ֎෦σʔλͷରԠΛ
 ϝιουͷґଘؔ܎͔Βऔಘ
  26.  82 @Usecase @AllArgsConstructor public class HairColorUsecase { private final

    HairColorRepository repository; @Nonnull public GetHairColorResponse getHairColors() { final var hairColors = repository.findAll(); final var hairColorDtos = hairColors.stream() .map(HairColorDtoMapper::map) .collect(Collectors.toList()); return GetHairColorResponse.builder() .hairColors(hairColorDtos) .build(); } } w )BJS$PMPS6TFDBTFΫϥεͷ
 HFU)BJS$PMPSTϝιου͸
 )BJS$PMPS3FQPTJUPSZΫϥεͷ
 pOE"MMϝιουʹґଘ͍ͯ͠Δ HowɿΤϯυϙΠϯτͱ֎෦σʔλͷରԠΛ
 ϝιουͷґଘؔ܎͔Βऔಘ
  27.  83 @Usecase @AllArgsConstructor public class HairColorUsecase { private final

    HairColorRepository repository; @Nonnull public GetHairColorResponse getHairColors() { final var hairColors = repository.findAll(); final var hairColorDtos = hairColors.stream() .map(HairColorDtoMapper::map) .collect(Collectors.toList()); return GetHairColorResponse.builder() .hairColors(hairColorDtos) .build(); } } w )BJS$PMPS6TFDBTFΫϥεͷ
 HFU)BJS$PMPSTϝιου͸
 )BJS$PMPS3FQPTJUPSZΫϥεͷ
 pOE"MMϝιουʹґଘ͍ͯ͠Δ HowɿΤϯυϙΠϯτͱ֎෦σʔλͷରԠΛ
 ϝιουͷґଘؔ܎͔Βऔಘ
  28. 95 Howɿը໘Ϋϥεͷґଘؔ܎Λऔಘ w "OESPJEΞϓϦͷ
 "DUJDJUZɾ'SBHNFOUΫϥεͷ
 ݺͼग़͠ͳͲͷґଘؔ܎Λར༻ͯ͠
 Ϣʔβʔ͕Ͳͷը໘͔ΒͲͷը໘ʹ
 ભҠ͠ಘΔͷ͔ͷભҠάϥϑΛ࡞੒ w ը໘ભҠάϥϑΛը૾ͱͯ͠ग़ྗ

    private fun onClickBlog() { val intent = 
 KireiBlogListTabActivity.intent(
 this, 
 salonId
 ) startActivity(intent) }
 
 private fun navigateToLogin() { val intent = LoginActivity.intent(this) startActivityForResult( intent, REQUEST_CODE_LOGIN ) }
  29. 99 ΞϓϦέʔγϣϯղੳ΁ͷར༻๏ CASE 3 : PRࠩ෼ͷӨڹൣғͷݕ஌ CASE 4 : ࢓༷೺ѲϚοϓͷࣗಈ࡞੒

    CASE 5 : αʔϏεؒґଘؔ܎ͷՄࢹԽ CASE 6 : ը໘ભҠਤͷࣗಈੜ੒