Sau khi đã có thiết kế gói, nghĩa là đã có kim chỉ nam cho các thiết kế tiếp theo, giờ là lúc chúng ta có thể thiết kế các tầng, và thiết kế lớp. Trong bài này chúng ta sẽ cùng nhau thiết kế tầng, thiết kế một lớp cụ thể là UserService và cài đặt nó nhé.

Thiết kế tầng

Như đã giải thích trong bài trước về tầm quan trọng của việc chia để trị và để đáp ứng các nguyên tắc SOLID, chúng ta có thể chia chương trình của chúng ta thành các tầng như sau:

whiteboard_exported_image
whiteboard_exported_image
  1. Tầng đầu tiên đó là Controller: Tầng này chứa các lớp tiếp nhận yêu cầu, gọi đến các lớp tầng Service để xử lý nghiệp vụ và phản hồi kết quả cho người sử dụng, dữ liệu của nó sử dụng bao gồm Request, Model và Response. Nó cũng sử dụng lớp RequestToModelConverter để chuyển đổi Request thành Model và sử dụng ModelToResponseConverter để chuyển model thành Response. Lưu ý rằng tầng này vẫn có thể có xử lý nghiệp vụ nhưng cần hạn chế.
  2. Tầng thứ 2 đó là Service: Tầng này chứa các lớp xử lý nghiệp vụ, dữ liệu mà nó sử dụng là Model và Entity. Nó cũng sử dụng lớp ModelToEntityConverter để chuyển đổi Model thành Entity và sử dụng EntityToModelConverter để chuyển Entity thành Model.
  3. Tầng thứ 3 đó là Repository: Tầng này chứa các giao diện để tương tác với cơ sở dữ liệu, nó không chứa lớp cài đặt gì cụ thể, mà chủ yếu chứa truy vấn đến cơ sở dữ liệu.

Thiết kế sơ đồ lớp cho UserService

Sau khi đã thiết kế sơ đồ tầng, bây giờ chúng ta có thể thiết kế sơ đồ lớp cho UserService như sau:

whiteboard_exported_image
whiteboard_exported_image

Trong sơ đồ này chúng ta có:

  1. Lớp UserService để xử lý các nghiệp vụ liên quan đến người dùng, lớp này sử dụng các lớp dữ liệu là User và UserModel. Lớp này cũng sử dụng lớp ModelToEntityConverter để chuyển đổi từ SaveUserModel sang User, sử dụng EntityToModelConverter để chuyển đổi từ User sang UserModel và sử dụng UserRepository để lưu User xuống cơ sở dữ liệu.
  2. Lớp ModelToEntityConverter để chuyển đổi dữ liệu từ dạng model (lớp UserModel) sang dữ liệu dạng Entity (Lớp User). Lớp này cũng sử dụng lớp PasswordEncoder, một lớp của spring security mà chúng ta sẽ khởi tạo bean cho nó ở trong lớp config.
  3. Lớp EntityToModelConverter để chuyển đổi dữ liệu từ dạng entity (lớp User) sang dữ liệu dạng model (Lớp UserModel).
  4. Interface UserRepository để tương tác với cơ sở dữ liệu, cụ thể là bảng users.

Mã nguồn cài đặt

Khai báo bean PasswordEncoder

Đầu tiên chúng ta sẽ cần khai báo bean PasswordEncoder với mã nguồn như sau:

package vn.techmaster.login.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class LoginSecurityBeanConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Ở đây chúng ta khai báo một lớp cấu hình, sau đó khai báo bean PasswordEncoder sử dụng lớp cài đặt BCryptPasswordEncoder nghĩa là chúng ta đang sử dụng thuật toán BCrypt để mã hoá mật khẩu.

Khai báo các lớp dữ liệu

Chúng ta sẽ khai báo các lớp dữ liệu User, SaveUserModel, UserModel như sau:

package vn.techmaster.login.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String username;

    private String email;

    private String password;

    @Column(name = "display_name")
    private String displayName;

    @Column(name = "activation_token")
    private String activationToken;

    @Enumerated(EnumType.STRING)
    private UserStatus status;
}
package vn.techmaster.login.entity;

public enum UserStatus {
    ACTIVATED,
    INACTIVATED
}
package vn.techmaster.login.model;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import vn.techmaster.login.entity.UserStatus;

@Getter
@Builder
public class SaveUserModel {
    private String username;
    private String email;
    private String password;
    private String displayName;
}
package vn.techmaster.login.model;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import vn.techmaster.login.entity.UserStatus;

@Getter
@Builder
public class UserModel {
    private long id;
    private String username;
    private String email;
    private String password;
    private String displayName;
    private String activationToken;
    private UserStatus status;
}

Lớp User ánh xạ với bảng users trong cơ sở dữ liệu nên sẽ có cả hàm getter và setter, tuy nhiên các lớp model nhu SaveUserModel và UserModel sẽ chỉ có các hàm getter, nghĩa là các đối tượng tạo ra sẽ không thay đổi dữ liệu được.

