クラス関係の種類
UML でクラス図を描くとき、線の種類によって関係の意味が変わる。Javaのコードとの対応を整理しよう。
| 関係 | UML記法 | Javaコードでの表現 | 意味 |
|---|---|---|---|
| 継承 | 実線+白抜き三角(→) | class A extends B |
AはBの一種(is-a)。Bのフィールド・メソッドを引き継ぐ |
| 実装 | 点線+白抜き三角 | class A implements I |
AはインターフェースIの契約を守る |
| コンポジション | 実線+黒菱形 | フィールドとして所有(ライフサイクル共有) | AはBを所有し、Aがなくなれば Bも消える |
| 集約 | 実線+白菱形 | フィールドとして参照(外部からも利用可能) | AはBを参照するが、Bは独立して存在できる |
| 依存 | 点線矢印(→) | メソッド引数・ローカル変数・戻り値 | Aのメソッドが一時的にBを使う(弱い関係) |
💡 コンポジションと集約の見分け方
フィールドの型が new でコンストラクタ内に生成されている場合はコンポジション、
コンストラクタや setter で外部から注入される場合は集約と判断するのが実用的だ。
バイトコード解析だけではどちらか判断しにくいため、ソースコード解析(JavaParser)を使うと精度が上がる。
リフレクションによる抽出
最も手軽な方法が Java の Reflection API を使ったアプローチだ。 ただし ソースコードも JAR も不要でクラスパス上に存在するだけでよい という利点がある一方、 継承・実装関係は取れても、フィールド型以外の依存は取りにくい。
import java.lang.reflect.*;
import java.util.*;
public class ClassRelationExtractor {
public static void extract(Class<?> clazz) {
System.out.println("=== " + clazz.getName() + " ===");
// 継承
Class<?> superClass = clazz.getSuperclass();
if (superClass != null && !superClass.equals(Object.class)) {
System.out.println(" extends: " + superClass.getName());
}
// 実装インターフェース
for (Class<?> iface : clazz.getInterfaces()) {
System.out.println(" implements: " + iface.getName());
}
// フィールド型(集約 / コンポジション の候補)
for (Field f : clazz.getDeclaredFields()) {
String typeName = f.getType().getName();
// java.lang.* 等の基本型は除外
if (!typeName.startsWith("java.") && !typeName.startsWith("[")) {
System.out.println(" field: " + f.getName() + " : " + typeName);
}
}
}
public static void main(String[] args) throws Exception {
extract(Class.forName("com.example.OrderService"));
}
}
バイトコード解析(ASM)による抽出
ASM はバイトコードを読み込んでビジターパターンで処理するライブラリだ。
ソースコードがなくても .class / JAR から完全な依存関係を抽出できる。
最も精度が高く、多くの静的解析ツールの基盤となっている。
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7</version>
</dependency>
import org.objectweb.asm.*;
import java.io.*;
import java.util.*;
import java.util.jar.*;
public class AsmClassRelationExtractor {
// 結果格納: sourceClass → Set<targetClass>
private final Map<String, Set<String>> relations = new LinkedHashMap<>();
/** JAR ファイルを走査してすべてのクラスを解析 */
public void analyzeJar(String jarPath) throws IOException {
try (JarFile jar = new JarFile(jarPath)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class")) {
try (InputStream is = jar.getInputStream(entry)) {
analyzeClass(is);
}
}
}
}
}
private void analyzeClass(InputStream is) throws IOException {
ClassReader cr = new ClassReader(is);
cr.accept(new ClassVisitor(Opcodes.ASM9) {
private String currentClass;
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
currentClass = name.replace('/', '.');
relations.putIfAbsent(currentClass, new LinkedHashSet<>());
// 継承
if (superName != null && !superName.equals("java/lang/Object")) {
relations.get(currentClass).add("extends:" + superName.replace('/', '.'));
}
// 実装
if (interfaces != null) {
for (String iface : interfaces) {
relations.get(currentClass).add("implements:" + iface.replace('/', '.'));
}
}
}
@Override
public FieldVisitor visitField(int access, String name,
String descriptor, String signature, Object value) {
// フィールドの型からクラス参照を取得
Type type = Type.getType(descriptor);
if (type.getSort() == Type.OBJECT) {
String typeName = type.getClassName();
if (!typeName.startsWith("java.")) {
relations.get(currentClass).add("field:" + typeName);
}
}
return null;
}
}, ClassReader.SKIP_CODE);
}
public void printRelations() {
relations.forEach((cls, deps) -> {
if (!deps.isEmpty()) {
System.out.println(cls + ":");
deps.forEach(d -> System.out.println(" → " + d));
}
});
}
public static void main(String[] args) throws Exception {
AsmClassRelationExtractor extractor = new AsmClassRelationExtractor();
extractor.analyzeJar("target/myapp.jar");
extractor.printRelations();
}
}
ソースコード解析(JavaParser)による抽出
JavaParser はソースコードの AST(抽象構文木)を解析する。 コンポジションと集約の区別が可能で、ローカル変数の型など依存関係もより正確に取れる。 ソースコードが手元にある場合に有効だ。
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
<version>3.26.1</version>
</dependency>
import com.github.javaparser.*;
import com.github.javaparser.ast.*;
import com.github.javaparser.ast.body.*;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.io.*;
import java.nio.file.*;
public class JavaParserClassExtractor {
public static void main(String[] args) throws Exception {
// ソースディレクトリを走査
Files.walk(Path.of("src/main/java"))
.filter(p -> p.toString().endsWith(".java"))
.forEach(JavaParserClassExtractor::analyze);
}
static void analyze(Path javaFile) {
try {
CompilationUnit cu = StaticJavaParser.parse(javaFile);
cu.accept(new VoidVisitorAdapter<Void>() {
@Override
public void visit(ClassOrInterfaceDeclaration n, Void arg) {
String className = n.getFullyQualifiedName().orElse(n.getNameAsString());
System.out.println("Class: " + className);
// 継承
n.getExtendedTypes().forEach(t ->
System.out.println(" extends: " + t.getNameAsString()));
// 実装
n.getImplementedTypes().forEach(t ->
System.out.println(" implements: " + t.getNameAsString()));
// フィールド(コンポジション/集約の判定)
n.getFields().forEach(f -> {
String typeName = f.getCommonType().asString();
// new で初期化 → コンポジション候補
boolean isComposition = f.getVariables().stream()
.anyMatch(v -> v.getInitializer()
.map(i -> i.isObjectCreationExpr()).orElse(false));
String relType = isComposition ? "composes" : "aggregates";
if (!typeName.matches("(String|int|long|boolean|double|float|List|Map|Set|Object)")) {
System.out.println(" " + relType + ": " + typeName + " (" + f.getVariable(0).getNameAsString() + ")");
}
});
super.visit(n, arg);
}
}, null);
} catch (Exception e) {
System.err.println("Parse error: " + javaFile + " — " + e.getMessage());
}
}
}
PlantUML / Mermaid への変換
抽出した関係情報を図として出力する。PlantUML と Mermaid はどちらもテキストから図を生成できる。
@startuml
' 継承(実線+白抜き三角)
OrderServiceImpl --|> OrderService
' 実装(点線+白抜き三角)
OrderServiceImpl ..|> Transactional
' コンポジション(黒菱形)
OrderServiceImpl *-- OrderRepository : orderRepo
' 集約(白菱形)
OrderServiceImpl o-- InventoryClient : inventoryClient
' 依存(点線矢印)
OrderServiceImpl ..> PaymentRequest : «uses»
@enduml
classDiagram
OrderServiceImpl --|> OrderService : extends
OrderServiceImpl ..|> Transactional : implements
OrderServiceImpl *-- OrderRepository : composes
OrderServiceImpl o-- InventoryClient : aggregates
OrderServiceImpl ..> PaymentRequest : uses
コード変化と図の変化(before/after)
インターフェースを追加した場合のコード変化と、それに対応する図の変化を確認しよう。
// Before: InventoryServiceImpl に直接依存(密結合)
public class OrderServiceImpl {
private InventoryServiceImpl inventoryService; // 具象クラス
}
// After: InventoryService インターフェースに依存(疎結合)
public class OrderServiceImpl {
private InventoryService inventoryService; // インターフェース
}
✅ 図から依存方向のまずさが見える
Before の図では OrderServiceImpl → InventoryServiceImpl という実装クラスへの依存が可視化される。
After の図では OrderServiceImpl → InventoryService(interface) と変化し、
依存の逆転(DIP)が実現できていることが一目で確認できる。
このように図の変化を見ることでリファクタリングの効果を視覚的に検証できる。