Kiểu cho luồng điều khiển với kiểm tra xác nhận


27

Tôi thấy mình viết rất nhiều mã như thế này:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     // do some stuff; might be lengthy
     int myresult = whatever;
     return myResult;
  }
  else {
    return -1;
  }
}

Nó có thể trở nên khá lộn xộn, đặc biệt là nếu nhiều kiểm tra có liên quan. Trong những trường hợp như vậy, tôi đã thử nghiệm các kiểu thay thế, chẳng hạn như kiểu này:

int netWorth(Person* person) {
  if (Person==NULL) {
    return -1;
  }
  if (!(person->isAlive))  {
    return -1;
  }
  int assets = person->assets;
  if (assets==-1)  {
    return -1;
  }
  int liabilities = person->liabilities;
  if (liabilities==-1) {
    return -1;
  }
  return assets - liabilities;
}

Tôi quan tâm đến ý kiến ​​về các lựa chọn phong cách ở đây. [Đừng lo lắng quá nhiều về các chi tiết của các tuyên bố riêng lẻ; đó là dòng kiểm soát tổng thể mà tôi quan tâm.]


8
Cho phép tôi chỉ ra rằng bạn có một lỗi đặc tả khá nghiêm trọng trong ví dụ của bạn. Ví dụ: nếu tài sản == 42 và nợ phải trả == 43, bạn sẽ tuyên bố người đó không tồn tại.
John R. Strohm

Sẽ không ném một ngoại lệ và để cho mã khách hàng quản lý xác nhận sẽ tốt hơn?
Tulains Córdova

@ TulainsCórdova Ngoại lệ có thể không khả dụng hoặc có lẽ dữ liệu không hợp lệ không đủ đặc biệt để tác động hiệu suất của việc xây dựng dấu vết ngăn xếp, v.v.
Hulk

Câu trả lời:


27

Đối với loại vấn đề này Martin Fowler đề xuất mẫu Đặc điểm kỹ thuật :

... mẫu thiết kế, theo đó các quy tắc kinh doanh có thể được kết hợp lại bằng cách kết nối các quy tắc kinh doanh lại với nhau bằng logic boolean.
 
Một mẫu đặc tả phác thảo một quy tắc kinh doanh có thể kết hợp với các quy tắc kinh doanh khác. Trong mẫu này, một đơn vị logic nghiệp vụ kế thừa chức năng của nó từ lớp Đặc tả tổng hợp trừu tượng. Lớp Đặc tả tổng hợp có một hàm gọi là IsSatisfiedBy trả về giá trị boolean. Sau khi khởi tạo, đặc tả được "xâu chuỗi" với các thông số kỹ thuật khác, làm cho các thông số kỹ thuật mới có thể dễ dàng duy trì, nhưng có khả năng tùy biến cao. Ngoài ra, khi khởi tạo, logic nghiệp vụ có thể, thông qua việc gọi phương thức hoặc đảo ngược điều khiển, trạng thái của nó bị thay đổi để trở thành đại biểu của các lớp khác như kho lưu trữ bền vững ...

Ở trên nghe có vẻ hơi cao (ít nhất là với tôi), nhưng khi tôi thử nó trong mã của mình, nó đã hoạt động khá trơn tru và hóa ra dễ thực hiện và đọc.

Theo cách tôi thấy, ý tưởng chính là "trích xuất" mã thực hiện kiểm tra thành (các) phương thức / đối tượng chuyên dụng.

Với netWorthví dụ của bạn , điều này có thể xem xét như sau:

int netWorth(Person* person) {
  if (isSatisfiedBySpec(person)) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
}

#define BOOLEAN int // assuming C here
BOOLEAN isSatisfiedBySpec(Person* person) {
  return Person != NULL
      && person->isAlive
      && person->assets != -1
      && person->liabilities != -1;
}

Trường hợp của bạn xuất hiện khá đơn giản để tất cả các kiểm tra có vẻ phù hợp với một danh sách đơn giản trong một phương thức. Tôi thường phải chia thành nhiều phương pháp để làm cho nó đọc tốt hơn.

Tôi cũng thường nhóm / trích xuất các phương thức liên quan đến "spec" trong một đối tượng chuyên dụng, mặc dù trường hợp của bạn có vẻ ổn mà không cần điều đó.

  // ...
  Specification s, *spec = initialize(s, person);
  if (spec->isSatisfied()) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
  // ...

Câu hỏi này tại Stack Overflow khuyến nghị một vài liên kết ngoài một liên kết được đề cập ở trên: Ví dụ mẫu đặc tả . Cụ thể, các câu trả lời đề xuất Kích thước 'Tìm hiểu mẫu Đặc tả' để biết thêm về một ví dụ và đề cập đến bài báo "Thông số kỹ thuật" do Eric Evans và Martin Fowler viết .


8

Tôi thấy việc di chuyển xác nhận sang chức năng của nó dễ dàng hơn, nó giúp giữ cho ý định của các chức năng khác sạch hơn, vì vậy ví dụ của bạn sẽ như thế này.

int netWorth(Person* person) { 
    if(validPerson(person)) {
        int assets = person->assets;
        int liabilities = person->liabilities;
        return assets - liabilities;
    }
    else {
        return -1;
    }
}

bool validPerson(Person* person) { 
    if(person!=NULL && person->isAlive
      && person->assets !=-1 && person->liabilities != -1)
        return true;
    else
        return false;
}

2
Tại sao bạn có iftrong validPerson? person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1Thay vào đó chỉ đơn giản là trở về .
David Hammen

3

Một điều mà tôi thấy công việc đặc biệt tốt là giới thiệu một lớp xác nhận vào mã của bạn. Trước tiên, bạn có một phương thức thực hiện tất cả các xác nhận lộn xộn và trả về các lỗi (như -1trong ví dụ của bạn ở trên) khi có sự cố. Khi xác thực xong, hàm sẽ gọi một hàm khác để thực hiện công việc thực tế. Bây giờ chức năng này không cần thực hiện tất cả các bước xác thực vì chúng đã được thực hiện. Điều đó có nghĩa là, hàm làm việc giả định rằng đầu vào là hợp lệ. Làm thế nào bạn nên đối phó với các giả định? Bạn khẳng định chúng trong mã.

Tôi nghĩ rằng điều này làm cho mã rất dễ đọc. Phương thức xác nhận chứa tất cả các mã lộn xộn để xử lý lỗi ở phía người dùng. Phương pháp làm việc ghi lại rõ ràng các giả định của nó với các xác nhận và sau đó không phải làm việc với dữ liệu không hợp lệ.

Xem xét việc tái cấu trúc ví dụ này của bạn:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     return myFunctionWork(person)
  }
  else {
    return -1;
  }
}

int myFunction(Person *person) {
  assert( person != NULL);  
  // Do work and return
}
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.