クラス関係の種類

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 も不要でクラスパス上に存在するだけでよい という利点がある一方、 継承・実装関係は取れても、フィールド型以外の依存は取りにくい。

Java — Reflection で継承・実装関係を取得
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 から完全な依存関係を抽出できる。 最も精度が高く、多くの静的解析ツールの基盤となっている。

XML — Maven 依存関係 (pom.xml)
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.7</version>
</dependency>
Java — ASM でクラス関係を抽出する ClassVisitor
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(抽象構文木)を解析する。 コンポジションと集約の区別が可能で、ローカル変数の型など依存関係もより正確に取れる。 ソースコードが手元にある場合に有効だ。

XML — Maven 依存関係
<dependency>
    <groupId>com.github.javaparser</groupId>
    <artifactId>javaparser-symbol-solver-core</artifactId>
    <version>3.26.1</version>
</dependency>
Java — JavaParser でフィールド型を取得
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());
        }
    }
}
Reflection API
ソース不要。実行時クラスパス必要。継承・実装・フィールド型のみ取得可。
ASM
クラスファイル / JAR から解析。高速。依存関係を網羅的に取得。
JavaParser
ソースコード必要。コンポジション/集約の区別が可能。最も精度が高い。

PlantUML / Mermaid への変換

抽出した関係情報を図として出力する。PlantUML と Mermaid はどちらもテキストから図を生成できる。

PlantUML — クラス図の記法
@startuml
' 継承(実線+白抜き三角)
OrderServiceImpl --|> OrderService

' 実装(点線+白抜き三角)
OrderServiceImpl ..|> Transactional

' コンポジション(黒菱形)
OrderServiceImpl *-- OrderRepository : orderRepo

' 集約(白菱形)
OrderServiceImpl o-- InventoryClient : inventoryClient

' 依存(点線矢印)
OrderServiceImpl ..> PaymentRequest : «uses»

@enduml
Mermaid — クラス図の記法
classDiagram
    OrderServiceImpl --|> OrderService : extends
    OrderServiceImpl ..|> Transactional : implements
    OrderServiceImpl *-- OrderRepository : composes
    OrderServiceImpl o-- InventoryClient : aggregates
    OrderServiceImpl ..> PaymentRequest : uses

コード変化と図の変化(before/after)

インターフェースを追加した場合のコード変化と、それに対応する図の変化を確認しよう。

Java — Before: 具体クラスに直接依存
// Before: InventoryServiceImpl に直接依存(密結合)
public class OrderServiceImpl {
    private InventoryServiceImpl inventoryService; // 具象クラス
}
Java — After: インターフェースに依存(疎結合)
// After: InventoryService インターフェースに依存(疎結合)
public class OrderServiceImpl {
    private InventoryService inventoryService; // インターフェース
}

図から依存方向のまずさが見える

Before の図では OrderServiceImpl → InventoryServiceImpl という実装クラスへの依存が可視化される。 After の図では OrderServiceImpl → InventoryService(interface) と変化し、 依存の逆転(DIP)が実現できていることが一目で確認できる。 このように図の変化を見ることでリファクタリングの効果を視覚的に検証できる。