Xác thực tham số đầu vào trong trình gọi: sao chép mã?


16

Đâu là nơi tốt nhất để xác nhận các tham số đầu vào của hàm: trong trình gọi hoặc trong chính hàm đó?

Khi tôi muốn cải thiện phong cách mã hóa của mình, tôi cố gắng tìm ra các thực tiễn tốt nhất hoặc một số quy tắc cho vấn đề này. Khi nào và cái gì tốt hơn.

Trong các dự án trước đây của tôi, chúng tôi thường kiểm tra và xử lý mọi tham số đầu vào bên trong hàm, (ví dụ nếu nó không phải là null). Bây giờ, tôi đã đọc ở đây trong một số câu trả lời và trong cuốn sách Lập trình viên thực dụng, rằng việc xác thực tham số đầu vào là trách nhiệm của người gọi.

Vì vậy, nó có nghĩa là, tôi nên xác nhận các tham số đầu vào trước khi gọi hàm. Ở mọi nơi chức năng được gọi. Và điều đó đặt ra một câu hỏi: không phải nó tạo ra một bản sao điều kiện kiểm tra ở mọi nơi mà hàm được gọi sao?

Tôi không quan tâm chỉ trong các điều kiện null, nhưng trong việc xác thực bất kỳ biến đầu vào nào (giá trị âm cho sqrthàm, chia cho 0, kết hợp sai giữa trạng thái và mã ZIP, hoặc bất cứ điều gì khác)

Có một số quy tắc làm thế nào để quyết định nơi kiểm tra điều kiện đầu vào?

Tôi đang suy nghĩ về một số đối số:

  • khi việc xử lý biến không hợp lệ có thể thay đổi, sẽ tốt khi xác thực nó ở phía người gọi (ví dụ: sqrt()hàm - trong một số trường hợp tôi có thể muốn làm việc với số phức, vì vậy tôi xử lý điều kiện trong trình gọi)
  • Khi điều kiện kiểm tra giống nhau ở mọi người gọi, tốt hơn là kiểm tra nó bên trong chức năng, để tránh trùng lặp
  • xác thực tham số đầu vào trong trình gọi chỉ diễn ra một trước khi gọi nhiều hàm với tham số này. Do đó, việc xác thực một tham số trong mỗi chức năng là không hiệu quả
  • giải pháp đúng phụ thuộc vào trường hợp cụ thể

Tôi hy vọng câu hỏi này không trùng lặp với bất kỳ câu hỏi nào khác, tôi đã tìm kiếm vấn đề này và tôi đã tìm thấy những câu hỏi tương tự nhưng họ không đề cập chính xác trường hợp này.

Câu trả lời:


15

Nó phụ thuộc. Quyết định nơi đặt xác nhận phải dựa trên mô tả và sức mạnh của hợp đồng ngụ ý (hoặc tài liệu) theo phương pháp. Xác nhận là một cách tốt để tăng cường tuân thủ một hợp đồng cụ thể. Nếu vì lý do nào đó phương pháp có một hợp đồng rất nghiêm ngặt, thì có, bạn phải kiểm tra trước khi gọi.

Đây là một khái niệm đặc biệt quan trọng khi bạn tạo một phương thức công khai , bởi vì về cơ bản bạn đang quảng cáo rằng một số phương thức thực hiện một số thao tác. Nó tốt hơn làm những gì bạn nói nó làm!

Lấy phương pháp sau đây làm ví dụ:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Hợp đồng ngụ ý là DeletePersongì? Lập trình viên chỉ có thể giả định rằng nếu có bất kỳ Personthông qua, nó sẽ bị xóa. Tuy nhiên, chúng tôi biết rằng điều này không phải lúc nào cũng đúng. Điều gì nếu plà một nullgiá trị? Điều gì nếu pkhông tồn tại trong cơ sở dữ liệu? Nếu cơ sở dữ liệu bị ngắt kết nối thì sao? Do đó, DeletePerson dường như không hoàn thành tốt hợp đồng của mình. Đôi khi, nó xóa một người và đôi khi nó ném NullReferenceException hoặc DatabaseNotConnectedException hoặc đôi khi nó không làm gì cả (chẳng hạn như nếu người đó đã bị xóa).

