Là kiểm tra điều kiện dư thừa chống lại thực tiễn tốt nhất?


16

Tôi đã phát triển phần mềm trong ba năm qua, nhưng gần đây tôi mới thức dậy rằng tôi không biết gì về các thực hành tốt. Điều này đã khiến tôi bắt đầu đọc cuốn sách Clean Code , điều này giúp cuộc sống của tôi trở nên tốt hơn, nhưng tôi đang cố gắng tìm hiểu sâu về một số cách tiếp cận tốt nhất để viết chương trình của mình.

Tôi có một chương trình Python trong đó tôi ...

  1. sử dụng argparse required=Trueđể thực thi hai đối số, cả hai đều là tên tệp. đầu tiên là tên tệp đầu vào, thứ hai là tên tệp đầu ra
  2. có chức năng readFromInputFilekiểm tra đầu tiên để xem tên tệp đầu vào đã được nhập
  3. có chức năng writeToOutputFilekiểm tra đầu tiên để xem tên tệp đầu ra đã được nhập

Chương trình của tôi đủ nhỏ để tôi tin rằng việc kiểm tra # 2 và # 3 là dư thừa và cần được loại bỏ, do đó giải phóng cả hai chức năng khỏi một ifđiều kiện không cần thiết . Tuy nhiên, tôi cũng đã được tin rằng "kiểm tra hai lần là ổn" và có thể là giải pháp phù hợp trong một chương trình nơi các hàm có thể được gọi từ một vị trí khác trong đó không xảy ra phân tích cú pháp đối số.

(Ngoài ra, nếu đọc hoặc ghi thất bại, tôi có một try exceptchức năng trong mỗi chức năng để đưa ra một thông báo lỗi thích hợp.)

Câu hỏi của tôi là: tốt nhất là tránh tất cả các kiểm tra điều kiện dư thừa? Logic của một chương trình có nên vững chắc đến mức chỉ cần thực hiện kiểm tra một lần không? Có bất kỳ ví dụ tốt minh họa điều này hoặc ngược lại?

EDIT: Cảm ơn tất cả các câu trả lời! Tôi đã học được điều gì đó từ mỗi. Nhìn thấy rất nhiều quan điểm cho tôi hiểu rõ hơn nhiều về cách tiếp cận vấn đề này và xác định một giải pháp dựa trên yêu cầu của tôi. Cảm ơn bạn!


Đây là một phiên bản tổng quát của câu hỏi của bạn: softwareengineering.stackexchange.com/questions/19549/ . Tôi sẽ không nói nó là bản sao vì nó có trọng tâm khá lớn, nhưng có lẽ nó giúp.
Doc Brown

Câu trả lời:


15

Những gì bạn đang yêu cầu được gọi là "sự mạnh mẽ", và không có câu trả lời đúng hay sai. Nó phụ thuộc vào quy mô và độ phức tạp của chương trình, số lượng người làm việc trong đó và tầm quan trọng của việc phát hiện các thất bại.

Trong các chương trình nhỏ bạn viết một mình và chỉ cho bản thân bạn, sự mạnh mẽ thường là mối quan tâm nhỏ hơn nhiều so với khi bạn sẽ viết một chương trình phức tạp bao gồm nhiều thành phần, có thể được viết bởi một nhóm. Trong các hệ thống như vậy, có các ranh giới giữa các thành phần dưới dạng API công khai và tại mỗi ranh giới, thường nên xác thực các tham số đầu vào, ngay cả khi "logic của chương trình phải chắc chắn đến mức các kiểm tra đó là dự phòng ". Điều đó làm cho việc phát hiện lỗi khá dễ dàng và giúp cho thời gian sửa lỗi nhỏ hơn.

Trong trường hợp của bạn, bạn phải tự quyết định, loại vòng đời bạn mong đợi cho chương trình của bạn. Đây có phải là một chương trình bạn mong đợi sẽ được sử dụng và duy trì qua nhiều năm? Sau đó, thêm một kiểm tra dự phòng có thể tốt hơn, vì sẽ không chắc là mã của bạn sẽ được tái cấu trúc trong tương lai readwritechức năng của bạn và có thể được sử dụng trong một ngữ cảnh khác.

Hay đó là một chương trình nhỏ chỉ dành cho mục đích học tập hoặc vui chơi? Sau đó, những kiểm tra kép sẽ không cần thiết.

Trong ngữ cảnh của "Mã sạch", người ta có thể hỏi liệu kiểm tra kép có vi phạm nguyên tắc DRY không. Trên thực tế, đôi khi, ít nhất là ở một mức độ nhỏ nào đó: xác nhận đầu vào có thể được hiểu là một phần của logic kinh doanh của chương trình và việc này xảy ra ở hai nơi có thể dẫn đến các vấn đề bảo trì thông thường do vi phạm DRY. Mạnh mẽ so với DRY thường là một sự đánh đổi - sự mạnh mẽ đòi hỏi sự dư thừa trong mã, trong khi DRY cố gắng giảm thiểu sự dư thừa. Và với sự phức tạp của chương trình ngày càng tăng, sự mạnh mẽ ngày càng trở nên quan trọng hơn là DRY trong xác nhận.

