Java中Null Object 設計模式

今天,我將討論較少使用的空對象模式。在面向對象編程中,我們經常處理空對象。空對象是指沒有任何引用的對象,或者定義爲中性/空功能/行爲的對象。在訪問任何成員或調用任何方法時,需要檢查這些null對象,以確保它們不是null。這是因爲成員或方法通常不能在null對象上調用。

Null Object Pattern

  • null對象設計模式描述了null對象的使用及其在系統中的行爲。
  • 空對象模式處理空對象。
  • 我們不檢查空對象,而是定義空行爲或調用不做行爲。
  • 這些null對象還可以用於在數據不可用的情況下提供默認行爲。
  • 這種方法相對於可工作的默認實現的優點是null對象是非常可預測的,並且沒有副作用——它什麼也不做。
  • null對象模式還可以用作測試的存根,以防測試無法使用資源。

在使用Null對象模式之前,我們應該瞭解:

  • 這種模式應謹慎使用。它可以使錯誤出現在正常的程序執行中。
  • 我們不應該僅僅爲了避免null檢查和使代碼更易於閱讀而實現這種模式。實際上,如果代碼移動到另一個地方,比如null對象類,那麼讀取它就會比較困難。
  • 我們必須執行額外的測試,以確保沒有任何地方需要指定null而不是null對象。

讓我們看一個例子來更好地理解這個模式。

Example of Null Objects

創建一個抽象類(或接口)來指定各種功能。在這個示例中,我使用了shape接口。請注意,我在接口中也創建了isNull()方法。有一個方法很好,我喜歡它,因爲我可以更好地識別和控制空定義的對象。該方法將爲所有的具體類返回false。而且,它只會爲null對象類返回true。

        package design.nullobject;        
        
        public interface Shape {
        
        double area();
            double perimeter();
            void draw();
            // nice to have method to indicate null object
            boolean isNull();
        }

您將需要創建一個具體的類來擴展這個類或實現接口。每個具體類將定義功能的特定版本。我定義了三種形狀:圓形、矩形和三角形。這些具體的類將定義不同類型的形狀。

下面是Circle類的代碼:

        package design.nullobject;        
        
        public class Circle implements Shape {
          // sides
            private final double radius;
        
            public Circle() {
                this(1.0d);
            }   
            public Circle(double radius) {
                this.radius = radius;
            }
        
            @Override public double area() {
                // Area = π r^2
                return Math.PI * Math.pow(radius, 2);
            }
        
            @Override public double perimeter() {
                // Perimeter = 2πr
                return 2 * Math.PI * radius;
            }
        
            @Override public void draw() {
            System.out.println("Drawing Circle with area: " + area() + " and perimeter: " + perimeter());
            }
        
            @Override
            public boolean isNull() {
            return false;
            }
        }

下面的是 Rectangle 類的代碼:

        package design.nullobject;        
        
        public class Rectangle implements Shape {
        // sides
            private final double width; 
            private final double length;
        
            public Rectangle() {
                this(1.0d ,1.0d);
            }
            public Rectangle(double width, double length) {
                this.width = width;
                this.length = length;
            }
        
            @Override
            public double area() {
                // A = w * l
                return width * length;
            }
        
            @Override
            public double perimeter() {
                // P = 2(w + l)
                return 2 * (width + length);
            }
        
            @Override 
            public void draw() {
            System.out.println("Drawing Rectangle with area: " + area() + " and perimeter: " + perimeter());
            }
        
            @Override
            public boolean isNull() {
            return false;
            }
        }

Triangle 類:

        package design.nullobject;        
        
        public class Triangle implements Shape {
            // sides
            private final double a;
            private final double b;
            private final double c;
        
            public Triangle() {
                this(1.0d, 1.0d, 1.0d);
            }
        
            public Triangle(double a, double b, double c) {
                this.a = a;
                this.b = b;
                this.c = c;
            }
        
            @Override
            public double area() {
                // Using Heron's formula:
                // Area = SquareRoot(s * (s - a) * (s - b) * (s - c)) 
                // where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle 
                double s = (a + b + c) / 2;
                return Math.sqrt(s * (s - a) * (s - b) * (s - c));
            }
        
            @Override
            public double perimeter() {
                // P = a + b + c
                return a + b + c;
            }
        
            @Override public void draw() {
            System.out.println("Drawing Triangle with area: " + area() + " and perimeter: " + perimeter());
            }
        
            @Override
            public boolean isNull() {
            return false;
            }
        }