Các API như thế này nổi tiếng là khó sử dụng, bởi vì khi bạn gọi đây là "hộp đen" của một phương thức, tất cả các loại điều khủng khiếp đều có thể xảy ra.

Dưới đây là một số cách bạn có thể cải thiện hợp đồng:

  • Thêm xác nhận và thêm một ngoại lệ cho hợp đồng. Điều này làm cho hợp đồng mạnh hơn , nhưng yêu cầu người gọi thực hiện xác nhận. Tuy nhiên, sự khác biệt là bây giờ họ biết yêu cầu của họ. Trong trường hợp này, tôi giao tiếp điều này với một nhận xét XML C #, nhưng thay vào đó bạn có thể thêm một throws(Java), sử dụng Asserthoặc sử dụng một công cụ hợp đồng như Hợp đồng mã.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Lưu ý bên lề: Đối số chống lại phong cách này thường là nó gây ra xác thực trước quá mức bởi tất cả các mã gọi, nhưng theo kinh nghiệm của tôi thì điều này thường không xảy ra. Hãy nghĩ về một kịch bản mà bạn đang cố gắng xóa một người null. Làm thế nào điều đó xảy ra? Người null đến từ đâu? Ví dụ, nếu đây là UI, tại sao phím Xoá được xử lý nếu không có lựa chọn hiện tại? Nếu nó đã bị xóa, không nên xóa nó khỏi màn hình? Rõ ràng có những trường hợp ngoại lệ cho điều này, nhưng khi một dự án phát triển, bạn sẽ thường cảm ơn mã như thế này để ngăn chặn các lỗi xâm nhập sâu vào hệ thống.

  • Thêm xác nhận và mã phòng thủ. Điều này làm cho hợp đồng lỏng lẻo hơn , bởi vì bây giờ phương pháp này không chỉ xóa người. Tôi đã thay đổi tên phương thức để phản ánh điều này, nhưng có thể không cần thiết nếu bạn nhất quán trong API của mình. Cách tiếp cận này có ưu và nhược điểm của nó. Sự chuyên nghiệp mà bây giờ bạn có thể gọi là TryDeletePersonchuyển qua tất cả các loại đầu vào không hợp lệ và không bao giờ lo lắng về các ngoại lệ. Tất nhiên, con lừa là người dùng mã của bạn có thể sẽ gọi phương thức này quá nhiều hoặc có thể gây khó khăn trong việc gỡ lỗi trong trường hợp p là null. Đây có thể được coi là một sự vi phạm nhẹ của Nguyên tắc Trách nhiệm Đơn lẻ , vì vậy hãy giữ ý nghĩ đó nếu một cuộc chiến ngọn lửa nổ ra.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Kết hợp các phương pháp. Đôi khi bạn muốn một chút cả hai, nơi bạn muốn người gọi bên ngoài tuân thủ chặt chẽ các quy tắc (để buộc họ phải chịu trách nhiệm về mã), nhưng bạn muốn mã riêng của mình linh hoạt.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

Theo kinh nghiệm của tôi, tập trung vào các hợp đồng mà bạn ngụ ý thay vì một quy tắc cứng hoạt động tốt nhất. Mã hóa phòng thủ dường như hoạt động tốt hơn trong trường hợp người gọi khó hoặc khó xác định liệu một thao tác có hợp lệ hay không. Các hợp đồng nghiêm ngặt dường như hoạt động tốt hơn khi bạn mong đợi người gọi chỉ thực hiện các cuộc gọi phương thức khi chúng thực sự, thực sự có ý nghĩa.


Cảm ơn câu trả lời rất hay với ví dụ. Tôi thích quan điểm của phương pháp "phòng thủ" và "hợp đồng nghiêm ngặt".
srnka

7

Đó là vấn đề quy ước, tài liệu và trường hợp sử dụng.

Không phải tất cả các chức năng đều như nhau. Không phải tất cả các yêu cầu đều như nhau. Không phải tất cả xác nhận là bằng nhau.

Ví dụ: nếu dự án Java của bạn cố gắng tránh các con trỏ null bất cứ khi nào có thể ( ví dụ: xem các đề xuất kiểu Guava ), bạn có còn xác thực mọi đối số hàm để đảm bảo rằng nó không null? Điều này có thể không cần thiết, nhưng rất có thể là bạn vẫn làm điều đó, để dễ dàng tìm ra lỗi hơn. Nhưng bạn có thể sử dụng một xác nhận mà trước đó bạn đã ném NullPulumException.