Khai báo hàm chuyển đổi từ model sang entity

Chúng ta sẽ chuyển đổi từ SaveUserModel sang User với mã nguồn như sau:

package vn.techmaster.login.converter;

import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import vn.techmaster.login.entity.User;
import vn.techmaster.login.entity.UserAccessToken;
import vn.techmaster.login.entity.UserStatus;
import vn.techmaster.login.model.SaveUserModel;

import java.time.LocalDateTime;
import java.util.UUID;

import static vn.techmaster.login.constant.LoginConstants.ACCESS_TOKEN_EXPIRED_IN_HOUR;

@Component
@AllArgsConstructor
public class ModelToEntityConverter {

    private final PasswordEncoder passwordEncoder;

    public User toEntity(SaveUserModel model) {
        User entity = new User();
        entity.setUsername(model.getUsername());
        entity.setEmail(model.getEmail());
        entity.setPassword(
            passwordEncoder.encode(model.getPassword())
        );
        entity.setDisplayName(model.getDisplayName());
        entity.setActivationToken(UUID.randomUUID().toString());
        entity.setStatus(UserStatus.INACTIVATED);
        return entity;
    }
}

Khai báo hàm chuyển đổi từ model sang entity

Chúng ta sẽ chuyển đổi từ User sang UserModel với mã nguồn như sau:

package vn.techmaster.login.converter;

import org.springframework.stereotype.Component;
import vn.techmaster.login.entity.User;
import vn.techmaster.login.model.UserModel;

@Component
public class EntityToModelConverter {

    public UserModel toModel(User entity) {
        if (entity == null) {
            return null;
        }
        return UserModel.builder()
            .id(entity.getId())
            .username(entity.getUsername())
            .email(entity.getEmail())
            .password(entity.getPassword())
            .displayName(entity.getDisplayName())
            .activationToken(entity.getActivationToken())
            .status(entity.getStatus())
            .build();
    }
}

Khai báo interface UserRepository

Chúng ta sẽ khai báo interface UserRepository với mã nguồn như sau:

package vn.techmaster.login.repo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import vn.techmaster.login.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);

    User findByEmail(String email);
}

Ở đây chúng ta sẽ có những hàm mặc định được thừa kế từ JpaRepository, ngoài ra chúng ta cũng sẽ khai báo thêm hai hàm findByUsername và findByEmail để lấy người dùng theo tên đăng nhập và email.

Khai báo lớp UserService

Bây giờ sẽ đến phần chính, chúng ta sẽ sử dụng tất cả các lớp đã khai báo để tạo ra lớp UserService với mã nguồn như sau:

package vn.techmaster.login.service;

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import vn.techmaster.login.converter.EntityToModelConverter;
import vn.techmaster.login.converter.ModelToEntityConverter;
import vn.techmaster.login.entity.User;
import vn.techmaster.login.entity.UserStatus;
import vn.techmaster.login.model.SaveUserModel;
import vn.techmaster.login.model.UserModel;
import vn.techmaster.login.repo.UserRepository;

@Service
@AllArgsConstructor
public class UserService {

    private final MailService mailService;
    private final UserRepository userRepository;
    private final EntityToModelConverter entityToModelConverter;
    private final ModelToEntityConverter modelToEntityConverter;

    public void addUser(SaveUserModel model) {
        User entity = modelToEntityConverter.toEntity(model);
        entity = userRepository.save(entity);
        mailService.sendActivationEmail(
            entity.getEmail(),
            entity.getDisplayName(),
            entity.getUsername(),
            entity.getActivationToken()
        );
    }

    public void activeUser(long userId) {
        userRepository
            .findById(userId)
            .ifPresent(user -> {
                user.setActivationToken(null);
                user.setStatus(UserStatus.ACTIVATED);
                userRepository.save(user);
            });
    }

    public UserModel getUserById(long userId) {
        return entityToModelConverter.toModel(
            userRepository.findById(userId).orElse(null)
        );
    }

    public UserModel getUserByUsername(String username) {
        return entityToModelConverter.toModel(
            userRepository.findByUsername(username)
        );
    }

    public UserModel getUserByEmail(String username) {
        return entityToModelConverter.toModel(
            userRepository.findByEmail(username)
        );
    }
}

Lớp này sẽ có các hàm:

  1. addUser: Thêm một người dùng vào cơ sở dữ liệu.
  2. activeUser: Kích hoạt người dùng.
  3. getUserById: Lấy thông tin người dùng theo Id.
  4. getUserByUsername: Lấy thông tin người dùng theo username.
  5. getUserByEmail: Lấy thông tin người dùng theo email.

Tổng kết

Như vậy chúng ta đã cùng nhau tạo ra lớp UserService, trong bài tiếp theo, chúng ta sẽ cài đặt API cho phép người dùng đăng ký nhé.


Cám ơn bạn đã quan tâm đến bài viết|video này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a