大家好,我是你的Odoo技術伙伴。在開發復雜的業務流程時,我們有時會遇到這樣的需求:在對一個對象進行一系列復雜操作之前,保存其當前狀態,以便在操作失敗或用戶希望撤銷時,能夠一鍵恢復到操作之前的樣子。或者,我們需要追蹤一個對象(如一份合同)在不同時間點的所有歷史版本。
實現這種“狀態快照”和“時光倒流”功能的背后,正是我們今天要探討的設計模式——備忘錄模式(Memento Pattern)。
一、什么是備忘錄模式?
讓我們從一個大家都很熟悉的場景開始:玩電子游戲時的存檔。
- 你(發起人 Originator): 游戲中的主角,擁有各種狀態(生命值、等級、位置、裝備)。
- 游戲存檔文件(備忘錄 Memento): 一個包含了你當前所有狀態的“快照”。這個文件本身可能是一個加密的二進制文件,你無法直接看懂或修改它的內容。
- 游戲系統(負責人 Caretaker): 負責管理所有的存檔文件。它可以讓你創建新存檔、讀取舊存檔,但它不關心存檔文件里的具體內容。
流程是這樣的:
- 在挑戰一個強大的BOSS之前,你選擇“保存游戲”。游戲主角(發起人)將自己的當前狀態打包成一個存檔(備忘錄)。
- 游戲系統(負責人)接收這個存檔,并將其保存在一個存檔槽里。
- 不幸的是,你挑戰失敗了。你選擇“讀取存檔”。
- 游戲系統(負責人)從存檔槽里取出之前的存檔文件,并將其交還給游戲主角(發起人)。
- 游戲主角(發起人)使用這個存檔文件,將自己的所有狀態恢復到了挑戰BOSS之前的樣子。
備忘錄模式的核心思想是:在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣以后就可將該對象恢復到原先保存的狀態。
關鍵在于:
- 封裝性: 只有發起人自己知道如何創建和恢復備忘錄。負責人和備忘錄本身都無法訪問或修改狀態的細節。
- 狀態隔離: 對象的狀態被提取出來,獨立于對象本身進行存儲和管理。
二、Odoo中的備忘錄模式:追蹤與審計的基石
在Odoo中,你可能不會顯式地去創建一個Memento
類。但是,備忘錄模式的思想被巧妙地應用在了幾個核心功能中,尤其是那些與歷史追蹤和版本控制相關的場景。
1. 字段追蹤 (tracking=True
) 與 Chatter
這是Odoo中備忘錄模式最直觀、最普遍的應用。當你為一個字段設置tracking=True
時,你就啟動了一個針對該字段的“自動存檔”系統。
class SaleOrder(models.Model):_inherit = 'sale.order'# 當 stage_id 字段的值發生變化時,系統會自動創建一個“備忘錄”stage_id = fields.Many2one('sale.order.stage', string='Stage', tracking=True)user_id = fields.Many2one('res.users', string='Salesperson', tracking=True)
讓我們來分解這個場景:
- 發起人(Originator):
sale.order
記錄。它擁有stage_id
和user_id
等內部狀態。 - 備忘錄(Memento): 當字段值變化時,
mail.tracking.value
模型中創建的一條新記錄。這條記錄精確地捕獲了“哪個字段,從什么舊值,變成了什么新值”。它就是一個包含了部分狀態變化的“微型快照”。 - 負責人(Caretaker):
- Chatter (
mail.thread
): 它負責“保管”和“展示”這些備忘錄。你在Chatter里看到的“Stage changed from Quotation to Sales Order”這樣的消息,就是負責人對備忘錄的可視化呈現。 - Odoo的ORM和事務系統: 它們負責在
write
操作發生時,自動地創建這些備忘錄,并將它們與發起人(sale.order
記錄)關聯起來。
- Chatter (
這個過程如何體現備忘錄模式?
- 狀態捕獲: Odoo ORM在保存(
write
)對象前,檢測到被追蹤字段的變化,并捕獲了其新舊值。 - 外部存儲: 這個狀態變化信息被存儲在獨立的
mail.tracking.value
表中,而不是sale.order
表自身。 - 封裝性:
sale.order
模型并不直接關心這些追蹤記錄是如何存儲的,它只負責在狀態變化時,通過_track_subtype
等方法發出一個“需要存檔”的信號。Chatter(負責人)也不知道狀態變化的具體業務含義,它只負責展示。 - 恢復(概念上): 雖然Odoo的Chatter主要用于審計和追蹤,不提供一鍵“恢復”功能,但它完整地保存了歷史狀態。一個開發者可以基于這些“備忘錄”(追蹤記錄),編寫一個手動的方法來將
sale.order
恢復到之前的某個狀態。
2. 假設的“草稿版本”功能(自定義實現)
讓我們設想一個更貼近經典備忘錄模式的自定義場景:為復雜的報價單提供“保存草稿”和“恢復草稿”的功能。
假設我們有一個復雜的報價單,用戶在正式發送給客戶前,可能會進行多次修改和測算。我們希望提供一個功能,讓用戶可以隨時保存一個“草稿版本”,并在需要時恢復到這個版本。
# 偽代碼,用于說明思想
import jsonclass Quotation(models.Model):_name = 'sale.quotation'_inherit = ['mail.thread']# ... 報價單的各種字段 ...order_line = fields.One2many(...)# 負責人(Caretaker)的一部分:存儲備忘錄的地方memento_ids = fields.One2many('sale.quotation.memento', 'quotation_id')def create_memento(self, name):"""發起人(Originator)創建備忘錄的方法"""self.ensure_one()# 1. 捕獲內部狀態state_snapshot = {'note': self.note,'payment_term_id': self.payment_term_id.id,'lines': [line.read()[0] for line in self.order_line]}# 2. 創建備忘錄對象,但將狀態封裝在json字段中# 備忘錄本身不知道這些數據的具體含義self.env['sale.quotation.memento'].create({'name': name,'quotation_id': self.id,'state_data': json.dumps(state_snapshot)})def restore_from_memento(self, memento):"""發起人(Originator)從備忘錄恢復狀態的方法"""self.ensure_one()# 1. 從備忘錄獲取狀態數據state_snapshot = json.loads(memento.state_data)# 2. 恢復自身狀態# 只有發起人自己知道如何解讀和應用這些數據self.order_line.unlink() # 先清空舊的行self.write({'note': state_snapshot.get('note'),'payment_term_id': state_snapshot.get('payment_term_id'),'order_line': [(0, 0, line_vals) for line_vals in state_snapshot.get('lines', [])]})class QuotationMemento(models.Model):_name = 'sale.quotation.memento'_description = 'Quotation Snapshot (Memento)'name = fields.Char('Version Name')quotation_id = fields.Many2one('sale.quotation')# 備忘錄的核心:存儲狀態,但不暴露其內部結構state_data = fields.Text('State Data (JSON)', readonly=True)def action_restore(self):"""負責人的一個動作,觸發恢復"""self.quotation_id.restore_from_memento(self)
這個自定義實現完整地展示了備忘錄模式的三個角色及其職責,提供了一個真正的“存檔/讀檔”功能。
三、優勢與適用場景
優勢
- 保護封裝性: 將對象的狀態快照功能,從對象本身的核心業務邏輯中分離出來。狀態的保存和恢復細節由發起人自己控制,外部世界(負責人)無法篡改備忘錄的內部。
- 簡化發起人: 發起人不需要關心狀態的存儲和管理,它只需要在需要時創建備忘錄或從備忘錄中恢復即可,職責更加單一。
- 高內聚,松耦合: 備忘錄模式提供了一種狀態恢復的實現機制,而客戶端(負責人)與這個機制是松耦合的。
適用場景
- 需要提供一個可撤銷(Undo)或可回滾(Rollback)操作的場景。
- 需要對一個對象的歷史版本進行追蹤和審計時(如Odoo的
tracking
功能)。 - 當需要保存的內部狀態非常復雜,不希望將這些狀態直接暴露給外部時。
結論
備忘錄模式在Odoo中是一種“幕后英雄”式的設計模式。它不像觀察者模式或工廠模式那樣隨處可見,但它在確保數據可追溯性、提供審計日志、以及構建可恢復操作等方面,提供了堅實的設計思想基礎。
Odoo的字段追蹤(tracking=True
)功能,就是對備忘錄模式最經典、最成功的應用。它自動地為我們捕獲、存儲和展示了對象狀態變化的“備忘錄”,極大地提升了系統的透明度和可審計性。
作為Odoo開發者,理解備忘錄模式,將幫助你更好地利用Odoo的追蹤功能,并在需要實現“撤銷”或“版本控制”等高級功能時,為你提供一個清晰、可靠的設計思路。