TDD thích cách tiếp cận các vấn đề về thuật toán


10

Tôi đã thất bại trong một bài kiểm tra thuật toán với Codility vì tôi đã cố gắng tìm một giải pháp tốt hơn, và cuối cùng tôi không có gì.

Vì vậy, nó làm tôi suy nghĩ nếu tôi có thể sử dụng một cách tiếp cận tương tự như TDD? Tức là nếu tôi thường có thể phát triển một giải pháp dần dần theo cách tương tự?

Nếu tôi đang viết một thuật toán sắp xếp, tôi có thể chuyển từ Bubbledort tiêu chuẩn sang bong bóng 2 chiều, nhưng sau đó một thứ tiên tiến hơn như Quicksort sẽ là một "bước nhảy lượng tử", nhưng ít nhất tôi sẽ có thể kiểm chứng dễ dàng.

Lời khuyên khác cho các bài kiểm tra như vậy? Một điều tôi sẽ làm trong lần tới là sử dụng nhiều phương thức / hàm hơn các vòng lặp bên trong. Ví dụ, trong sắp xếp, bạn thường cần một trao đổi. Nếu đó là một phương thức tôi chỉ cần sửa đổi mã gọi. Tôi thậm chí có thể có các giải pháp nâng cao hơn như các lớp dẫn xuất.

Với các vấn đề "Thuật toán" so với "bình thường", ý tôi là các vấn đề trong đó Độ phức tạp thời gian là quan trọng. Vì vậy, thay vì vượt qua nhiều bài kiểm tra như trong TDD, bạn sẽ làm cho nó "cư xử tốt hơn".

Với "tương tự như TDD", ý tôi là:

  1. Viết bài kiểm tra tương đối tự động để tiết kiệm thời gian cho bài kiểm tra pr tăng dần.
  2. Phát triển gia tăng.
  3. Kiểm tra hồi quy, khả năng phát hiện nếu mã bị hỏng hoặc ít nhất là nếu chức năng thay đổi giữa các mức tăng.

Tôi nghĩ rằng điều này nên dễ hiểu nếu bạn so sánh

  1. Viết shell-sort trực tiếp
  2. Nhảy từ bubbleort sang quicksort (Viết lại toàn bộ)
  3. Di chuyển dần dần từ sắp xếp bong bóng một chiều sang sắp xếp vỏ (Nếu có thể).

Bạn có ý nghĩa gì bởi "tương tự TDD"? Rõ ràng bạn có thể cố gắng sử dụng TDD để phát triển hàm sắp xếp, và sau đó sử dụng các bài kiểm tra đơn vị để xác thực hàm vẫn hoạt động khi bạn thay thế thuật toán sắp xếp bằng một thuật toán hiệu quả hơn, nhưng có vẻ như bạn có một câu hỏi khác?
Doc Brown

"dần dần" :-) - Xem câu cuối được thêm vào "Vì vậy, thay vào đó ..."
Olav

2
Chắc chắn bạn có thể cố gắng giải quyết nhiều vấn đề với giải pháp làm việc (nhưng không hiệu quả lắm) trước, sau đó cải thiện nó. Điều này không có cách nào bị hạn chế đối với các vấn đề về thuật toán hoặc lập trình và nó không có nhiều điểm chung với TDD. Điều này có trả lời câu hỏi của bạn không?
Doc Brown

@DocBrown Không - Xem ví dụ Bubbledort / Quicksort. TDD "hoạt động" tốt vì cách tiếp cận gia tăng hoạt động tốt cho nhiều loại vấn đề. Các vấn đề algoritmic có thể khác nhau.
Olav

Vì vậy, bạn có nghĩa là "có thể giải quyết các câu hỏi thiết kế thuật toán theo cách tăng dần" (giống như TDD là một cách tiếp cận gia tăng), chứ không phải "bằng TDD", phải không? Vui lòng làm rõ.
Doc Brown

