Viết hàm trả về nhiều giá trị

Chào các bạn, không biết có cách nào cho 1 hàm trả về nhiều giá trị không ? mình ví dụ cụ thể như này: mình viết hàm kiểm tra đăng nhập trả về boolean, trong hàm này chia ra nhiều trường hợp để check: như mail không tồn tại, đúng mail nhưng sai pass, đúng mail đúng pass nhưng acc bị khóa hoặc mail chưa xác thực ,…

public boolean checkLogin(email, password){
    if(sai mail) return false;
    if(sai pass) return false;
    if(acc bị khóa) return false;
    if(mail chưa xác thực) return false;
    return true; // pass qua các case trên là đăng nhập thành công
}

Mình muốn không những trả về boolean và còn phải kèm theo 1 thông báo lỗi nữa. Mình có 3 cách để giải quyết trường hợp này nhưng không tối ưu lắm.

  1. Tạo một đối tượng kết quả login với 2 thuộc tính là :
    • boolean kết quả.
    • String message.
      ==> cho phương thức login trả về đối tượng này.
  2. login() trả về một hashMap với key là mã lỗi còn value là thông báo kèm theo.
  3. Cho login() trả về int, với 1 là đăng nhập thành công, 2 là sai mail, 3 là sai pass, 4 là bị khóa acc, … rồi hàm nào gọi phương thức login() thì tự xử lý tiếp.

Không biết có cách nào khác không, hay có kểu dữ liệu nào mà gồm nhiều giá trị bên trong giống như tọa độ A(x,y) B(x,y,z) . Cảm ơn mọi người (Mình hỏi chung thôi, không phụ thuộc ngôn ngữ nào cả)

EDIT (@library) Cậu nên tham khảo cách dùng markdown để format cho những post sau nhé!

  • Cơ bản nhất thì người ta sẽ dùng cách thứ 3, nhưng thay vì int, thì sẽ trả về kiểu enum như sau:
enum LoginResult{
    OK,
    ERR_WRONG_EMAIL,
    ERR_WRONG_PASS,
    ERR_LOCKED_ACC,
    ERR_UNREGISTERED_EMAIL,
   ....
}

Rồi có 1 chỗ map cái enum này qua string như bạn đề cập.

  • Cách thuần OOP thì chắc là checkLogin sẽ throw exception với message nếu check invalid.

  • Cách mới hơn chút chính là dùng plus type như std::variant bên C++:

    struct OK{};
    struct ERR{
      std::string message;
    };
    using LoginResult = std::variant<OK, ERR>;

Như bên Rust thì có std::result, hay haskell thì có Data.Either chính là cùng 1 nguyên lý

7 Likes

Có 1 cách khác cho cậu, đó là dùng exception.

    public void checkLogin(email, password) throws Exception {
        if(sai mail) throw WrongEmailFormatException("You shall not pass! Invalid email format");
        if(sai pass)  throw WrongPasswordException("You shall not pass! Boohoo, wrong password!");
        if(acc bị khóa)  throw AccountLockException("You shall not pass! This account got locked!");
        if(mail chưa xác thực)  throw InvalidEmailException("You shall not pass! The email is not verified.");
        // Congrats! You passed!
    }

Ở bên ngoài, cậu sẽ có đoạn code xử lý exception, với từng exception sẽ đưa ra thông điệp tương ứng, và không cần phải boolean gì cả.
Tớ nghĩ cách này elegant nhất :smile:


