使用Spring Boot和領域驅動設計實現模塊化整體

用模塊化整體架構編寫的代碼實際上是什么樣的?借助 Spring Boot 和 DDD,我們踏上了編寫可維護和可演化代碼的旅程。

當談論模塊化整體代碼時,我們的目標是以下幾點:

  1. 應用程序被組織成模塊。每個模塊解決業務問題的不同部分。
  2. 模塊是松散耦合的。不同模塊之間沒有循環依賴關系,因為它會導致代碼難以維護。
  3. 完整的應用程序在運行時部署為單個單元。這是整體部分。
  4. 模塊的公共接口(暴露給其他模塊的行為)是靈活的并且可以原子地更改。與微服務不同,當我們需要更改模塊的公共接口時,使用該接口的其他模塊可以一起更改并推出。

邊界的確定仍然很重要。不同之處在于,模塊導致邊界錯誤的成本比微服務要低得多。因此,在項目開始時,當對業務問題的共同理解較低時,從整體模塊開始比從微服務開始更安全。

我們如何識別模塊邊界?根據我的經驗,領域驅動設計的模式是解決這個問題的最佳工具之一。

業務問題
讓我們來模擬圖書館和圖書借閱流程。這里是需求:圖書館和圖書借閱流程。這里是要求:圖書借閱流程。以下是要求:

  • 圖書館有數千本書。圖書館有成千上萬本書。同一本書可能有多個副本。同一本書可以有多個副本。
  • 在納入圖書館之前,每本書的背面或其中一頁尾頁都會印上一個條形碼。每本書的背面或其中一頁尾部都有一個條形碼。圖書,每本書的背面或其中一頁尾部都有一個條形碼。該條形碼編號可唯一標識書本背面或其中一頁尾部的條形碼。該條形碼編號可唯一標識圖書。
  • 圖書館讀者可以在有書的情況下借閱圖書。通常,讀者在圖書館找到該書,然后到流通處借閱。有時,讀者可以直接到服務臺按書名借書。通常情況下,讀者在圖書館找到圖書,然后到流通處借閱。有時,讀者可以直接到服務臺按書名借書。通常情況下,讀者在圖書館找到圖書后到流通臺借閱。有時,讀者可以直接到服務臺按書名查找圖書,然后到流通臺借閱。有時,讀者可以直接到服務臺按書名 "圖書館 "查找圖書,然后到流通臺借閱。有時,讀者可以直接到服務臺按書名.desk 要求借書,然后到流通臺借出。有時,讀者可以直接到服務臺按書名要求借書,然后到流通臺按書名借書。
  • 圖書的借出期固定為兩周。
  • 借書時,讀者可以去借書處,也可以把書扔到圖書投放區。

劃分子域
讓我們把這個圖書館域分解成幾個子域。其中一個子域是圖書的借閱過程。這個子域的主要行為者是想要借書的讀者。

另一個子域是圖書盤點子域,即圖書盤點以及添加和刪除帶有條形碼的圖書。這個子域的主要角色是圖書管理員或條形碼管理員。該子域的主要參與者是圖書管理員或管理員。

還可以確定更多的子域--如讀者管理,在允許讀者借閱圖書前對讀者進行身份識別和驗證、圖書報告和分析、向讀者發出通知等。但由于我們沒有這方面的要求,所以暫時不考慮這些子域。已確定的子域--如讀者管理,在允許讀者借閱圖書前對讀者進行身份識別和驗證、圖書報告和分析、向讀者發出通知等。但由于我們沒有這方面的需求,所以暫時不考慮。

請注意,這些子域是我們第一次嘗試對需求進行細分。它可能是正確的,也可能是完全錯誤的。更重要的是,我們要根據目前對問題的理解進行嘗試。隨著時間的推移,我們會有更多的了解,我們可能需要重組子域。這可能是正確的,也可能是完全錯誤的。更重要的是,我們要根據目前對問題的理解進行嘗試。隨著時間的推移,我們會獲得更多的見解,我們可能需要重組子域。

構建解決方案
對于我們發現的每個子域,我們通過設計一個有界上下文來逐個解決子域問題。這些有界上下文也就是我們的模塊化單體應用中的模塊。

src/main/javajava
└── example
├── borrow
│ ? ├── LoanLoan
│ ? ├── LoanController
│ ? ├── (+) LoanDto
│ ? ├── (+) LoanManagement
│ ? ├── LoanMapper ? ? ?
│ ? ├── LoanRepository
│ ? └── LoanWithBookDto
└── inventoryinventory
├── Book
├── BookController
├── (+) BookDto
├── (+) BookManagement
├── BookMapper
└── BookRepository