現在,最重要的一步是創建一個空對象類,它擴展抽象類或接口並定義“不做任何事情”行爲。“不做”行爲類似於在數據不可用的情況下的默認行爲。

        package design.nullobject;        
        
        public class NullShape implements Shape {
        // no sides
        
            @Override
            public double area() {
            return 0.0d;
            }
        
            @Override
            public double perimeter() {
            return 0.0d;
            }
        
            @Override
            public void draw() {
            System.out.println("Null object can't be draw");
            }
        
            @Override
            public boolean isNull() {
            return true;
            }
        
        }

現在,我們定義Factory類來創建各種類型的形狀。

爲這個示例創建ShapeFactory類。

        package design.nullobject;        
        
        public class ShapeFactory {
        
        public static Shape createShape(String shapeType) {
            Shape shape = null;
            if ("Circle".equalsIgnoreCase(shapeType)) {
            shape = new Circle();
            } else if ("Rectangle".equalsIgnoreCase(shapeType)) {
            shape = new Rectangle();
            } else if ("Triangle".equalsIgnoreCase(shapeType)) {
            shape = new Triangle();
            } else {
            shape = new NullShape();
            }
            return shape;
        }
        }

爲了簡單起見,我沒有收到ShapeFactory方法中形狀邊的參數。因此,工廠正在創建具有固定邊值的不同Shape對象。

最後一步,創建一個主類來執行和測試代碼:

        package design.nullobject;        
        
        import design.nullobject.ShapeFactory;
        
        public class ShapeMain {
        
            public static void main(String[] args) {
                String[] shapeTypes = new String[] { "Circle", null, "Triangle", "Pentagon", "Rectangle", "Trapezoid"};
                for (String shapeType : shapeTypes) {
                    Shape shape = ShapeFactory.createShape(shapeType);
                    // no null-check required since shape factory always creates shape objects
                    System.out.println("Shape area: " + shape.area());
                    System.out.println("Shape Perimeter: " + shape.perimeter());
                    shape.draw();
                    System.out.println();
                }
            }
        
        }

下面是代碼的輸出:

        Shape area: 3.141592653589793        
        Shape Perimeter: 6.283185307179586
        Drawing Circle with area: 3.141592653589793 and perimeter: 6.283185307179586
        
        Shape area: 0.0
        Shape Perimeter: 0.0
        Null object can't be draw
        
        Shape area: 0.4330127018922193
        Shape Perimeter: 3.0
        Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
        
        Shape area: 0.0
        Shape Perimeter: 0.0
        Null object can't be draw
        
        Shape area: 1.0
        Shape Perimeter: 4.0
        Drawing Rectangle with area: 1.0 and perimeter: 4.0
        
        Shape area: 0.0
        Shape Perimeter: 0.0
        Null object can't be draw

在Java 8中,我們有java.util.Optional處理空引用的類。這個類最初來自於Guava API。

Optional

該類的目的是提供一個類型級別的解決方案,用於表示可選值,而不是使用空引用。

下面,我將演示java.util.Optional的一些有用api。有幾個靜態api來創建可選對象:

Optional.empty() : 要創建一個空的可選對象,使用空API:

        @Test        
        public void optionalEmptyTest() {
            Optional<Shape> empty = Optional.empty();
            assertFalse(empty.isPresent());
        }

Optional.of(): 要創建一個非空的可選對象,如果我們確信我們有一個我們想讓它成爲可選的對象,使用API。如果您使用null調用這個API,它將拋出null指針異常 ( NullPointerException )。

        @Test        
        public void optionalOfTest() {
          Shape shape = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.of(shape);
          assertTrue(nonEmpty.isPresent());
        }
        
        @Test(expected = NullPointerException.class)
        public void optionalOfWithNullTest() {
          Shape shape = null;
          Optional.of(shape);
        }

Optional.ofNullable():在上面的代碼中,當我們處於空或非空狀態時,這兩個api都很有用。如果我們不確定對象,對象可能爲空,也可能不爲空。爲此,使用 ofNullable API。

        @Test        
        public void optionalOfNullableTest() {
        Shape shape1 = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.ofNullable(shape1);
          assertTrue(nonEmpty.isPresent());
        
          Shape shape2 = null;
            Optional<Shape> empty = Optional.ofNullable(shape2);
          assertFalse(empty.isPresent());
        }

Additional APIs

isPresent():此方法返回true,且僅當包裝在可選對象中的對象不是空的(非空)。

        @Test        
        public void optionalIsPresentTest() {
        Shape shape = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.of(shape);
          assertTrue(nonEmpty.isPresent());
        
            Optional<Shape> empty = Optional.empty();
          assertFalse(empty.isPresent());
        }

ifPresent(): 這使我們能夠在包裝值上運行代碼,如果發現它是非空的。

        @Test        
        public void optionalIfPresentTest() {
        Shape shape = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.of(shape);
          nonEmpty.ifPresent(circle -> circle.draw());
        
            Optional<Shape> empty = Optional.empty();
          empty.ifPresent(circle -> circle.draw());
        }