Nếu dự án ở C ++ thì sao? Quy ước / truyền thống trong C ++ là ghi lại các điều kiện tiên quyết, nhưng chỉ xác minh chúng (nếu có) trong các bản dựng gỡ lỗi.

Trong cả hai trường hợp, bạn có một tài liệu điều kiện tiên quyết về chức năng của mình: không có tranh luận có thể được null. Thay vào đó, bạn có thể mở rộng miền của hàm để bao gồm null với hành vi được xác định, ví dụ: "nếu bất kỳ đối số nào là null, sẽ ném ngoại lệ". Tất nhiên, đó lại là di sản C ++ của tôi khi nói ở đây - trong Java, nó đủ phổ biến để ghi lại các điều kiện tiên quyết theo cách này.

Nhưng không phải tất cả các điều kiện tiên quyết thậm chí có thể được kiểm tra hợp lý. Ví dụ, thuật toán tìm kiếm nhị phân có điều kiện tiên quyết là chuỗi được tìm kiếm phải được sắp xếp. Nhưng xác minh rằng đó chắc chắn là một hoạt động O (N), do đó, thực hiện điều đó trên mỗi cuộc gọi sẽ đánh bại điểm sử dụng thuật toán O (log (N)) ngay từ đầu. Nếu bạn đang lập trình một cách phòng thủ, bạn có thể thực hiện kiểm tra ít hơn (ví dụ: xác minh rằng với mỗi phân vùng bạn tìm kiếm, các giá trị bắt đầu, giữa và cuối được sắp xếp), nhưng điều đó không bắt được tất cả các lỗi. Thông thường, bạn sẽ chỉ cần dựa vào điều kiện tiên quyết được thực hiện.

Một nơi thực sự mà bạn cần kiểm tra rõ ràng là ở ranh giới. Đầu vào bên ngoài cho dự án của bạn? Xác nhận, xác nhận, xác nhận. Một vùng màu xám là ranh giới API. Nó thực sự phụ thuộc vào mức độ bạn muốn tin tưởng mã khách hàng, mức độ thiệt hại đầu vào không hợp lệ và mức độ hỗ trợ bạn muốn cung cấp trong việc tìm kiếm lỗi. Tất cả mọi ranh giới đặc quyền phải được tính là bên ngoài, tất nhiên - các tòa nhà, ví dụ, chạy trong bối cảnh đặc quyền nâng cao và do đó phải rất cẩn thận để xác nhận. Bất kỳ xác nhận như vậy tất nhiên phải là nội bộ cho tòa nhà.


Cảm ơn câu trả lời của bạn. Bạn có thể, xin vui lòng, cung cấp liên kết đến đề nghị phong cách ổi? Tôi không thể google và tìm hiểu ý của bạn về nó. +1 để xác thực các ranh giới.
srnka

Đã thêm liên kết. Đây thực sự không phải là một hướng dẫn đầy đủ về phong cách, chỉ là một phần của tài liệu về các tiện ích không null.
Sebastian Redl

6

Xác thực tham số nên là mối quan tâm của chức năng được gọi. Hàm nên biết những gì được coi là đầu vào hợp lệ và những gì không. Người gọi có thể không biết điều này, đặc biệt là khi họ không biết cách thực hiện chức năng bên trong. Hàm nên được dự kiến ​​để xử lý bất kỳ sự kết hợp của các giá trị tham số từ người gọi.

Vì hàm chịu trách nhiệm xác thực các tham số, bạn có thể viết các bài kiểm tra đơn vị đối với hàm này để đảm bảo nó hoạt động như dự định với cả các giá trị tham số hợp lệ và không hợp lệ.


Cảm ơn về câu trả lời. Vì vậy, bạn nghĩ rằng, chức năng đó nên kiểm tra cả các tham số đầu vào hợp lệ và không hợp lệ trong mọi trường hợp. Một cái gì đó khác với khẳng định cuốn sách Lập trình viên thực dụng: "xác nhận tham số đầu vào là trách nhiệm của người gọi". Đó là suy nghĩ tốt "Hàm nên biết những gì được coi là hợp lệ ... Người gọi có thể không biết điều này" ... Vì vậy, bạn không muốn sử dụng các điều kiện trước?
srnka