Các option khác tớ đoán cậu phần nào hiểu được sao nó không tối ưu.

  1. Việc tạo 1 đối tượng có 2 thuộc tính chạy được, nhưng đồng nghĩa với nó là tất cả các validation khác cũng phải có đối tượng tương ứng.
  2. Việc sử dụng hash map chạy được, nhưng rất khó đọc. Ai đó sử dụng method của cậu sẽ không biết key của map là gì, và value của map là gì, và kỳ vọng có bao nhiêu entry trong map được trả về.
    Mặt khác, trả về multiple result cho 1 hàm thường được coi là bad practice, vì hàm của cậu làm nhiều công việc khác nhau 1 lúc (1 hàm chỉ nên làm duy nhất 1 nhiệm vụ thôi).
  3. Trả về mã lỗi cũng chạy được, nhưng 1 lần nữa, cậu cũng không hiểu mã lỗi 1 nghĩa là gì.
    Trả về enum thì dễ đọc hơn, tuy nhiên cậu cũng sẽ có các enum khác nhau cho từng validation.

Về exception, cậu hoàn toàn có thể có 1 exception duy nhất ValidationFailedException, với thông điệp lỗi rõ ràng. Nó sẽ dễ cho cậu khi handle bên ngoài, và nó là 1 luồng khác, cậu không cần quan tâm nó khi đọc luồng chính.
Vậy nên, exception handling thường là cách elegant nhất cho cậu.

6 Likes

Bạn thêm ngôn ngữ mà bạn đang dùng đi. Cách trả về dạng Either<Value, Error> thường sử dụng trong một số ngôn ngữ thôi.

5 Likes

1 hoặc 3, cái nào cũng được
nếu làm bài tập thì không quá quan trọng

cách 3 gọn gàng hơn, tuy nhiên cần define rõ ràng các mã code

4 Likes

Mình thì sẽ chọn làm theo cách 3

1 Like

Tạo từng cái customException riêng là ok nhất.

4 Likes

Đến thời điểm hiện tại cách này mình cảm thấy tối ưu nhất.

1 Like

Mình thấy cách 3 thường dùng cho hệ thống nhúng, khi mà có cả trăm cả nghìn lỗi hoạt động rồi driver các kiểu, người ta sẽ define, comment cho nó dễ hiểu, hay viết thành cả tài liệu đặc tả hoặc hướng dẫn sử dụng. Cách này cũng dễ dùng nữa.

2 Likes
    public void checkLogin(email, password) throws Exception {
        if(sai mail) throw WrongEmailFormatException("You shall not pass! Invalid email format");
        if(sai pass)  throw WrongPasswordException("You shall not pass! Boohoo, wrong password!");
        if(acc bị khóa)  throw AccountLockException("You shall not pass! This account got locked!");
        if(mail chưa xác thực)  throw InvalidEmailException("You shall not pass! The email is not verified.");
        // Congrats! You passed!
    }

Nếu app của mình có chức năng đa ngôn ngữ (i18n) thì nội dung text exception (chữ màu đỏ bên trên) sẽ thay bằng một biến hoặc load từ database ra đúng không bạn @library?

Hay là mình dùng cách này:

  • Tạo nhiều class custom cho từng loại exception.
  • Hàm checkLogin sẽ throw ra một trong những class exception đó.
  • Hàm nào thực thi lời gọi hàm đến checkLogin sẽ instanceof để phân loại lỗi ra.

Nhưng mình nghĩ nó giống với trường hợp return về số vì trường hợp này có thể không biết ý nghĩ của class đó là gì? Nhưng mình vẫn có thể gọi các thuộc tính của đối tượng exception vừa tạo ra theo kiểu : ClassName.showErrorMessage(), ClassName.showErrorCode(), … và việc xử lý nội dung cho từng loại exception mình sẽ xử lý bên trong class exception như thông thường?

class WrongEmailFormatException {  // custom exception
    constructor() {  
        //load from database
        this.EN_message = "You shall not pass! Invalid email format";
        this.VI_message = "Vui lòng nhập đúng định dạng email"; 
    }
}

function validateAccount(account) {
    if (!account.email.includes("@")) 
           throw new WrongEmailFormatException();
    return account;
}

// ___________________MAIN____________________
try {
    let account = validateAccount({ "email": 'abc' });
} catch (err) {
    if (err instanceof WrongEmailFormatException) {
        console.log(err.EN_message);
        console.log(err.VI_message);
    }
}

