Nếu mô hình đang xác thực dữ liệu, không nên đưa ra ngoại lệ vào đầu vào xấu?


9

Đọc câu hỏi SO này , có vẻ như việc đưa ra các ngoại lệ để xác thực đầu vào của người dùng sẽ không được chấp nhận.

Nhưng ai nên xác nhận dữ liệu này? Trong các ứng dụng của tôi, tất cả các xác nhận được thực hiện trong lớp nghiệp vụ, bởi vì chỉ có chính lớp đó thực sự biết giá trị nào là hợp lệ cho mỗi một thuộc tính của nó. Nếu tôi đã sao chép các quy tắc để xác thực một thuộc tính cho bộ điều khiển, có thể các quy tắc xác thực thay đổi và bây giờ có hai nơi cần thực hiện sửa đổi.

Là tiền đề của tôi rằng xác nhận nên được thực hiện trên lớp kinh doanh sai?

Những gì tôi làm

Vì vậy, mã của tôi thường kết thúc như thế này:

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      throw new ValidationException("Name cannot be empty");
    }
    $this->name = $n;
  }

  public function setAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        throw new ValidationException("Age $a is not valid");
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      throw new ValidationException("Age $a is out of bounds");
    }
    $this->age = $a;
  }

  // other getters, setters and methods
}

Trong bộ điều khiển, tôi chỉ truyền dữ liệu đầu vào cho mô hình và bắt các ngoại lệ được ném để hiển thị (các) lỗi cho người dùng:

<?php
$person = new Person();
$errors = array();

// global try for all exceptions other than ValidationException
try {

  // validation and process (if everything ok)
  try {
    $person->setAge($_POST['age']);
  } catch (ValidationException $e) {
    $errors['age'] = $e->getMessage();
  }

  try {
    $person->setName($_POST['name']);
  } catch (ValidationException $e) {
    $errors['name'] = $e->getMessage();
  }

  ...
} catch (Exception $e) {
  // log the error, send 500 internal server error to the client
  // and finish the request
}

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Đây có phải là một phương pháp xấu?

Phương pháp luân phiên

Có lẽ tôi nên tạo các phương thức cho isValidAge($a)trả về true / false và sau đó gọi chúng từ bộ điều khiển?

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if ($this->isValidName($n)) {
      $this->name = $n;
    } else {
      throw new Exception("Invalid name");
    }
  }

  public function setAge($a) {
    if ($this->isValidAge($a)) {
      $this->age = $a;
    } else {
      throw new Exception("Invalid age");
    }
  }

  public function isValidName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      return false;
    }
    return true;
  }

  public function isValidAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        return false;
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      return false;
    }
    return true;
  }

  // other getters, setters and methods
}

Và bộ điều khiển về cơ bản sẽ giống nhau, chỉ thay vì thử / bắt ngay bây giờ nếu / khác:

<?php
$person = new Person();
$errors = array();
if ($person->isValidAge($age)) {
  $person->setAge($age);
} catch (Exception $e) {
  $errors['age'] = "Invalid age";
}

if ($person->isValidName($name)) {
  $person->setName($name);
} catch (Exception $e) {
  $errors['name'] = "Invalid name";
}

...

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Vậy, tôi nên làm gì?

Tôi khá hài lòng với phương pháp ban đầu của mình và các đồng nghiệp mà tôi đã chỉ ra nói chung đã thích nó. Mặc dù vậy, tôi có nên thay đổi sang phương pháp thay thế? Hay tôi đang làm điều này sai lầm khủng khiếp và tôi nên tìm cách khác?


Tôi đã sửa đổi mã "gốc" một chút để xử lý ValidationExceptionvà các ngoại lệ khác
Carlos Campderrós

2
Một vấn đề với việc hiển thị các thông báo ngoại lệ cho người dùng cuối là mô hình đột nhiên cần biết ngôn ngữ mà người dùng nói, nhưng đó chủ yếu là mối quan tâm đối với Chế độ xem.
Bart van Ingen Schenau

@BartvanIngenSchenau bắt tốt. Các ứng dụng của tôi luôn là ngôn ngữ đơn, nhưng thật tốt khi nghĩ đến các vấn đề nội địa hóa có thể phát sinh trong bất kỳ triển khai nào.
Carlos Campderrós

Các ngoại lệ cho xác nhận chỉ là một cách ưa thích để đưa các loại vào quy trình. Bạn có thể đạt được kết quả tương tự bằng cách trả về một đối tượng thực hiện giao diện xác nhận như thế nào IValidateResults.
Phản ứng

Câu trả lời:


7