Câu trả lời:


9

Xem thêm nỗ lực của Ron Jeffries để tạo ra bộ giải Sudoku với TDD , rất tiếc là không hoạt động.

Thuật toán đòi hỏi một sự hiểu biết đáng kể về các nguyên tắc thiết kế thuật toán. Với những nguyên tắc này, thực sự có thể tiến hành tăng dần, với một kế hoạch, giống như Peter Norvig đã làm .

Trong thực tế, đối với các thuật toán đòi hỏi nỗ lực thiết kế không tầm thường, hầu như luôn luôn là nỗ lực đó tăng dần trong tự nhiên. Nhưng mỗi "gia tăng", nhỏ bé trong mắt một nhà thiết kế thuật toán, trông giống như một bước nhảy lượng tử (để mượn cụm từ của bạn) cho một người không có chuyên môn hoặc kiến ​​thức tương tự với họ thuật toán cụ thể này.

Đây là lý do tại sao một nền giáo dục cơ bản trong lý thuyết CS kết hợp với nhiều thực hành lập trình thuật toán cũng quan trọng không kém. Biết rằng một "kỹ thuật" cụ thể (các khối thuật toán xây dựng nhỏ) tồn tại là một chặng đường dài hướng tới việc thực hiện những bước nhảy lượng tử gia tăng này.


Tuy nhiên, có một số khác biệt quan trọng giữa tiến bộ gia tăng trong thuật toán và TDD.

Một trong những khác biệt đã được JeffO đề cập : Một thử nghiệm xác minh tính chính xác của dữ liệu đầu ra tách biệt với thử nghiệm khẳng định hiệu năng giữa việc thực hiện khác nhau của cùng một thuật toán (hoặc các thuật toán khác nhau đưa ra cùng một giải pháp).

Trong TDD, người ta thêm một yêu cầu mới dưới dạng thử nghiệm và thử nghiệm này ban đầu sẽ không vượt qua (màu đỏ). Sau đó, yêu cầu được thỏa mãn (màu xanh lá cây). Cuối cùng mã được tái cấu trúc.

Trong phát triển thuật toán, yêu cầu thường không thay đổi. Kiểm tra xác minh tính chính xác của kết quả được viết trước hoặc ngay sau khi hoàn thành dự thảo (rất tự tin nhưng chậm) của thuật toán. Kiểm tra tính chính xác dữ liệu này hiếm khi thay đổi; người ta không thay đổi nó thành thất bại (màu đỏ) như là một phần của nghi thức TDD.

Tuy nhiên, trong khía cạnh này, phân tích dữ liệu khác biệt rõ rệt với phát triển thuật toán, bởi vì các yêu cầu phân tích dữ liệu (cả bộ đầu vào và kết quả mong đợi) chỉ được định nghĩa một cách lỏng lẻo theo cách hiểu của con người. Do đó, các yêu cầu thay đổi thường xuyên ở cấp độ kỹ thuật. Sự thay đổi nhanh chóng này đặt phân tích dữ liệu ở đâu đó giữa phát triển thuật toán và phát triển ứng dụng phần mềm nói chung - trong khi vẫn nặng về thuật toán, các yêu cầu cũng thay đổi "quá nhanh" theo sở thích của bất kỳ lập trình viên nào.

Nếu yêu cầu thay đổi, nó thường yêu cầu một thuật toán khác.

Trong phát triển thuật toán, việc thay đổi (thắt chặt) kiểm tra so sánh hiệu suất thành thất bại (màu đỏ) là ngớ ngẩn - nó không cung cấp cho bạn cái nhìn sâu sắc về các thay đổi tiềm năng đối với thuật toán của bạn sẽ cải thiện hiệu suất.

