Kiến trúc nhiều tầng
EzyPlatform duy trì một kiến trúc với 3 tầng chính như hình dưới đây:
Các tầng chính này bao gồm:
- Tầng controller.
- Tầng service.
- Tầng repository.
Tầng controller
Đây là tầng tương đối phức tạp, nó chịu trách nhiệm tiếp nhận yêu cầu từ người dùng, gọi đến tầng service để xử lý nghiệp vụ, tổng hợp dữ liệu và trả về cho người dùng, chính vì sự phức tạp này mà lại nó được chia thành 4 tầng khác nhau:
- Tầng view và API controller.
- Tầng validator.
- Tầng controller service.
- Tầng decorator.
Tầng view và API controller
Bởi vì EzyPlatform phục vụ cho cả web lẫn mọi loại hình dự án khác như app hay IoT vậy nên nó cần cung cấp 2 loại controller.
- View controller: Loại này trả về dữ liệu dạng HTML cho trình duyệt hiển thị cho người dùng, mã nguồn ví dụ có thể như sau:
@Controller @AllArgsConstructor public class HomeController { private final ViewFactory viewFactory; @DoGet("/") public View homeGet(@RequestParam("lang") String language) { return viewFactory .newHomeViewBuilder(language) .build(); } }
Ở đây bạn thấy hàm trả về View, để ra được HTML bên trong EzyPlatform sẽ sử dụng ezyhttp framework thông qua các bước chuyển đổi phức tạp đển biến View thành HTML.
- API controller: Loại này trả về dữ liệu dạng JSON để mọi dự án có thể sử dụng được, mã nguồn ví dụ sẽ như sau:
@Api @Controller("/api/v1") @AllArgsConstructor public class ApiLibraryController { private final WebLibraryItemControllerService libraryItemControllerService; private final WebCommonValidator webCommonValidator; @DoGet("/library") public PaginationModel<LibraryItemResponse> libraryGet( @RequestParam(value = "keyword") String keyword, @RequestParam(value = "nextPageToken") String nextPageToken, @RequestParam(value = "prevPageToken") String prevPageToken, @RequestParam(value = "lastPage") boolean lastPage, @RequestParam(value = "limit", defaultValue = "12") int limit ) { webCommonValidator.validatePageSize(limit); webCommonValidator.validateSearchKeyword(keyword); return libraryItemControllerService.getPublicLibraryItems( keyword, nextPageToken, prevPageToken, lastPage, limit ); } }
Thông thường uri của API sẽ có dạng /api/v{phiên bản} để phân biệt với uri của view controller.
Tầng validator
Đây là tầng kiểm tra dữ liệu được gửi đến controller, nó sẽ đảm bảo các dữ liệu khi đến các bước tiếp theo sẽ sạch và hợp lệ, điều này nhằm tránh bị lặp lại các mã nguồn xác thực ở mọi nơi, giúp mã nguồn trở nên tinh gọn hơn, mã nguồn ví dụ có thể trông thế này:
@AllArgsConstructor public class LibraryItemValidator { private final LibraryItemService libraryItemService; public LibraryItemModel validateLibraryItemId(long libraryItemId) { LibraryItemModel model = libraryItemService.getLibraryItemById( libraryItemId ); if (model == null) { throw new HttpNotFoundException( singletonMap("libraryItem", "notFound") ); } return model; } }
Tầng controller service
Tầng này để xử lý một số nghiệp vụ mà tầng service không thể xử lý hết được, những nghiệp vụ này thông thường liên quan đến nhiều lớp service, có thể coi các lớp ở tầng này là mediator design pattern. Mã nguồn ví dụ có thể như sau:
@Service @AllArgsConstructor public class WebQuizControllerService { private final WebQuizService quizService; private final WebQuizModelDecorator quizModelDecorator; public WebFinishQuizQuestionResponse finishQuizQuestion( long quizId, long questionId, long userId ) { boolean correct = quizService.isCorrectQuizQuestionUserAnswer( quizId, questionId, userId ); if (correct) { quizService.tryToFinishQuiz(quizId, userId); } return new WebFinishQuizQuestionResponse(correct); } }
Tầng decorator
Đây là tầng tổng hợp dữ liệu từ nhiều nguồn khác nhau. Dữ liệu đầu vào sẽ là các model và dữ liệu đầu ra sẽ là response. Mã nguồn ở tầng này cũng tương đối phức tạp khi nó phải gọi đến rất nhiều service thì mới tổng hợp được ra đủ dữ liệu để trả cho người dùng. Mã nguồn ví dụ sẽ như sau:
@EzySingleton @AllArgsConstructor public class WebQuizModelDecorator { private final WebEQuestionService questionService; private final WebQuizService quizService; private final WebQuizQuestionService quizQuestionService; private final WebElearningModelToResponseConverter modelToResponseConverter; public WebUserQuizResponse decorateToUserQuizResponse( QuizModel model, long userId ) { List<Long> questionIds = quizQuestionService .getPublishedQuestionIdsByQuizId(model.getId()); return modelToResponseConverter.toUserQuizResponse( model, questionService.getQuestionsByIds(questionIds), quizService.getQuizCorrectQuestionIdsByQuizIdAndUserId( model.getId(), userId ) ); } }
Tầng service
Đây là tầng sẽ xử lý hầu hết các logic phức tạp của EzyPlatform, chính vì thế mà sẽ có những lớp rất nhiều hàm và sẽ rất dài, một số nghiệp vụ mà nó xử lý có thể bao gồm:
- Tương tác với cơ sở dữ liệu thông qua các lớp từ tầng repository.
- Tính toán.
- Đồng bộ dữ liệu.
- Đưa ra quyết định nên làm gì với dữ liệu.
- ...
Mã nguồn ví dụ có thể như sau:
@AllArgsConstructor public class EClassLessonService { private final ClockProxy clock; private final EClassLessonExtractor eclassLessonExtractor; private final ELearningSettingService settingService; private final EClassRepository eclassRepository; private final EClassLessonRepository eclassLessonRepository; private final EClassLessonCancellationRepository eclassLessonCancellationRepository; private final EClassLessonDocumentRepository eclassLessonDocumentRepository; private final EClassLessonExamRepository eclassLessonExamRepository; private final ElearningEntityToEntityConverter entityToEntityConverter; private final ElearningEntityToModelConverter entityToModelConverter; private final ElearningModelToEntityConverter modelToEntityConverter; public void replaceClassLessons( long classId, LocalDateTime startTime, int lessons, int lessonDuration, EClassLessonDurationType lessonDurationType, EClassScheduleType scheduleType, List<SaveEClassScheduleModel> schedules, List<MonthDay> holidays ) { List<AddEClassLessonModel> addEClassLessonModels = eclassLessonExtractor.extract( startTime.toLocalDate(), lessons, lessonDuration, lessonDurationType, scheduleType, schedules, holidays ); eclassLessonRepository.deleteByClassId(classId); AtomicInteger lessonIndex = new AtomicInteger(); eclassLessonRepository.save( newArrayList( addEClassLessonModels, it -> modelToEntityConverter.toEntity( classId, it, lessonIndex.incrementAndGet() ) ) ); } }
Tầng repository
Đây là tầng tương tác với cơ sở dữ liệu, nó cũng có những sự phức tạp riêng khi bạn có thể tạo 2 loại repository khác nhau:
- Repository ở dạng interface để truy vấn đơn giản.
- TransactionalRepository ở dạng class để xử lý các transaction phức tạp.
Mã nguồn ví dụ cho dạng interface có thể như sau:
public interface EClassStudentRepository extends EzyDatabaseRepository<EClassStudentId, EClassStudent> { @EzyQuery( "SELECT e.classId FROM EClassStudent e " + "WHERE e.classId IN ?0 AND e.studentId = ?1" ) List<IdResult> findClassIdsByClassIdsAndStudentId( Collection<Long> classIds, long userId ); @EzyQuery( "SELECT b.studentId FROM EClass a " + "INNER JOIN EClassStudent b ON a.id = b.classId " + "WHERE a.id = ?0" ) List<IdResult> findStudentIdsByClassId( long classId ); }
Mã nguồn ví dụ cho dạng class sẽ như sau:
ublic class EClassTransactionalRepository extends EzyJpaRepository<Long, EClass> { @Override public void save(EClass entity) { EntityManager entityManager = databaseContext.createEntityManager(); try { EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); try { if (isBlank(entity.getCode())) { List<?> maxIds = entityManager.createQuery( "SELECT e.id FROM EClass e ORDER BY e.id DESC" ) .setMaxResults(1) .getResultList(); long maxId = (maxIds.isEmpty() ? 0L : (Long) maxIds.get(0)) + 1L; String code = formatCode(maxId); entity.setCode(code); } EClass result = entityManager.merge(entity); transaction.commit(); idProxy.setId(result, entity); } catch (Exception e) { transaction.rollback(); throw e; } } finally { entityManager.close(); } } private static String formatCode(long code) { return String.format("%06d", code); } @Override protected Class<EClass> getEntityType() { return EClass.class; } }
Tuân thủ những nguyên tắc chung
Thiết kế ra kiến trúc là một chuyện còn chuyện có thể đảm bảo mọi thứ phải đúng theo kiến trúc lại là chuyện khác. Chính vì vậy mà EzyPlatform tuân thủ rất chặt chẽ các nguyên tắc clean code, ví dụ:
- Phải thiết kế trước: Trước khi lập trình phải ghi nhớ kiến trúc chung, nếu những mã nguồn nào quá phức tạp, có nguy cơ phá vỡ kiến trúc thì phải thiết kế cẩn thận.
- Single responsibility: Mỗi hàm, mỗi lớp nên thực hiện các tính năng riêng biệt, như mỗi người nên tập trung vào thế mạnh của mình thay vì làm quá nhiều việc đồng thời.
- Open-Close: Đóng với việc sửa đổi và mở với việc mở rộng, hạn chế thay đổi mã nguồn ổn định và tạo mã mới nếu cần.
- Liskov substitution: Sử dụng tham số mà không làm phá vỡ chương trình đang chạy ổn định.
- Interface segregation: Chia nhỏ các giao diện lập trình.
- Dependency inversion: EzyPlatform sẽ chịu trách nhiệm khởi tạo và quản lý các phụ thuộc, các plugin sẽ chỉ cần lo nghiệp vụ của mình mà thôi.
- Keep it simple, stupid (KISS): Giữ cho mọi thứ đơn giản, dễ theo dõi.
- Write DRY code: Không lặp lại mã nguồn giống nhau ở nhiều nơi.
- You aren’t going to need it (YAGNI): Bỏ đi những thứ không dùng đến và những thứ không cần thiết nữa.
- Composition over inheritance: Sử dụng các lớp hỗn hợp thay vì thừa kế để có thể sử dụng chung các thành phần và giảm thiểu mã nguồn unit test.
- Separation of concerns: EzyPlatform chia mã nguồn thành nhiều tầng và nhiều thành phần để tách biệt các mối quan tâm.
- Self-comment: Đặt tên biến, hàm, lớp rõ ràng dễ hiểu để không cần phải viết tài liệu.
- Document your code: Đối với các mã nguồn phức tạp thì cần viết tài liệu mô tả và hướng dẫn sử dụng.
- Refactoring: Nếu phần mềm hoạt động ổn định thì không nên thay đổi, nhưng nếu có cơ hội thì có thể sửa đổi mã nguồn sao cho tối ưu hơn, cần có chiến lược và kinh nghiệm.
- Clean code: Làm sạch mã nguồn, giúp nó trở nên trong sáng, dễ theo dõi và nâng cấp hơn.
- Unit test: Viết các mã nguồn kiểm thử các hàm để đảm bảo những logic nhỏ nhất đều hoạt động đúng.
- Đừng lạm dụng thư viện: EzyPlatform hầu hết sử dụng các thư viện của Young Monkeys phát triển nên không để xảy ra tình trạng lạm dụng.
- Không được quên bảo mật: Khi lập trình cần chú ý các nguyên tắc bảo mật để bảo vệ phần mềm, hệ thống và người dùng.
- Xây dựng chiến lược log: Cần có chiến lược lưu lại lịch sử hoạt động của phần mềm để thuận tiện cho việc gỡ lỗi và vận hành.
- Cẩn thận với SQL: EzyPlatform có nhiều bảng và đã cung cấp một số chỉ mục thường xuyên được sử dụng.
Tổng kết
Kiến trúc mã nguồn của EzyPlatform là đa tầng với nhiều thành phần khác nhau nhằm mục tiêu "chia để trị". Vì một dự án phức tạp như EzyPlatform nếu tập trung mã nguồn ở một lớp, một gói nào đó tương lai sẽ càng ngày càng bị đầy lên và việc nâng câp sẽ là bất khả thi.