Là hoàn toàn phụ thuộc vào các chức năng thuần túy xấu (đặc biệt, để thử nghiệm)?


8

Để mở rộng một chút về tiêu đề, tôi đang cố gắng đưa ra một kết luận nào đó về việc có cần thiết hay không tuyên bố rõ ràng (tức là tiêm) các hàm thuần mà phụ thuộc vào một số chức năng hoặc lớp khác.

Đây có phải là một đoạn mã đã cho ít kiểm tra hơn hoặc được thiết kế tệ hơn nếu nó sử dụng các hàm thuần túy mà không yêu cầu chúng không? Tôi muốn để có được một kết luận về vấn đề này, cho bất kỳ loại chức năng tinh khiết từ đơn giản, chức năng có nguồn gốc (ví dụ max(), min()- bất kể ngôn ngữ) những phong tục, nhiều phức tạp mà lần lượt có thể ngầm phụ thuộc vào chức năng thuần túy khác.

Mối quan tâm thông thường là nếu một số mã chỉ sử dụng trực tiếp một phụ thuộc, bạn sẽ không thể kiểm tra nó một cách riêng lẻ, cụ thể là bạn sẽ kiểm tra cùng lúc tất cả những thứ mà bạn âm thầm mang theo bên mình. Nhưng điều này bổ sung khá nhiều nồi hơi nếu bạn phải thực hiện nó cho mọi chức năng nhỏ, vì vậy tôi tự hỏi liệu điều này có còn giữ cho các chức năng thuần túy không, và tại sao hoặc tại sao không.


2
Tôi không hiểu câu hỏi này. Có vấn đề gì nếu một hàm hoàn toàn cho mục đích DI?
Telastyn

Vì vậy, bạn đề xuất rằng các hàm thuần túy, hàm không tinh khiết và các lớp được tiêm giống nhau, bất kể kích thước của chúng, phụ thuộc bắc cầu hay bất cứ thứ gì. Có lẽ bạn có thể giải thích về điều đó?
DanielM

Không, tôi không hiểu những gì bạn đang hỏi. Các chức năng hiếm khi được đưa vào bất cứ thứ gì, và khi có, người tiêu dùng không thể đảm bảo độ tinh khiết của chúng.
Telastyn

1
Bạn có thể tiêm mọi chức năng, nhưng thường không có giá trị trong việc chế nhạo các chức năng này trong một thử nghiệm có nghĩa là tiêm chúng không có giá trị. Ví dụ, vì các toán tử về cơ bản là các hàm tĩnh, người ta có thể quyết định tiêm toán tử + và giả định nó trong một thử nghiệm. Trừ khi có bất kỳ giá trị nào, bạn sẽ không làm điều đó.
dùng2180613

1
Các nhà khai thác đã vượt qua tâm trí của tôi quá. Nó chắc chắn sẽ bất tiện, nhưng điểm tôi đang cố gắng tìm ra là làm thế nào để đưa ra quyết định tiêm gì và không nên tiêm gì. Nói cách khác, làm thế nào để quyết định khi nào có giá trị và khi nào không. Lấy cảm hứng từ câu trả lời của @ Goyo, tôi nghĩ rằng một tiêu chí tốt là tiêm tất cả mọi thứ không phải là một phần nội tại của hệ thống, để giữ cho hệ thống và các phụ thuộc của nó được kết nối lỏng lẻo; và ngược lại, chỉ sử dụng (do đó, không tiêm) những thứ thực sự là một phần của bản sắc của hệ thống, trong đó hệ thống có tính gắn kết cao.
DanielM

Câu trả lời:


10

Không, nó không tệ

Các bài kiểm tra bạn viết không nên quan tâm đến cách một lớp hoặc chức năng nhất định được thực hiện. Thay vào đó, cần đảm bảo rằng họ tạo ra kết quả mà bạn muốn bất kể chính xác chúng được thực hiện như thế nào.

Ví dụ, xem xét các lớp sau:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

Bạn sẽ muốn kiểm tra chức năng 'Round' để đảm bảo rằng nó trả về những gì bạn mong đợi, bất kể chức năng đó được thực hiện như thế nào. Bạn có thể sẽ viết một bài kiểm tra tương tự như sau;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

Từ quan điểm thử nghiệm, miễn là hàm Tọa độ 2: Vòng () của bạn trả về những gì bạn mong đợi, bạn không quan tâm đến cách thức triển khai.

Trong một số trường hợp, tiêm một hàm thuần túy có thể là một cách thực sự xuất sắc để làm cho một lớp có thể kiểm tra hoặc mở rộng hơn.

Tuy nhiên, trong hầu hết các trường hợp, chẳng hạn như hàm Tọa độ 2: Vòng () ở trên, không có nhu cầu thực sự để tiêm hàm min / max / floor / ceil / trunc. Hàm được thiết kế để làm tròn các giá trị của nó thành số nguyên gần nhất. Các bài kiểm tra mà bạn viết nên kiểm tra xem hàm có thực hiện việc này không.

