package com.ankurm.structure; import com.ankurm.structure.byfeature.ByFeatureApp; import com.ankurm.structure.bylayer.ByLayerApp; import org.junit.jupiter.api.Test; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import javax.tools.*; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.*; import java.util.*; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** Each test writes one transcript under output/; the article quotes those files verbatim. */ class StructureTests { static final Path SRC = Path.of("src/main/java/com/ankurm/structure"); static final Path CLASSES = Path.of("target/classes"); // ---------------------------------------------------------------- 01 the two trees @Test void packageTrees() throws IOException { try (Transcript t = new Transcript("01-package-trees.txt", "The same application, two layouts (src/main/java/com/ankurm/structure)")) { for (String variant : List.of("bylayer", "byfeature")) { t.section(variant); Path root = SRC.resolve(variant); List all; try (Stream s = Files.walk(root)) { all = s.sorted().toList(); } int files = 0; for (Path p : all) { int depth = root.relativize(p).getNameCount(); String indent = " ".repeat(depth); if (p.equals(root)) { t.line("%s/", variant); } else if (Files.isDirectory(p)) { t.line("%s%s/", indent, p.getFileName()); } else { t.line("%s%s", indent, p.getFileName()); files++; } } t.line("(%d source files)", files); assertThat(files).isGreaterThan(8); } } } // ---------------------------------------------------------------- 02 what is public @Test void visibility() throws Exception { Map publicCount = new LinkedHashMap<>(); try (Transcript t = new Transcript("02-public-surface.txt", "Top-level types per layout and whether each one is public")) { for (String variant : List.of("bylayer", "byfeature")) { t.section(variant); Path root = CLASSES.resolve("com/ankurm/structure").resolve(variant); List names = new ArrayList<>(); try (Stream s = Files.walk(root)) { s.filter(p -> p.toString().endsWith(".class") && !p.getFileName().toString().contains("$")) .sorted().forEach(p -> names.add(CLASSES.relativize(p).toString() .replace('/', '.').replaceAll("\\.class$", ""))); } int pub = 0; for (String n : names) { Class c = Class.forName(n, false, getClass().getClassLoader()); boolean isPublic = java.lang.reflect.Modifier.isPublic(c.getModifiers()); if (isPublic) pub++; t.line("%-14s %s", isPublic ? "public" : "package-private", n.substring("com.ankurm.structure.".length())); } t.line("%d of %d top-level types are public", pub, names.size()); publicCount.put(variant, pub); } } // package-by-feature exposes strictly less assertThat(publicCount.get("byfeature")).isLessThan(publicCount.get("bylayer")); } // ---------------------------------------------------------------- 03 what the compiler enforces @Test void compilerBoundary() throws Exception { try (Transcript t = new Transcript("03-compiler-boundary.txt", "Can code in one package reach another package's repository?")) { t.section("by layer: controller/Shortcut.java reaches into repository/"); List layer = compile("com.ankurm.structure.bylayer.controller.Shortcut", "package com.ankurm.structure.bylayer.controller;\n" + "class Shortcut {\n" + " Object repo = new com.ankurm.structure.bylayer.repository.BookRepository();\n" + "}\n"); layer.forEach(t::line); assertThat(layer).containsExactly("compiled with 0 errors"); t.section("by feature: loan/Shortcut.java reaches into book/"); List feature = compile("com.ankurm.structure.byfeature.loan.Shortcut", "package com.ankurm.structure.byfeature.loan;\n" + "class Shortcut {\n" + " Object repo = new com.ankurm.structure.byfeature.book.BookRepository();\n" + "}\n"); feature.forEach(t::line); assertThat(feature.get(0)).contains("is not public").contains("cannot be accessed from outside package"); } } private List compile(String className, String source) throws IOException { JavaCompiler javac = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); JavaFileObject file = new SimpleJavaFileObject(URI.create("string:///" + className.replace('.', '/') + ".java"), JavaFileObject.Kind.SOURCE) { @Override public CharSequence getCharContent(boolean ignore) { return source; } }; Path out = Files.createTempDirectory("shortcut"); List opts = List.of("-proc:none", "-d", out.toString(), "-classpath", CLASSES.toString()); boolean ok = javac.getTask(null, null, diagnostics, opts, null, List.of(file)).call(); List lines = new ArrayList<>(); for (Diagnostic d : diagnostics.getDiagnostics()) { if (d.getKind() == Diagnostic.Kind.ERROR) { lines.add("error: " + d.getMessage(Locale.ENGLISH)); } } if (ok) lines.add("compiled with 0 errors"); return lines; } // ---------------------------------------------------------------- 04 both layouts expose one API @Test void sameApiBothLayouts() throws Exception { HttpClient http = HttpClient.newHttpClient(); Map> results = new LinkedHashMap<>(); try (Transcript t = new Transcript("04-same-api-both-layouts.txt", "The same requests against both layouts")) { for (var variant : List.of("bylayer", "byfeature")) { Class app = variant.equals("bylayer") ? ByLayerApp.class : ByFeatureApp.class; try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(app) .properties("server.port=0", "spring.main.banner-mode=off", "logging.level.root=WARN").run()) { String base = "http://localhost:" + ctx.getEnvironment().getProperty("local.server.port"); t.section(variant); List seen = new ArrayList<>(); for (String[] call : new String[][]{ {"GET", "/books", null}, {"GET", "/books/1", null}, {"GET", "/books/99", null}, {"POST", "/books", "{\"title\":\"Clean Code\",\"author\":\"Robert Martin\",\"pages\":464}"}, {"POST", "/books", "{\"title\":\"\",\"author\":\"x\",\"pages\":0}"}}) { HttpRequest.Builder rb = HttpRequest.newBuilder(URI.create(base + call[1])); if (call[2] == null) rb.GET(); else rb.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(call[2])); HttpResponse r = http.send(rb.build(), HttpResponse.BodyHandlers.ofString()); String line = call[0] + " " + call[1] + " -> " + r.statusCode(); t.line(line); t.line(" %s", r.body()); seen.add(call[0] + " " + call[1] + " " + r.statusCode() + (r.statusCode() == 400 ? "" : " " + r.body().replaceAll("\"instance\":\"[^\"]*\",?", ""))); } if (variant.equals("byfeature")) { for (String[] call : new String[][]{ {"POST", "/loans", "{\"bookId\":2,\"member\":\"Asha\"}"}, {"POST", "/loans", "{\"bookId\":7,\"member\":\"Asha\"}"}, {"GET", "/loans", null}}) { HttpRequest.Builder rb = HttpRequest.newBuilder(URI.create(base + call[1])); if (call[2] == null) rb.GET(); else rb.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(call[2])); HttpResponse r = http.send(rb.build(), HttpResponse.BodyHandlers.ofString()); t.line(call[0] + " " + call[1] + " -> " + r.statusCode()); t.line(" %s", r.body()); } } results.put(variant, seen); } } t.section("comparison"); boolean same = results.get("bylayer").equals(results.get("byfeature")); t.line("status codes and bodies identical for the five shared requests: %s", same); assertThat(same).isTrue(); } } }