Do đó, trong phát triển thuật toán, cả kiểm tra tính chính xác và kiểm tra hiệu năng đều không phải là kiểm tra TDD. Thay vào đó, cả hai đều là bài kiểm tra hồi quy . Cụ thể, kiểm tra hồi quy chính xác ngăn bạn thực hiện các thay đổi đối với thuật toán sẽ phá vỡ tính chính xác của nó; kiểm tra hiệu năng ngăn bạn thực hiện các thay đổi đối với thuật toán sẽ làm cho nó chạy chậm hơn.

Bạn vẫn có thể kết hợp TDD như một phong cách làm việc cá nhân, ngoại trừ nghi thức "đỏ - xanh - tái cấu trúc" không thực sự cần thiết và cũng không đặc biệt có lợi cho quá trình phát triển thuật toán.

Tôi sẽ lập luận rằng các cải tiến thuật toán thực sự là kết quả của việc tạo ra các hoán vị ngẫu nhiên (không cần thiết chính xác) cho các sơ đồ luồng dữ liệu của thuật toán hiện tại, hoặc trộn và kết hợp chúng giữa các triển khai đã biết trước đó.


TDD được sử dụng khi có nhiều yêu cầu có thể được thêm dần vào bộ kiểm tra của bạn.

Ngoài ra, nếu thuật toán của bạn dựa trên dữ liệu, mỗi phần dữ liệu thử nghiệm / trường hợp thử nghiệm có thể được thêm dần dần. TDD cũng sẽ hữu ích. Vì lý do này, cách tiếp cận "giống như TDD" của "thêm dữ liệu thử nghiệm mới - cải thiện mã để làm cho nó xử lý dữ liệu này một cách chính xác - tái cấu trúc" cũng sẽ hoạt động cho công việc phân tích dữ liệu kết thúc mở, trong đó các mục tiêu của thuật toán được mô tả bằng con người từ ngữ lập dị và thước đo thành công của nó cũng được đánh giá theo thuật ngữ xác định của con người.

Nó cố gắng dạy một cách để làm cho nó ít áp đảo hơn là cố gắng đáp ứng tất cả (hàng chục hoặc hàng trăm) yêu cầu trong một nỗ lực duy nhất. Nói cách khác, TDD được kích hoạt khi bạn có thể ra lệnh rằng các yêu cầu nhất định hoặc mục tiêu kéo dài có thể tạm thời bị bỏ qua trong khi bạn đang thực hiện một số dự thảo ban đầu về giải pháp của mình.

TDD không thay thế cho khoa học máy tính. Đó là một cái nạng tâm lý giúp các lập trình viên vượt qua cú sốc khi phải đáp ứng nhiều yêu cầu cùng một lúc.

Nhưng nếu bạn đã có một triển khai cho kết quả chính xác, TDD sẽ xem xét mục tiêu đã hoàn thành và mã sẵn sàng được đưa ra (để tái cấu trúc hoặc cho người dùng lập trình viên khác). Theo một cách nào đó, nó khuyến khích bạn không tối ưu hóa sớm mã của mình, bằng cách khách quan đưa ra tín hiệu rằng mã đó "đủ tốt" (để vượt qua tất cả các bài kiểm tra chính xác).


Trong TDD, cũng tập trung vào "các yêu cầu vi mô" (hoặc các phẩm chất tiềm ẩn). Ví dụ: xác nhận tham số, xác nhận, ném và xử lý ngoại lệ, v.v. TDD giúp đảm bảo tính chính xác của các đường dẫn thực thi thường không được thực hiện trong quá trình thực thi phần mềm thông thường.

Một số loại mã thuật toán cũng chứa những điều này; những điều này có thể tuân theo TDD. Nhưng vì quy trình công việc chung của thuật toán không phải là TDD, nên các thử nghiệm như vậy (về xác nhận tham số, xác nhận và ném và xử lý ngoại lệ) có xu hướng được viết sau khi mã thực thi đã được viết (ít nhất là một phần).


Hai từ trích dẫn đầu tiên trong bài viết của bạn ("Chú Bob") nghĩa là gì?
Robert Harvey