圖書庫存有界上下文圖書庫存有界上下文
讓我們通過子域建模來設計圖書庫存的有界上下文。我們可以借助聚合模式來實現這一目的。

聚合是數據存儲傳輸的基本要素--您需要加載或保存整個聚合。事務不應跨越聚合邊界。

在這個子域中,最需要持久化的是 "圖書"。在 Java 中,我們可以將聚合建模為 JPA 實體。

@Entity
@Getter
@NoArgsConstructor
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"barcode"}))
class Book {

? ? @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

? ? private String title;

? ? @Embedded
private Barcode inventoryNumber;

? ? private String isbn;

? ? @Embedded
@AttributeOverride(name = "name", column = @Column(name = "author"))
private Author author;

? ? @Enumerated(EnumType.STRING)
private BookStatus status;

? ? @Version
private Long version;

? ? public Book(String title, Barcode inventoryNumber, String isbn, Author author) {
this.title = title;
this.inventoryNumber = inventoryNumber;
this.isbn = isbn;
this.author = author;
this.status = BookStatus.AVAILABLE;
}

? ? public boolean isAvailable() {
return BookStatus.AVAILABLE.equals(this.status);
}

? ? public boolean isIssued() {
return BookStatus.ISSUED.equals(this.status);
}

? ? public Book markIssued() {
if (this.status.equals(BookStatus.ISSUED)) {
throw new IllegalStateException("Book is already issued!");
}
this.status = BookStatus.ISSUED;
return this;
}

? ? public Book markAvailable() {
this.status = BookStatus.AVAILABLE;
return this;
}

? ? public record Barcode(String barcode) {
}

? ? public record Author(String name) {
}

? ? public enum BookStatus {
AVAILABLE, ISSUED
}
}

源碼:?GitHub.

聚合
圖書聚合由圖書實體和三個值對象(條形碼、BookStatus 和作者)組成。我們沒有把作者變成另一個實體,因為我們沒有圍繞它的任何業務需求。在現實世界中,我們應該咨詢領域專家,了解未來是否會有需求,并據此決定實體和值對象。

在這個聚合中,Book 也充當聚合根,這意味著對這個聚合的任何更改(如修改 Book 的狀態)都必須只通過 Book 實體進行,并且僅限于模塊本身。就代碼而言,這意味著不應有一個公共設置器方法 setStatus() 可供應用程序的其他模塊訪問。

請注意,上述實現不僅包含狀態,還包含行為--markIssued()、markAvailable()。在領域模型中包含行為非常重要,否則就會變成貧血模型。

接下來,我們需要一個存儲庫來與數據庫交互。有了 Spring Data,這就變得輕而易舉了:

interface BookRepository extends JpaRepository<Book, Long> {

? ? Optional findByIsbn(String isbn);

? ? Optional findByInventoryNumber(Book.Barcode inventoryNumber);

? ? List findByStatus(Book.BookStatus status);
}

添加了一些常用搜索方法,可通過國際標準書號、條形碼和狀態查找圖書。請注意,該資源庫接口的可見性是包私有的,而不是公共的。

接下來,我們將通過 BookManagement 服務創建模塊的公共接口。

@Transactional
@Service
@RequiredArgsConstructor
public class BookManagement {

? ? private final BookRepository bookRepository;
private final BookMapper mapper;

? ? public BookDto addToInventory(String title, Book.Barcode inventoryNumber, String isbn, String authorName) {
var book = new Book(title, inventoryNumber, isbn, new Book.Author(authorName));
return mapper.toDto(bookRepository.save(book));
}

? ? public void removeFromInventory(Long bookId) {
var book = bookRepository.findById(bookId)
.orElseThrow(() -> new IllegalArgumentException("Book not found!"));
if (book.issued()) {
throw new IllegalStateException("Book is currently issued!");
}
bookRepository.deleteById(bookId);
}

? ? public void issue(String barcode) {
var inventoryNumber = new Book.Barcode(barcode);
var book = bookRepository.findByInventoryNumber(inventoryNumber)
.map(Book::markIssued)
.orElseThrow(() -> new IllegalArgumentException("Book not found!"));
bookRepository.save(book);
}

? ? public void release(String barcode) {
var inventoryNumber = new Book.Barcode(barcode);
var book = bookRepository.findByInventoryNumber(inventoryNumber)
.map(Book::markAvailable)
.orElseThrow(() -> new IllegalArgumentException("Book not found!"));
bookRepository.save(book);
}

? ? @Transactional(readOnly = true)
public Optional locate(Long id) {
return bookRepository.findById(id)
.map(mapper::toDto);
}

? ? @Transactional(readOnly = true)
public List issuedBooks() {
return bookRepository.findByStatus(Book.BookStatus.ISSUED)
.stream()
.map(mapper::toDto)
.toList();
}
}

