Kiểm tra điều kiện tiên quyết hay không


8

Tôi đã muốn tìm một câu trả lời chắc chắn cho câu hỏi có hay không kiểm tra thời gian chạy để xác thực đầu vào cho các mục đích đảm bảo khách hàng đã bị mắc kẹt trong thỏa thuận thiết kế của họ theo hợp đồng. Ví dụ, hãy xem xét một hàm tạo lớp đơn giản:

class Foo
{
public:
  Foo( BarHandle bar )
  {
    FooHandle handle = GetFooHandle( bar );
    if( handle == NULL ) {
      throw std::exception( "invalid FooHandle" );
    }
  }
};

Tôi sẽ tranh luận trong trường hợp này rằng người dùng không nên cố gắng xây dựng Foomà không có giá trị BarHandle. Có vẻ không đúng khi xác minh rằng barnó hợp lệ bên trong hàm tạo của Foonó. Nếu tôi chỉ đơn giản là tài liệu Foocủa hàm tạo đó yêu cầu một giá trị hợp lệ BarHandle , thì điều đó có đủ không? Đây có phải là một cách thích hợp để thực thi điều kiện tiên quyết của tôi trong thiết kế bằng hợp đồng?

Cho đến nay, mọi thứ tôi đọc đều có ý kiến ​​trái chiều về điều này. Có vẻ như 50% mọi người sẽ nói để xác minh barlà hợp lệ, 50% còn lại sẽ nói rằng tôi không nên làm điều đó, ví dụ như xem xét trường hợp người dùng xác minh BarHandlelà đúng, nhưng kiểm tra lần thứ hai (và không cần thiết) cũng đang được thực hiện bên trong nhà Fooxây dựng của.


Câu trả lời:


10

Tôi không nghĩ rằng có một câu trả lời duy nhất cho điều này. Tôi nghĩ rằng điều chính cần thiết là tính nhất quán - hoặc bạn thực thi tất cả các điều kiện tiên quyết trên một chức năng, hoặc nếu không bạn không cố gắng thực thi bất kỳ trong số chúng. Thật không may, điều đó khá hiếm - điều thường xảy ra là thay vì nghĩ về các điều kiện tiên quyết và thực thi chúng, các lập trình viên thêm các đoạn mã để thực thi các điều kiện tiên quyết mà vi phạm xảy ra gây ra lỗi trong quá trình thử nghiệm, nhưng thường để các khả năng khác có thể gây ra lỗi nhưng đã không xảy ra để phát sinh trong thử nghiệm.

Trong nhiều trường hợp, việc cung cấp hai lớp: một lớp cho việc sử dụng "nội bộ" là không hợp lý, không cố gắng thực thi bất kỳ điều kiện tiên quyết nào, và sau đó là lần thứ hai cho việc sử dụng "bên ngoài" chỉ thực thi các điều kiện tiên quyết, sau đó gọi ra điều kiện tiên quyết.

Tuy nhiên, tôi nghĩ rằng tốt hơn là nên áp dụng các điều kiện tiên quyết trong nút nguồn chứ không chỉ là tài liệu. Một ngoại lệ hay khẳng định là nhiều khó khăn hơn để bỏ qua so với tài liệu và nhiều hơn nữa khả năng nghỉ đồng bộ với phần còn lại của mã này.


Về nguyên tắc, tôi đồng ý với đoạn cuối của bạn. Mặc dù điều đó bây giờ có nghĩa là có ba điều cần được giữ đồng bộ; tài liệu, bản thân khẳng định và các trường hợp kiểm tra chứng minh các xác nhận đang thực hiện công việc của họ (nếu bạn tin vào những điều đó)!
Oliver Charlesworth

@OliCharlesworth: Vâng, nó tạo ra điều thứ ba để giữ đồng bộ, nhưng nó thiết lập một (việc thực thi trong mã nguồn) là điều thường được tin cậy khi có bất đồng. Mặt khác, bạn thường không biết.
Jerry Coffin

2
@JerryCoffin Tôi có thể kiểm tra xem có phải foolà NULL không, nhưng là NULL không phải là cách duy nhất foocó thể không hợp lệ. Ví dụ, những gì về -1 cast cho a FooHandle? Tôi không thể xác minh tất cả các cách có thể xử lý có thể không hợp lệ. NULL là một lựa chọn rõ ràng và một cái gì đó thường được kiểm tra, nhưng không phải là kiểm tra kết luận. Bạn muốn giới thiệu gì ở đây?
void.pulum

@RobertDailey: Cuối cùng, gần như không thể đảm bảo chống lại mọi hành vi lạm dụng có thể xảy ra, đặc biệt là khi / nếu tham gia casting. Với việc truyền, người dùng có thể lật đổ mọi thứ bạn có thể kiểm tra. Điều tôi nhấn mạnh nhất là sự khác biệt giữa 1) giả sử các tham số là tốt và thêm kiểm tra cho những điều sai trong thử nghiệm và 2) tìm ra các điều kiện tiên quyết chính xác nhất có thể và thực thi chúng cũng như bạn có thể .
Jerry Coffin