@RobertHarvey Theo chú Bob, TDD có thể được sử dụng để khám phá thuật toán. Theo một ngôi sao sáng khác, nó không hoạt động. Tôi nghĩ rằng cả hai nên được đề cập (tức là bất cứ khi nào ai đó đề cập đến một ví dụ, một người cũng có nghĩa vụ phải đề cập đến ví dụ khác) để mọi người có được thông tin cân bằng về các ví dụ tích cực và tiêu cực.
rwong

ĐỒNG Ý. Nhưng bạn có hiểu sự nhầm lẫn của tôi? Đoạn đầu tiên của bạn dường như trích dẫn ai đó thốt ra dòng chữ "Chú Bob." Ai đang nói vậy?
Robert Harvey

@RobertHarvey đặt hàng tuân thủ.
rwong

2

Đối với vấn đề của bạn, bạn sẽ có hai bài kiểm tra:

  1. Một thử nghiệm để đảm bảo thuật toán vẫn chính xác. vd: Nó được sắp xếp chính xác?
  2. Kiểm tra so sánh hiệu suất - có hiệu suất được cải thiện. Điều này có thể gặp khó khăn, vì vậy nó giúp chạy thử nghiệm trên cùng một máy, cùng dữ liệu và hạn chế sử dụng bất kỳ tài nguyên nào khác. Một máy chuyên dụng giúp.

Những gì cần kiểm tra hoặc phạm vi kiểm tra đầy đủ là điều gây tranh cãi, nhưng tôi nghĩ rằng các phần phức tạp trong ứng dụng của bạn cần được tinh chỉnh (được thay đổi nhiều) là những ứng cử viên hoàn hảo để kiểm tra tự động. Những phần của ứng dụng thường được xác định rất sớm. Sử dụng một cách tiếp cận TDD với phần này sẽ có ý nghĩa.

Có thể giải quyết các vấn đề phức tạp không nên bị cản trở bởi sự miễn cưỡng thử các phương pháp khác nhau. Có một bài kiểm tra tự động sẽ giúp trong lĩnh vực này. Ít nhất bạn sẽ biết bạn không làm cho nó tồi tệ hơn.


1

Vì vậy, thay vì vượt qua nhiều bài kiểm tra như trong TDD, bạn sẽ làm cho nó "cư xử tốt hơn".

Sắp xếp

Bạn có thể kiểm tra thời gian chạy và độ phức tạp. Kiểm tra thời gian chạy sẽ cần phải tha thứ một chút để cho phép tranh chấp về tài nguyên hệ thống. Nhưng, trong nhiều ngôn ngữ, bạn cũng có thể ghi đè hoặc tiêm phương thức và đếm các cuộc gọi chức năng thực tế. Và, nếu bạn tự tin thuật toán hiện tại của bạn là tối ưu, bạn có thể giới thiệu một bài kiểm tra mà chỉ đơn giản đòi hỏi sort()invoke compare()lần ít hơn việc thực hiện ngây thơ.

Hoặc, bạn có thể so sánh sort()trên hai bộ và đảm bảo chúng chia tỷ lệ trong compare()các cuộc gọi theo độ phức tạp mục tiêu của bạn (hoặc ở đó, nếu bạn mong đợi một số điểm không nhất quán).

Và, nếu về mặt lý thuyết bạn có thể chứng minh rằng một tập hợp kích thước Nkhông yêu cầu nghiêm ngặt không quá N*log(N)so sánh, thì thể hợp lý để hạn chế việc bạn đã làm việc sort()với các N*log(N)yêu cầu compare()...

Tuy nhiên ...

Đáp ứng yêu cầu về hiệu năng hoặc độ phức tạp không đảm bảo rằng việc triển khai cơ bản là {Al ThuậtmX} . Và, tôi cho rằng điều này là bình thường. Không quan trọng thuật toán nào được sử dụng, miễn là bạn đạt được yêu cầu của mình, bao gồm mọi yêu cầu về độ phức tạp, hiệu suất hoặc tài nguyên quan trọng.

