如何使用充血模型實現防彈程式碼 - DZone Java

banq發表於2019-04-27

瞭解有關在Java應用程式中透過使用充血模型+構建器等設計器模式設計防彈程式碼的方法。
毫無疑問,優秀的編碼實踐帶來了諸多好處,例如干淨的程式碼,易於維護以及流暢的API。但是,最佳實踐是否有助於資料完整性?
本貼主要涉及新的儲存技術,例如NoSQL資料庫,它們沒有開發人員在使用SQL模式時通常會有的原生驗證。

乾淨程式碼是一個好主題  它是將物件行為公開和資料隱藏,這與結構化程式設計不同,這篇文章目的是解釋使用充血模型而不是失血模型獲得資料完整性和防彈bulletproof程式碼的好處。

需求用例
這篇文章將建立一個系統,將足球運動員分成一個團隊; 該系統的規則是:

  • 玩家的名字是必需的
  • 所有球員必須有一個位置(守門員,前鋒,後衛和中場)。
  • 球員在球隊中進行的目標計數器
  • 聯絡電子郵件
  • 一個團隊有球員,並根據需要命名
  • 一支球隊無法處理超過二十名球員

根據收集的資訊,有第一個草案程式碼版本:

import java.math.BigDecimal;
public class Player {
    String name;
    Integer start;
    Integer end;
    String email;
    String position;
    Integer gols;
    BigDecimal salary;
}
public class Team {
    String name;
    List<Player> players;
}


這裡球員只能有一個固定的位置,需要重構,我們將使用 列舉替代String型別的位置position。

public enum Position {
    GOALKEEPER, DEFENDER, MIDFIELDER, FORWARD;
}


物件的封裝
下一步是關於安全性和封裝:目標是最小化可訪問性,因此只需將所有欄位定義為私有,那麼下一步是啥?使用public公開化  getter 和  setter 方法?方法的訪問方式預設應該是protected,這是基於封裝考慮的,考慮本文:

  • 在系統示例中,球員不會更改電子郵件,姓名和職位。因此,它不需要setter方法。
  • 最後一年last year的欄位表示玩家何時按合同離開球隊。當它是可選時,意味著沒有期望球員離開俱樂部。setter方法是必需的,但last year離職期必須等於或大於入職兩份。此外,在1863年足球出生之前的球員是無法玩足球比賽。
  • 只有團隊可以處理它的球員; 它必須是緊耦合(高聚合)


在  Team 類中,有一個用於新增球員的方法;getter方法可以返回團隊中的所有球員。新增球員必須驗證,例如不能新增空球員或不能對於於20個球員。對getter返回集合的關鍵點是直接返回集合例項時,客戶端可能會使用該方法直接將新元素寫入集合,例如clean,add等,因此要解決封裝問題,一個好的做法是返回一個只讀例項,例如unmodifiableList

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Team {
    static final int SIZE = 20;
    private String name;
    private List<Player> players = new ArrayList<>();
    @Deprecated
    Team() {
    }
    private Team(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void add(Player player) {
        Objects.requireNonNull(player, "player is required");
        if (players.size() == SIZE) {
            throw new IllegalArgumentException("The team is full");
        }
        this.players.add(player);
    }
    public List<Player> getPlayers() {
        return Collections.unmodifiableList(players);
    }
    public static Team of(String name) {
        return new Team(Objects.requireNonNull(name, "name is required"));
    }
}


下一步是關於Player類設計,所有欄位都有一個getter 方法,end欄位除外:

import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
public class Player {
    private String name;
    private Integer start;
    private Integer end;
    private String email;
    private Position position;
    private BigDecimal salary;
    private int goal = 0;
    public String getName() {
        return name;
    }
    public Integer getStart() {
        return start;
    }
    public String getEmail() {
        return email;
    }
    public Position getPosition() {
        return position;
    }
    public BigDecimal getSalary() {
        return salary;
    }
    public Optional<Integer> getEnd() {
        return Optional.ofNullable(end);
    }
    public void setEnd(Integer end) {
        if (end != null && end <= start) {
            throw new IllegalArgumentException("the last year of a player must be equal or higher than the start.");
        }
        this.end = end;
    }
}
    public int getGoal() {
        return goal;
    }
   public void goal() {
       goal++;
}


getEnd()使用Optional返回一個可能為空的欄位,setEnd欄位用於更新該球員離職情況,當然離職日期不能大於入職日期。(banq注:使用Lombok時會忽略這個問題)

例項建立
前面討論了public和private以及protected的糾結使用,現在該討論例項建立了,首先我們可能會建立一個接收所有引數的建構函式,這適合Team類,因為它有一個name引數,但是在球員Player中會有幾個問題:

  1. 首先是引數數量; 由於幾個原因,多個建構函式並不是一個好習慣。例如,如果相同型別的引數太多,則在更改順序時可能會出錯。
  2. 第二個是關於這些驗證的複雜性。

兩個步驟解決:
第一步是型別定義。當一個物件具有諸如金錢,日期之類的巨大複雜性時,使用型別定義是有意義的。下面是郵件型別:

import java.util.Objects;
import java.util.function.Supplier;
import java.util.regex.Pattern;
public final class Email implements Supplier<String> {
    private static final String EMAIL_PATTERN =
            "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
                    + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private static final Pattern PATTERN = Pattern.compile(EMAIL_PATTERN);
    private final String value;
    @Override
    public String get() {
        return value;
    }
    private Email(String value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Email email = (Email) o;
        return Objects.equals(value, email.value);
    }
    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }
    @Override
    public String toString() {
        return value;
    }
    public static Email of(String value) {
        Objects.requireNonNull(value, "o valor é obrigatório");
        if (!PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Email nao válido");
        }
        return new Email(value);
    }
}


