小さなアプリ構築の全体像
小さなアプリは「起動の配電盤(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 を使って安全に扱う——この型で作ると、小さなアプリは短時間で動き、拡張も楽になります。