@JerryCoffin Điều này khác với Lập trình phòng thủ, thường không được coi là "điều tốt"? Hầu hết thời gian, các kỹ thuật lập trình phòng thủ như thế này và nhiều thứ khác tôi từng thấy không thực dụng lắm. Đó là một thiết kế được thực hiện để chống lại đồng nghiệp của bạn thói quen mã hóa xấu hoặc những thứ khác thay vì tập trung vào chức năng thực tế và thực hiện các phương pháp của bạn. Tôi chỉ thấy điều này dễ dàng thoát khỏi tầm tay như một thói quen bổ sung thêm logic nồi hơi trên tất cả các chức năng của lớp. Bạn sẽ nghĩ rằng kiểm tra đơn vị loại bỏ sự cần thiết cho các kiểm tra tiền điều kiện này.
void.pulum

4

Đó là một câu hỏi rất khó, bởi vì có một số khái niệm khác nhau:

  • Đúng
  • Tài liệu
  • Hiệu suất

Tuy nhiên, đây chủ yếu là một tạo tác của một loại lỗi, trong trường hợp này. Nullity được thi hành tốt hơn bởi các ràng buộc kiểu, bởi vì trình biên dịch thực sự kiểm tra các ràng buộc đó. Tuy nhiên, vì không phải mọi thứ đều có thể được ghi lại trong một hệ thống loại, đặc biệt là trong C ++, bản thân câu hỏi vẫn còn giá trị.


Cá nhân, tôi nghĩ rằng tính chính xác và tài liệu là tối quan trọng. Nhanh và sai là vô ích. Nhanh và chỉ sai đôi khi tốt hơn một chút, nhưng cũng không mang lại nhiều điều cho bàn.

Hiệu suất mặc dù có thể rất quan trọng trong một số phần của chương trình và một số kiểm tra có thể khá rộng rãi (nghĩa là: chứng minh rằng đồ thị có hướng có tất cả các nút của nó cả có thể truy cập và có thể truy cập được). Vì vậy, tôi sẽ bỏ phiếu cho một cách tiếp cận kép.

Nguyên tắc thứ nhất: Thất bại nhanh . Đây là một nguyên tắc chỉ đạo trong lập trình phòng thủ nói chung, trong đó chủ trương phát hiện lỗi ở giai đoạn sớm nhất có thể. Tôi sẽ thêm Fail Hard vào phương trình.

if (not bar) { abort(); }

Thật không may, trong một môi trường sản xuất thất bại nặng nề không nhất thiết là giải pháp tốt nhất. Trong trường hợp này, một ngoại lệ cụ thể có thể giúp thoát khỏi đó một cách vội vàng, và để một số người xử lý cấp cao nắm bắt và xử lý trường hợp thất bại một cách thích hợp (rất có thể là đăng nhập và giả mạo trước một trường hợp mới).

Điều này tuy nhiên không giải quyết vấn đề kiểm tra đắt tiền . Ở những điểm nóng, những xét nghiệm đó có thể có giá quá cao. Trong trường hợp này, thật hợp lý khi chỉ bật thử nghiệm trong các bản dựng DEBUG.

Điều này cho chúng ta một giải pháp tốt đẹp và đơn giản:

  • SOFT_ASSERT(Cond_, Text_)
  • DEBUG_ASSERT(Cond_, Text_)

Trong đó hai macro được định nghĩa như vậy:

 #ifdef NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) { throw Exception(Text_, __FILE__, __LINE__); }
 #  define DEBUG_ASSERT(Cond_, Text_) while(false) {}
 #else // NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) {                                                       \
            std::cerr << __FILE__ << '#' << __LINE__ << ": " << Text_ << std::endl; \
            abort();                                                                \
        }
 #  define DEBUG_ASSERT(Cond_, Text_) SOFT_ASSERT(Cond_, Text_)
 #endif // NDEBUG

0

Một câu trích dẫn tôi đã nghe về điều này là:

"Hãy thận trọng trong những gì bạn làm và tự do trong những gì bạn chấp nhận."

Điều này có nghĩa là tuân theo các hợp đồng cho các đối số khi bạn gọi các hàm và kiểm tra tất cả các đầu vào trước khi hành động khi bạn viết các hàm.

Cuối cùng, nó phụ thuộc vào tên miền. Nếu bạn đang sử dụng API hệ điều hành, tốt hơn hết bạn nên kiểm tra mọi đầu vào, đừng tin tưởng tất cả dữ liệu đến là hợp lệ trước khi bạn bắt đầu hành động. Nếu bạn đang sử dụng thư viện cho người khác sử dụng, hãy tiếp tục, hãy để người dùng tự xoay sở (OpenGL xuất hiện trước tiên vì một số lý do không xác định).

EDIT: theo nghĩa OO, dường như có hai cách tiếp cận - một cách nói rằng một đối tượng không bao giờ bị sai lệch (tất cả các bất biến của nó phải đúng) trong toàn bộ thời gian mà một đối tượng có thể truy cập được và một cách tiếp cận khác nói rằng bạn có một hàm tạo không đặt tất cả các bất biến, sau đó bạn đặt thêm một vài giá trị và có hàm khởi tạo thứ hai kết thúc init.

Tôi giống như trước đây tốt hơn vì nó không đòi hỏi kiến ​​thức ma thuật hoặc dựa vào tài liệu hiện tại để biết phần khởi tạo nào mà nhà xây dựng không làm.


Đối với việc khởi tạo như vậy, tôi xây dựng đối tượng trình dựng sẵn chứa dữ liệu khởi tạo một phần và sau đó tạo ra đối tượng "hữu ích" hoàn toàn chính thức.
user470365
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.