Cuối cùng, nếu bạn muốn kiểm tra mã mà việc triển khai lớp / hàm phụ thuộc vào, bạn có thể làm như vậy bằng cách viết các bài kiểm tra cho sự phụ thuộc.

Ví dụ: nếu hàm Tọa độ 2: Vòng () được triển khai như vậy ...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

Nếu bạn muốn kiểm tra chức năng 'sàn', bạn có thể làm như vậy trong một thử nghiệm đơn vị riêng biệt.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)

Tất cả mọi thứ bạn nói đều đề cập đến các chức năng thuần túy, phải không? Nếu vậy, nó có gì khác biệt với những lớp hoặc lớp không thuần túy? Ý tôi là, bạn chỉ có thể mã hóa mọi thứ và áp dụng cùng một logic ("mọi thứ tôi phụ thuộc đã được kiểm tra"). Và một câu hỏi khác: bạn có thể mở rộng khi nào sẽ tuyệt vời hơn khi tiêm các hàm thuần túy và tại sao? Cảm ơn bạn!
DanielM

3
@DanielM Hoàn toàn - các thử nghiệm đơn vị nên bao gồm một số đơn vị hành vi có ý nghĩa riêng biệt - nếu tính hợp lệ của hành vi của "đơn vị" phụ thuộc hoàn toàn vào việc trả lại tối đa của một cái gì đó, thì việc trừu tượng hóa "max" không hữu ích và thực sự làm giảm tính hữu dụng của bài kiểm tra, quá. Mặt khác, nếu tôi trả lại "tối đa" của bất cứ thứ gì đến từ một số nhà cung cấp, thì việc khai thác nhà cung cấp là hữu ích, bởi vì những gì nó không phù hợp trực tiếp với hành vi dự định của đơn vị này và có thể xác minh tính chính xác của đơn vị này nơi khác Ghi nhớ - "đơn vị"! = "Lớp."
Ant P

2
Cuối cùng, mặc dù, việc đánh giá quá cao thứ này thường phản tác dụng. Đây là nơi thử nghiệm đầu tiên có xu hướng đi vào chính nó. Xác định hành vi biệt lập mà bạn muốn kiểm tra, sau đó viết bài kiểm tra và quyết định xem nên tiêm gì và không nên tiêm gì cho bạn.
Ant P

4

Được ngầm tùy thuộc vào chức năng thuần túy xấu

Từ quan điểm kiểm tra - Không, nhưng chỉ trong trường hợp các hàm thuần túy , khi hàm trả về luôn cùng một đầu ra cho cùng một đầu vào.

Làm thế nào bạn kiểm tra đơn vị sử dụng chức năng tiêm rõ ràng?
Bạn sẽ tiêm hàm giả (giả) và kiểm tra hàm đó đã được gọi với các đối số chính xác chưa.

Nhưng bởi vì chúng ta có hàm thuần túy , luôn trả về cùng một kết quả cho cùng một đối số - kiểm tra các đối số đầu vào là bằng nhau để kiểm tra kết quả đầu ra.

Với các hàm thuần túy, bạn không cần phải định cấu hình phụ thuộc / trạng thái.

Là lợi ích bổ sung, bạn sẽ nhận được đóng gói tốt hơn, bạn sẽ kiểm tra hành vi thực tế của đơn vị.
Và rất quan trọng từ quan điểm kiểm tra - bạn sẽ được tự do cấu trúc lại mã nội bộ của đơn vị mà không thay đổi các kiểm tra - ví dụ: bạn quyết định thêm một đối số cho hàm thuần túy - với hàm được chèn rõ ràng (giả định trong các bài kiểm tra) bạn sẽ cần thay đổi cấu hình kiểm tra, trong đó với chức năng được sử dụng ngầm, bạn không làm gì cả.

Tôi có thể tưởng tượng tình huống khi bạn cần tiêm chức năng thuần túy - là khi bạn muốn cung cấp cho người tiêu dùng để thay đổi hành vi của đơn vị.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

Đối với phương pháp trên, bạn hy vọng rằng tính toán chiết khấu có thể được thay đổi bởi người tiêu dùng, vì vậy bạn phải loại trừ kiểm tra logic của nó khỏi các thử nghiệm đơn vị cho CalcualteTotalPricephương pháp.


Tiêm chức năng không có nghĩa là bạn phải kiểm tra các cuộc gọi chức năng. Trên thực tế tôi sẽ khẳng định rằng SUT thực hiện đúng (liên quan đến trạng thái có thể quan sát được) khi nó được thông qua giả. OTOH nói đùa một phụ thuộc không phải là một phần của giao diện SUT sẽ là lạ trong mắt tôi.
Ngừng làm hại Monica

