DesignPattern - Behavioral - Visitor

Tue, Aug 7, 2018 閱讀時間 2 分鐘

Visitor

當必須對一組相似類型的對象執行操作時,我們使用訪問者模式。借助 visitor pattern,我們可以將操作邏輯從對象轉移到另一個類。例如,考慮一個購物車,我們可以在其中添加不同類型的商品(元素),當我們點擊結帳按鈕時,它會計算出我們需要支付的總金額。現在,我們可以在項目類中包含計算邏輯,或者我們可以使用 visitor pattern 將此邏輯移到另一個類中。因此,使用 visitor pattern,我們可以將邏輯移到另一個類。

visitor pattern 允許在不更改集合中任何對象的類的情況下定義操作。為此,visitor pattern 建議在稱為 visitor類的單獨類中定義操作。這將操作與其操作的對象集合分開。對於要定義的每個新操作,都會創建一個新的訪問者類。由於操作將在一組對像上執行,因此訪問者需要一種訪問這些對象的公共成員的方法。

  • Visitor:為對象結構中的每個 ConcreteElement 類聲明一個訪問操作。操作的名稱和簽名標識向訪問者發送訪問請求的類。這讓訪問者可以確定被訪問元素的具體類。然後訪問者可以通過其特定的界面直接訪問該元素。
  • ConcreteVisitor:實現訪問者聲明的每個操作。每個操作都實現了為結構中的相應對像類定義的算法片段。ConcreteVisitor 為算法提供上下文並存儲其本地狀態。這種狀態通常會在結構的遍歷過程中累積結果。
  • Element:定義一個接受訪問者作為參數的操作。
  • ConcreteElement:實現以訪問者為參數的 Accept 操作。

ObjectStructure:

  • 可以枚舉它的元素。
  • 可以提供高級界面以允許訪問者訪問其元素。
  • 可以是組合或集合,例如列表或集合。

    public interface ShoppingCartElement {

        public int accept(ShoppingCartVisitor visitor);
    }


    public class Beef implements ShoppingCartElement {

        private int price;
        private double weight;

        // constructor , getter , setter

        @Override
        //Notice the implementation of accept() method in concrete classes, 
        //its calling visit() method of Visitor and passing itself as argument.
        public int accept(ShoppingCartVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public class Fruit implements ShoppingCartElement {

        private int pricePerKg;
        private int weight;
        private String name;

        // constructor , getter , setter

        @Override
        public int accept(ShoppingCartVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public interface ShoppingCartVisitor {

      int visit(Beef book);
      int visit(Fruit fruit);
    }

    public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {

      @Override
      public int visit(Beef beef) {
         var cost = 0;
         //apply 5$ discount if book price is greater than 50
         if(beef.getPrice() > 50){
            cost = beef.getPrice() - 5;
         } else {
            cost = beef.getPrice();
         }
        
        log.info("beef: {}", beef);

        return cost;
      }

      @Override
      public int visit(Fruit fruit) {
         var cost = fruit.getPricePerKg() * fruit.getWeight();

         log.info("cost: " + cost);

         return cost;
      }
    }

    public static void main(String[] args) {

        var items = new ShoppingCartElement[]{
                new Beef(20, 2.0),
                new Beef(100, 3.0),
                new Fruit(10, 2, "Apple"), 
                new Fruit(5, 5, "Banana")
            };

        int total = calculatePrice(items);
        log.info("total: " + total);
    }

    private static int calculatePrice(ShoppingCartElement[] items) {

        var visitor = new ShoppingCartVisitorImpl();
        int sum = 0;
        for(ShoppingCartElement item : items){
            sum += item.accept(visitor);
        }
        return sum;
    }
  • 一個對象結構包含許多具有不同接口的對像類,並且您希望對這些對象執行依賴於它們的具體類的操作。
  • 當我們需要對對象結構中的對象執行許多不同且不相關的操作時,我們希望避免這些操作 “污染” 它們的 class。訪問者允許您通過在一個類中定義相關操作來將它們保持在一起。當我們需要與許多應用程序共享對象結構時,使用訪問者將操作放在那些需要它們的應用程序中。
  • 定義對象結構的類很少更改,但您經常希望在該結構上定義新操作。但是,更改對象結構類需要重新定義所有訪問者的接口,這可能是昂貴的。如果對象結構類經常更改,那麼最好在這些類中定義操作。