Hm…
Trong TH cậu cần cài đặt i18n, cậu nên dùng 1 component riêng để switch giữa các message lỗi giữa các ngôn ngữ.
Nó có thể trông thế này khi cậu throw ra lỗi:

// Error message in English: "You got busted!"
throw new WrongEmailException(i18n.getMessage("error.wrong_email", currentLanguage);

Ở file properties nào đó:

# language-en.properties file
error.wrong_email=You got busted!

# language-jp.properties file
error.wrong_email=ごめんね!

Hoặc thậm chí, cậu có thể convert message ở thời điểm catch exception, chẳng hạn:

throw new WrongEmailException("error.wrong-email");

// in a exception handler class, quite popular in spring
@ExceptionHandler(WrongEmaiException.class)
// return HTTP 400
public Response handleWrongEmail(WrongEmailException ex) {
  Response response = new Response();
  response.setMessage(i18n.getMessage(ex.getMessage(), currentLanguage);
  return response;
}

Dù cách này cũng không hẳn là flexible, vì có lúc cậu không muốn logic chuyển đổi message được thực hiện.

Load từ database cũng là một cách để cài đặt, nếu như cậu có quá nhiều message và ngôn ngữ.

Hay là mình dùng cách này:

  • Tạo nhiều class custom cho từng loại exception.
  • Hàm checkLogin sẽ throw ra một trong những class exception đó.
  • Hàm nào thực thi lời gọi hàm đến checkLogin sẽ instanceof để phân loại lỗi ra.

Dùng instanceof trong TH của cậu không phải là cách tốt khi cậu code OOP đâu.
Về cơ bản, nếu cậu phải dùng instanceof để phân loại hành vi và đưa ra action phù hợp với từng class, cậu hoàn toàn có thể dùng đa hình để làm việc đó nếu cậu sử dụng OOP.
Cậu cũng hoàn toàn có thể catch từng loại exception và thực thi hành động phù hợp. Tuy nhiên, trong TH i18n, cậu có thể thấy các hành động này đều giống nhau (chỉ là hiển thị message phụ thuộc vào ngôn ngữ thôi).


Trong code mà cậu đưa ra, có 1 vài vấn đề:

  • Exception không nên chứa logic và dữ liệu. Cậu có logic load dữ liệu vào exception từ DB, và toàn bộ dữ liệu liên quan tới message dưới các ngôn ngữ khác nhau ở constructor. Điều này không nên vì:
    • Exception không nên quan tâm tới việc chuyển đổi ngôn ngữ diễn ra như thế nào.
    • Exception không nên quan tâm tới việc lấy dữ liệu ngôn ngữ như thế nào.
    • Cậu sẽ không thể thay nội dung của message ở từng nơi khác nhau một cách flexible. Hay nói cách khác, exception của cậu bị coupling với message mất rồi.
      Có thể, tại 1 logic, cậu chỉ cần thông báo email định dạng sai, nhưng ở 1 logic khác, cậu cần mắng người dùng (ví dụ thôi nha, đừng làm thế trên production).
  • Load dữ liệu ở constructor đồng nghĩa với việc mỗi lần khởi tạo object, cậu lại gọi tới DB => thao tác này đắt đỏ và không cần thiết mọi lúc, nhất là khi cậu không thường xuyên thay đổi thông điệp lỗi.
    Dữ liệu này nên được cache lại ở app của cậu.
  • Tớ đoán cậu có thể catch loại exception thay vì dùng instanceof để check chứ? :smile:

Như tớ đề cập ở trên, cậu nên có 1 component i18n riêng để xử lý.

Hope it helps!

10 Likes

A post was merged into an existing topic: Bếp ga vs bếp từ

Ngôn ngữ C thường code theo cách này nhưng thành công trả về 0.

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?