Cuối cùng, hãy để tôi đưa ra một ví dụ có nghĩa là gì trong trường hợp của bạn. Giả sử yêu cầu của bạn thay đổi thành một cái gì đó như

  • chương trình cũng sẽ làm việc với một đối số, tên tệp đầu vào, nếu không có tên tệp đầu ra, nó được tự động xây dựng từ tên tệp đầu vào bằng cách thay thế hậu tố.

Điều đó có khả năng bạn cần thay đổi xác nhận kép của mình ở hai nơi không? Có lẽ là không, yêu cầu như vậy dẫn đến một thay đổi khi gọi argparse, nhưng không có thay đổi nào writeToOutputFile: chức năng đó vẫn sẽ yêu cầu tên tệp. Vì vậy, trong trường hợp của bạn, tôi sẽ bỏ phiếu để thực hiện xác nhận đầu vào hai lần, nguy cơ gặp sự cố bảo trì do có hai nơi cần thay đổi là IMHO thấp hơn nhiều so với rủi ro gặp sự cố bảo trì do lỗi che giấu do quá ít kiểm tra.


"... liên kết giữa các thành phần dưới dạng API công cộng ..." Tôi quan sát rằng "các lớp nhảy ranh giới" có thể nói như vậy. Vì vậy, những gì cần thiết là một lớp học; một lớp miền kinh doanh mạch lạc. Tôi đang suy luận từ OP này rằng nguyên tắc phổ biến của "nó đơn giản nên không cần một lớp học" đang hoạt động ở đây. Có thể có một lớp đơn giản bao bọc "đối tượng chính", thực thi các quy tắc kinh doanh như "một tệp phải có tên", nó không chỉ DRY lên mã hiện tại mà còn giữ DRY trong tương lai.
radarbob

@radarbob: những gì tôi đã viết không bị giới hạn ở OOP hoặc các thành phần dưới dạng các lớp. Điều này cũng áp dụng cho các thư viện tùy ý có API công khai, hướng đối tượng hoặc không.
Doc Brown

5

Sự dư thừa không phải là tội lỗi. Không cần dự phòng là.

  1. Nếu readFromInputFile()writeToOutputFile()là một hàm công khai (và theo quy ước đặt tên Python thì chúng là do tên của chúng không bắt đầu bằng hai dấu gạch dưới), nên một ngày nào đó các hàm có thể được sử dụng bởi một người tránh sử dụng. Điều đó có nghĩa là khi họ bỏ qua các đối số, họ sẽ không thấy thông báo lỗi đối số tùy chỉnh của bạn.

  2. Nếu readFromInputFile()và tự writeToOutputFile()kiểm tra các tham số, bạn lại nhận được thông báo lỗi tùy chỉnh giải thích sự cần thiết của tên tệp.

  3. Nếu readFromInputFile()writeToOutputFile()không tự kiểm tra các tham số, không có thông báo lỗi tùy chỉnh nào được hiển thị. Người dùng sẽ phải tự tìm ra ngoại lệ kết quả.

Tất cả giảm xuống còn 3. Viết một số mã thực sự sử dụng các hàm này để tránh argparse và tạo ra thông báo lỗi. Hãy tưởng tượng bạn hoàn toàn không nhìn vào các chức năng này và chỉ tin tưởng vào tên của chúng để cung cấp đủ sự hiểu biết để sử dụng. Khi đó là tất cả những gì bạn biết là có cách nào để bị nhầm lẫn bởi ngoại lệ? Có cần một thông báo lỗi tùy chỉnh?

Tắt một phần bộ não của bạn mà nhớ bên trong các chức năng đó là khó khăn. Nhiều đến mức một số người khuyên nên viết mã sử dụng trước khi mã được sử dụng. Bằng cách đó bạn đi đến vấn đề đã biết mọi thứ trông như thế nào từ bên ngoài. Bạn không cần phải làm TDD để làm điều đó nhưng nếu bạn làm TDD, bạn sẽ đến từ bên ngoài trước.


4

Mức độ mà bạn làm cho phương pháp của mình độc lậpcó thể sử dụng lại là một điều tốt. Điều đó có nghĩa là các phương thức nên được tha thứ trong những gì họ chấp nhận và họ nên có đầu ra được xác định rõ (chính xác trong những gì họ trả lại). Điều đó cũng có nghĩa là họ sẽ có thể xử lý một cách duyên dáng mọi thứ được truyền cho họ và không đưa ra bất kỳ giả định nào về bản chất của đầu vào, chất lượng, thời gian, v.v.