@Goyo, bạn sẽ cấu hình giả để chỉ trả về giá trị mong đợi cho đầu vào cụ thể. Vì vậy, giả của bạn sẽ "bảo vệ" rằng các đối số đầu vào chính xác đã được cung cấp.
Fabio

4

Trong một thế giới lý tưởng của lập trình OID RẮN, bạn sẽ thực hiện mọi hành vi bên ngoài. Trong thực tế, bạn sẽ luôn luôn sử dụng một số hàm đơn giản (hoặc không đơn giản) trực tiếp.

Nếu bạn đang tính toán tối đa một tập hợp số, có thể sẽ quá mức cần thiết để tiêm một maxhàm và giả định nó trong các bài kiểm tra đơn vị. Thông thường, bạn không quan tâm đến việc tách mã của bạn khỏi việc triển khai cụ thể maxvà kiểm tra mã riêng lẻ.

Nếu bạn đang làm một việc gì đó phức tạp như tìm đường dẫn chi phí tối thiểu trong biểu đồ thì tốt hơn bạn nên thực hiện cụ thể để linh hoạt và thử nghiệm nó trong các bài kiểm tra đơn vị để có hiệu suất tốt hơn. Trong trường hợp này, nó có thể có giá trị công việc tách rời thuật toán tìm đường dẫn từ mã sử dụng nó.

Không thể có câu trả lời cho "bất kỳ loại hàm thuần túy" nào, bạn phải quyết định nơi vẽ đường thẳng. Có quá nhiều yếu tố liên quan đến quyết định này. Cuối cùng, bạn phải cân nhắc lợi ích chống lại những rắc rối mà việc tách rời mang lại cho bạn, và điều đó phụ thuộc vào bạn và bối cảnh của bạn.

Tôi thấy trong các ý kiến ​​mà bạn hỏi về sự khác biệt giữa các lớp tiêm (thực ra bạn tiêm đối tượng) và các hàm. Không có sự khác biệt, chỉ có các tính năng ngôn ngữ làm cho nó trông khác nhau. Từ quan điểm trừu tượng, việc gọi hàm không khác gì gọi phương thức của đối tượng và bạn tiêm (hoặc không) hàm vì những lý do tương tự bạn tiêm (hoặc không) đối tượng: tách rời hành vi đó khỏi mã gọi và có khả năng sử dụng các cách thực hiện khác nhau của hành vi và quyết định nơi nào khác thực hiện để sử dụng.

Vì vậy, về cơ bản bất kỳ tiêu chí nào bạn thấy hợp lệ cho việc tiêm phụ thuộc, bạn có thể sử dụng nó bất kể phụ thuộc là một đối tượng hay hàm. Có thể có một số sắc thái tùy thuộc vào ngôn ngữ (nghĩa là nếu bạn đang sử dụng java thì đây không phải là vấn đề).


3
Tiêm không liên quan gì đến lập trình hướng đối tượng.
Frank Hileman

@FrankHileman Tốt hơn? Bạn cần tiêm phụ thuộc để phụ thuộc vào trừu tượng vì bạn không thể khởi tạo chúng.
Ngừng làm hại Monica

Sẽ có ích khi có một danh sách sơ bộ các yếu tố quan trọng nhất liên quan đến thực tiễn. Liên quan đến vấn đề này, tại sao bạn không quan tâm đến việc kết hợp với max()(trái ngược với điều gì khác)?
DanielM

RẮN không phải là "lý tưởng" và cũng không liên quan gì đến lập trình hướng đối tượng.
Frank Hileman

@FrankHileman Làm thế nào để RẮN không liên quan gì đến OOP? Năm nguyên tắc đã được Robert Martin đề xuất trong bài báo năm 2000 của ông trong một chương gọi là Nguyên tắc thiết kế lớp hướng đối tượng?
NickL

3

Các hàm thuần túy không ảnh hưởng đến khả năng kiểm tra lớp vì các thuộc tính của chúng:

  • đầu ra của họ (hoặc lỗi / ngoại lệ) chỉ phụ thuộc vào đầu vào của họ;
  • sản lượng của họ không phụ thuộc vào nhà nước thế giới;
  • hoạt động của họ không sửa đổi nhà nước thế giới.

Điều này có nghĩa là chúng gần như ở cùng một lĩnh vực với các phương thức riêng tư, hoàn toàn nằm dưới sự kiểm soát của lớp, với phần thưởng được thêm vào thậm chí không phụ thuộc vào trạng thái hiện tại của ví dụ (ví dụ "này"). Lớp người dùng "điều khiển" đầu ra vì nó kiểm soát hoàn toàn các đầu vào.

Các ví dụ bạn đã đưa ra, max () và min (), là xác định. Tuy nhiên, ví dụ ngẫu nhiên () và currentTime () là các thủ tục, không phải là các hàm thuần túy (chúng phụ thuộc vào / sửa đổi trạng thái ra khỏi băng tần), chẳng hạn. Hai cái cuối cùng sẽ phải được tiêm để có thể thử nghiệm.

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.