solid-principles について
Java コードで SOLID 原則を適用・レビューするスキルです。5 つの原則、違反の兆候、代表的な改善方法を示し、クラス設計のレビューとリファクタリングを支援します。
主な機能
- 015 つの SOLID 原則の解説
- 02設計上の違反を示す兆候
- 03原則別のレビュー質問
- 04よくある違反とリファクタリングパターンの対応
活用シーン
- 01Java のクラス設計レビュー
- 02責務が多すぎるクラスのリファクタリング
- 03継承、インターフェース、依存関係の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ decebals/ claude-code-java --skill solid-principlesインストール後、Claude Code で/solid-principlesを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
SOLID 原則スキル
Java コードで SOLID 原則を確認し、適用します。
使用する場面
- ユーザーが「SOLID をチェックして」/「SOLID レビュー」/「このクラスは責務が多すぎませんか?」と言ったとき
- クラス設計をレビューするとき
- 大規模なクラスをリファクタリングするとき
- 設計に重点を置いたコードレビューをするとき
クイックリファレンス
| 文字 | 原則 | 一言で言うと |
|---|---|---|
| S | 単一責任の原則 | 1 つのクラス = 変更理由は 1 つ |
| O | 開放/閉鎖の原則 | 拡張に対して開き、変更に対して閉じる |
| L | リスコフの置換原則 | サブタイプは基底型と置き換え可能でなければならない |
| I | インターフェース分離の原則 | 1 つの汎用インターフェースより、複数の特化したインターフェース |
| D | 依存関係逆転の原則 | 具体ではなく、抽象に依存する |
S - 単一責任の原則(SRP)
「クラスが持つ変更理由は 1 つだけであるべきです。」
違反例
// ❌ BAD: UserService does too much
public class UserService {
public User createUser(String name, String email) {
// validation logic
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// persistence logic
User user = new User(name, email);
entityManager.persist(user);
// notification logic
String subject = "Welcome!";
String body = "Hello " + name;
emailClient.send(email, subject, body);
// audit logic
auditLog.log("User created: " + email);
return user;
}
}
問題点:
- バリデーションが変更されたら?UserService を変更する
- メールテンプレートが変更されたら?UserService を変更する
- 監査形式が変更されたら?UserService を変更する
- 各関心事を個別にテストするのが困難
リファクタリング後
// ✅ GOOD: Each class has one responsibility
public class UserValidator {
public void validate(String name, String email) {
if (email == null || !email.contains("@")) {
throw new ValidationException("Invalid email");
}
}
}
public class UserRepository {
public User save(User user) {
entityManager.persist(user);
return user;
}
}
public class WelcomeEmailSender {
public void sendWelcome(User user) {
String subject = "Welcome!";
String body = "Hello " + user.getName();
emailClient.send(user.getEmail(), subject, body);
}
}
public class UserAuditLogger {
public void logCreation(User user) {
auditLog.log("User created: " + user.getEmail());
}
}
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final WelcomeEmailSender emailSender;
private final UserAuditLogger auditLogger;
public User createUser(String name, String email) {
validator.validate(name, email);
User user = repository.save(new User(name, email));
emailSender.sendWelcome(user);
auditLogger.logCreation(user);
return user;
}
}
SRP 違反の検出方法
- クラスに異なるドメインの
import文が多数ある - クラス名に「And」、「Manager」、「Handler」が含まれている(よくある)
- メソッドが無関係なデータを操作している
- ある領域の変更に、無関係なメソッドの修正が必要になる
- クラス名を簡潔に付けるのが難しい
簡易チェックの質問
- 「and」を使わずに、クラスの目的を 1 文で説明できますか?
- 異なるステークホルダーが、このクラスへの変更を依頼しますか?
- クラスのフィールドの大半を使わないメソッドがありますか?
O - 開放/閉鎖の原則(OCP)
「ソフトウェアの構成要素は、拡張に対して開かれ、変更に対して閉じられているべきです。」
違反例
// ❌ BAD: Must modify class to add new discount type
public class DiscountCalculator {
public double calculate(Order order, String discountType) {
if (discountType.equals("PERCENTAGE")) {
return order.getTotal() * 0.1;
} else if (discountType.equals("FIXED")) {
return 50.0;
} else if (discountType.equals("LOYALTY")) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
// Every new discount type = modify this class
return 0;
}
}
リファクタリング後
// ✅ GOOD: Add new discounts without modifying existing code
public interface DiscountStrategy {
double calculate(Order order);
boolean supports(String discountType);
}
public class PercentageDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.1;
}
@Override
public boolean supports(String discountType) {
return "PERCENTAGE".equals(discountType);
}
}
public class FixedDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return 50.0;
}
@Override
public boolean supports(String discountType) {
return "FIXED".equals(discountType);
}
}
public class LoyaltyDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
@Override
public boolean supports(String discountType) {
return "LOYALTY".equals(discountType);
}
}
// New discount? Just add new class, no modification needed
public class SeasonalDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.2;
}
@Override
public boolean supports(String discountType) {
return "SEASONAL".equals(discountType);
}
}
public class DiscountCalculator {
private final List<DiscountStrategy> strategies;
public DiscountCalculator(List<DiscountStrategy> strategies) {
this.strategies = strategies;
}
public double calculate(Order order, String discountType) {
return strategies.stream()
.filter(s -> s.supports(discountType))
.findFirst()
.map(s -> s.calculate(order))
.orElse(0.0);
}
}
OCP 違反の検出方法
- 型やステータスに応じた
if/elseまたはswitchが時間とともに増えている - 列挙型を使った振り分けに、新しい値が頻繁に追加される
- 変更のたびにコアクラスの修正が必要になる
OCP の一般的なパターン
| パターン | 使用する場面 |
|---|---|
| ストラテジー | 同じ操作に複数のアルゴリズムがある場合 |
| テンプレートメソッド | 構造は同じで、手順が異なる場合 |
| デコレーター | 動的に振る舞いを追加する場合 |
| ファクトリー | クラスを指定せずにオブジェクトを作成する場合 |
L - リスコフの置換原則(LSP)
「サブタイプは基底型と置き換え可能でなければなりません。」
違反例
// ❌ BAD: Square violates Rectangle contract
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Violates expected behavior!
}
@Override
public void setHeight(int height) {
this.width = height; // Violates expected behavior!
this.height = height;
}
}
// This test fails for Square!
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Square returns 16!
}
リファクタリング後
// ✅ GOOD: Separate abstractions
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
LSP のルール
| ルール | 意味 |
|---|---|
| 事前条件 | サブクラスは、より強い条件(より多くの要求)を課してはならない |
| 事後条件 | サブクラスは、より弱い条件(より少ない保証)にしてはならない |
| 不変条件 | サブクラスは、親クラスの不変条件を維持しなければならない |
| 履歴 | サブクラスは、継承した状態を予期せず変更してはならない |
LSP 違反の検出方法
- サブクラスが、親クラスにはない例外をスローする
- 親クラスがオブジェクトを返す場面で、サブクラスが null を返す
- サブクラスが親クラスの振る舞いを予期せず無視または上書きする
- メソッド呼び出しの前に
instanceofでチェックする - インターフェースのメソッドが空実装または例外をスローする実装になっている
簡易チェック
// If you see this, LSP might be violated
if (bird instanceof Penguin) {
// don't call fly()
} else {
bird.fly();
}
I - インターフェース分離の原則(ISP)
「クライアントは、使用しないインターフェースへの依存を強制されるべきではありません。」
違反例
// ❌ BAD: Fat interface forces unnecessary implementations
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
void writeReport();
}
// Robot can't eat or sleep!
public class Robot implements Worker {
@Override public void work() { /* OK */ }
@Override public void eat() { /* Can't eat! */ }
@Override public void sleep() { /* Can't sleep! */ }
@Override public void attendMeeting() { /* OK */ }
@Override public void writeReport() { /* Maybe */ }
}
// Intern doesn't attend meetings or write reports
public class Intern implements Worker {
@Override public void work() { /* OK */ }
@Override public void eat() { /* OK */ }
@Override public void sleep() { /* OK */ }
@Override public void attendMeeting() { /* Not allowed! */ }
@Override public void writeReport() { /* Not expected! */ }
}
リファクタリング後
// ✅ GOOD: Segregated interfaces
public interface Workable {
void work();
}
public interface Feedable {
void eat();
void sleep();
}
public interface Manageable {
void attendMeeting();
void writeReport();
}
// Combine what you need
public class Employee implements Workable, Feedable, Manageable {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
@Override public void sleep() { /* ... */ }
@Override public void attendMeeting() { /* ... */ }
@Override public void writeReport() { /* ... */ }
}
public class Robot implements Workable {
@Override public void work() { /* ... */ }
// No unnecessary methods!
}
public class Intern implements Workable, Feedable {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
@Override public void sleep() { /* ... */ }
// No meeting/report methods!
}
ISP 違反の検出方法
- 空のメソッド、または
throw new UnsupportedOperationException()を含む実装がある - インターフェースに 10 個以上のメソッドがある
- 異なるクライアントが、メソッドのまったく異なる一部だけを使用している
- インターフェースへの変更が、無関係な実装に影響する
Java 標準ライブラリにおける違反例
// java.util.List has many methods - but this is acceptable for collections
// However, be careful with your own interfaces!
// ❌ This interface is too fat for most use cases
public interface Repository<T> {
T findById(Long id);
List<T> findAll();
T save(T entity);
void delete(T entity);
void deleteById(Long id);
List<T> findByExample(T example);
Page<T> findAll(Pageable pageable);
List<T> findAllById(Iterable<Long> ids);
long count();
boolean existsById(Long id);
// ... 20 more methods
}
// ✅ Better: Split by use case
public interface ReadRepository<T> {
Optional<T> findById(Long id);
List<T> findAll();
}
public interface WriteRepository<T> {
T save(T entity);
void delete(T entity);
}
D - 依存関係逆転の原則(DIP)
「上位モジュールは下位モジュールに依存すべきではありません。両者とも抽象に依存するべきです。」
違反例
// ❌ BAD: High-level depends on low-level directly
public class OrderService {
private MySqlOrderRepository repository; // Concrete class!
private SmtpEmailSender emailSender; // Concrete class!
public OrderService() {
this.repository = new MySqlOrderRepository(); // Hard dependency
this.emailSender = new SmtpEmailSender(); // Hard dependency
}
public void createOrder(Order order) {
repository.save(order);
emailSender.send(order.getCustomerEmail(), "Order confirmed");
}
}
問題点:
- 実際の MySQL データベースなしではテストできない
- メールプロバイダーを差し替えられない
- OrderService が MySQL や SMTP の詳細を把握している
リファクタリング後
// ✅ GOOD: Depend on abstractions
// Abstractions (interfaces)
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(Long id);
}
public interface NotificationSender {
void send(String recipient, String message);
}
// High-level module depends on abstractions
public class OrderService {
private final OrderRepository repository;
private final NotificationSender notificationSender;
// Dependencies injected
public OrderService(OrderRepository repository,
NotificationSender notificationSender) {
this.repository = repository;
this.notificationSender = notificationSender;
}
public void createOrder(Order order) {
repository.save(order);
notificationSender.send(order.getCustomerEmail(), "Order confirmed");
}
}
// Low-level modules implement abstractions
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) { /* MySQL specific */ }
@Override
public Optional<Order> findById(Long id) { /* MySQL specific */ }
}
public class SmtpEmailSender implements NotificationSender {
@Override
public void send(String recipient, String message) { /* SMTP specific */ }
}
// Easy to test with mocks!
public class InMemoryOrderRepository implements OrderRepository {
private Map<Long, Order> orders = new HashMap<>();
@Override
public void save(Order order) {
orders.put(order.getId(), order);
}
@Override
public Optional<Order> findById(Long id) {
return Optional.ofNullable(orders.get(id));
}
}
Spring での DIP
// Spring handles dependency injection automatically
@Service
public class OrderService {
private final OrderRepository repository;
private final NotificationSender notificationSender;
// Constructor injection (recommended)
public OrderService(OrderRepository repository,
NotificationSender notificationSender) {
this.repository = repository;
this.notificationSender = notificationSender;
}
}
@Repository
public class JpaOrderRepository implements OrderRepository {
// Spring provides implementation
}
@Component
@Profile("production")
public class SmtpEmailSender implements NotificationSender { }
@Component
@Profile("test")
public class MockEmailSender implements NotificationSender { }
DIP 違反の検出方法
- ビジネスロジック内での
new ConcreteClass() - インポート文に実装パッケージが含まれている(例:
com.mysql、org.apache.http) - 実装を簡単に切り替えられない
- テストで実際のインフラストラクチャ(データベース、ネットワーク)が必要になる
SOLID レビューのチェックリスト
コードをレビューするときは、次の点を確認してください。
| 原則 | 質問 |
|---|---|
| SRP | このクラスには変更する理由が複数ありますか? |
| OCP | 新しい型や機能を追加する際に、このクラスの変更が必要になりますか? |
| LSP | サブクラスは、親クラスが想定されているあらゆる場所で使用できますか? |
| ISP | 空のメソッド実装、または例外をスローするメソッド実装はありますか? |
| DIP | 高水準のコードが具象実装に依存していますか? |
よく使われるリファクタリングパターン
| 違反 | リファクタリング |
|---|---|
| SRP - 神クラス | クラスの抽出、メソッドの移動 |
| OCP - 型の切り替え | ストラテジーパターン、ファクトリー |
| LSP - 破綻した継承 | 継承よりコンポジション、インターフェースの抽出 |
| ISP - 肥大化したインターフェース | インターフェースの分割、ロールインターフェース |
| DIP - 強い依存関係 | 依存性注入、抽象ファクトリー |
関連スキル
design-patterns- 実装パターン(ファクトリー、ストラテジー、オブザーバーなど)clean-code- コードレベルの原則(DRY、KISS、命名)java-code-review- 包括的なレビューのチェックリスト
コメント
Markdown 対応