Nếu một lập trình viên có thói quen viết các phương pháp đưa ra các giả định về những gì được truyền vào, dựa trên các ý tưởng như "nếu điều này bị hỏng, chúng ta có những điều lớn hơn để lo lắng" hoặc "tham số X không thể có giá trị Y vì phần còn lại của mã ngăn chặn nó ", sau đó đột nhiên bạn không thực sự có các thành phần tách rời, độc lập nữa. Các thành phần của bạn về cơ bản phụ thuộc vào hệ thống rộng hơn. Đó là một loại khớp nối chặt chẽ tinh tế và dẫn đến tổng chi phí sở hữu tăng theo cấp số nhân khi độ phức tạp của hệ thống tăng lên.

Lưu ý rằng điều này có thể có nghĩa là bạn xác nhận cùng một thông tin nhiều lần. Nhưng điều này là ổn. Mỗi thành phần chịu trách nhiệm xác nhận riêng theo cách riêng của nó . Đây không phải là vi phạm DRY, bởi vì các xác nhận là do các thành phần độc lập được ghép nối và thay đổi xác nhận trong một không nhất thiết phải được sao chép chính xác trong các thành phần khác. Không có sự dư thừa ở đây. X có trách nhiệm kiểm tra đầu vào của nó cho nhu cầu của chính mình và chuyển một số cho Y. Y có trách nhiệm kiểm tra đầu vào của chính nó cho nhu cầu của mình .


1

Giả sử bạn có một hàm (tính bằng C)

void readInputFile (const char* path);

Và bạn không thể tìm thấy bất kỳ tài liệu nào về đường dẫn. Và sau đó bạn nhìn vào việc thực hiện và nó nói

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Điều này không chỉ kiểm tra đầu vào của hàm mà còn cho người dùng biết chức năng rằng đường dẫn không được phép là NULL hoặc một chuỗi rỗng.


0

Nói chung, kiểm tra hai lần không phải lúc nào cũng tốt hay xấu. Luôn có nhiều khía cạnh của câu hỏi trong trường hợp cụ thể của bạn mà vấn đề phụ thuộc vào. Trong trường hợp của bạn:

  • Chương trình này lớn như thế nào? Nó càng nhỏ thì càng rõ ràng rằng người gọi làm đúng. Khi chương trình của bạn phát triển lớn hơn, điều quan trọng hơn là chỉ định chính xác các điều kiện tiên quyết và hậu điều kiện của mỗi thói quen là gì.
  • các đối số đã được kiểm tra bởi các argparsemô-đun. Việc sử dụng một thư viện và sau đó tự mình thực hiện công việc là một ý tưởng tồi. Tại sao lại sử dụng thư viện?
  • Có khả năng phương thức của bạn sẽ được sử dụng lại trong bối cảnh mà người gọi không kiểm tra các đối số? Càng có nhiều khả năng, nó càng quan trọng để xác nhận các đối số.
  • Chuyện gì xảy ra nếu một cuộc tranh cãi không đi mất tích? Không tìm thấy một tập tin đầu vào có thể sẽ ngừng xử lý hoàn toàn. Đó có lẽ là một chế độ thất bại rõ ràng rất dễ khắc phục. Các loại lỗi ngấm ngầm là những lỗi mà chương trình tiếp tục hoạt động và tạo ra kết quả sai mà bạn không nhận thấy .

0

Kiểm tra hai lần của bạn dường như ở những nơi mà chúng hiếm khi được sử dụng. Vì vậy, những kiểm tra này chỉ đơn giản là làm cho chương trình của bạn mạnh mẽ hơn:

Một kiểm tra quá nhiều sẽ không bị tổn thương, một quá ít có thể.

Tuy nhiên, nếu bạn đang kiểm tra bên trong một vòng lặp được lặp đi lặp lại thường xuyên, bạn nên suy nghĩ về việc loại bỏ sự dư thừa, ngay cả khi bản thân việc kiểm tra hầu như không tốn kém so với những gì diễn ra sau kiểm tra.


Và vì bạn đã có nó rồi, nó không đáng để bỏ công sức, trừ khi nó ở trong một vòng lặp hoặc một cái gì đó.
StarWeaver

0

Có lẽ bạn có thể thay đổi quan điểm của mình:

Nếu có gì sai, kết quả là gì? Nó sẽ làm hại đến ứng dụng của bạn / người dùng?

Tất nhiên bạn luôn có thể tranh luận, dù kiểm tra nhiều hay ít thì tốt hơn hay tệ hơn nhưng đó là một câu hỏi khá kinh viện. Và vì bạn đang làm việc với phần mềm trong thế giới thực , có những hậu quả trong thế giới thực.

Từ bối cảnh bạn đang đưa ra:

  • một tập tin đầu vào A
  • một tập tin đầu ra B