Cách tiếp cận mà tôi đã sử dụng trước đây là đặt tất cả các lớp Xác thực dành riêng cho logic xác thực.

Sau đó, bạn có thể đưa các lớp Xác thực này vào Lớp Trình bày để xác thực đầu vào sớm. Và, không có gì ngăn các lớp Model của bạn sử dụng các lớp giống nhau để thực thi tính toàn vẹn dữ liệu.

Theo cách tiếp cận này, sau đó bạn có thể xử lý các lỗi Xác thực khác nhau tùy thuộc vào lớp xảy ra trong:

  • Nếu Xác thực tính toàn vẹn dữ liệu không thành công trong Mô hình, thì hãy ném Ngoại lệ.
  • Nếu Xác thực nhập liệu của người dùng không thành công trong Lớp trình bày, sau đó hiển thị một mẹo hữu ích và trì hoãn đẩy giá trị cho Mô hình của bạn.

Vì vậy, bạn có lớp PersonValidatorvới tất cả logic để xác nhận các thuộc tính khác nhau của a PersonPersonlớp phụ thuộc vào điều này PersonValidator, phải không? Ưu điểm mà đề xuất của bạn đưa ra so với phương pháp thay thế mà tôi đề xuất trong câu hỏi là gì? Tôi chỉ thấy khả năng tiêm các lớp Xác thực khác nhau cho một Person, nhưng tôi không thể nghĩ về bất kỳ trường hợp nào cần điều này.
Carlos Campderrós

Tôi đồng ý rằng việc thêm một lớp hoàn toàn mới để xác nhận là quá mức cần thiết, ít nhất là trong trường hợp tương đối đơn giản này. Nó có thể hữu ích cho một vấn đề phức tạp hơn nhiều.

Chà, đối với một ứng dụng mà bạn dự định bán cho nhiều người / công ty thì có thể có ý nghĩa, bởi vì mỗi công ty có thể có các quy tắc khác nhau để xác thực phạm vi hợp lệ cho độ tuổi của một Người. Vì vậy, nó có thể hữu ích, nhưng thực sự quá mức cho nhu cầu của tôi. Dù sao, +1 cho bạn cũng vậy
Carlos Campderrós

1
Thu thập xác nhận từ mô hình có ý nghĩa từ quan điểm khớp nối và gắn kết là tốt. Trong kịch bản đơn giản này, nó có thể là quá mức cần thiết, nhưng nó sẽ chỉ lấy một quy tắc xác thực "trường chéo" duy nhất để làm cho lớp Trình xác thực riêng biệt hấp dẫn hơn nhiều.
Seth M.

8

Tôi khá hài lòng với phương pháp ban đầu của mình và các đồng nghiệp mà tôi đã chỉ ra nói chung đã thích nó. Mặc dù vậy, tôi có nên thay đổi sang phương pháp thay thế? Hay tôi đang làm điều này sai lầm khủng khiếp và tôi nên tìm cách khác?

Nếu bạn và đồng nghiệp của bạn hài lòng với nó, tôi thấy không cần phải thay đổi.

Điều duy nhất đáng nghi ngờ từ góc độ thực dụng là bạn đang ném Exceptionchứ không phải là một cái gì đó cụ thể hơn. Vấn đề là nếu bạn nắm bắt Exception, cuối cùng bạn có thể bắt được các ngoại lệ không liên quan gì đến việc xác thực đầu vào của người dùng.


Bây giờ có nhiều người nói những điều như "ngoại lệ chỉ nên được sử dụng cho những điều đặc biệt và XYZ không phải là ngoại lệ". (Ví dụ: Câu trả lời của @ dann1111 ... nơi anh ấy gắn nhãn lỗi của người dùng là "hoàn toàn bình thường".)

Phản ứng của tôi với điều đó là không có tiêu chí khách quan nào để quyết định liệu thứ gì đó ("XY Z") có đặc biệt hay không. Đó là một biện pháp chủ quan . (Thực tế là bất kỳ chương trình nào cần kiểm tra lỗi trong đầu vào của người dùng không làm cho các lỗi xảy ra là "bình thường". Trên thực tế, "bình thường" phần lớn là vô nghĩa theo quan điểm khách quan.)

Có một hạt sự thật trong câu thần chú đó. Trong một số ngôn ngữ (hoặc chính xác hơn, một số triển khai ngôn ngữ ) tạo ngoại lệ, ném và / hoặc bắt đắt hơn đáng kể so với các điều kiện đơn giản. Nhưng nếu bạn nhìn từ góc độ đó, bạn cần so sánh chi phí tạo / ném / bắt với chi phí của các bài kiểm tra bổ sung mà bạn có thể cần thực hiện nếu bạn tránh sử dụng ngoại lệ. Và "phương trình" phải tính đến xác suất mà ngoại lệ cần phải được ném.