1
Bạn có thể sử dụng các điều kiện trước nếu bạn muốn (xem câu trả lời của Sebastian ), nhưng tôi thích phòng thủ và xử lý mọi loại đầu vào có thể.
Bernard

4

Trong chính chức năng. Nếu chức năng được sử dụng nhiều lần, bạn sẽ không muốn xác minh tham số cho mỗi lần gọi chức năng.

Hơn nữa, nếu chức năng được cập nhật theo cách như vậy sẽ ảnh hưởng đến việc xác thực tham số, bạn phải tìm kiếm mọi lần xuất hiện của xác thực người gọi để cập nhật chúng. Nó không đáng yêu :-).

Bạn có thể tham khảo Điều khoản bảo vệ

Cập nhật

Xem trả lời của tôi cho từng kịch bản bạn đã cung cấp.

  • khi việc xử lý biến không hợp lệ có thể thay đổi, sẽ tốt khi xác thực nó ở phía người gọi (ví dụ: sqrt()hàm - trong một số trường hợp tôi có thể muốn làm việc với số phức, vì vậy tôi xử lý điều kiện trong trình gọi)

    Câu trả lời

    Đa số các ngôn ngữ lập trình hỗ trợ số nguyên và số thực theo mặc định, không phải số phức, do đó việc triển khai chúng sqrtchỉ chấp nhận số không âm. Trường hợp duy nhất bạn có một sqrthàm trả về số phức là khi bạn sử dụng ngôn ngữ lập trình hướng vào toán học, như Mathicala

    Hơn nữa, sqrtđối với hầu hết các ngôn ngữ lập trình đã được triển khai, do đó bạn không thể sửa đổi nó và nếu bạn cố gắng thay thế việc triển khai (xem bản vá khỉ), thì các cộng tác viên của bạn sẽ bị sốc hoàn toàn về lý do tại sao sqrtđột nhiên chấp nhận số âm.

    Nếu bạn muốn một cái, bạn có thể bọc nó xung quanh sqrtchức năng tùy chỉnh của bạn để xử lý số âm và trả về số phức.

  • Khi điều kiện kiểm tra giống nhau ở mọi người gọi, tốt hơn là kiểm tra nó bên trong chức năng, để tránh trùng lặp

    Câu trả lời

    Có, đây là một cách thực hành tốt để tránh phân tán xác thực tham số trên mã của bạn.

  • xác thực tham số đầu vào trong trình gọi chỉ diễn ra một trước khi gọi nhiều hàm với tham số này. Do đó, việc xác thực một tham số trong mỗi chức năng là không hiệu quả

    Câu trả lời

    Sẽ thật tuyệt nếu người gọi là một chức năng, bạn có nghĩ vậy không?

    Nếu các chức năng trong trình gọi được sử dụng bởi người gọi khác, điều gì ngăn bạn xác thực tham số trong các chức năng được gọi bởi người gọi?

  • giải pháp đúng phụ thuộc vào trường hợp cụ thể

    Câu trả lời

    Nhằm mục đích cho mã duy trì. Di chuyển xác thực tham số của bạn đảm bảo một nguồn sự thật về những gì hàm có thể chấp nhận hoặc không.


Cảm ơn về câu trả lời. Sqrt () chỉ là một ví dụ, hành vi tương tự với tham số đầu vào có thể được sử dụng bởi nhiều hàm khác. "nếu chức năng được cập nhật theo cách như vậy sẽ ảnh hưởng đến việc xác thực tham số, bạn phải tìm kiếm mọi lần xuất hiện của xác thực người gọi" - Tôi không đồng ý với điều này. Sau đó, chúng ta có thể nói tương tự cho giá trị trả về: nếu hàm được cập nhật theo cách đó sẽ ảnh hưởng đến giá trị trả về, bạn phải sửa từng người gọi ... Tôi nghĩ rằng hàm phải có một nhiệm vụ được xác định rõ để thực hiện ... Mặt khác sự thay đổi trong người gọi là cần thiết nào.
srnka

2

Một hàm nên nêu trước và sau điều kiện của nó.
Các điều kiện trước là các điều kiện phải được người gọi đáp ứng trước khi nó có thể sử dụng đúng chức năng và có thể (và thường làm) bao gồm tính hợp lệ của các tham số đầu vào.
Các điều kiện hậu là những lời hứa mà hàm thực hiện cho người gọi của nó.