Tôi giả sử bạn đang làm chuyển đổi từ A đến B . Nếu AB nhỏ và biến đổi nhỏ, hậu quả là gì?

1) Bạn quên chỉ định nơi đọc từ: Kết quả là không có gì . Và thời gian thực hiện sẽ ngắn hơn dự kiến. Bạn nhìn vào kết quả - hoặc tốt hơn: tìm kiếm một kết quả bị thiếu, thấy rằng bạn đã gọi lệnh sai cách, bắt đầu lại và tất cả đều ổn

2) Bạn quên chỉ định tệp đầu ra. Điều này dẫn đến các kịch bản khác nhau:

a) Đầu vào được đọc cùng một lúc. Hơn việc chuyển đổi bắt đầu và kết quả sẽ được viết, nhưng thay vào đó bạn nhận được một lỗi. Tùy thuộc vào thời gian, người dùng của bạn phải chờ (phụ thuộc vào khối lượng dữ liệu được xử lý) điều này có thể gây khó chịu.

b) Đầu vào được đọc từng bước. Sau đó, quá trình viết ngay lập tức thoát ra như trong (1) và người dùng bắt đầu lại.

Kiểm tra cẩu thả có thể được coi là OK trong một số trường hợp. Nó hoàn toàn phụ thuộc vào usecase của bạn và ý định của bạn là gì.

Ngoài ra: Bạn nên tránh hoang tưởng và không thực hiện quá nhiều phép nhân đôi.


0

Tôi sẽ lập luận rằng các bài kiểm tra không dư thừa.

  • Bạn có hai hàm công khai yêu cầu tên tệp làm tham số đầu vào. Nó là thích hợp để xác nhận các tham số của họ. Các chức năng có khả năng có thể được sử dụng trong bất kỳ chương trình nào cần chức năng của chúng.
  • Bạn có một chương trình yêu cầu hai đối số phải là tên tệp. Nó xảy ra để sử dụng các chức năng. Nó là thích hợp cho chương trình để kiểm tra các thông số của nó.

Trong khi tên tệp đang được kiểm tra hai lần, chúng đang được kiểm tra cho các mục đích khác nhau. Trong một chương trình nhỏ nơi bạn có thể tin tưởng các tham số cho các chức năng đã được xác minh, việc kiểm tra các chức năng có thể được coi là dự phòng.

Một giải pháp mạnh mẽ hơn sẽ có một hoặc hai trình xác nhận tên tệp.

  • Đối với một tệp đầu vào, bạn có thể muốn xác minh rằng tham số đã chỉ định một tệp có thể đọc được.
  • Đối với tệp đầu ra, bạn có thể muốn xác minh tham số đó là tệp có thể ghi hoặc tên tệp hợp lệ có thể được tạo và ghi vào.

Tôi sử dụng hai quy tắc khi nào thực hiện hành động:

  • Làm chúng càng sớm càng tốt. Điều này hoạt động tốt cho những thứ sẽ luôn luôn được yêu cầu. Từ quan điểm của chương trình này, đây là kiểm tra các giá trị argv và các xác nhận tiếp theo trong logic chương trình sẽ là dự phòng. Nếu các chức năng được chuyển đến một thư viện, thì chúng không còn dư thừa, vì thư viện không thể tin rằng tất cả người gọi đã xác nhận các tham số.
  • Làm chúng càng muộn càng tốt. Điều này hoạt động rất tốt cho những thứ sẽ hiếm khi được yêu cầu. Từ quan điểm của chương trình này, đây là kiểm tra các tham số chức năng.

0

Việc kiểm tra là dư thừa. Khắc phục sự cố này, yêu cầu bạn xóa readFromInputFile và writeToOutputFile và thay thế chúng bằng readFromStream và writeToStream.

Tại điểm mà mã nhận được luồng tệp, bạn biết rằng bạn có một luồng hợp lệ được kết nối với một tệp hợp lệ hoặc bất cứ thứ gì khác mà một luồng có thể được kết nối. Điều này tránh kiểm tra dư thừa.

Sau đó, bạn có thể hỏi, bạn vẫn cần mở luồng ở đâu đó. Có, nhưng điều đó xảy ra trong nội bộ trong phương pháp phân tích đối số. Bạn có hai kiểm tra ở đó, một để kiểm tra xem tên tệp có bắt buộc không, kiểm tra còn lại là tệp được chỉ bởi tên tệp là tệp hợp lệ trong ngữ cảnh đã cho (ví dụ: tệp đầu vào tồn tại, thư mục đầu ra có thể ghi được). Đó là các loại kiểm tra khác nhau, vì vậy chúng không dư thừa và chúng xảy ra trong phương pháp phân tích cú pháp đối số (chu vi ứng dụng) thay vì trong ứng dụng cốt lõi.

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.