Đối số khác chống lại các ngoại lệ là họ có thể làm cho mã khó hiểu hơn. Nhưng mặt trái là khi chúng được sử dụng một cách thích hợp, chúng có thể làm cho mã dễ hiểu hơn.


Nói tóm lại - quyết định sử dụng hay không sử dụng ngoại lệ nên được đưa ra sau khi cân nhắc công đức ... và KHÔNG dựa trên một số giáo điều đơn giản.


Điểm tốt về chung chung Exceptionbị ném / bắt. Tôi thực sự ném một số lớp con của riêng mình Exception, và mã của các setters thường không làm gì có thể ném ngoại lệ khác.
Carlos Campderrós

Tôi đã sửa đổi mã "gốc" một chút để xử lý Xác thực ngoại lệ và các ngoại lệ khác / cc @ dan1111
Carlos Campderrós

1
+1, tôi muốn có một Xác thực ngoại lệ mô tả hơn là quay trở lại thời kỳ đen tối của việc phải kiểm tra giá trị trả về của mỗi cuộc gọi phương thức. Mã đơn giản = có khả năng ít lỗi hơn.
Heinzi

2
@ dan1111 - Trong khi tôi tôn trọng quyền của bạn để có một ý kiến, không có gì trong bình luận của bạn là bất cứ điều gì khác hơn ý kiến. Không có mối liên hệ logic giữa "tính bình thường" của xác nhận và cơ chế xử lý các lỗi xác thực. Tất cả những gì bạn đang làm là đọc thuộc lòng giáo điều.
Stephen C

@StephenC, khi phản ánh tôi cảm thấy rằng tôi đã nêu trường hợp của mình quá mạnh mẽ. Tôi đồng ý rằng đó là một sở thích cá nhân nhiều hơn.

6

Theo tôi, rất hữu ích khi phân biệt lỗi ứng dụnglỗi người dùng và chỉ sử dụng ngoại lệ cho lỗi trước.

  • Các ngoại lệ được dự định để bao gồm những thứ ngăn chương trình của bạn thực thi đúng cách .

    Chúng là những sự cố bất ngờ ngăn bạn tiếp tục và thiết kế của chúng phản ánh điều này: chúng phá vỡ sự thực thi bình thường và nhảy đến một nơi cho phép xử lý lỗi.

  • Lỗi người dùng như đầu vào không hợp lệ là hoàn toàn bình thường (theo quan điểm của chương trình của bạn) và không nên được ứng dụng của bạn coi là bất ngờ .

    Nếu người dùng nhập sai giá trị và bạn hiển thị thông báo lỗi, chương trình của bạn đã "thất bại" hay có lỗi theo bất kỳ cách nào? Không. Ứng dụng của bạn đã thành công - với một loại đầu vào nhất định, nó tạo ra đầu ra chính xác trong tình huống đó.

    Xử lý lỗi người dùng, vì nó là một phần của thực thi thông thường, nên là một phần của luồng chương trình bình thường của bạn, thay vì xử lý bằng cách nhảy ra với một ngoại lệ.

Tất nhiên có thể sử dụng các ngoại lệ cho mục đích khác ngoài mục đích của họ, nhưng làm như vậy sẽ gây nhầm lẫn cho mô hình và rủi ro hành vi không chính xác khi những lỗi đó xảy ra.

Mã ban đầu của bạn có vấn đề:

  • Người gọi setAge()phương thức phải biết quá nhiều về xử lý lỗi nội bộ của phương thức: người gọi cần biết rằng một ngoại lệ được ném khi tuổi không hợp lệ và không có ngoại lệ nào khác có thể được ném trong phương thức . Giả định này có thể bị phá vỡ sau nếu bạn thêm chức năng bổ sung bên trong setAge().
  • Nếu người gọi không bắt ngoại lệ, ngoại lệ tuổi không hợp lệ sau đó sẽ được xử lý theo một số cách khác, rất có thể là mờ đục. Hoặc thậm chí gây ra một vụ tai nạn ngoại lệ chưa được xử lý. Hành vi không tốt cho dữ liệu không hợp lệ được nhập.

