網頁

2019/4/23

Java MVC 商業邏輯(Business Logic)該放在哪?

最近在工作時,很愛把修改物件自身屬性的邏輯放在類似Model的物件裡面,因為這樣做很直覺。你不用把物件的屬性拿出來,修改,然後再放回去。這就讓我思考之前原本的(從工作中學來的)方式,把這些變動邏輯寫在Service層裡的作法是否正確?

我是在Struts2,Spring MVC那時候入行的,看到別人寫的Web程式都是經典的MVC三層架構,也就是Model-View-Controller。通常會以下幾包package:

  • com.abc.controller:負責接收前端傳來的請求,並委任Service處理業務邏輯,然後將結果返回前端。
  • com.abc.service:負責處理業務邏輯,將處理完的結果返回Controller或呼叫DAO存入資料庫。
  • com.abc.dao:負責和資料庫互動,由Service呼叫。
  • com.abc.model:單存反映某個業務模型狀態的POJO類,例如訂單(Order)。

我發現每個人包括我都是在Controller注入Service,然後把全部的業務邏輯寫在Service,然後Service去呼叫DAO和資料庫互動,而Model呢,就只是一個單純的POJO類,僅裝載資料但不含任何業務邏輯。因為大家都把Model視為POJO,所以Model不能有行為和商業邏輯是很合理的,以前我都是這麼認為。

直到今天我查到How accurate is “Business logic should be in a service, not in a model”?這篇,節錄重點如下:

In an MVP/MVC/MVVM/MV* architecture, services don't exist at all. Or if they do, the term is used to refer to any generic object that can be injected into a controller or view model. The business logic is in your model. If you want to create "service objects" to orchestrate complicated operations, that's seen as an implementation detail. A lot of people, sadly, implement MVC like this, but it's considered an anti-pattern (Anemic Domain Model) because the model itself does nothing, it's just a bunch of properties for the UI.

Some people mistakenly think that taking a 100-line controller method and shoving it all into a service somehow makes for a better architecture. It really doesn't; all it does is add another, probably unnecessary layer of indirection. Practically speaking, the controller is still doing the work, it's just doing so through a poorly named "helper" object. I highly recommend Jimmy Bogard's Wicked Domain Models presentation for a clear example of how to turn an anemic domain model into a useful one. It involves careful examination of the models you're exposing and which operations are actually valid in a business context.

For example, if your database contains Orders, and you have a column for Total Amount, your application probably shouldn't be allowed to actually change that field to an arbitrary value, because (a) it's history and (b) it's supposed to be determined by what's in the order as well as perhaps some other time-sensitive data/rules. Creating a service to manage Orders does not necessarily solve this problem, because user code can still grab the actual Order object and change the amount on it. Instead, the order itself should be responsible for ensuring that it can only be altered in safe and consistent ways.

上面的重點是說,Model本身並不應該只是單存放一堆properties且毫無作為的POJO。例如一個訂單(Order)類別通常會有數量(amount)屬性,而在業務操作中可能會增加數量,在過往我通常都是在Service層修改如下

@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderDao orderDao;
    ...
    @Transactional
    public void addAmount(int orderId, int amount) {
        ...
        Order order = orderDao.getOrderbyId(orderId);
        int oldAmount = order.getAmount;
        int newAmount = oldAmount + amount;
        order.setAmount(newAmount);
        ...
        orderDao.update(order);

    }
    ...

}

上面都是把邏輯寫在Service層中,為何不放入Order(Model)裡呢?

@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderDao orderDao;
    ...
    @Transactional
    public void addAmount(int orderId, int amount) {
        ...
        Order order = orderDao.getOrderbyId(orderId);
        order.addAmount(amount);
        ...
        orderDao.update(order);

    }
    ...

}

public class Order {
    ...
    private int amount;

    public void addAmount(int amount) {
        this.amount += amount;
    }
    ...
}

我思考的問題在於很多設計模式在業務邏輯引能撰寫在Controller-Service-Dao這種架構下變得難以發揮,某些時候如不將邏輯內聚在Model中,似乎很多Pattern無法使用,典型的類子就是四散多個Service中重複的if elseswitch case

暫時的結論就是或許Model類不應該只是一個沒有作用的POJO,而是可以帶業務邏輯的類別,所以才稱之為Model。

另外之前很容易把負責與資料庫映射的類別和Model視為同一個東西,然而兩者用途不同


不過把邏輯放在Model類前必須要思考這做法是否違反類別的單一職責原則Single Responsibility Principle (SRP)及單元測試程式是否容易撰寫等問題。

參考:

沒有留言:

張貼留言