如何使用充血模型實現防彈程式碼 - DZone Java
瞭解有關在Java應用程式中通過使用充血模型+構建器等設計器模式設計防彈程式碼的方法。
毫無疑問,優秀的編碼實踐帶來了諸多好處,例如干淨的程式碼,易於維護以及流暢的API。但是,最佳實踐是否有助於資料完整性?
本貼主要涉及新的儲存技術,例如NoSQL資料庫,它們沒有開發人員在使用SQL模式時通常會有的原生驗證。
乾淨程式碼 是一個好主題 它是將物件行為公開和資料隱藏,這與結構化程式設計不同,這篇文章目的是解釋使用充血模型而不是失血模型獲得資料完整性和防彈bulletproof程式碼的好處。
需求用例
這篇文章將建立一個系統,將足球運動員分成一個團隊; 該系統的規則是:
- 玩家的名字是必需的
- 所有球員必須有一個位置(守門員,前鋒,後衛和中場)。
- 球員在球隊中進行的目標計數器
- 聯絡電子郵件
- 一個團隊有球員,並根據需要命名
- 一支球隊無法處理超過二十名球員
根據收集的資訊,有第一個草案程式碼版本:
<b>import</b> java.math.BigDecimal; <b>public</b> <b>class</b> Player { String name; Integer start; Integer end; String email; String position; Integer gols; BigDecimal salary; } <b>public</b> <b>class</b> Team { String name; List<Player> players; }
這裡球員只能有一個固定的位置,需要重構,我們將使用列舉 替代String型別的位置position。
<b>public</b> enum Position { GOALKEEPER, DEFENDER, MIDFIELDER, FORWARD; }
物件的封裝
下一步是關於安全性和封裝:目標是最小化可訪問性,因此只需將所有欄位定義為私有,那麼下一步是啥?使用public公開化 getter 和 setter 方法?方法的訪問方式預設應該是protected,這是基於封裝考慮的,考慮本文:
- 在系統示例中,球員不會更改電子郵件,姓名和職位。因此,它不需要setter方法。
- 最後一年last year的欄位表示玩家何時按合同離開球隊。當它是可選時,意味著沒有期望球員離開俱樂部。setter方法是必需的,但last year離職期必須等於或大於入職兩份。此外,在1863年足球出生 之前的球員是無法玩足球比賽。
- 只有團隊可以處理它的球員; 它必須是緊耦合(高聚合)
在 Team 類中,有一個用於新增球員的方法;getter方法可以返回團隊中的所有球員。新增球員必須驗證,例如不能新增空球員或不能對於於20個球員。對getter返回集合的關鍵點是直接返回集合例項時,客戶端可能會使用該方法直接將新元素寫入集合,例如clean,add等,因此要解決封裝問題,一個好的做法是返回一個只讀例項,例如unmodifiableList :
<b>import</b> java.util.ArrayList; <b>import</b> java.util.Collections; <b>import</b> java.util.List; <b>import</b> java.util.Objects; <b>public</b> <b>class</b> Team { <b>static</b> <b>final</b> <b>int</b> SIZE = 20; <b>private</b> String name; <b>private</b> List<Player> players = <b>new</b> ArrayList<>(); @Deprecated Team() { } <b>private</b> Team(String name) { <b>this</b>.name = name; } <b>public</b> String getName() { <b>return</b> name; } <b>public</b> <b>void</b> add(Player player) { Objects.requireNonNull(player, <font>"player is required"</font><font>); <b>if</b> (players.size() == SIZE) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"The team is full"</font><font>); } <b>this</b>.players.add(player); } <b>public</b> List<Player> getPlayers() { <b>return</b> Collections.unmodifiableList(players); } <b>public</b> <b>static</b> Team of(String name) { <b>return</b> <b>new</b> Team(Objects.requireNonNull(name, </font><font>"name is required"</font><font>)); } } </font>
下一步是關於Player類設計,所有欄位都有一個getter 方法,end欄位除外:
<b>import</b> java.math.BigDecimal; <b>import</b> java.util.Objects; <b>import</b> java.util.Optional; <b>public</b> <b>class</b> Player { <b>private</b> String name; <b>private</b> Integer start; <b>private</b> Integer end; <b>private</b> String email; <b>private</b> Position position; <b>private</b> BigDecimal salary; <b>private</b> <b>int</b> goal = 0; <b>public</b> String getName() { <b>return</b> name; } <b>public</b> Integer getStart() { <b>return</b> start; } <b>public</b> String getEmail() { <b>return</b> email; } <b>public</b> Position getPosition() { <b>return</b> position; } <b>public</b> BigDecimal getSalary() { <b>return</b> salary; } <b>public</b> Optional<Integer> getEnd() { <b>return</b> Optional.ofNullable(end); } <b>public</b> <b>void</b> setEnd(Integer end) { <b>if</b> (end != <b>null</b> && end <= start) { <b>throw</b> <b>new</b> IllegalArgumentException(<font>"the last year of a player must be equal or higher than the start."</font><font>); } <b>this</b>.end = end; } } <b>public</b> <b>int</b> getGoal() { <b>return</b> goal; } <b>public</b> <b>void</b> goal() { goal++; } </font>
getEnd()使用Optional返回一個可能為空的欄位,setEnd欄位用於更新該球員離職情況,當然離職日期不能大於入職日期。(banq注:使用Lombok時會忽略這個問題)
例項建立
前面討論了public和private以及protected的糾結使用,現在該討論例項建立了,首先我們可能會建立一個接收所有引數的建構函式,這適合Team類,因為它有一個name引數,但是在球員Player中會有幾個問題:
- 首先是引數數量; 由於幾個原因,多個建構函式並不是一個好習慣。例如,如果相同型別的引數太多,則在更改順序時可能會出錯。
- 第二個是關於這些驗證的複雜性。
兩個步驟解決:
第一步是型別定義。當一個物件具有諸如金錢,日期之類的巨大複雜性時,使用型別定義是有意義的。下面是郵件型別:
<b>import</b> java.util.Objects; <b>import</b> java.util.function.Supplier; <b>import</b> java.util.regex.Pattern; <b>public</b> <b>final</b> <b>class</b> Email implements Supplier<String> { <b>private</b> <b>static</b> <b>final</b> String EMAIL_PATTERN = <font>"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"</font><font> + </font><font>"[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"</font><font>; <b>private</b> <b>static</b> <b>final</b> Pattern PATTERN = Pattern.compile(EMAIL_PATTERN); <b>private</b> <b>final</b> String value; @Override <b>public</b> String get() { <b>return</b> value; } <b>private</b> Email(String value) { <b>this</b>.value = value; } @Override <b>public</b> <b>boolean</b> equals(Object o) { <b>if</b> (<b>this</b> == o) { <b>return</b> <b>true</b>; } <b>if</b> (o == <b>null</b> || getClass() != o.getClass()) { <b>return</b> false; } Email email = (Email) o; <b>return</b> Objects.equals(value, email.value); } @Override <b>public</b> <b>int</b> hashCode() { <b>return</b> Objects.hashCode(value); } @Override <b>public</b> String toString() { <b>return</b> value; } <b>public</b> <b>static</b> Email of(String value) { Objects.requireNonNull(value, </font><font>"o valor é obrigatório"</font><font>); <b>if</b> (!PATTERN.matcher(value).matches()) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Email nao válido"</font><font>); } <b>return</b> <b>new</b> Email(value); } } </font>
建立了電子郵件型別後,我們有了Player類的新版本:
<b>import</b> javax.money.MonetaryAmount; <b>import</b> java.time.Year; <b>import</b> java.util.Objects; <b>import</b> java.util.Optional; <b>public</b> <b>class</b> Player { <b>private</b> String id; <b>private</b> String name; <b>private</b> Year start; <b>private</b> Year end; <b>private</b> Email email; <b>private</b> Position position; <b>private</b> MonetaryAmount salary; <font><i>//...</i></font><font> } </font>
構建器模式
Builder模式遵循負責建立球員例項的責任,它避免了更改輸入引數順序可能導致的錯誤。
通常我們還是需要一個預設建構函式,將Deprecated 註釋放在此建構函式上以顯示它不是推薦的方法,內部類適合用於製造構建器,因為它可以建立僅訪問球員構建器的私有建構函式。
<b>import</b> javax.money.MonetaryAmount; <b>import</b> java.time.Year; <b>import</b> java.util.Objects; <b>import</b> java.util.Optional; <b>public</b> <b>class</b> Player { <b>static</b> <b>final</b> Year SOCCER_BORN = Year.of(1863); <font><i>//hide</i></font><font> <b>private</b> Player(String name, Year start, Year end, Email email, Position position, MonetaryAmount salary) { <b>this</b>.name = name; <b>this</b>.start = start; <b>this</b>.end = end; <b>this</b>.email = email; <b>this</b>.position = position; <b>this</b>.salary = salary; } @Deprecated Player() { } <b>public</b> <b>static</b> PlayerBuilder builder() { <b>return</b> <b>new</b> PlayerBuilder(); } <b>public</b> <b>static</b> <b>class</b> PlayerBuilder { <b>private</b> String name; <b>private</b> Year start; <b>private</b> Year end; <b>private</b> Email email; <b>private</b> Position position; <b>private</b> MonetaryAmount salary; <b>private</b> PlayerBuilder() { } <b>public</b> PlayerBuilder withName(String name) { <b>this</b>.name = Objects.requireNonNull(name, </font><font>"name is required"</font><font>); <b>return</b> <b>this</b>; } <b>public</b> PlayerBuilder withStart(Year start) { Objects.requireNonNull(start, </font><font>"start is required"</font><font>); <b>if</b> (Year.now().isBefore(start)) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"you cannot start in the future"</font><font>); } <b>if</b> (SOCCER_BORN.isAfter(start)) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Soccer was not born on this time"</font><font>); } <b>this</b>.start = start; <b>return</b> <b>this</b>; } <b>public</b> PlayerBuilder withEnd(Year end) { Objects.requireNonNull(end, </font><font>"end is required"</font><font>); <b>if</b> (start != <b>null</b> && start.isAfter(end)) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"the last year of a player must be equal or higher than the start."</font><font>); } <b>if</b> (SOCCER_BORN.isAfter(end)) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Soccer was not born on this time"</font><font>); } <b>this</b>.end = end; <b>return</b> <b>this</b>; } <b>public</b> PlayerBuilder withEmail(Email email) { <b>this</b>.email = Objects.requireNonNull(email, </font><font>"email is required"</font><font>); <b>return</b> <b>this</b>; } <b>public</b> PlayerBuilder withPosition(Position position) { <b>this</b>.position = Objects.requireNonNull(position, </font><font>"position is required"</font><font>); <b>return</b> <b>this</b>; } <b>public</b> PlayerBuilder withSalary(MonetaryAmount salary) { Objects.requireNonNull(salary, </font><font>"salary is required"</font><font>); <b>if</b> (salary.isNegativeOrZero()) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"A player needs to earn money to play; otherwise, it is illegal."</font><font>); } <b>this</b>.salary = salary; <b>return</b> <b>this</b>; } <b>public</b> Player build() { Objects.requireNonNull(name, </font><font>"name is required"</font><font>); Objects.requireNonNull(start, </font><font>"start is required"</font><font>); Objects.requireNonNull(email, </font><font>"email is required"</font><font>); Objects.requireNonNull(position, </font><font>"position is required"</font><font>); Objects.requireNonNull(salary, </font><font>"salary is required"</font><font>); <b>return</b> <b>new</b> Player(name, start, end, email, position, salary); } } } </font>
根據此原則使用構建器模式,Java開發人員知道例項何時存在並具有有效資訊:
CurrencyUnit usd = Monetary.getCurrency(Locale.US); MonetaryAmount salary = Money.of(1 _000_000, usd); Email email = Email.of(<font>"[email protected]"</font><font>); Year start = Year.now(); Player marta = Player.builder().withName(</font><font>"Marta"</font><font>) .withEmail(email) .withSalary(salary) .withStart(start) .withPosition(Position.FORWARD) .build(); </font>
Team類不需要了,因為它已經很平滑了:
Team bahia = Team.of(<font>"Bahia"</font><font>); Player marta = Player.builder().withName(</font><font>"Marta"</font><font>) .withEmail(email) .withSalary(salary) .withStart(start) .withPosition(Position.FORWARD) .build(); bahia.add(marta); </font>
當Java開發人員談論驗證時,無法避開實現驗證的Java規範:Bean Validation 。這使得Java開發人員可以更方便地使用註釋建立驗證 。至關重要的是要指出BV不會使POO概念無效。換句話說,避免鬆散耦合,SOLID 原則仍然有效,而不是放棄那些概念。
因此,BV可以仔細檢查驗證或執行驗證 Builder 以返回例項,只有它傳遞了驗證。
換句話說,SOLID 原則仍然有效,因此,BV可以仔細檢查驗證或執行驗證 Builder以返回例項,只有它傳遞了驗證。
<b>import</b> javax.money.MonetaryAmount; <b>import</b> javax.validation.constraints.NotBlank; <b>import</b> javax.validation.constraints.NotNull; <b>import</b> javax.validation.constraints.PastOrPresent; <b>import</b> javax.validation.constraints.PositiveOrZero; <b>import</b> java.time.Year; <b>import</b> java.util.Objects; <b>import</b> java.util.Optional; <b>public</b> <b>class</b> Player { <b>static</b> <b>final</b> Year SOCCER_BORN = Year.of(1863); @NotBlank <b>private</b> String name; @NotNull @PastOrPresent <b>private</b> Year start; @PastOrPresent <b>private</b> Year end; @NotNull <b>private</b> Email email; @NotNull <b>private</b> Position position; @NotNull <b>private</b> MonetaryAmount salary; @PositiveOrZero <b>private</b> <b>int</b> goal = 0; <font><i>//continue</i></font><font> } <b>import</b> javax.validation.constraints.NotBlank; <b>import</b> javax.validation.constraints.NotNull; <b>import</b> javax.validation.constraints.Size; <b>import</b> java.util.ArrayList; <b>import</b> java.util.Collections; <b>import</b> java.util.List; <b>import</b> java.util.Objects; <b>public</b> <b>class</b> Team { <b>static</b> <b>final</b> <b>int</b> SIZE = 20; @NotBlank <b>private</b> String name; @NotNull @Size(max = SIZE) <b>private</b> List<Player> players = <b>new</b> ArrayList<>(); </font><font><i>//continue</i></font><font> } </font>
總而言之,本文演示瞭如何使用最佳設計實踐使程式碼防彈。此外,我們同時獲得物件和資料完整性。這些技術與儲存技術無關 - 開發人員可以在任何企業軟體中使用這些原則。重要的是說測試是必不可少的,但這超出了文章的範圍。
可以在GitHub上 找到原始碼。