Mã thay thế cũng có vấn đề:

  • Một phương pháp bổ sung, có thể không cần thiết isValidAge()đã được giới thiệu.
  • Bây giờ setAge()phương thức phải giả định rằng người gọi đã được kiểm tra isValidAge()(một giả định khủng khiếp) hoặc xác nhận lại tuổi. Nếu nó xác nhận tuổi một lần nữa, setAge() vẫn phải cung cấp một số loại xử lý lỗi và bạn sẽ quay lại bình phương một lần nữa.

Đề xuất thiết kế

  • Hãy setAge()trở về đúng với thành công và sai về thất bại.

  • Kiểm tra giá trị trả về setAge()và nếu thất bại, thông báo cho người dùng rằng tuổi không hợp lệ, không phải là ngoại lệ, nhưng với chức năng bình thường hiển thị lỗi cho người dùng.


Vậy thì tôi nên làm thế nào? Với phương pháp thay thế mà tôi đã đề xuất hoặc với một cái gì đó hoàn toàn khác mà tôi chưa từng nghĩ đến? Ngoài ra, tiền đề của tôi là "xác nhận nên được thực hiện trên lớp doanh nghiệp" là sai?
Carlos Campderrós

@ CarlosCampderrós, xem cập nhật; Tôi đã thêm thông tin đó như bạn nhận xét. Thiết kế ban đầu của bạn đã xác thực ở đúng nơi, nhưng thật sai lầm khi sử dụng các ngoại lệ để thực hiện xác nhận đó.

Phương thức thay thế buộc setAgephải xác nhận lại một lần nữa, nhưng về cơ bản logic là "nếu nó hợp lệ thì đặt ngoại lệ khác ném ngoại lệ", nó không đưa tôi trở lại bình phương.
Carlos Campderrós

2
Một vấn đề tôi thấy cả với phương pháp thay thế và thiết kế được đề xuất là chúng mất khả năng phân biệt TẠI SAO tuổi không hợp lệ. Nó có thể được thực hiện để trả về đúng hoặc một chuỗi lỗi (vâng, php rất bẩn), nhưng điều này có thể dẫn đến nhiều vấn đề, bởi vì "The entered age is out of bounds" == truevà mọi người nên luôn luôn sử dụng ===, vì vậy cách tiếp cận này sẽ có vấn đề hơn so với vấn đề mà nó cố gắng giải quyết
Carlos Campderrós

2
Nhưng sau đó, mã hóa ứng dụng thực sự mệt mỏi bởi vì mỗi setAge()khi bạn thực hiện ở bất cứ đâu, bạn phải kiểm tra xem nó có thực sự hoạt động không. Ném ngoại lệ có nghĩa là bạn không nên lo lắng về việc nhớ kiểm tra mọi thứ đã ổn. Như tôi thấy, cố gắng đặt giá trị không hợp lệ thành thuộc tính / thuộc tính là một điều gì đó đặc biệt và sau đó, đáng để ném Exception. Mô hình không nên quan tâm nếu nó nhận đầu vào từ cơ sở dữ liệu hoặc từ người dùng. Nó không bao giờ nên nhận đầu vào xấu, vì vậy tôi thấy việc ném một ngoại lệ ở đó là hợp pháp.
Carlos Campderrós

4

Theo quan điểm của tôi (Tôi là một người Java), việc bạn triển khai nó theo cách đầu tiên là hoàn toàn hợp lệ.

Điều hợp lệ là một đối tượng ném Ngoại lệ khi một số điều kiện tiên quyết không được đáp ứng (ví dụ: chuỗi rỗng). Trong Java, khái niệm về các ngoại lệ được kiểm tra được đặt vào mục đích như vậy - các ngoại lệ phải được khai báo trong chữ ký để được ném một cách có chủ đích và người gọi cần phải nắm bắt rõ ràng. Ngược lại, các ngoại lệ không được kiểm tra (còn gọi là RuntimeExceptions), có thể xảy ra bất cứ lúc nào mà không cần xác định mệnh đề bắt trong mã. Mặc dù cái đầu tiên được sử dụng cho các trường hợp có thể phục hồi (ví dụ: nhập sai người dùng, tên tệp không tồn tại), cái sau được sử dụng cho các trường hợp người dùng / lập trình viên không thể làm bất cứ điều gì về (ví dụ: Bộ nhớ ngoài).

Mặc dù vậy, như bạn đã đề cập bởi @Stephen C, hãy xác định các ngoại lệ của riêng bạn và nắm bắt cụ thể những trường hợp đó để không bắt người khác vô tình.

