首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 策略模式完整實戰:圖片儲存格式動態切換、Spring 整合、避免 if-else

     編輯:Java 相關知識

前言

策略模式將算法族群封裝為可互換的策略物件,讓客戶端在執行期動態切換行為,徹底消除冗長的 if-else/switch。本文以「圖片儲存格式選擇」為例,完整示範:介面定義、具體策略、策略工廠、Spring 整合 (註解驅動/條件裝配)、效能優化 (單例/原型)、測試驗證。

1. 策略介面

public interface ImageSaver {
    /** 儲存圖片,回傳實際檔案路徑 */
    Path save(BufferedImage image, Path targetDir, String baseName) throws IOException;
    /** 策略識別碼,對應前端/配置傳入的值 */
    String getFormatId();
    /** 顯示名稱 (給 UI 下拉選單) */
    String getDisplayName();
}

2. 具體策略實作

@Component("gifSaver")
public class GifSaver implements ImageSaver {
    @Override public String getFormatId() { return "gif"; }
    @Override public String getDisplayName() { return "GIF (動畫/256色)"; }

    @Override
    public Path save(BufferedImage image, Path targetDir, String baseName) throws IOException {
        Path out = targetDir.resolve(baseName + ".gif");
        try (ImageOutputStream ios = ImageIO.createImageOutputStream(Files.newOutputStream(out))) {
            ImageWriter writer = ImageIO.getImageWritersByFormatName("gif").next();
            writer.setOutput(ios);
            writer.write(image);
            writer.dispose();
        }
        return out;
    }
}

@Component("jpgSaver")
public class JpgSaver implements ImageSaver {
    @Override public String getFormatId() { return "jpg"; }
    @Override public String getDisplayName() { return "JPEG (高壓縮/有損)"; }

    @Override
    public Path save(BufferedImage image, Path targetDir, String baseName) throws IOException {
        Path out = targetDir.resolve(baseName + ".jpg");
        try (ImageOutputStream ios = ImageIO.createImageOutputStream(Files.newOutputStream(out))) {
            ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
            ImageWriteParam param = writer.getDefaultWriteParam();
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(0.85f); // 可配置化
            writer.setOutput(ios);
            writer.write(null, new IIOImage(image, null, null), param);
            writer.dispose();
        }
        return out;
    }
}

@Component("pngSaver")
public class PngSaver implements ImageSaver {
    @Override public String getFormatId() { return "png"; }
    @Override public String getDisplayName() { return "PNG (無損/透明)"; }

    @Override
    public Path save(BufferedImage image, Path targetDir, String baseName) throws IOException {
        Path out = targetDir.resolve(baseName + ".png");
        ImageIO.write(image, "png", out.toFile()); // 簡易寫法
        return out;
    }
}

3. 策略工廠 (核心:Map 快速查找 + 條件建構)

@Component
public class ImageSaverFactory {
    private final Map<String, ImageSaver> savers = new ConcurrentHashMap<>();

    @Autowired
    public ImageSaverFactory(List<ImageSaver> saverList) {
        for (ImageSaver s : saverList) {
            savers.put(s.getFormatId().toLowerCase(), s);
        }
    }

    public ImageSaver getSaver(String formatId) {
        ImageSaver saver = savers.get(formatId.toLowerCase());
        if (saver == null) {
            throw new IllegalArgumentException("Unsupported format: " + formatId +
                    ". Available: " + savers.keySet());
        }
        return saver;
    }

    public List<ImageSaver> getAllSavers() {
        return List.copyOf(savers.values());
    }
}

4. 服務層使用 (無 if-else)

@Service
public class ImageStorageService {
    private final ImageSaverFactory factory;

    public ImageStorageService(ImageSaverFactory factory) {
        this.factory = factory;
    }

    public Path storeImage(BufferedImage image, String formatId, Path targetDir, String baseName) throws IOException {
        ImageSaver saver = factory.getSaver(formatId);
        return saver.save(image, targetDir, baseName);
    }

    public List<ImageSaver> listAvailableFormats() {
        return factory.getAllSavers();
    }
}

5. Controller 暴露 API

@RestController
@RequestMapping("/api/images")
public class ImageController {
    private final ImageStorageService service;

    public ImageController(ImageStorageService service) {
        this.service = service;
    }

    @PostMapping("/save")
    public ResponseEntity<Map<String, Object>> save(
            @RequestParam("file") MultipartFile file,
            @RequestParam("format") String format) throws IOException {
        BufferedImage img = ImageIO.read(file.getInputStream());
        Path saved = service.storeImage(img, format, Paths.get("uploads"), "img_" + System.currentTimeMillis());
        return ResponseEntity.ok(Map.of("path", saved.toString(), "format", format));
    }

    @GetMapping("/formats")
    public List<Map<String, String>> formats() {
        return service.listAvailableFormats().stream()
                .map(s -> Map.of("id", s.getFormatId(), "name", s.getDisplayName()))
                .toList();
    }
}

6. 進階:條件裝配 (僅載入授權格式)

@Component
@ConditionalOnProperty(prefix = "image.formats", name = "enabled", havingValue = "true", matchIfMissing = true)
public class WebpSaver implements ImageSaver {
    @Override public String getFormatId() { return "webp"; }
    @Override public String getDisplayName() { return "WebP (現代/高壓縮)"; }
    @Override public Path save(...) { /* WebP 需額外依賴 */ }
}

application.yml:

image:
  formats:
    webp:
      enabled: false  # 預設關閉,授權後開啟

7. 單元測試

@ExtendWith(MockitoExtension.class)
class ImageStorageServiceTest {
    @Mock ImageSaverFactory factory;
    @Mock ImageSaver mockSaver;
    @InjectMocks ImageStorageService service;

    @Test void storeImage_delegatesToCorrectSaver() throws IOException {
        when(factory.getSaver("png")).thenReturn(mockSaver);
        when(mockSaver.save(any(), any(), any())).thenReturn(Paths.get("test.png"));

        Path result = service.storeImage(new BufferedImage(10,10,BufferedImage.TYPE_INT_RGB), "png", Paths.get("."), "test");
        assertEquals(Paths.get("test.png"), result);
        verify(mockSaver).save(any(), any(), any());
    }

    @Test void storeImage_unknownFormat_throws() {
        when(factory.getSaver("tiff")).thenThrow(new IllegalArgumentException("Unsupported"));
        assertThrows(IllegalArgumentException.class, () ->
            service.storeImage(null, "tiff", Paths.get("."), "test"));
    }
}

策略模式 vs 其他模式快速比較

模式適用場景關鍵差異
策略模式算法族群可互換、執行期動態切換行為封裝為獨立類別,Client 持有介面引用
工廠模式物件建構邏輯複雜、需統一入口關注「如何建立」,策略關注「如何執行」
狀態模式物件行為隨內部狀態改變狀態轉換內建,策略由外部注入
模板方法演算法骨架固定、部分步驟可變繼承結構,策略用組合

最佳實踐清單

  1. 策略介面盡量窄:單一職責,避免「上帝介面」。
  2. 策略類別預設單例:無狀態策略標記 @Scope("singleton") (預設);有狀態 (如壓縮品質可調) 用 @Scope("prototype")。
  3. 工廠用 Map O(1) 查找:避免 if-else 鏈,擴充新格式只需新增 Bean。
  4. 異常統一處理:工廠拋 IllegalArgumentException,Controller 轉 400 + 友善訊息。
  5. 測試覆蓋:每個策略獨立測試 + 整合測試驗證工廠路由。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved