Design Patterns trong Spring Framework
NỘI DUNG BÀI VIẾT
1. Giới thiệu
Design Patterns trong Spring Boot là một phần thiết yếu của phát triển phần mềm. Các giải pháp này không chỉ giải quyết các vấn đề lặp lại mà còn giúp các nhà phát triển hiểu được thiết kế của một framework bằng cách nhận ra các patterns phổ biến.
Trong bài hướng dẫn này, chúng ta sẽ xem xét bốn mẫu patterns phổ biến nhất được sử dụng trong Spring Framework:
- Singleton Pattern
- Factory Method Pattern
- Proxy Pattern
- Template Pattern
Chúng ta sẽ xem xét cách Spring sử dụng các pattern này để giảm bớt gánh nặng cho các nhà phát triển và giúp người dùng nhanh chóng thực hiện các tác vụ tẻ nhạt..
Tổng hợp 200+ tài liệu, sách, bài thực hành, video hướng dẫn lập trình… từ cơ bản đến nâng cao
2. Singleton Pattern
2.1. Singleton Beans
Nói chung, một singleton là duy nhất trên toàn cầu cho một ứng dụng, nhưng trong Spring, hạn chế này được nới lỏng. Thay vào đó, Spring giới hạn một singleton cho một đối tượng trên mỗi vùng chứa Spring IoC. Trong thực tế, điều này có nghĩa là Spring sẽ chỉ tạo một bean cho mỗi loại cho mỗi ngữ cảnh ứng dụng.
Cách tiếp cận của Spring khác với định nghĩa chặt chẽ về singleton vì một ứng dụng có thể có nhiều hơn một Spring container. Do đó, nhiều đối tượng của cùng một lớp có thể tồn tại trong một ứng dụng duy nhất nếu chúng ta có nhiều vùng chứa.
Mặc định thì Spring sẽ tạo tất cả các bean dưới dạng các tệp đơn.
2.2. Autowired Singletons
Ví dụ, chúng ta có thể tạo hai controller trong một application context và inject một bean cùng type vào mỗi controller.
Đầu tiên, tôi tạo một BookRepository quản lý các object Book.
Tiếp theo, tôi tạo LibraryController, sử dụng BookRepository để trả về số lượng sách trong thư viện:
@RestController
public class LibraryController {
@Autowired
private BookRepository repository;
@GetMapping("/count")
public Long findCount() {
System.out.println(repository);
return repository.count();
}
}
Code language: CSS (css)
Cuối cùng, tôi tạo BookController, tập trung vào các hành động dành riêng cho Book, chẳng hạn như tìm sách theo ID của nó:
@RestController
public class BookController {
@Autowired
private BookRepository repository;
@GetMapping("/book/{id}")
public Book findById(@PathVariable long id) {
System.out.println(repository);
return repository.findById(id).get();
}
}
Code language: CSS (css)
Sau đó, tôi khởi động ứng dụng này và thực hiện GET /count và /book/1:
curl -X GET http://localhost:8080/count
curl -X GET http://localhost:8080/book/1
Code language: JavaScript (javascript)
Output:
com.hdd.spring.patterns.singleton.BookRepository@3ea9524f com.hdd.spring.patterns.singleton.BookRepository@3ea9524f
Các ID đối tượng BookRepository trong LibraryController và BookController là giống nhau, chứng tỏ rằng Spring đã đưa cùng một bean vào cả hai controller.
Chúng ta có thể tạo các phiên bản riêng biệt của bean BookRepository bằng cách thay đổi phạm vi bean từ singleton thành nguyên mẫu bằng cách sử dụng chú thích @Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE).
Làm như vậy hướng dẫn Spring tạo các đối tượng riêng biệt cho từng BookRepository bean mà nó tạo ra. Do đó, nếu tôi kiểm tra lại ID đối tượng của BookRepository trong mỗi controller của chúng tôi, tôi thấy rằng chúng không còn giống nhau nữa.
3. Factory Method Pattern
Factory Method Pattern yêu cầu một class Factory với một phương thức trừu tượng để tạo đối tượng mong muốn.
Thông thường, chúng ta muốn tạo các đối tượng khác nhau dựa trên một ngữ cảnh cụ thể.
Ví dụ: ứng dụng của chúng tôi có thể yêu cầu đối tượng phương tiện. Trong môi trường hàng hải, chúng tôi muốn tạo ra tàu thuyền, nhưng trong môi trường hàng không, chúng tôi muốn tạo ra máy bay:
Để thực hiện điều này, chúng ta có thể tạo một factory implementation cho từng đối tượng mong muốn và trả về đối tượng mong muốn từ phương thức concrete factory.
3.1. Application Context
Spring sử dụng kỹ thuật này ở gốc của Dependency Injection (DI) framwork của nó.
Về cơ bản, Spring coi một bean container như một factory produces beans.
Do đó, Spring định nghĩa giao diện BeanFactory như một bản tóm tắt của một vùng chứa bean:
public interface BeanFactory {
getBean(Class<T> requiredType);
getBean(Class<T> requiredType, Object... args);
getBean(String name);
// ...
}
Code language: JavaScript (javascript)
Mỗi method getBean() đều được xem là factory method, trả về một bean phù hợp với các tiêu chí được cung cấp cho phương thức, như type và name của bean.
Spring sau đó đã mở rộng BeanFactory với interface ApplicationContext, cái cấu hình ứng dụng bổ sung.
Spring sử dụng cấu hình này để khởi động container bean dựa trên một số cấu hình bên ngoài, chẳng hạn như tệp XML hoặc annotation Java.
Sử dụng class ApplicationContext giống như ApplicationConfigApplicationContext, sau đó chúng ta có thể tạo bean thông qua các phương thức gốc khác nhau để được kế thừa từ interface BeanFactory.
Đầu tiên, tôi tạo một cấu hình ứng dụng đơn giản:
@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}
Code language: JavaScript (javascript)
Tiếp theo, tôi tạo một class đơn giản, Foo, không chấp nhận đối số phương thức khởi tạo:
@Component
public class Foo {
}
Code language: PHP (php)
Sau đó tạo class Bar, có chứa một contructor với một tham số là name:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {
private String name;
public Bar(String name) {
this.name = name;
}
// Getter ...
}
Code language: JavaScript (javascript)
Cuối cùng, tôi tạo bean thông qua AnnotationConfigApplicationContext implementation của ApplicationContext:
@Test
public void whenGetSimpleBean_thenReturnConstructedBean() {
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Foo foo = context.getBean(Foo.class);
assertNotNull(foo);
}
@Test
public void whenGetPrototypeBean_thenReturnConstructedBean() {
String expectedName = "Some name";
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Bar bar = context.getBean(Bar.class, expectedName);
assertNotNull(bar);
assertThat(bar.getName(), is(expectedName));
}
Code language: JavaScript (javascript)
Sử dụng phương thức getBean(), chúng ta có thể tạo ra các bean đã được cấu hình chỉ bằng cách sử dụng class type và trong trường hợp contructor parameter của phương thức khởi tạo Bar.
3.2. External Configuration
Pattern này rất linh hoạt vì chúng ta có thể thay đổi hoàn toàn hành vi của ứng dụng dựa trên cấu hình bên ngoài.
Nếu chúng ta muốn thay đổi việc triển khai các đối tượng tự động của ứng dụng, tôi có thể điều chình việc triển khai ApplicationContext mà tôi sử dụng.
Ví dụ: chúng ta có thể thay đổi AnnotationConfigApplicationContext thành một lớp cấu hình dựa trên XML, chẳng hạn như ClassPathXmlApplicationContext:
@Test
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() {
String expectedName = "Some name";
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
// Same test as before ...
}
Code language: JavaScript (javascript)
4. Proxy Pattern
Proxy là một tool tiện dụng trong thế giới kỹ thuật số của chúng ta và chúng ta có thể sử dụng chúng rất thường xuyên bên ngoài phần mềm (chẳng han như network proxy). Trong code, Proxy Pattern là một kỹ thuật cho phép một đối tượng – proxy – kiểm soát quyền truy cập vào đối tượng khác – chủ thể hoặc service.
4.1. Transactions
Để tạo proxy, chúng tôi tạo một đối tượng triển khai giao diện giống như chủ đề của chúng tôi và chứa tham chiếu đến chủ thể.
Sau đó, chúng tôi có thể sử dụng proxy thay cho chủ thể.
Tring Spring, các bean được ủy quyền để kiểm soát quyền truy cập vào bean bên dưới. Tôi thấy cách tiếp cận này khi sử dụng các transactioin:
@Service
public class BookManager {
@Autowired
private BookRepository repository;
@Transactional
public Book create(String author) {
System.out.println(repository.getClass().getName());
return repository.create(author);
}
}
Code language: CSS (css)
Trong class BookManager, tôi đã chú thích phương thức bằng annotation @Transactional. Annotation này hướng dẫn Spring thực thi nguyên tử phương thức tạo. Nếu không có proxy, Spring sẽ không thể kiểm soát quyền truy cập vào bean BookRepository và đảm bảo tính nhất quán giao dịch của nó.
4.2. CGLib Proxies
Thay vào đó, Spring tạo ra một proxy bao bọc bean BookRepository và bean để thực thi phương thức tạo một cách nguyên tử.
Khi chúng ta gọi phương thức tạo BookManager, có thể thấy đầu ra:
com.hdd.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c
Code language: PHP (php)
Thông thường, chúng ta mong đợi thấy một ID đối tượng BookRepository tiêu chuẩn; thay vào đó, chúng ta thấy một ID đối tượng EnhancerBySpringCGLIB.
Phía sau, Spring đã bao bọc đối tượng BookRepository của chúng ta bên trong dưới dạng đối tượng EnhancerBySpringCGLIB. Spring do đó kiểm soát quyền truy cập vào đối tượng BookRepository (đảm bảo tính nhất quán của giao dịch).
Nói tóm lại thì Spring sử dụng 2 loại proxy:
- CGLib Proxies – được sử dụng khi proxy các lớp.
- JDK Dynamic Proxies – được sử dụng khi proxy interface.
Trong khi tôi sử dụng các giao dịch để hiển thị các proxy cơ bản, Spring sẽ sử dụng proxy cho bất kỳ trường hợp nào mà nó phải kiểm soát quyền truy cập vào bean.
5. Template Method Pattern
Trong nhiều framework, một phần đáng kể của code là code soạn thảo.
Ví dụ: khi thực thi một câu truy vấn trên database, phải hoàn thành một loạt các bước sau:
- Establish a connection
- Execute query
- Perform cleanup
- Close the connection
Các bước này là một kịch bản lý tưởng cho Template Method Pattern.
5.1. Templates & Callbacks
Template method pattern là một kỹ thuật xác định các bước cần thiết cho một hành động, triển khai các bước soạn sẵn và để các bước có thể tùy chỉnh dưới dạng trừu tượng. Các lớp con sau đó có thể triển khai lớp trừu tượng này và cung cấp một triển khai cụ thể cho các bước còn thiếu.
Chúng ta có thể tạo một mẫu trong trường hợp truy vấn cơ sở dữ liệu của chúng tôi:
public abstract DatabaseQuery {
public void execute() {
Connection connection = createConnection();
executeQuery(connection);
closeConnection(connection);
}
protected Connection createConnection() {
// Connect to database...
}
protected void closeConnection(Connection connection) {
// Close connection...
}
protected abstract void executeQuery(Connection connection);
}
Code language: PHP (php)
Ngoài ra, chúng tôi có thể cung cấp bước còn thiếu bằng cách cung cấp callback method.
Callback method là một method cho phép chủ thể báo hiệu cho khách hàng rằng một số hành động mong muốn đã hoàn thành.
Trong một số trường hợp, đối tượng có thể sử dụng lệnh callback này để thực hiện các hành động – chẳng hạn như kết quả ánh xạ.
Ví dụ: thay vì có một phương thức executeQuery, chúng ta có thể cung cấp cho phương thức thực thi một chuỗi truy vấn và một phương thức gọi lại để xử lý kết quả.
Đầu tiên, chúng ta tạo phương thức gọi lại lấy một đối tượng Result và ánh xạ nó tới một đối tượng kiểu T:
public interface ResultsMapper<T> {
public T map(Results results);
}
Code language: PHP (php)
Sau đó, chúng tôi thay đổi lớp DatabaseQuery của mình để sử dụng lệnh gọi lại này:
public abstract DatabaseQuery {
public <T> T execute(String query, ResultsMapper<T> mapper) {
Connection connection = createConnection();
Results results = executeQuery(connection, query);
closeConnection(connection);
return mapper.map(results);
]
protected Results executeQuery(Connection connection, String query) {
// Perform query...
}
}
Code language: PHP (php)
Cơ chế callback này chính xác là phương pháp mà Spring sử dụng với lớp JdbcTemplate.
5.2. JdbcTemplate
JdbcTemplate cung cấp method truy vấn, phương thức này chấp nhận một chuỗi truy vấn và đối tượng ResultSetExtractor:
public class JdbcTemplate {
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
// Execute query...
}
// Other methods...
}
Code language: PHP (php)
ResultSetExtractor chuyển đổi đối tượng ResultSet – đại diện cho kết quả của truy vấn – thành một đối tượng miền thuộc loại T:
@FunctionalInterface
public interface ResultSetExtractor<T> {
T extractData(ResultSet rs) throws SQLException, DataAccessException;
}
Code language: PHP (php)
Spring giảm thêm mã soạn sẵn bằng cách tạo các giao diện gọi lại cụ thể hơn.
Ví dụ: interface RowMapper được sử dụng để chuyển đổi một hàng dữ liệu SQL đơn lẻ thành một đối tượng miền kiểu T.
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
Code language: PHP (php)
Để điều chỉnh giao diện RowMapper với ResultSetExtractor mong đợi, Spring tạo lớp RowMapperResultSetExtractor:
public class JdbcTemplate {
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
// Other methods...
}
Code language: PHP (php)
Thay vì cung cấp logic để chuyển đổi toàn bộ đối tượng ResultSet, bao gồm cả việc lặp qua các hàng, tôi có thể cung cấp logic cho cách chuyển đổi một hàng:
public class BookRowMapper implements RowMapper<Book> {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book book = new Book();
book.setId(rs.getLong("id"));
book.setTitle(rs.getString("title"));
book.setAuthor(rs.getString("author"));
return book;
}
}
Code language: PHP (php)
Với trình chuyển đổi này, sau đó tôi có thể truy vấn cơ sở dữ liệu bằng JdbcTemplate và ánh xạ từng hàng kết quả:
JdbcTemplate template = // create template...
template.query("SELECT * FROM books", new BookRowMapper());
Code language: JavaScript (javascript)
Ngoài quản lý cơ sở dữ liệu JDBC, Spring cũng sử dụng các mẫu cho:
- Java Message Service (JMS)
- Java Persistence API (JPA)
- Hibernate (now deprecated)
- Transactions
6. Kết luận
Trong hướng dẫn này, tôi đã giải thích bốn trong số các Design Patterns phổ biến nhất được áp dụng trong Spring Framework.
Tôi cũng đã khám phá cách Spring sử dụng các mẫu này để cung cấp các tính năng phong phú đồng thời giảm bớt gánh nặng cho các nhà phát triển.
Tham khảo bài viét khác:
https://hocspringboot.net/2021/06/11/design-pattern-la-gi-design-pattern-trong-java/
Leave a Reply