有幾點需要注意。BookManagement 服務返回的是 DTO 而不是圖書實體。它使用 MapStruct 驅動的映射器將實體轉換為 DTO,反之亦然。通過在服務層只返回 DTO,我們保護了領域模型(實體)不會泄漏到控制器層和表現層。對于小型項目來說,這似乎有些矯枉過正,但對于相當大的項目來說,未來的自己會感謝你將域限制在服務層內。

其次,除了 DTO 之外,BookManagement 是其他模塊唯一可以訪問的類。為此,我們將所有其他類都封裝為私有類。還有其他方法可以實現這一點,我們稍后再討論。

最后,我們可以通過為客戶端創建 REST API 來完成有界上下文的實現。這就是 BookController 類。我們只依賴服務層,而不注入存儲庫。這樣可以確保 API 始終按照服務層的保證返回 DTO。

@RestController
@RequiredArgsConstructor
class BookController {

? ? private final BookManagement books;

? ? @PostMapping("/books")
ResponseEntity addBookToInventory(@RequestBody AddBookRequest request) {
var bookDto = books.addToInventory(request.title(), new Barcode(request.inventoryNumber()), request.isbn(), request.author());
return ResponseEntity.ok(bookDto);
}

? ? @DeleteMapping("/books/{id}")
ResponseEntity removeBookFromInventory(@PathVariable("id") Long id) {
books.removeFromInventory(id);
return ResponseEntity.ok().build();
}

? ? @GetMapping("/books/{id}")
ResponseEntity viewSingleBook(@PathVariable("id") Long id) {
return books.locate(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

? ? @GetMapping("/books")
ResponseEntity<List > viewIssuedBooks() {
return ResponseEntity.ok(books.issuedBooks());
}

? ? record AddBookRequest(String title, String inventoryNumber,
String isbn, String author) {
}
}

通過 "庫存有界上下文",我們已經滿足了前面列出的前兩個要求。

下面"借閱有界上下文BC"將滿足其余要求。

借閱BC
借閱BC處理圖書館讀者借出和借入圖書的事務。它依賴于 "庫存 "綁定上下文來檢查圖書的可用性,并在圖書可用的情況下發放讀者所需的圖書。

在這個子域中需要建模的概念是借書。領域專家告訴我們,這個概念的術語是 "借閱"(Loan)。它是一個長期存在的實體,會隨著時間的推移經歷不同的狀態,并且必須遵循業務規則。因此,它將是這個有界上下文的聚合集合體。

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Loan {

? ? @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

? ? private String bookBarcode;

? ? private Long patronId;

? ? private LocalDate dateOfIssue;

? ? private int loanDurationInDays;

? ? private LocalDate dateOfReturn;

? ? @Enumerated(EnumType.STRING)
private LoanStatus status;

? ? @Version
private Long version;

? ? Loan(String bookBarcode) {
this.bookBarcode = bookBarcode;
this.dateOfIssue = LocalDate.now();
this.loanDurationInDays = 14;
this.status = LoanStatus.ACTIVE;
}

? ? public static Loan of(String bookBarcode) {
return new Loan(bookBarcode);
}

? ? public boolean isActive() {
return LoanStatus.ACTIVE.equals(this.status);
}

? ? public boolean isOverdue() {
return LoanStatus.OVERDUE.equals(this.status);
}

? ? public boolean isCompleted() {
return LoanStatus.COMPLETED.equals(this.status);
}

? ? public void complete() {
if (isCompleted()) {
throw new IllegalStateException("Loan is not active!");
}
this.status = LoanStatus.COMPLETED;
this.dateOfReturn = LocalDate.now();
}

? ? public enum LoanStatus {
ACTIVE, OVERDUE, COMPLETED
}
}

請注意,圖書實體沒有外鍵關系。相反,我們在 "借閱 "模型中存儲了分配給每本書的圖書館庫存編號(條形碼)。這是一個唯一標識符,因此可以安全地用作參考。

這是允許領域模型驅動實體模型而不是相反的結果。通過不使用外鍵關系,我們還避免了取值策略(懶惰/急迫)和級聯策略帶來的無數問題。在 Loan 和 Book 之間沒有 JPA 多對一關系模型。它是在領域模型中直觀定義的,并由聚合不變式強制執行。

當然,缺點是數據庫不再能保護我們免受數據損壞。因此,需要對應用層的實現進行測試。

讓我們抵制尋求實體建模的沖動,轉而將領域建模作為構建解決方案的第一步。

接下來,我們將看看借閱管理服務(LoanManagement service),有趣的事情就在這里發生。

@Transactional
@Service
@RequiredArgsConstructor
public class LoanManagement {

? ? private final LoanRepository loanRepository;
private final BookManagement books;
private final LoanMapper mapper;

? ? public LoanDto checkout(String barcode) {
books.issue(barcode);
var loan = Loan.of(barcode);
var savedLoan = loanRepository.save(loan);
return mapper.toDto(savedLoan);
}

? ? public LoanDto checkin(Long loanId) {
var loan = loanRepository.findById(loanId)
.orElseThrow(() -> new IllegalArgumentException("No loan found"));
books.release(loan.getBookBarcode());
loan.complete();
return mapper.toDto(loanRepository.save(loan));
}

? ? @Transactional(readOnly = true)
public List activeLoans() {
return loanRepository.findLoansWithStatus(LoanStatus.ACTIVE);
}

? ? @Transactional(readOnly = true)
public Optional locate(Long loanId) {
return loanRepository.findById(loanId)
.map(mapper::toDto);
}
}

首先要注意的是,LoanManagement 服務依賴于 BookManagement 服務。在借出操作中,需要發放圖書。在簽到操作中,需要釋放已簽發的圖書。

其次,checkout 和 checkin 的實現根本不執行任何不變式檢查。它們只需調用貸款聚合或圖書管理服務的方法,然后由這些方法執行不變性檢查。這樣,LoanManagement 服務的實現就非常清晰易懂了。

最后,與 BookManagement 類似,該服務只返回 Loan DTO,而不返回實體本身。

Borrow 邊界上下文還包含在 LoanController 中實現的 REST API。實現過程非常簡單,可直接在 GitHub 上查看。

該項目包含 Springdoc 依賴項,用于生成基于 Swagger 的文檔,可訪問 http://localhost:8080/swagger-ui.html。

org.springdoc springdoc-openapi-starter-webmvc-ui ${springdoc-openapi-starter-webmvc-ui.version}

要啟動應用程序,請運行 mvn spring-boot:run。

源碼:?GitHub.

局限性
在討論我們實施方案的局限性之前,讓我們先回顧一下我們的實施方案。

  • 我們應用了 DDD 原則來構建模塊化解決方案。

  • 領域模型是包含數據和行為的真正聚合體。它們負責驗證不變式。

  • 代碼是可測試的,結構是模塊化的,希望也是易于理解的。

但還有一些地方可以改進。

有界上下文BC之間的緊密耦合
如前所述,"借用 "BC與 "庫存 "BC之間存在緊密耦合。如果 "庫存 "BC "不可用"(在單體中不太可能),那么 "借用 "BC就無法運行。

此外,結賬請求在一次事務中更新了 Loan 和 Book 兩個聚合。這違反了在一個事務中只更新一個聚合的推薦做法。

和其他事情一樣,這也是一種權衡。作為一個單體應用程序,我們處理的是單個數據庫,這允許我們更新多個聚合,并保持實現簡單。在下一篇博客中,我們將看到一組新的需求將如何迫使我們嘗試不同的解決方案。

有界上下文BC的獨立測試
緊密耦合的直接后果是,測試單個受限上下文BC(借用)需要處理所有從屬上下文(庫存)。

這一點在《借閱管理》(LoanManagement)的集成測試中很明顯。借出測試必須斷言借出圖書的狀態已更新為 ISSUED。同樣,簽入測試也必須斷言已歸還圖書的狀態已更新為 AVAILABLE。不需要模擬或注入 BookManagement 服務就能測試簽出行為,這不是很好嗎?

@Transactional
@SpringBootTest
class LoanManagementIT {

? ? @Autowired
LoanManagement loans;

? ? @Autowired
BookManagement books;

? ? @Test
void shouldCreateLoanAndIssueBookOnCheckout() {
var loanDto = loans.checkout("13268510");
assertThat(loanDto.status()).isEqualTo(LoanStatus.ACTIVE);
assertThat(loanDto.bookBarcode()).isEqualTo("13268510");
assertThat(books.locate(1L).get().status()).hasToString("ISSUED");
}

? ? @Test
void shouldCompleteLoanAndReleaseBookOnCheckin() {
var loan = loans.checkin(10L);
assertThat(loan.status()).isEqualTo(LoanStatus.COMPLETED);
assertThat(books.locate(2L).get().status()).hasToString("AVAILABLE");
}
}

控制受限上下文BC的接口
如前所述,每個有界上下文BC只公開供其他有界上下文BC(DTO 和服務類)使用的特定類。它們是上下文的接口。這可以通過控制類的可見性來實現。

遺憾的是,這需要仔細和持續的監督。一不小心就會忘記并破壞規則(例如,新開發人員加入項目),最終導致接口擴展。如果任其發展,代碼很快就會變得一團糟,無法維護。使用類可見性還可以限制每個上下文的子包。

在理想情況下,如果我們能使用測試來自動防止跨邊界上下文包的非法訪問,那就再好不過了。

https://www.jdon.com/70712.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/215644.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/215644.shtml
英文地址,請注明出處:http://en.pswp.cn/news/215644.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

springcloud微服務篇--1.認識微服務

一、服務架構演變。 單體架構&#xff1a; 將業務的所有功能集中在一個項目中開發&#xff0c;打成一個包部署。 優點&#xff1a;架構簡單 &#xff0c;部署成本低。 缺點&#xff1a;耦合度高 分布式架構 根據業務功能對系統進行拆分&#xff0c;每個業務模塊作為獨立項…

[idea]idea連接clickhouse23.6.2.18

一、安裝驅動 直接在pom.xml加上那個lz4也是必要的不然會報錯 <dependency><groupId>com.clickhouse</groupId><artifactId>clickhouse-jdbc</artifactId><version>0.4.2</version></dependency><dependency><group…

歌唱比賽計分 (8 分)設有10名歌手(編號為1-10)參加歌詠比賽

未采用結構體的解法&#xff0c;通過二維數組解題 #include <stdio.h> void rank(int arr[10][6] ) { int str[4] { 0 }; int a1[6] { 0 }; int k 0; int i 0; int z 0; int j 0; int temp 0; double s1[10][2] { 0 }; dou…

(1)mysql容器化部署

mysql容器化部署&#xff1a; 數據持久化&#xff08;方便數據保存及遷移&#xff09;: 需要持久化兩個目錄: 創建/mysql (1)mysql配置文件: /mysql/mysql-cnf/my.cnf vim my.cnf [mysqld] pid-file /var/run/mysqld/mysqld.pid socket /var/run/mysqld/…

【51單片機系列】使用74HC595控制數碼管顯示

使用74HC595結合數碼管顯示字符。 proteus仿真設計如下&#xff0c;74HC595的輸出端連接到動態數碼管的位選和靜態數碼管的段選&#xff0c;動態數碼管的段選連接到P0口。這兩個數碼管都是共陰極的。 靜態數碼管顯示字符0-F&#xff0c;軟件設計如下&#xff1a; /*實現功能&a…

Java:SpringBoot獲取當前運行的環境activeProfile

代碼示例 /*** 啟動監聽器*/ Component public class AppListener implements ApplicationListener<ApplicationReadyEvent> {Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 獲取當前的環境&#xff0c;如果是test&#xff0c;則直接返回Co…

redis實際應用實現合集

一、redis實現搶紅包的功能&#xff08;set 數據結構&#xff09; 分兩種情況&#xff1a; 情況一: 從10個觀眾中隨機抽2名幸運觀眾 首先需要把10個觀眾的id&#xff08;具體是什么id可以根據實際業務情況自己定義&#xff09;放到redis 的 set 集合里 然后隨機抽取2名幸運…

【hcie-cloud】【8】華為云Stack_LLD設計【部署設計、資源設計、服務設計、學習推薦、縮略語】【下】

設計概覽、整體架構設計、網絡設計 看下面-這篇文章 【hcie-cloud】【7】華為云Stack_LLD設計【設計概覽、整體架構設計、網絡設計、部署設計、資源設計、服務設計】【上】 部署設計 云平臺整體部署架構 圖中在Region下每個灰底都代表一個數據中心&#xff0c;AZ1可以跨數據…

yarn系統架構與安裝

1.1 YARN系統架構 YARN的基本思想是將資源管理和作業調度/監視功能劃分為單獨的守護進程。其思想是擁有一個全局ResourceManager (RM)&#xff0c;以及每個應用程序擁有一個ApplicationMaster (AM)。應用程序可以是單個作業&#xff0c;也可以是一組作業。 一個ResourceManage…

ai智能機器人外呼系統怎么操作?

什么是ai智能機器人外呼&#xff1f;ai智能機器人外呼怎么操作&#xff1f;當下&#xff0c;很多企業主已經認識到&#xff0c;AI外呼是一種高效的拉新引流手段。但具體到實際應用中&#xff0c;實現的效果好像并沒有那么理想。從企業外呼的結果來看&#xff0c;接通率是可以達…

【信息安全】-ISO/IEC 27001-2022(翻譯)

文章目錄 范圍規范性引用文件3 術語和定義4 組織環境&#xff08;P&#xff09;4.1 理解組織及其環境4.2 理解相關方的需求和期望組織應確定:a) 信息安全管理體系相關方;b) 這些相關方的相關要求;c) 哪些要求可以通過信息安全管理體系得到解決。注:相關方的要求可包括法律、法規…

Ceph入門到精通-ceph二次開發開源協議考慮

Ceph 是一個開源的分布式存儲系統&#xff0c;它由多個組件組成&#xff0c;包括分布式對象存儲&#xff08;RADOS&#xff09;、分布式塊存儲&#xff08;RBD&#xff09;和分布式文件系統&#xff08;CephFS&#xff09;等。Ceph 采用了 GNU Lesser General Public License&a…

kuboard如何部署nacos?

? kuboard如何部署nacos&#xff1f; 這個快速開始手冊是幫忙您快速在您的電腦上&#xff0c;下載、安裝并使用 Nacos。 項目包含一個可構建的Nacos Docker Image&#xff0c;旨在利用StatefulSets在Kubernetes上部署Nacos。 在高級使用中,Nacos在K8S擁有自動擴容縮容和數據…

“華為杯”研究生數學建模競賽2019年-【華為杯】B題:天文導航中的星圖識別

目錄 摘 要: 一、問題重述 二、模型假設 三、符號說明 四、問題分析

nginx 前端服務調用后端服務報426

nginx 前端服務調用后端服務報426 在配置文件中加上一句配置 2&#xff1a;外掛出來

三(三)ts非基礎類型(接口)

說明 在面向對象語言中&#xff0c;接口是一個很重要的概念&#xff0c;它是對行為的抽象&#xff0c;而具體如何行動需要由類去實現。 TypeScript 中的接口是一個非常靈活的概念&#xff0c;除了可用于對類的一部分行為進行抽象以外&#xff0c;也常用于對「對象的形狀&…

屏幕分辨率修改工具SwitchResX mac功能特點

SwitchResX mac是可用于修改和管理顯示器的分辨率和刷新率。 SwitchResX mac功能和特點 支持多種分辨率和刷新率&#xff1a;SwitchResX可以添加和管理多種分辨率和刷新率&#xff0c;包括自定義分辨率和刷新率。 自動切換分辨率&#xff1a;SwitchResX可以根據應用程序和窗口…

絕地求生:追尋槍王之路,為什么PUBG老玩家要進行訓練?

作為一款全球熱門的射擊游戲&#xff0c;《絕地求生&#xff1a;大逃殺》&#xff08;PUBG&#xff09;吸引了大批熱衷于挑戰極限的玩家。在這個槍戰沙盒中&#xff0c;角逐者們需要不斷提升自己的戰術、槍法和反應速度&#xff0c;才能在百人對戰中脫穎而出。那么為什么PUBG老…

1274:【例9.18】合并石子

【算法分析】 【算法分析】 首先我們要先讀懂題意&#xff0c;可能有部分同學在讀題的時候就有點難以理解。 我們首先來分析一個比較簡單的問題&#xff0c;現在一共有三堆石頭&#xff0c;每堆石子的數量分別是3&#xff0c;4&#xff0c;11。求合并成一堆石頭的最小得分。…

Hanlp自然語言處理如何再Spring Boot中使用

一、HanLP HanLP (Hankcs NLP) 是一個自然語言處理工具包&#xff0c;具有功能強大、性能高效、易于使用的特點。HanLP 主要支持中文文本處理&#xff0c;包括分詞、詞性標注、命名實體識別、依存句法分析、關鍵詞提取、文本分類、情感分析等多種功能。 HanLP 可以在 Java、Py…