Khi tính hợp lệ của các tham số của hàm là một phần của các điều kiện trước, thì trách nhiệm của người gọi là đảm bảo các tham số đó là hợp lệ. Nhưng điều đó không có nghĩa là mọi người gọi phải kiểm tra rõ ràng từng thông số trước khi gọi. Trong hầu hết các trường hợp, không cần kiểm tra rõ ràng vì logic bên trong và các điều kiện trước của người gọi đã đảm bảo rằng các tham số là hợp lệ.

Là một biện pháp an toàn chống lại lỗi lập trình (lỗi), bạn có thể kiểm tra xem các tham số được truyền vào một chức năng có thực sự đáp ứng các điều kiện trước đã nêu hay không. Vì các thử nghiệm này có thể tốn kém, nên có thể tắt chúng đi để xây dựng bản phát hành. Nếu các thử nghiệm này thất bại, thì chương trình sẽ bị chấm dứt, bởi vì nó có thể đã gặp phải một lỗi.

Mặc dù thoạt nhìn, kiểm tra trong người gọi dường như mời sao chép mã, nhưng thực tế nó là cách khác. Việc kiểm tra callee dẫn đến việc sao chép mã và rất nhiều công việc không cần thiết đang được thực hiện.
Chỉ cần nghĩ về nó, tần suất bạn truyền tham số qua một số lớp chức năng, chỉ thực hiện những thay đổi nhỏ đối với một số trong số chúng trên đường đi. Nếu bạn luôn áp dụng phương thức check-in-callee , mỗi chức năng trung gian đó sẽ phải thực hiện lại việc kiểm tra cho từng tham số.
Và bây giờ hãy tưởng tượng rằng một trong những tham số đó được cho là một danh sách được sắp xếp.
Với kiểm tra trong người gọi, chỉ có chức năng đầu tiên sẽ phải đảm bảo rằng danh sách đó thực sự được sắp xếp. Tất cả những người khác biết danh sách đã được sắp xếp (vì đó là những gì họ đã nêu trong điều kiện trước) và có thể vượt qua nó mà không cần kiểm tra thêm.


+1 Cảm ơn bạn đã trả lời. Phản ánh tuyệt vời: "Việc kiểm tra callee dẫn đến việc sao chép mã và rất nhiều công việc không cần thiết đang được thực hiện". Và trong câu: "Trong hầu hết các trường hợp, không cần kiểm tra rõ ràng vì logic bên trong và các điều kiện trước của người gọi đã đảm bảo" - bạn có ý gì với biểu thức "logic bên trong"? Chức năng DBC?
srnka

@srnka: Với "logic bên trong", ý tôi là các tính toán và quyết định trong một hàm. Nó thực chất là việc thực hiện các chức năng.
Bart van Ingen Schenau 23/2/13

0

Thông thường bạn không thể biết ai, khi nào và như thế nào sẽ gọi hàm bạn đã viết. Tốt nhất là giả định điều tồi tệ nhất: chức năng của bạn sẽ được gọi với các tham số không hợp lệ. Vì vậy, bạn chắc chắn nên bao gồm điều đó.

Tuy nhiên, nếu ngôn ngữ bạn sử dụng hỗ trợ các ngoại lệ, bạn có thể không kiểm tra một số lỗi nhất định và chắc chắn rằng một ngoại lệ sẽ bị ném, nhưng trong trường hợp này, bạn phải chắc chắn mô tả trường hợp trong tài liệu (bạn cần phải có tài liệu). Ngoại lệ sẽ cung cấp cho người gọi đủ thông tin về những gì đã xảy ra và cũng sẽ thu hút sự chú ý đến các đối số không hợp lệ.


Trên thực tế, có thể tốt hơn để xác thực tham số và, nếu tham số không hợp lệ, hãy tự ném một ngoại lệ. Đây là lý do: những chú hề gọi thói quen của bạn mà không bận tâm để chắc chắn rằng họ đã cung cấp cho nó dữ liệu hợp lệ là những người không bận tâm kiểm tra mã trả về lỗi cho biết họ đã truyền dữ liệu không hợp lệ. Ném một ngoại lệ FORCES vấn đề cần được khắc phục.
John R. Strohm
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.