問題1:命名
它的最大缺陷(在我看來)是命名本身。 “訪客”模式。 當我們用google搜索它時,我們很可能會在相關的Wikipedia文章中找到自己,顯示類似這樣的有趣圖像:

維基百科訪客模式示例
對。 對于我們98%的人在日常軟件工程工作中對車輪,發動機和車身的思考而言,這很明顯,因為我們知道,機修工向我們收取幾千美元的汽車維修費用后,我們會首先訪問車輪,然后是發動機,然后最終訪問我們的錢包并接受我們的現金。 如果我們很不幸,他也會在我們工作時拜訪我們的妻子,但她永遠不會接受那個忠實的靈魂。
但是,解決工作中其他問題的2%的人呢? 就像我們為電子銀行系統,證券交易所客戶,Intranet門戶等編寫復雜的數據結構時一樣。為什么不將訪客模式應用于真正的分層數據結構? 喜歡文件夾和文件? (好的,畢竟不是那么復雜)
好的,所以我們將“訪問”文件夾,每個文件夾將讓其文件“接受”為“訪客”,然后我們也讓訪問者“訪問”這些文件。 什么?? 汽車讓其零件接納訪客,然后讓訪客自我訪問嗎? 這些條款具有誤導性。 它們是通用的,適合設計模式。 但是它們會殺死您的現實設計,因為沒有人會考慮“接受”和“訪問”,而實際上您是在讀/寫/刪除/修改文件系統。
問題2:多態
當應用于錯誤的情況時,這比命名引起的頭痛甚至更多。 訪客為什么在地球上認識其他所有人? 為什么訪問者需要針對層次結構中每個涉及元素的方法? 多態性和封裝要求將實現隱藏在API的后面。 (我們數據結構的)API可能以某種方式實現了復合模式 ,即其部分繼承自公共接口。 好吧,當然,車輪不是汽車,我妻子也不是機械師。 但是當我們采用文件夾/文件結構時,它們不是全部都是java.util.File對象嗎?
了解問題
實際的問題不是訪問代碼的命名和可怕的API詳細程度,而是對模式的誤解。 這不是最適合訪問帶有許多不同類型對象的大型復雜數據結構的模式。 這種模式最適合于訪問幾種不同類型的簡單數據結構,但要訪問數百名訪問者。 取文件和文件夾。 那是一個簡單的數據結構。 您有兩種類型。 一個可以包含另一個,兩者共享一些屬性。 各種訪客可能是:
- CalculateSizeVisitor
- FindOldestFileVisitor
- DeleteAllVisitor
- FindFilesByContentVisitor
- ScanForVirusesVisitor
- …你給它起名字
我仍然不喜歡命名,但是這種模式在這種范例中可以完美地工作。
那么,訪客模式何時“錯誤”?
我想以jOOQ QueryPart結構為例。 其中有很多,可以對各種SQL查詢結構進行建模,從而使jOOQ可以構建和執行任意復雜度的SQL查詢。 讓我們舉幾個例子:
- 健康)狀況
- 組合條件
- 領域
- 表格欄位
- 字段清單
還有更多。 它們中的每一個都必須能夠執行兩個操作:渲染SQL和綁定變量。 那將使兩個訪問者每個人都知道……40-50種類型……? 也許在遙遠的將來,jOOQ查詢將能夠呈現JPQL或其他某種查詢類型。 那將使3位訪客面對40-50種類型。 顯然,在這里,經典的訪客模式是一個錯誤的選擇。 但是我仍然想“訪問” QueryPart,將渲染和綁定委托給較低的抽象級別。
那么如何實現呢?
很簡單:堅持使用復合模式! 它允許您向每個人都必須實現的數據結構添加一些API元素。
因此,憑直覺,第一步就是
interface QueryPart {// Let the QueryPart return its SQLString getSQL();// Let the QueryPart bind variables to a prepared// statement, given the next bind index, returning// the last bind indexint bind(PreparedStatement statement, int nextIndex);
}
使用此API,我們可以輕松地抽象SQL查詢并將職責委派給較低級別??的工件。 例如,一個BetweenCondition。 它負責在[lower]和[upper]條件之間正確排序[field]的各部分,語法正確地呈現SQL,并將部分任務委派給其child-QueryParts:
class BetweenCondition {Field field;Field lower;Field upper;public String getSQL() {return field.getSQL() + ' between ' +lower.getSQL() + ' and ' +upper.getSQL();}public int bind(PreparedStatement statement, int nextIndex) {int result = nextIndex;result = field.bind(statement, result);result = lower.bind(statement, result);result = upper.bind(statement, result);return result;}
}
另一方面,BindValue主要負責變量綁定
class BindValue {Object value;public String getSQL() {return '?';}public int bind(PreparedStatement statement, int nextIndex) {statement.setObject(nextIndex, value);return nextIndex + 1;}
}
結合起來,我們現在可以輕松創建這種形式的條件: 在之間? 和?。 當實現更多QueryPart時,我們還可以想象像MY_TABLE.MY_FIELD BETWEEN嗎? 如果可以使用適當的字段實現,則使用AND(選擇?從雙精度)。 這就是使復合模式如此強大,通用的API和許多封裝行為的組件,從而將行為的一部分委派給子組件的原因。
第2步負責API的演變
到目前為止,我們已經看到了復合模式,非常直觀,但是功能非常強大。 但是遲早,我們將需要更多的參數,因為我們發現要將狀態從父級QueryPart傳遞給子級。 例如,我們希望能夠內聯某些子句的某些綁定值。 也許某些SQL方言不允許BETWEEN子句中的綁定值。 如何使用當前的API處理該問題? 擴展它,添加一個“布爾內聯”參數? 沒有! 這就是發明訪客模式的原因之一。 為了使復合結構元素的API保持簡單(只需執行“接受”)。 但是在這種情況下,用“上下文”替換參數比實現真正的訪客模式好得多:
interface QueryPart {// The QueryPart now renders its SQL to the contextvoid toSQL(RenderContext context);// The QueryPart now binds its variables to the contextvoid bind(BindContext context);
}
上面的上下文包含這樣的屬性(setter和render方法返回上下文本身,以允許方法鏈接):
interface RenderContext {// Whether we're inlining bind variablesboolean inline();RenderContext inline(boolean inline);// Whether fields should be rendered as a field declaration// (as opposed to a field reference). This is used for aliased fieldsboolean declareFields();RenderContext declareFields(boolean declare);// Whether tables should be rendered as a table declaration// (as opposed to a table reference). This is used for aliased tablesboolean declareTables();RenderContext declareTables(boolean declare);// Whether we should cast bind variablesboolean cast();// Render methodsRenderContext sql(String sql);RenderContext sql(char sql);RenderContext keyword(String keyword);RenderContext literal(String literal);// The context's 'visit' methodRenderContext sql(QueryPart sql);
}
BindContext也是如此。 如您所見,該API相當可擴展,可以添加新屬性,還可以添加其他常見的呈現SQL的方法。 但是BetweenCondition不必放棄有關如何呈現其SQL以及是否允許綁定變量的封裝知識。 它會將這些知識保留給自己:
class BetweenCondition {Field field;Field lower;Field upper;// The QueryPart now renders its SQL to the contextpublic void toSQL(RenderContext context) {context.sql(field).keyword(' between ').sql(lower).keyword(' and ').sql(upper);}// The QueryPart now binds its variables to the contextpublic void bind(BindContext context) {context.bind(field).bind(lower).bind(upper);}
}
另一方面,BindValue主要負責變量綁定
class BindValue {Object value;public void toSQL(RenderContext context) {context.sql('?');}public void bind(BindContext context) {context.statement().setObject(context.nextIndex(), value);}
}
結論:將其命名為上下文模式,而不是訪客模式
快速跳到訪客模式時要小心。 在許多情況下,您將使設計變得腫,從而使其完全不可讀且難以調試。 這里是要記住的規則,總結如下:
- 如果您有許多訪問者并且數據結構相對簡單(幾種類型),那么訪問者模式可能就可以了。
- 如果您有很多類型,并且訪問者組相對較少(很少有行為),則訪問者模式是過大的,請堅持使用復合模式
- 為了簡化API的演變,請將您的復合對象設計為具有采用單個上下文參數的方法。
- 突然之間,您將再次遇到“幾乎訪問者”模式,其中context = visitor,“ visit”和“ accept” =“您專有的方法名稱”
同時,“上下文模式”與“復合模式”一樣直觀,而與“訪問者模式”一樣強大,結合了兩個方面的優勢。
參考: 訪問者模式是我們的JCG合作伙伴 Lukas Eder在JAVA,SQL和JOOQ博客上再次訪問的 。
翻譯自: https://www.javacodegeeks.com/2012/05/visitor-pattern-re-visited.html