好幾年前在之前的公司曾帶過幾個資策會出來的學弟。
記得剛 On Board 時我給他們幾個作業,Code Review 的時候也特別強調「要符合專案的架構風格與一些設計原則」。其實對 Junior 來說,他們以前專注的是「程式能不能跑、結果對不對」,效能、維護性、延展性這些詞,對當時的他們來說太遠了。
另外套句我碩士教授說的:
「你們那個系統沒多少人用,吹毛求疵那個效能幹嘛,不如把時間花在其他事情上。」
如今我已經在能反駁他的職位與工作需求上,發現畢業七八年後的我才真正體會那句話背後的現實。
軟體一旦上線,性能與維護都是長期成本。好的程式能提升協作效率,能降低 review、除錯、交接甚至是升級的時間。
而 OOP 與 SOLID ,正是這些「長期維護成本」的根本解方。
OOP:物件導向程式設計的起點 OOP 的四大核心是:
封裝(Encapsulation)、抽象(Abstraction)、繼承(Inheritance)、多型(Polymorphism)
目的是讓系統更容易維護、復用與擴充。
範例:顧客下單(Place Order) 以下是一個典型的 Spring 三層架構範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @RestController @RequiredArgsConstructor @RequestMapping("/orders") public class OrderController { private final OrderService orderService; @PostMapping public PlaceOrderRs place (@RequestBody PlaceOrderRq rq) { return orderService.place(rq); } } @Service @RequiredArgsConstructor public class OrderService extends BaseService { private final ItemRepository itemRepository; private final OrderRepository orderRepository; private final RabbitTemplate rabbitTemplate; private static final String ORDER_EXCHANGE = "order.events" ; private static final String ORDER_PLACED_RK = "order.placed" ; @Transactional public PlaceOrderRs place (PlaceOrderRq rq) { require(rq != null , "request must not be null" ); require(!rq.getLines().isEmpty(), "lines must not be empty" ); BigDecimal total = BigDecimal.ZERO; List<OrderLineEntity> lines = new ArrayList <>(); for (OrderLineRq line : rq.getLines()) { var item = itemRepository.findById(Long.parseLong(line.getItemId())) .orElseThrow(() -> bad("item not found: " + line.getItemId())); require(line.getQty() > 0 , "qty must > 0" ); BigDecimal amount = item.getPrice().multiply(BigDecimal.valueOf(line.getQty())); total = total.add(amount); lines.add(new OrderLineEntity (item.getId(), item.getName(), item.getPrice(), line.getQty())); } OrderEntity order = new OrderEntity (); order.setTotal(total); order.setStatus("PLACED" ); order.setLines(lines); updateAuditFields(order); var saved = orderRepository.save(order); publishOrderPlaced(saved); return new PlaceOrderRs (saved.getId().toString(), total); } private void publishOrderPlaced (OrderEntity order) { var event = new OrderPlacedEvent (order.getId(), order.getTotal(), order.getLines().stream() .map(l -> new OrderPlacedEvent .Line(l.getItemId(), l.getItemName(), l.getPriceSnapshot(), l.getQty())) .toList()); rabbitTemplate.convertAndSend(ORDER_EXCHANGE, ORDER_PLACED_RK, event); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j public abstract class BaseService { @Autowired protected Clock clock; protected void updateAuditFields (Auditable entity) { var now = LocalDateTime.now(clock); if (entity.getCreatedAt() == null ) entity.setCreatedAt(now); entity.setUpdatedAt(now); } protected void require (boolean condition, String msg) { if (!condition) throw new IllegalArgumentException (msg); } protected RuntimeException bad (String msg) { return new IllegalArgumentException (msg); } }
三層架構下的 OOP 封裝 (Encapsulation) 的概念是把資料與行為包在一起,外界只透過方法使用物件,不直接操作內部狀態 。
例如 OrderLineEntity 內有 private 的 total 物件, 並提供 public 的 getter, setter 方法調用;OrderService 封裝下單流程,Controller 只要呼叫 place()。藉由封裝可以隱藏內部實作,只暴露必要介面,也就是說,外部只應該看到物件能做什麼,而不需要知道它怎麼做。
抽象 (Abstraction) 隱藏細節,讓使用者只看到介面層次。 Controller 不知道 DB 細節,只知道有個 OrderService.place(), 而 Service 只依賴抽象的 PaymentRepository,不在乎底層是 MySQL、PostgreSQL 或是外部 API。 這樣一來,當需求變動時,我們能在不修改 Service 的情況下自由替換底層實作。
繼承 (Inheritance) 繼承的目的,是共用行為並延伸差異。這裡的 updateAuditFields 就是繼承 (Inheritance) 的實例,統一管理審計欄位更新,而 OrderService 在下單邏輯中直接呼叫它完成欄位更新。
多型 (Polymorphism) 同一介面可有不同實作,執行時依物件而異。 不同 OrderRepository(JPA、MyBatis、Mock)都能被注入運作。
OOP 的侷限 上述是常見的三層架構, 好讀好理解, 但其實暗藏地雷:
OrderService 同時負責驗證、計算、組訂單、發事件,違反單一職責原則。
若要加上「折扣」、「定價策略」或「不同事件通道」,都得改動 OrderService。
測試難度高,耦合嚴重,會讓大家只想寫 Happy Path。
接下來,我們用 SOLID 原則 將這段程式「演化」成可長期維護的架構。
SOLID:讓 OOP 真正能長大的五個原則 重構前後對照:從 Controller-Service 到 Clean Architecture 重構前 - 傳統三層架構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @RestController @RequiredArgsConstructor @RequestMapping("/orders") public class OrderController { private final OrderService orderService; @PostMapping public PlaceOrderRs place (@RequestBody PlaceOrderRq rq) { return orderService.place(rq); } } @Service @RequiredArgsConstructor public class OrderService extends BaseService { private final ItemRepository itemRepository; private final OrderRepository orderRepository; private final RabbitTemplate rabbitTemplate; @Transactional public PlaceOrderRs place (PlaceOrderRq rq) { require(rq != null , "request must not be null" ); require(!rq.getLines().isEmpty(), "lines must not be empty" ); BigDecimal total = BigDecimal.ZERO; List<OrderLineEntity> lines = new ArrayList <>(); for (OrderLineRq line : rq.getLines()) { var item = itemRepository.findById(Long.parseLong(line.getItemId())) .orElseThrow(() -> bad("item not found: " + line.getItemId())); require(line.getQty() > 0 , "qty must > 0" ); BigDecimal amount = item.getPrice().multiply(BigDecimal.valueOf(line.getQty())); total = total.add(amount); lines.add(new OrderLineEntity (item.getId(), item.getName(), item.getPrice(), line.getQty())); } OrderEntity order = new OrderEntity (); order.setTotal(total); order.setStatus("PLACED" ); order.setLines(lines); updateAuditFields(order); var saved = orderRepository.save(order); publishOrderPlaced(saved); return new PlaceOrderRs (saved.getId().toString(), total); } }
重構後 - Clean Architecture:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 @RestController @RequiredArgsConstructor @RequestMapping("/orders") public class OrderController { private final PlaceOrderUseCase placeOrderUseCase; @PostMapping public PlaceOrderResponse place (@RequestBody PlaceOrderRequest request) { var command = new PlaceOrderCommand ( request.customerId(), request.lines().stream() .map(l -> new PlaceOrderCommand .PlaceOrderLine(l.itemId(), l.qty())) .toList() ); var result = placeOrderUseCase.execute(command); return new PlaceOrderResponse (result.orderId(), result.total()); } } @Service @RequiredArgsConstructor public class PlaceOrderUseCaseImpl implements PlaceOrderUseCase { private final ItemGateway itemGateway; private final OrderGateway orderGateway; private final EventPublisher eventPublisher; private final PricingPolicy pricingPolicy; private final Clock clock; @Transactional public PlaceOrderResult execute (PlaceOrderCommand cmd) { var order = new Order (); for (var line : cmd.lines()) { var item = itemGateway.findById(Long.parseLong(line.itemId())) .orElseThrow(() -> new IllegalArgumentException ("item not found" )); var unitPrice = pricingPolicy.unitPriceFor(item, line.qty()); order.addLine(item, line.qty(), unitPrice); } order.place(); order.touch(LocalDateTime.now(clock)); var saved = orderGateway.save(order); eventPublisher.publishOrderPlaced(saved); return new PlaceOrderResult (saved.getId().toString(), saved.getTotal()); } } public class Order { private OrderId id; private List<OrderLine> lines = new ArrayList <>(); private BigDecimal total = BigDecimal.ZERO; private OrderStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; public void addLine (Item item, int quantity, BigDecimal unitPrice) { require(quantity > 0 , "Quantity must be positive" ); require(unitPrice.compareTo(BigDecimal.ZERO) >= 0 , "Unit price must be non-negative" ); var line = new OrderLine (item.id(), item.name(), unitPrice, quantity); lines.add(line); total = total.add(line.subTotal()); } public void place () { require(!lines.isEmpty(), "Cannot place order with no lines" ); this .status = OrderStatus.PLACED; } public void touch (LocalDateTime timestamp) { if (this .createdAt == null ) { this .createdAt = timestamp; } this .updatedAt = timestamp; } public BigDecimal getTotal () { return total; } public OrderStatus getStatus () { return status; } }
對照分析
維度
重構前 (Controller-Service)
重構後 (Clean Architecture)
職責分離
Service 承擔多重職責
UseCase 協調,Domain 封裝邏輯
業務邏輯
散落在 Service 方法中
集中在領域模型內
測試難度
需要整合測試,耦合嚴重
可單元測試各組件
擴充性
修改現有類別
新增策略類別,符合開放封閉
依賴方向
依賴具體框架技術
依賴抽象接口
S:單一職責原則(Single Responsibility) 重構前問題: OrderService 同時負責:
重構後解決:
PlaceOrderUseCase:協調業務流程
Order 領域模型:封裝訂單業務規則
PricingPolicy:專注定價策略
EventPublisher:處理事件發布
O:開放封閉原則(Open/Closed Principle) 重構前問題: 新增定價策略需要修改 OrderService:
1 2 3 4 5 6 7 8 if (isCampaignPeriod()) { price = item.getPrice().multiply(new BigDecimal ("0.9" )); } else if (isMemberPrice(user)) { price = item.getPrice().multiply(new BigDecimal ("0.95" )); } else { price = item.getPrice(); }
重構後解決: 透過策略模式擴充:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface PricingPolicy { BigDecimal unitPriceFor (Item item, int qty) ; } @Component class StandardPricingPolicy implements PricingPolicy { public BigDecimal unitPriceFor (Item item, int qty) { return item.price(); } } @Component class MemberPricingPolicy implements PricingPolicy { public BigDecimal unitPriceFor (Item item, int qty) { return item.price().multiply(new BigDecimal ("0.95" )); } }
L:里氏替換原則(Liskov Substitution Principle) 重構前問題: 直接依賴 Spring Data JPA:
1 private final OrderRepository orderRepository;
重構後解決: 自定義 Gateway 接口:
1 2 3 4 5 6 7 8 9 10 public interface OrderGateway { Order save (Order order) ; Optional<Order> findById (OrderId id) ; } @Repository class JpaOrderGateway implements OrderGateway { private final SpringDataOrderJpa jpa; }
I:介面隔離原則(Interface Segregation Principle) 重構前問題: Repository 介面可能過於龐大:
1 2 3 public interface OrderRepository extends JpaRepository <OrderEntity, Long> { }
重構後解決: 按職責分離接口:
1 2 3 4 5 6 7 8 9 10 11 public interface ItemGateway { Optional<Item> findById (Long id) ; } public interface OrderGateway { Order save (Order order) ; } public interface EventPublisher { void publishOrderPlaced (Order order) ; }
D:依賴反轉原則(Dependency Inversion Principle) 重構前問題: 高層模組依賴低層模組:
1 2 3 Controller → Service → Repository (JPA) → Database ↓ RabbitTemplate → Message Queue
重構後解決: 高層定義接口,低層實現:
1 2 3 Controller → UseCase (業務邏輯) ↓ Ports (接口定義) ← Adapters (實作)
結論, 追求更好的環境 最後整理了結論:
三層架構在專案初期很快,但當業務複雜度成長時,修改一個功能可能引發多處變動
Clean Architecture 透過依賴反轉和介面隔離,讓系統能夠優雅地適應變化
清晰的架構邊界讓多人開發時減少衝突
新成員能透過架構快速理解業務邏輯的分佈
重構前:OrderService 的測試需要準備大量 Mock,測試程式碼複雜
重構後:每個組件都可以獨立測試,測試程式碼更專注、更簡單
當需要更換資料庫、消息隊列或框架時,Clean Architecture 讓這些變更侷限在基礎設施層,不影響核心業務邏輯
回頭看教授當年的那句話:「你們那個系統沒多少人用,吹毛求疵那個效能幹嘛」,我現在有了更深的理解。他說的其實是成本效益的權衡 ——在資源有限的情況下,應該把精力花在刀刃上。
說實在, 如果三層式架構這麼多毛病, 怎麼還沒被淘汰呢? 反過來說, 若三層式架構就足夠應付業務邏輯, 還能快速交付, 又為何不使用呢?
在這種環境下,我們也只能更努力的去追求更好, 資源與需求更大的企業,然後保持學習與對程式品質的初心。