Tuy nhiên, một cách khác là sử dụng các Đối tượng truyền dữ liệu đơn giản là các thùng chứa dữ liệu mà không có logic. Sau đó, bạn bàn giao DTO như vậy cho trình xác nhận hợp lệ hoặc chính Đối tượng mô hình để xác thực và chỉ khi thành công, hãy thực hiện các cập nhật trong Đối tượng mô hình. Cách tiếp cận này thường được sử dụng khi logic trình bày và logic ứng dụng được phân tách các tầng (bản trình bày là một trang web, ứng dụng một dịch vụ web). Bằng cách này, chúng được phân tách vật lý, nhưng nếu bạn có cả hai trên một tầng (như trong ví dụ của bạn), bạn phải đảm bảo rằng sẽ không có cách giải quyết để đặt giá trị mà không cần xác thực.


4

Với chiếc mũ Haskell của tôi, cả hai cách tiếp cận đều sai.

Điều xảy ra về mặt khái niệm là trước tiên bạn có một loạt các byte và sau khi phân tích cú pháp và xác nhận hợp lệ, sau đó bạn có thể xây dựng một Người.

Người có những bất biến nhất định, chẳng hạn như giới hạn của tên và tuổi.

Có thể đại diện cho một Người chỉ có một cái tên, nhưng không có tuổi là điều bạn muốn tránh bằng mọi giá, bởi vì đó là điều tạo ra sự đồng hành. Ví dụ bất biến nghiêm ngặt có nghĩa là bạn không cần phải kiểm tra sự hiện diện của tuổi sau này.

Vì vậy, trong thế giới của tôi, Person được tạo ra một cách nguyên tử bằng cách sử dụng một hàm tạo hoặc hàm duy nhất. Hàm xây dựng hoặc hàm đó có thể kiểm tra lại tính hợp lệ của các tham số, nhưng không nên xây dựng nửa người.

Thật không may, Java, PHP và các ngôn ngữ OO khác làm cho tùy chọn chính xác khá dài dòng. Trong các API Java thích hợp, các đối tượng trình xây dựng thường được sử dụng. Trong một API như vậy, việc tạo một người sẽ trông giống như thế này:

Person p = new Person.Builder().setName(name).setAge(age).build();

hoặc dài dòng hơn:

Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
// Person object must have name and age here

Trong các trường hợp này, bất kể trường hợp ngoại lệ được ném hay trường hợp xác thực xảy ra, không thể nhận được một cá thể Person không hợp lệ.


Tất cả những gì bạn đã làm ở đây là chuyển vấn đề sang lớp Builder mà bạn chưa thực sự trả lời.
Cypher

2
Tôi đã bản địa hóa vấn đề cho hàm builder.build () được thực thi nguyên tử. Chức năng đó là một danh sách của tất cả các bước xác minh. Có một sự khác biệt rất lớn giữa phương pháp này và phương pháp tiếp cận đặc biệt. Lớp Builder không có bất biến nào ngoài các kiểu đơn giản, trong khi lớp Person có bất biến mạnh. Xây dựng các chương trình chính xác là tất cả về việc thực thi các bất biến mạnh mẽ trong dữ liệu của bạn.
user239558

Nó vẫn không trả lời câu hỏi (ít nhất là không hoàn toàn). Bạn có thể giải thích về cách các thông báo lỗi riêng lẻ được truyền từ lớp Builder lên ngăn xếp cuộc gọi đến Chế độ xem không?
Cypher

Ba khả năng: build () có thể đưa ra các ngoại lệ cụ thể, như trong ví dụ đầu tiên của OP. Có thể có Bộ công khai <String> xác thực () trả về một tập hợp các lỗi có thể đọc được của con người. Có một Bộ công khai <Error> xác thực () cho các lỗi sẵn sàng i18n. Vấn đề là điều này xảy ra trong quá trình chuyển đổi thành đối tượng Person.
user239558

2

Theo cách nói của giáo dân:

Cách tiếp cận đầu tiên là cách chính xác.

Cách tiếp cận thứ hai giả định rằng các lớp nghiệp vụ đó sẽ chỉ được gọi bởi các bộ điều khiển đó và chúng sẽ không bao giờ được gọi từ bối cảnh khác.

Các lớp kinh doanh phải ném Ngoại lệ mỗi khi một quy tắc kinh doanh bị vi phạm.

Trình điều khiển hoặc lớp trình bày phải quyết định xem nó ném chúng hay nó có hiệu lực riêng để ngăn chặn các ngoại lệ xảy ra.

Hãy nhớ rằng: các lớp của bạn sẽ có khả năng được sử dụng trong các bối cảnh khác nhau và bởi các nhà tích hợp khác nhau. Vì vậy, họ phải đủ thông minh để ném ngoại lệ cho đầu vào xấu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.