建立了電子郵件型別後,我們有了Player類的新版本:

import javax.money.MonetaryAmount;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;

public class Player {
    private String id;
    private String name;
    private Year start;
    private Year end;
    private Email email;
    private Position position;
    private MonetaryAmount salary;
//...
}


構建器模式
Builder模式遵循負責建立球員例項的責任,它避免了更改輸入引數順序可能導致的錯誤。
通常我們還是需要一個預設建構函式,將Deprecated 註釋放在此建構函式上以顯示它不是推薦的方法,內部類適合用於製造構建器,因為它可以建立僅訪問球員構建器的私有建構函式。


import javax.money.MonetaryAmount;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;
public class Player {
    static final Year SOCCER_BORN = Year.of(1863);
    //hide
    private Player(String name, Year start, Year end, Email email, Position position, MonetaryAmount salary) {
        this.name = name;
        this.start = start;
        this.end = end;
        this.email = email;
        this.position = position;
        this.salary = salary;
    }
    @Deprecated
    Player() {
    }
    public static PlayerBuilder builder() {
        return new PlayerBuilder();
    }
    public static class PlayerBuilder {
        private String name;
        private Year start;
        private Year end;
        private Email email;
        private Position position;
        private MonetaryAmount salary;
        private PlayerBuilder() {
        }
        public PlayerBuilder withName(String name) {
            this.name = Objects.requireNonNull(name, "name is required");
            return this;
        }
        public PlayerBuilder withStart(Year start) {
            Objects.requireNonNull(start, "start is required");
            if (Year.now().isBefore(start)) {
                throw new IllegalArgumentException("you cannot start in the future");
            }
            if (SOCCER_BORN.isAfter(start)) {
                throw new IllegalArgumentException("Soccer was not born on this time");
            }
            this.start = start;
            return this;
        }
        public PlayerBuilder withEnd(Year end) {
            Objects.requireNonNull(end, "end is required");
            if (start != null && start.isAfter(end)) {
                throw new IllegalArgumentException("the last year of a player must be equal or higher than the start.");
            }
            if (SOCCER_BORN.isAfter(end)) {
                throw new IllegalArgumentException("Soccer was not born on this time");
            }
            this.end = end;
            return this;
        }
        public PlayerBuilder withEmail(Email email) {
            this.email = Objects.requireNonNull(email, "email is required");
            return this;
        }
        public PlayerBuilder withPosition(Position position) {
            this.position = Objects.requireNonNull(position, "position is required");
            return this;
        }
        public PlayerBuilder withSalary(MonetaryAmount salary) {
            Objects.requireNonNull(salary, "salary is required");
            if (salary.isNegativeOrZero()) {
                throw new IllegalArgumentException("A player needs to earn money to play; otherwise, it is illegal.");
            }
            this.salary = salary;
            return this;
        }
        public Player build() {
            Objects.requireNonNull(name, "name is required");
            Objects.requireNonNull(start, "start is required");
            Objects.requireNonNull(email, "email is required");
            Objects.requireNonNull(position, "position is required");
            Objects.requireNonNull(salary, "salary is required");
            return new Player(name, start, end, email, position, salary);
        }
    }
}


根據此原則使用構建器模式,Java開發人員知道例項何時存在並具有有效資訊:

 CurrencyUnit usd = Monetary.getCurrency(Locale.US);
     MonetaryAmount salary = Money.of(1 _000_000, usd);
     Email email = Email.of("marta@marta.com");
     Year start = Year.now();
     Player marta = Player.builder().withName("Marta")
         .withEmail(email)
         .withSalary(salary)
         .withStart(start)
         .withPosition(Position.FORWARD)
         .build();


Team類不需要了,因為它已經很平滑了:

Team bahia = Team.of("Bahia");
  Player marta = Player.builder().withName("Marta")
      .withEmail(email)
      .withSalary(salary)
      .withStart(start)
      .withPosition(Position.FORWARD)
      .build();
  bahia.add(marta);


當Java開發人員談論驗證時,無法避開實現驗證的Java規範:Bean Validation。這使得Java開發人員可以更方便地使用註釋建立驗證。至關重要的是要指出BV不會使POO概念無效。換句話說,避免鬆散耦合,SOLID原則仍然有效,而不是放棄那些概念。
因此,BV可以仔細檢查驗證或執行驗證  Builder 以返回例項,只有它傳遞了驗證。
換句話說,SOLID原則仍然有效,因此,BV可以仔細檢查驗證或執行驗證  Builder以返回例項,只有它傳遞了驗證。

import javax.money.MonetaryAmount;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PastOrPresent;
import javax.validation.constraints.PositiveOrZero;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;
public class Player {
    static final Year SOCCER_BORN = Year.of(1863);
    @NotBlank
    private String name;
    @NotNull
    @PastOrPresent
    private Year start;
    @PastOrPresent
    private Year end;
    @NotNull
    private Email email;
    @NotNull
    private Position position;
    @NotNull
    private MonetaryAmount salary;
    @PositiveOrZero
    private int goal = 0;
    //continue
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Team {
    static final int SIZE = 20;
    @NotBlank
    private String name;
    @NotNull
    @Size(max = SIZE)
    private List<Player> players = new ArrayList<>();
    //continue
}


總而言之,本文演示瞭如何使用最佳設計實踐使程式碼防彈。此外,我們同時獲得物件和資料完整性。這些技術與儲存技術無關 - 開發人員可以在任何企業軟體中使用這些原則。重要的是說測試是必不可少的,但這超出了文章的範圍。

可以在GitHub上找到原始碼。

相關文章