Nhưng, nếu bạn muốn đảm bảo sử dụng một thuật toán cụ thể, bạn sẽ cần phải tẻ nhạt hơn và có chiều sâu hơn. Những thứ như..

  • Đảm bảo rằng chính xác số lượng cuộc gọi dự kiến ​​đến compare()swap()(hoặc bất cứ điều gì) được thực hiện
  • Bao bọc tất cả các chức năng / phương thức chính và đảm bảo, với một tập dữ liệu có thể dự đoán được, rằng các cuộc gọi xảy ra theo đúng thứ tự dự kiến
  • Kiểm tra trạng thái làm việc sau mỗi N bước để đảm bảo nó thay đổi chính xác như mong đợi

Nhưng một lần nữa, nếu bạn đang cố gắng đảm bảo rằng {Al ThuậtmX} được sử dụng cụ thể, có thể có các tính năng của {Al ThuậtmX} mà bạn quan tâm là điều đó quan trọng hơn để kiểm tra xem cuối cùng {Al ThuậtmX} có thực sự được sử dụng không ...


Cũng lưu ý, TDD không phủ nhận sự cần thiết phải nghiên cứu, suy nghĩ hoặc lập kế hoạch phát triển phần mềm. Nó cũng không phủ nhận sự cần thiết phải động não và thử nghiệm, ngay cả khi bạn không thể dễ dàng khẳng định về Google và bảng trắng trong bộ thử nghiệm tự động của bạn .


Điểm tuyệt vời liên quan đến khả năng khẳng định giới hạn về số lượng hoạt động. Tôi sẽ lập luận rằng đó là sức mạnh của (giả, sơ khai và gián điệp), thứ gì đó có thể được sử dụng trong TDD, cũng có thể được sử dụng trong các loại thử nghiệm khác.
rwong

Tôi không thấy nhiều điểm khi viết bài kiểm tra trước khi viết mã trong kịch bản kiểm tra. Nó thường là một tiêu chí cuối cùng sau khi các tiêu chí chức năng được đáp ứng. Đôi khi bạn có thể thực hiện các số ngẫu nhiên trước và trường hợp xấu nhất sau đó, nhưng việc kiểm tra viết vẫn sẽ mất nhiều thời gian so với một số bản in thông minh. (Với một số thống kê, bạn có thể có thể viết một số mã chung, nhưng không phải trong khi thử nghiệm) Trong thế giới thực tôi nghĩ bạn sẽ muốn biết liệu hiệu suất có đột ngột giảm trong một số trường hợp nhất định không.
Olav

Nếu bạn nhìn vào codility.com/programmers/task/stone_wall, bạn sẽ biết nếu bạn có nhiều hơn N độ phức tạp, ngoại trừ các trường hợp đặc biệt mà bạn phải làm việc trong khoảng thời gian rất dài chẳng hạn.
Olav

@Olav "bài kiểm tra viết sẽ mất nhiều thời gian so với một số bản in thông minh" ... để thực hiện một lần ... uhh .. có thể , nhưng cũng rất gây tranh cãi. Để làm lặp đi lặp lại ở mỗi bản dựng? ... Chắc chắn không phải.
Svidgen

@Olav "Trong thế giới thực, tôi nghĩ bạn sẽ muốn biết nếu hiệu suất đột ngột giảm trong một số trường hợp nhất định." Trong các dịch vụ trực tiếp, bạn sẽ sử dụng một số như Relic mới để theo dõi hiệu suất tổng thể - không chỉ là một số phương pháp nhất định. Và lý tưởng, các bài kiểm tra của bạn sẽ cho bạn biết khi các mô-đun và phương thức quan trọng về hiệu suất không đáp ứng được kỳ vọng trước khi bạn triển khai.
Svidgen
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.