Kent Beck Implementation Pattern Principles(2)-Minimize Repetition

Z-xuan Hong
8 min readDec 21, 2019

--

這一次介紹的Principle非常有用,因為很多Design Pattern背後的動機基本上都是要降低重複性。

  • Command Pattern:透過把行為封裝成物件,可以把Command傳遞給需要的物件,以達到減少行為的重複
  • State Pattern:透過把狀態封裝成物件,可以移除對於狀態的判斷(Switch Case),以減少判斷的重複
  • Template Method:透過把相似的步驟封裝在SuperClass,差異化的部分讓Subclass Override,以減少相似步驟的重複

上述舉了多個例子來證明降低重複的重要性,在我們學程式時,常常被教導不要有重複的程式碼,有個很重要的問題,為什麼重複程式碼不好呢?

為什麼重複程式碼不好?我們可以透過不同角度去觀看:

  • 改變:當我們要改變程式碼時,如果數個地方有重複程式碼,我們就要修改數個地方,而且要確保修改後每個地方的程式碼是一致的,萬一不一致,會導致系統產生Bug。
  • 測試:當我們要測試程式碼時,如果有數個地方有重複程式碼,我們就會有數個重複的測試,因此當Production Code改變時,數個Test Case也會跟著改變,會增加維護的困難度。
  • 溝通:當我們看到數個地方有很多相似的地方,我們很難辨別這些程式是處理相似的問題,還是不相似的問題,會減少程式碼溝通的效率,程式碼應該要讓人可以輕易的看出哪些是相似、哪些是完全不同、哪些是完全一樣,。

說了這麼多,其實可以用一句話解釋:當程式碼重複在數個地方時,我們將不能達到Local Consequence

上一次介紹了Local Consequence Principle,有興趣的朋友可以自行了解。

這次也用真實例子來解釋這個Principle:假設我們有Circle、Shape、ShapePrinter,ShapePrinter用來顯示數個Shape的面積。

public abstract class Shape {    }public class Circle extends Shape {
private final double radius;

public Circle(final double radius) {
this.radius = radius;
}

public double area() {
return this.radius * this.radius * Math.PI;
}
}
public class Rectangle extends Shape {
private final double width;
private final double height;

public Rectangle(final double width, final double height) {
this.width = width;
this.height = height;
}

public double area() {
return this.width * this.height;
}
}

public class ShapePrinter {
public static void main(String[] args) {
Circle circle = new Circle(2);
Rectangle rectangle = new Rectangle(3, 4);
ShapePrinter printer = new ShapePrinter();
printer.printArea(circle, rectangle);
}

private void printArea(Shape ...shapes) {
Stream.of(shapes)
.forEach(shape -> {
if (isCircle(shape)) {
Circle circle = (Circle)shape;
System.out.println(circle.area());
} else if (isRectangle(shape)) {
Rectangle rectangle = (Rectangle)shape;
System.out.println(rectangle.area());
}
});
}

private boolean isCircle(Shape shape) {
return shape instanceof Circle;
}

private boolean isRectangle(Shape shape) {
return shape instanceof Rectangle;
}
}

在ShapePrinter的print函數,我們用Switch Case來判斷此物件是哪個Class的Instance,對於初學者來說,這是非常正常的事情,透過if .. else if 的方式,來達到控制流程的改變。

但這種寫法,會導致程式難以擴充,如果為了顯示周長,我們需要新增一個函數:printPerimeter,這是正常現象但是需要再寫一樣的Swich Case而且只有呼叫method那一行程式碼不一樣,就會開始覺得麻煩了。

而且如果其他物件需要知道Shape的面積,那麼那個物件也會有一樣的Swich Case,最後導致一堆Switch Case參雜在不同物件。

如果有看過SOLID的人也應該知道,上述的程式明顯違反Open Closed Principle(假設要新增Class: Square時,需要修改ShapePrinter)。

我們已經知道為什麼這樣寫不好,那麼如何解決呢?

答案很簡單:Polymorphism(訊息的傳遞由接收者決定,而不是傳送者)

修改後的程式碼如下

public abstract class Shape {
public abstract double area();

}
public class Circle extends Shape {
private final double radius;

public Circle(final double radius) {
this.radius = radius;
}

@Override
public double area() {
return this.radius * this.radius * Math.PI;
}
}
public class Rectangle extends Shape {
private final double width;
private final double height;

public Rectangle(final double width, final double height) {
this.width = width;
this.height = height;
}

@Override
public double area() {
return this.width * this.height;
}
}
public class ShapePrinter {
public static void main(String[] args) {
Circle circle = new Circle(2);
Rectangle rectangle = new Rectangle(3, 4);
ShapePrinter printer = new ShapePrinter();
printer.printArea(circle, rectangle);
}

private void printArea(Shape ...shapes) {
Stream.of(shapes)
.forEach(shape -> {
System.out.println(shape.area());
});
}
}

從程式碼我們看到,只需要一行即可取代掉上面一堆Switch Case,這也是Polymorphism好用的地方,之後我們的Codebase也不會有一堆判斷Shape的Switch Case。

當我們努力去遵守Minimize Repetition這個原則時,我們可以透過Polymorphism來幫助我們減少重複Switch Case(Class Level)、Extract Method減少重複程式碼(Method Level)。

還記得大二時上物件導向學期末快結束時,老師說了一句話:好的物件導向程式,判斷邏輯不會太多,那時不是很理解,現在只覺得感同深受。

結論:Minimize Repetition是為了達到Local Consequence,讓改變只限制在一個地方(One Method、One Class、One Package、One Component)。

--

--

Z-xuan Hong
Z-xuan Hong

Written by Z-xuan Hong

熱愛軟體開發、物件導向、自動化測試、框架設計,擅長透過軟工幫助企業達到成本最小化、價值最大化。單元測試企業內訓、導入請私訊信箱:t107598042@ntut.org.tw。

Responses (1)