get():如果包裝對象不爲空,則返回一個值。否則,它將拋出一個沒有此類元素異常(NoSuchElementException)。

        @Test        
        public void optionalGetTest() {
        Shape shape = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.ofNullable(shape);
          assertNotNull(nonEmpty.get());
        }
        
        @Test(expected = NoSuchElementException.class)
        public void optionalGetWithNullTest() {
          Shape shape = null;
            Optional<Shape> empty = Optional.ofNullable(shape);
          empty.get();
        }

orElse(): 這用於檢索包裝在可選實例中的對象。該方法有一個參數作爲默認對象。使用orElse,如果包裝對象存在,則返回包裝對象,如果包裝對象不存在,則返回給orElse的參數。

        @Test        
        public void optionalOrElseTest() {
         Shape shape = ShapeFactory.createShape("Rectangle");
        Shape shape1 = ShapeFactory.createShape("Circle");   
            Optional<Shape> nonEmpty = Optional.of(shape1);
          assertEquals(shape1, nonEmpty.orElse(shape));
        
            Optional<Shape> empty = Optional.empty();
          empty.ifPresent(circle -> circle.draw());
          assertEquals(shape, empty.orElse(shape));
        }

orElseGet() : 這個方法與orElse類似。唯一的區別是,如果可選對象不存在,它不接受返回的對象,而是接受一個supplier functional接口,該接口被調用並返回調用的值。

        @Test    
    public void optionalOrElseGetTest() {
        Shape shape = ShapeFactory.createShape("Rectangle");
        Shape shape1 = ShapeFactory.createShape("Circle");   
        Optional<Shape> nonEmpty = Optional.of(shape1);
        assertEquals(shape1, nonEmpty.orElseGet(() -> ShapeFactory.createShape("Rectangle")));
    
        Optional<Shape> empty = Optional.empty();
        empty.ifPresent(circle -> circle.draw());
        // comparing the area of the shape since orElseGet will create another instance of rectangle
        assertEquals(shape.area(), empty.orElseGet(() -> ShapeFactory.createShape("Rectangle")).area(), 0.001d);
    }

orElseThrow(): 這與orElse類似。唯一的區別是,它添加了一種處理缺失值的新方法。當包裝對象不存在時,它不會返回默認值,而是拋出一個給定的(作爲參數)異常。

        @Test(expected = IllegalArgumentException.class)        
        public void optionalOrElseThrowWithNullTest() {
          Shape shape = null;
          Optional<Shape> empty = Optional.ofNullable(shape);
          empty.orElseThrow(IllegalArgumentException::new);
        }

現在,我們可以使用Optional來編寫ShapeFactory和Main,如下所示:

        package design.nullobject;        
        
        public class ShapeFactoryJava8 {
        
        public static Optional<Shape> createShape(String shapeType) {
                  Shape shape = null;
                  if ("Circle".equalsIgnoreCase(shapeType)) {
                      shape = new Circle();
                  } else if ("Rectangle".equalsIgnoreCase(shapeType)) {
                      shape = new Rectangle();
                  } else if ("Triangle".equalsIgnoreCase(shapeType)) {
                      shape = new Triangle();
                  } else {
                      // no need to have NullShape anymore
                      shape = null;
                  }
              // using ofNullable because shape may be not null.
                  return Optional.ofNullable(shape);
        }
        }

執行和測試代碼的主類:

        package design.nullobject;        
        
        import java.util.Arrays;
        import java.util.Optional;
        
        public class ShapeMainJava8 {
        
            public static void main(String[] args) {
                String[] shapeTypes = new String[] { "Circle", null, "Triangle", "Pentagon", "Rectangle", "Trapezoid"};
                Arrays.asList(shapeTypes).stream().forEach(shapeType -> {
                    Optional<Shape> optionalShape = ShapeFactoryJava8.createShape(shapeType);
                  optionalShape.ifPresent((shape) -> {
                    // null-check is done by ifPresent of Optional
                    System.out.println("Shape area: " + shape.area());
                    System.out.println("Shape Perimeter: " + shape.perimeter());
                    shape.draw();
                    System.out.println();
                    });
                });
            }
        
        }

下面是代碼的輸出:

        Shape area: 3.141592653589793        
        Shape Perimeter: 6.283185307179586
        Drawing Circle with area: 3.141592653589793 and perimeter: 6.283185307179586
        
        Shape area: 0.4330127018922193
        Shape Perimeter: 3.0
        Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
        
        Shape area: 1.0
        Shape Perimeter: 4.0
        Drawing Rectangle with area: 1.0 and perimeter: 4.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章