Java | 基礎文法:小さなアプリの構築

Java Java
スポンサーリンク

小さなアプリ構築の全体像

小さなアプリは「起動の配電盤(main)」「入力(CLI/ファイル)」「ドメインロジック」「永続化(ファイル/メモリ)」「出力(標準出力/ファイル)」の5点を薄く分けると、読みやすくテストしやすくなります。最初は最小機能で動かし、入力検証と終了コード、文字コード(UTF-8)、例外の取扱いを整える——この型を身体に入れると、どんな題材でも迷わず形にできます。


ひな型(main は薄く、ロジックは外へ)

最小の main と責務の切り分け

main は「設定読み込み→依存の組み立て→CLIに委譲→終了コードを返す」だけに集中します。業務ロジックは Service/Repository などのクラスへ分割します。

public class App {
    public static void main(String[] args) {
        int exit = 0;
        try {
            var svc = Bootstrap.buildService();     // 依存の組み立て
            exit = new Cli(svc).run(args);          // 引数に応じて処理へ委譲
        } catch (Exception e) {
            System.err.println("fatal: " + e.getMessage());
            exit = 1;
        } finally {
            System.exit(exit);
        }
    }
}

final class Bootstrap {
    static Service buildService() {
        var store = new FileStore(java.nio.file.Path.of("data.txt"));
        return new Service(store);
    }
}
Java

入力の設計(コマンドライン引数と使い方)

位置引数とオプションの分解

小規模なら自前で十分です。必須引数の不足は使い方(usage)で知らせ、失敗終了を返します。順不同のオプションはフラグやキー付きで受けます。

final class Cli {
    private final Service svc;
    Cli(Service svc) { this.svc = svc; }

    int run(String[] args) {
        if (args.length == 0) return usage();
        return switch (args[0]) {
            case "add" -> add(args);
            case "list" -> list(args);
            default -> usage();
        };
    }

    private int usage() {
        System.err.println("usage: App add <text> | list");
        return 1;
    }

    private int add(String[] args) {
        if (args.length < 2) return usage();
        svc.add(args[1]);
        System.out.println("added");
        return 0;
    }

    private int list(String[] args) {
        svc.list().forEach(System.out::println);
        return 0;
    }
}
Java

ドメインロジックと永続化(テストしやすい構造)

ロジックは純粋関数に寄せ、I/O を端へ

ロジック(検証・整形)と I/O(保存・読込)を分離すると、テストが簡単になります。永続化はインターフェースで抽象化し、実装を差し替え可能にします。

import java.util.List;
import java.util.ArrayList;

interface Store {
    void save(List<String> items) throws java.io.IOException;
    List<String> load() throws java.io.IOException;
}

final class FileStore implements Store {
    private final java.nio.file.Path path;
    FileStore(java.nio.file.Path path) { this.path = path; }

    @Override public void save(List<String> items) throws java.io.IOException {
        var s = String.join("\n", items);
        java.nio.file.Files.writeString(path, s, java.nio.charset.StandardCharsets.UTF_8);
    }
    @Override public List<String> load() throws java.io.IOException {
        if (!java.nio.file.Files.exists(path)) return new ArrayList<>();
        var s = java.nio.file.Files.readString(path, java.nio.charset.StandardCharsets.UTF_8);
        var list = new ArrayList<String>();
        for (var line : s.split("\\R")) if (!line.isBlank()) list.add(line);
        return list;
    }
}

final class Service {
    private final Store store;
    Service(Store store) { this.store = store; }

    void add(String raw) {
        var text = normalize(raw);
        if (text.isEmpty()) throw new IllegalArgumentException("empty item");
        try {
            var items = store.load();
            items.add(text);
            store.save(items);
        } catch (java.io.IOException e) {
            throw new RuntimeException("save failed", e);
        }
    }
    java.util.List<String> list() {
        try { return store.load(); }
        catch (java.io.IOException e) { throw new RuntimeException("load failed", e); }
    }

    private String normalize(String s) {
        return s == null ? "" : s.trim().replaceAll("\\s+", " ");
    }
}
Java

出力・終了コード・例外(重要ポイントの深掘り)

標準出力と標準エラーを使い分ける

成功の結果は System.out、使い方や失敗は System.err に。シェル連携やログ収集で扱いやすくなります。

終了コードの契約を決める

成功は 0、引数エラーは 1、I/O 失敗は 2 など、プロジェクトで統一した番号を使います。main で必ず System.exit(code) を呼びます。

例外は「ユーザー向けメッセージ」に変換

深い層では適切な例外を投げ、入口の main/Cli でメッセージ+終了コードへ変換します。スタックトレースは調査が必要なときだけ出します。


入出力の安全化(文字コードとパス)

UTF-8 を明示し、プラットフォーム差を排除

ファイル読み書きでは常に UTF-8 を指定します。パスは Path.of(...) を使い、文字列連結で OS 依存の区切りを作らないようにします。

var p = java.nio.file.Path.of("data.txt");
java.nio.file.Files.writeString(p, "江東区 東京", java.nio.charset.StandardCharsets.UTF_8);
Java

例題で身につける:CLI Todo アプリ

機能と起動例

「add」「list」の2コマンド。add はテキストを追加、list は全件表示。空白を含むテキストは引用符で渡します。

# 追加
java App add "Buy milk"
java App add "Call Mom"

# 一覧
java App list
Java

出力例:

Buy milk
Call Mom

完成コード(ひな型+CLI+Service+FileStore)

上で示した App/Cli/Service/FileStore を同一パッケージに置けば動きます。最小構成で、永続化差し替え(メモリ/ファイル)、入力検証、UTF-8 指定、終了コードの扱いまで揃っています。


小さく作って磨く(拡張の道筋)

機能追加の定石

  • 削除機能: remove <index> を追加。境界チェックを Service に、表示は Cli。
  • 完了フラグ: Task クラスに done を追加。保存形式を JSON に切り替えると拡張に強くなります。
  • 設定: AppConfig で保存先パスを外だし。main の Bootstrap で読み込み。

テストの導入

Store をモック/メモリ実装に差し替えて Service の単体テストを書きます。CLI は引数の例を多数用意し、終了コードと出力を検証します。

static void testNormalize() {
    var svc = new Service(new Store() {
        java.util.List<String> buf = new java.util.ArrayList<>();
        public void save(java.util.List<String> items) { buf = items; }
        public java.util.List<String> load() { return buf; }
    });
    svc.add("  Buy   milk ");
    assert "Buy milk".equals(svc.list().get(0));
}
Java

仕上げのアドバイス(重要部分のまとめ)

main は配電盤として薄く保ち、CLI で引数を検証・振り分け、業務ロジックを Service に閉じ込め、永続化は Store インターフェースで抽象化して差し替え可能にする。出力は System.out、失敗や使い方は System.err、終了コードは契約どおり返す。ファイルは UTF-8 を明示し、パスは Path を使って安全に扱う——この型で作ると、小さなアプリは短時間で動き、拡張も楽になります。

タイトルとURLをコピーしました