Khi nào kiểm tra loại OK?


53

Giả sử một ngôn ngữ với một số loại an toàn vốn có (ví dụ: không phải JavaScript):

Đưa ra một phương thức chấp nhận a SuperType, chúng tôi biết rằng trong hầu hết các trường hợp, trong đó chúng tôi có thể bị cám dỗ thực hiện kiểm tra loại để chọn một hành động:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Chúng ta thường nên, nếu không luôn luôn, tạo một phương thức duy nhất, có thể ghi đè lên SuperTypevà thực hiện điều này:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... trong đó mỗi kiểu con được doSomething()thực hiện riêng . Phần còn lại của ứng dụng của chúng tôi sau đó có thể không biết gì về việc bất kỳ thứ gì được đưa ra SuperTypethực sự là a SubTypeAhay a SubTypeB.

Tuyệt vời.

Nhưng, chúng tôi vẫn đưa ra các is ahoạt động giống như trong hầu hết các ngôn ngữ, an toàn loại. Và điều đó cho thấy một nhu cầu tiềm năng để thử nghiệm loại rõ ràng.

Vì vậy, trong những tình huống, nếu có, chúng ta nên hoặc phải thực hiện kiểm tra loại rõ ràng?

Tha thứ cho sự đãng trí của tôi hoặc thiếu sáng tạo. Tôi biết tôi đã làm nó trước đây; nhưng, thật lòng từ lâu lắm rồi tôi không thể nhớ liệu những gì mình làm có tốt không! Và trong bộ nhớ gần đây, tôi không nghĩ rằng tôi đã gặp phải nhu cầu kiểm tra các loại bên ngoài JavaScript cao bồi của mình.



4
Thật đáng để chỉ ra rằng cả Java và C # đều không có chung chung trong phiên bản đầu tiên của họ. Bạn phải chuyển đến và từ Object để sử dụng container.
Doval

6
"Kiểm tra kiểu" hầu như luôn đề cập đến việc kiểm tra xem mã có tuân theo quy tắc gõ tĩnh của ngôn ngữ hay không. Những gì bạn có nghĩa là thường được gọi là thử nghiệm loại .


3
Đây là lý do tại sao tôi thích viết C ++ và tắt RTTI. Khi một nghĩa đen không thể kiểm tra các loại đối tượng trong thời gian chạy, nó buộc nhà phát triển phải tuân thủ thiết kế OO tốt liên quan đến câu hỏi được hỏi ở đây.

Câu trả lời:


48

"Không bao giờ" là câu trả lời chính tắc cho "khi nào kiểm tra loại được?" Không có cách nào để chứng minh hay bác bỏ điều này; nó là một phần của hệ thống niềm tin về những gì tạo nên "thiết kế tốt" hay "thiết kế hướng đối tượng tốt". Đó cũng là hokum.

Để chắc chắn, nếu bạn có một tập hợp các lớp tích hợp và cũng có nhiều hơn một hoặc hai hàm cần loại thử nghiệm trực tiếp đó, có lẽ bạn đang LÀM SAU. Những gì bạn thực sự cần là một phương thức được triển khai khác nhau trong SuperTypevà các kiểu con của nó. Đây là một phần và phần của lập trình hướng đối tượng, và toàn bộ các lớp lý do và kế thừa tồn tại.

Trong trường hợp này, kiểm tra loại rõ ràng là sai không phải vì kiểm tra loại vốn đã sai, mà bởi vì ngôn ngữ đã có một cách rõ ràng, có thể mở rộng, để thực hiện phân biệt đối xử loại và bạn đã không sử dụng nó. Thay vào đó, bạn rơi trở lại một thành ngữ nguyên thủy, mong manh, không thể mở rộng.

Giải pháp: Sử dụng thành ngữ. Như bạn đã đề xuất, hãy thêm một phương thức cho mỗi lớp, sau đó để các thuật toán lựa chọn phương thức và kế thừa tiêu chuẩn xác định trường hợp nào được áp dụng. Hoặc nếu bạn không thể thay đổi các loại cơ sở, lớp con và thêm phương thức của bạn vào đó.

Quá nhiều cho sự khôn ngoan thông thường, và một số câu trả lời. Một số trường hợp trong đó kiểm tra loại rõ ràng có ý nghĩa:

  1. Đó là một lần. Nếu bạn có nhiều phân biệt đối xử loại, bạn có thể mở rộng các loại hoặc phân lớp. Nhưng bạn thì không. Bạn chỉ có một hoặc hai nơi bạn cần kiểm tra rõ ràng, vì vậy không đáng để bạn quay lại và làm việc thông qua hệ thống phân cấp lớp để thêm các hàm làm phương thức. Hoặc không đáng nỗ lực thực tế để thêm loại tổng quát, thử nghiệm, đánh giá thiết kế, tài liệu hoặc các thuộc tính khác của các lớp cơ sở cho việc sử dụng đơn giản, hạn chế như vậy. Trong trường hợp đó, việc thêm một hàm thực hiện kiểm tra trực tiếp là hợp lý.

  2. Bạn không thể điều chỉnh các lớp học. Bạn nghĩ về phân lớp - nhưng bạn không thể. Nhiều lớp trong Java, ví dụ, được chỉ định final. Bạn cố gắng ném vào public class ExtendedSubTypeA extends SubTypeA {...} và trình biên dịch sẽ cho bạn, không có gì chắc chắn, rằng những gì bạn đang làm là không thể. Xin lỗi, duyên dáng và tinh tế của mô hình hướng đối tượng! Ai đó quyết định bạn không thể mở rộng các loại của họ! Thật không may, nhiều thư viện tiêu chuẩn là final, và làm cho các lớp finallà hướng dẫn thiết kế chung. Một chức năng chạy cuối là những gì còn lại cho bạn.

    BTW, điều này không giới hạn ở các ngôn ngữ gõ tĩnh. Ngôn ngữ động Python có một số lớp cơ sở, dưới vỏ được triển khai trong C, thực sự không thể sửa đổi. Giống như Java, bao gồm hầu hết các loại tiêu chuẩn.

  3. Mã của bạn là bên ngoài. Bạn đang phát triển với các lớp và đối tượng đến từ một loạt các máy chủ cơ sở dữ liệu, công cụ phần mềm trung gian và các cơ sở mã khác mà bạn không thể kiểm soát hoặc điều chỉnh. Mã của bạn chỉ là một người tiêu dùng thấp của các đối tượng được tạo ra ở nơi khác. Ngay cả khi bạn có thể phân lớp SuperType, bạn sẽ không thể có được các thư viện mà bạn phụ thuộc vào việc tạo các đối tượng trong các lớp con của mình. Họ sẽ trao cho bạn các phiên bản loại họ biết chứ không phải biến thể của bạn. Điều này không phải lúc nào cũng đúng ... đôi khi chúng được xây dựng để linh hoạt và chúng tự động khởi tạo các thể hiện của các lớp mà bạn cho chúng ăn. Hoặc họ cung cấp một cơ chế để đăng ký các lớp con mà bạn muốn các nhà máy của họ xây dựng. Các trình phân tích cú pháp XML có vẻ đặc biệt tốt trong việc cung cấp các điểm nhập như vậy; xem ví dụ hoặc lxml trong Python . Nhưng hầu hết các cơ sở mã không cung cấp các phần mở rộng như vậy. Họ sẽ trao lại cho bạn các lớp học mà họ đã xây dựng và biết về. Nói chung sẽ không có ý nghĩa để ủy quyền kết quả của họ vào kết quả tùy chỉnh của bạn chỉ để bạn có thể sử dụng bộ chọn loại hướng đối tượng hoàn toàn. Nếu bạn định thực hiện phân biệt đối xử, bạn sẽ phải làm điều đó tương đối thô bạo. Mã kiểm tra loại của bạn sau đó trông khá thích hợp.

  4. Thuốc generic của người nghèo / nhiều công văn. Bạn muốn chấp nhận nhiều loại khác nhau cho mã của mình và cảm thấy rằng việc có một loạt các phương thức rất cụ thể không phù hợp. public void add(Object x)có vẻ hợp lý, nhưng không phải là một mảng của addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, và addStringcác biến thể (đến tên một vài). Có các chức năng hoặc phương pháp có loại siêu cao và sau đó xác định những việc cần làm trên cơ sở từng loại - chúng sẽ không giành cho bạn Giải thưởng Tinh khiết tại Hội nghị chuyên đề Thiết kế Booch-Liskov hàng năm, nhưng bỏ qua Đặt tên Hungary sẽ cung cấp cho bạn một API đơn giản hơn. Theo một nghĩa nào đó, bạn is-ahoặcis-instance-of thử nghiệm đang mô phỏng chung hoặc đa công văn trong ngữ cảnh ngôn ngữ không thực sự hỗ trợ nó.

    Built-in hỗ trợ ngôn ngữ cho cả Genericsvịt gõ giảm nhu cầu cho loại hình kiểm tra bằng cách "làm một cái gì duyên dáng và thích hợp" nhiều khả năng. Các công văn nhiều lựa chọn / giao diện nhìn thấy trong các ngôn ngữ như Julia và đi tương tự thay thế thử nghiệm loại trực tiếp với cơ chế tích hợp cho lựa chọn loại dựa trên "những việc cần làm." Nhưng không phải tất cả các ngôn ngữ đều hỗ trợ những điều này. Java, ví dụ, nói chung là một lần gửi và các thành ngữ của nó không thân thiện lắm với việc gõ vịt.

    Nhưng ngay cả với tất cả các tính năng phân biệt đối xử loại này - kế thừa, tổng quát, gõ vịt và gửi nhiều lần - đôi khi chỉ cần thuận tiện để có một thói quen duy nhất, hợp nhất làm cho thực tế rằng bạn đang làm gì đó dựa trên loại đối tượng rõ ràng và ngay lập tức. Trong siêu lập trình tôi đã tìm thấy nó về cơ bản là không thể tránh khỏi. Việc quay trở lại các cuộc điều tra loại trực tiếp sẽ cấu thành "chủ nghĩa thực dụng trong hành động" hay "mã hóa bẩn" sẽ phụ thuộc vào triết lý thiết kế và niềm tin của bạn.


Nếu một thao tác không thể được thực hiện theo một loại cụ thể, nhưng có thể được thực hiện theo hai loại khác nhau mà nó có thể chuyển đổi hoàn toàn, quá tải chỉ phù hợp nếu cả hai chuyển đổi sẽ mang lại kết quả giống hệt nhau. Mặt khác, người ta kết thúc với các hành vi nhàu nát như trong .NET: (1.0).Equals(1.0f)mang lại true [đối số thúc đẩy double], nhưng (1.0f).Equals(1.0)mang lại sai [đối số thúc đẩy object]; trong Java, Math.round(123456789*1.0)mang lại 123456789, nhưng Math.round(123456789*1.0)mang lại 123456792 [đối số thúc đẩy floatthay vì double].
supercat

Đây là đối số cổ điển chống lại việc đúc / leo thang kiểu tự động. Kết quả xấu và nghịch lý, ít nhất là trong các trường hợp cạnh. Tôi đồng ý, nhưng không chắc bạn dự định nó liên quan đến câu trả lời của tôi như thế nào.
Jonathan Eunice

Tôi đã phản hồi quan điểm số 4 của bạn dường như đang ủng hộ việc quá tải thay vì sử dụng các phương pháp được đặt tên khác với các loại khác nhau.
supercat

2
@supercat Đổ lỗi cho thị lực kém của tôi, nhưng hai biểu cảm Math.roundtrông giống hệt tôi. Có gì khác biệt?
Lily Chung

2
@IstvanChung: Rất tiếc ... sau này cần phải có được Math.round(123456789)[dấu hiệu của những gì có thể xảy ra nếu ai đó viết lại Math.round(thing.getPosition() * COUNTS_PER_MIL)để trả lại một giá trị vị trí chưa định tỷ lệ, không nhận ra rằng getPositiontrả về một inthoặc long.]
supercat

25

Tình huống chính tôi từng cần là khi so sánh hai đối tượng, chẳng hạn như trong một equals(other)phương thức, có thể yêu cầu các thuật toán khác nhau tùy thuộc vào loại chính xác other. Ngay cả sau đó, nó khá hiếm.

Một tình huống khác mà tôi đã gặp phải, rất hiếm khi xảy ra, là sau khi khử lưu huỳnh hoặc phân tích cú pháp, đôi khi bạn cần nó để chuyển sang một loại cụ thể hơn một cách an toàn.

Ngoài ra, đôi khi bạn chỉ cần một bản hack để xử lý mã bên thứ ba mà bạn không kiểm soát. Đó là một trong những điều bạn không thực sự muốn sử dụng một cách thường xuyên, nhưng rất vui vì nó ở đó khi bạn thực sự cần nó.


1
Tôi đã rất vui mừng trong trường hợp giải trừ, đã nhớ sai khi sử dụng nó ở đó; nhưng, bây giờ tôi không thể tưởng tượng tôi sẽ có như thế nào! Tôi biết tôi đã thực hiện một số kiểu tra cứu kỳ lạ cho điều đó; nhưng tôi không chắc liệu điều đó có cấu thành thử nghiệm không . Có thể tuần tự hóa là một song song gần hơn: phải thẩm vấn các đối tượng cho loại cụ thể của chúng.
Svidgen

1
Tuần tự hóa thường được thực hiện bằng cách sử dụng đa hình. Khi khử lưu huỳnh, bạn thường làm một cái gì đó như thế BaseClass base = deserialize(input), vì bạn chưa biết loại này, sau đó bạn làm if (base instanceof Derived) derived = (Derived)baseđể lưu trữ nó dưới dạng loại dẫn xuất chính xác của nó.
Karl Bielefeldt

1
Bình đẳng có ý nghĩa. Theo kinh nghiệm của tôi, các phương thức như vậy thường có dạng giống như nếu hai đối tượng này có cùng loại cụ thể, trả về việc tất cả các trường của chúng có bằng nhau không; nếu không, hãy trả về false (hoặc không thể so sánh được).
Jon Purdy

2
Cụ thể, thực tế là bạn thấy mình thử nghiệm loại là một chỉ báo tốt cho thấy so sánh bình đẳng đa hình là một tổ của vipers.
Steve Jessop

12

Trường hợp tiêu chuẩn (nhưng hy vọng là hiếm) trông như thế này: nếu trong tình huống sau đây

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

các hàm DoSomethingAhoặc DoSomethingBkhông thể dễ dàng được thực hiện như các hàm thành viên của cây thừa kế của SuperType/ SubTypeA/ SubTypeB. Ví dụ, nếu

  • các kiểu con là một phần của thư viện mà bạn không thể thay đổi, hoặc
  • nếu thêm mã cho DoSomethingXXXthư viện đó có nghĩa là giới thiệu một phụ thuộc bị cấm.

Lưu ý thường có những tình huống bạn có thể khắc phục sự cố này (ví dụ: bằng cách tạo trình bao bọc hoặc bộ điều hợp cho SubTypeASubTypeBhoặc cố gắng thực hiện lại DoSomethinghoàn toàn về các hoạt động cơ bản của SuperType), nhưng đôi khi các giải pháp này không đáng để gây rắc rối hoặc thực hiện những thứ phức tạp hơn và ít mở rộng hơn so với làm bài kiểm tra loại rõ ràng.

Một ví dụ từ công việc hôm nay của tôi: Tôi đã có một tình huống là tôi sẽ song song hóa việc xử lý một danh sách các đối tượng (thuộc loại SuperType, với chính xác hai loại phụ khác nhau, trong đó rất khó có khả năng sẽ có nhiều hơn nữa). Phiên bản chưa được bao gồm hai vòng lặp: một vòng lặp cho các đối tượng của kiểu con A, gọi DoSomethingAvà vòng lặp thứ hai cho các đối tượng của kiểu con B, gọi DoSomethingB.

Các phương thức "DoS SomethingA" và "DoS SomethingB" đều là các phép tính chuyên sâu về thời gian, sử dụng thông tin ngữ cảnh không có sẵn trong phạm vi của các kiểu con A và B. (vì vậy sẽ không có ý nghĩa gì khi thực hiện chúng như các hàm thành viên của các kiểu con). Từ quan điểm của "vòng lặp song song" mới, nó làm cho mọi thứ dễ dàng hơn nhiều bằng cách xử lý chúng đồng nhất, vì vậy tôi đã thực hiện một chức năng tương tự như DoSomethingToở trên. Tuy nhiên, nhìn vào việc triển khai "DoS SomethingA" và "DoS SomethingB" cho thấy họ làm việc rất khác nhau trong nội bộ. Vì vậy, cố gắng thực hiện một "DoS Something" chung chung bằng cách mở rộng SuperTypevới nhiều phương thức trừu tượng không thực sự hiệu quả, hoặc có nghĩa là hoàn toàn vượt qua mọi thứ.


2
Bất kỳ cơ hội nào bạn có thể thêm một ví dụ nhỏ, cụ thể để thuyết phục bộ não sương mù của tôi kịch bản này không được đưa ra?
Svidgen

Để rõ ràng, tôi không nói rằng nó giả. Tôi nghi ngờ hôm nay tôi cực kỳ mù sương.
Svidgen

@svidgen: điều này khác xa với việc nuôi ong, thực sự tôi đã gặp những tình huống này ngày hôm nay (mặc dù tôi không thể đăng ví dụ này ở đây vì nó có chứa nội bộ doanh nghiệp). Và tôi đồng ý với các câu trả lời khác rằng sử dụng toán tử "is" phải là một ngoại lệ và chỉ được thực hiện trong các trường hợp hiếm.
Doc Brown

Tôi nghĩ rằng chỉnh sửa của bạn, mà tôi bỏ qua, đưa ra một ví dụ tốt. ... Có những trường hợp trong đó đây là OK ngay cả khi bạn làm điều khiển SuperTypevà đó là lớp con?
Svidgen

6
Đây là một ví dụ cụ thể: thùng chứa cấp cao nhất trong phản hồi JSON từ dịch vụ web có thể là từ điển hoặc mảng. Thông thường, bạn có một số công cụ biến JSON thành các đối tượng thực (ví dụ như NSJSONSerializationtrong Obj-C), nhưng bạn không muốn tin tưởng đơn giản rằng phản hồi có chứa loại mà bạn mong đợi, vì vậy trước khi sử dụng, bạn hãy kiểm tra nó (ví dụ if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb

5

Như chú Bob gọi nó:

When your compiler forgets about the type.

Trong một trong các tập Clean Coder của mình, ông đã đưa ra một ví dụ về lệnh gọi hàm được sử dụng để trả về Employees. Managerlà một loại phụ của Employee. Giả sử chúng ta có một dịch vụ ứng dụng chấp nhận Managerid của nó và triệu tập anh ta đến văn phòng :) Hàm này getEmployeeById()trả về loại siêu Employee, nhưng tôi muốn kiểm tra xem người quản lý có được trả lại trong trường hợp sử dụng này không.

Ví dụ:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Ở đây tôi đang kiểm tra xem liệu nhân viên trả về bằng truy vấn có thực sự là người quản lý hay không (nghĩa là tôi mong đợi nó sẽ là người quản lý và nếu không thì sẽ thất bại nhanh).

Không phải là ví dụ tốt nhất, nhưng rốt cuộc đó là chú Bob.

Cập nhật

Tôi đã cập nhật ví dụ nhiều nhất có thể từ bộ nhớ.


1
Tại sao không Managerthực hiện summon()chỉ ném ngoại lệ trong ví dụ này?
Svidgen

@svidgen có CEOthể triệu tập Managers.
dùng253751

@svidgen, sau đó sẽ không rõ ràng rằng workerRep repository.getEmployeeById (empId) dự kiến ​​sẽ trả lại người quản lý
Ian

@ Tôi không thấy đó là một vấn đề. Nếu mã cuộc gọi đang yêu cầu một Employee, nó chỉ nên quan tâm rằng nó có được một cái gì đó hoạt động như một Employee. Nếu các lớp con khác nhau Employeecó các quyền, trách nhiệm khác nhau, v.v., điều gì làm cho kiểm tra kiểu trở thành một lựa chọn tốt hơn so với một hệ thống quyền thực sự?
Svidgen

3

Khi nào kiểm tra loại OK?

Không bao giờ.

  1. Bằng cách có hành vi được liên kết với loại đó trong một số chức năng khác, bạn vi phạm Nguyên tắc Đóng mở , vì bạn có thể tự do sửa đổi hành vi hiện có của loại bằng cách thay đổi ismệnh đề hoặc (trong một số ngôn ngữ hoặc tùy thuộc vào kịch bản) bởi vì bạn không thể mở rộng loại mà không sửa đổi phần bên trong của hàm thực hiện iskiểm tra.
  2. Quan trọng hơn, isséc là một dấu hiệu mạnh mẽ cho thấy bạn đang vi phạm Nguyên tắc thay thế Liskov . Bất cứ điều gì làm việc với SuperTypenên hoàn toàn không biết những loại phụ có thể có.
  3. Bạn đang ngầm liên kết một số hành vi với tên của loại. Điều này làm cho mã của bạn khó bảo trì hơn vì các hợp đồng ngầm này được trải đều trong mã và không được bảo đảm để được thực thi một cách phổ biến và nhất quán như các thành viên lớp thực tế.

Tất cả những gì đã nói, iskiểm tra có thể ít tệ hơn so với các lựa chọn thay thế khác. Đặt tất cả các chức năng phổ biến vào một lớp cơ sở là nặng tay và thường dẫn đến các vấn đề tồi tệ hơn. Sử dụng một lớp duy nhất có cờ hoặc enum cho trường hợp "loại" là gì ... tệ hơn là khủng khiếp, vì giờ đây bạn đang truyền bá hệ thống lách luật cho tất cả người tiêu dùng.

Nói tóm lại, bạn nên luôn luôn coi kiểm tra loại là một mùi mã mạnh. Nhưng như với tất cả các nguyên tắc, sẽ có lúc bạn buộc phải lựa chọn giữa việc vi phạm nguyên tắc nào là ít gây khó chịu nhất.


3
Có một lưu ý nhỏ: triển khai các loại dữ liệu đại số bằng các ngôn ngữ không hỗ trợ chúng. Tuy nhiên, việc sử dụng kế thừa và đánh máy hoàn toàn là một chi tiết triển khai hacky ở đó; mục đích không phải là để giới thiệu các kiểu con, mà là để phân loại các giá trị. Tôi chỉ đưa nó lên vì ADT rất hữu ích và không bao giờ là vòng loại rất mạnh, nhưng nếu không thì tôi hoàn toàn đồng ý; instanceofrò rỉ chi tiết thực hiện và phá vỡ sự trừu tượng.
Doval

17
"Không bao giờ" là một từ tôi không thực sự thích trong bối cảnh như vậy, đặc biệt là khi nó mâu thuẫn với những gì bạn viết dưới đây.
Doc Brown

10
Nếu "không bao giờ" bạn thực sự có nghĩa là "đôi khi", thì bạn đã đúng.
Caleb

2
OK có nghĩa là cho phép, chấp nhận được, v.v., nhưng không nhất thiết là lý tưởng hay tối ưu. Tương phản với không ổn : nếu có gì đó không ổn thì bạn không nên làm gì cả. Như bạn thấy, sự cần thiết phải kiểm tra loại một cái gì đó có thể là một dấu hiệu của các vấn đề sâu sắc hơn trong các mã, nhưng có những lúc đó là thích hợp nhất, tùy chọn ít nhất là xấu, và trong những tình huống như vậy nó rõ ràng là OK để sử dụng những công cụ tại của bạn xử lý. (Nếu dễ dàng tránh được chúng trong mọi tình huống, có lẽ chúng sẽ không ở đó ngay từ đầu.) Câu hỏi giúp xác định những tình huống đó và không bao giờ giúp đỡ.
Caleb

2
@supercat: Một phương thức nên yêu cầu loại ít cụ thể nhất có thể đáp ứng yêu cầu của nó . IEnumerable<T>không hứa hẹn một yếu tố "cuối cùng" tồn tại. Nếu phương thức của bạn cần một phần tử như vậy, thì nó sẽ yêu cầu một kiểu đảm bảo sự tồn tại của một phần tử. Và các kiểu con kiểu đó sau đó có thể cung cấp các triển khai hiệu quả của phương pháp "Lần cuối".
cHao

2

Nếu bạn có một cơ sở mã lớn (hơn 100 nghìn dòng mã) và gần với vận chuyển hoặc đang làm việc trong một chi nhánh mà sau này sẽ phải hợp nhất, do đó sẽ có rất nhiều chi phí / rủi ro để thay đổi nhiều đối phó.

Sau đó, đôi khi bạn có tùy chọn của một khúc xạ lớn của hệ thống, hoặc một số thử nghiệm kiểu bản địa hóa đơn giản. Điều này tạo ra một khoản nợ kỹ thuật cần được trả lại càng sớm càng tốt, nhưng thường thì không.

(Không thể đưa ra một ví dụ, vì bất kỳ mã nào đủ nhỏ để sử dụng làm ví dụ, cũng đủ nhỏ để thiết kế tốt hơn có thể nhìn thấy rõ.)

Hay nói cách khác, khi mục đích là được trả lương của bạn chứ không phải để nhận được phiếu bầu của Google vì sự sạch sẽ trong thiết kế của bạn.


Một trường hợp phổ biến khác là mã UI, ví dụ như khi bạn hiển thị một UI khác cho một số loại nhân viên, nhưng rõ ràng bạn không muốn các khái niệm UI thoát vào tất cả các lớp miền miền của bạn.

Bạn có thể sử dụng thử nghiệm kiểu kiểu Cameron, để quyết định phiên bản giao diện người dùng nào sẽ hiển thị hoặc có một bảng tra cứu ưa thích nào đó chuyển đổi từ các lớp miền miền Cameron sang các lớp UI UI. Bảng tra cứu chỉ là một cách để ẩn kiểu thử nghiệm của Cameron ở một nơi.

(Mã cập nhật cơ sở dữ liệu có thể có cùng các vấn đề như mã UI, tuy nhiên bạn có xu hướng chỉ có một bộ mã cập nhật cơ sở dữ liệu, nhưng bạn có thể có nhiều màn hình khác nhau phải thích ứng với loại đối tượng được hiển thị.)


Mẫu khách truy cập thường là một cách tốt để giải quyết trường hợp UI của bạn.
Ian Goldby

@IanGoldby, đôi khi có thể đồng ý, tuy nhiên bạn vẫn đang thực hiện "kiểm tra loại", chỉ ẩn một chút.
Ian

Ẩn theo cùng nghĩa là nó bị ẩn khi bạn gọi một phương thức ảo thông thường? Hay bạn ám chỉ điều gì khác? Mẫu khách truy cập tôi đã sử dụng không có tuyên bố có điều kiện phụ thuộc vào loại. Tất cả đều được thực hiện bởi ngôn ngữ.
Ian Goldby

@IanGoldby, ý tôi là ẩn theo nghĩa là nó có thể làm cho mã WPF hoặc WinForms khó hiểu hơn. Tôi hy vọng rằng đối với một số UI cơ sở web, nó sẽ hoạt động rất tốt.
Ian

2

Việc triển khai LINQ sử dụng rất nhiều kiểu kiểm tra để tối ưu hóa hiệu suất có thể, và sau đó là một dự phòng cho IEnumerable.

Ví dụ rõ ràng nhất có lẽ là phương thức ElementAt (đoạn trích nhỏ của nguồn .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Nhưng có rất nhiều vị trí trong lớp Enumerable nơi sử dụng một mẫu tương tự.

Vì vậy, có lẽ tối ưu hóa hiệu năng cho một kiểu con thường được sử dụng là sử dụng hợp lệ. Tôi không chắc làm thế nào điều này có thể đã được thiết kế tốt hơn.


Nó có thể được thiết kế tốt hơn bằng cách cung cấp một phương tiện thuận tiện để giao diện có thể cung cấp các triển khai mặc định, và sau đó IEnumerable<T>bao gồm nhiều phương thức như List<T>cùng với một thuộc Featurestính cho biết phương thức nào có thể hoạt động tốt, chậm hoặc hoàn toàn không, cũng như các giả định khác nhau mà người tiêu dùng có thể đưa ra một cách an toàn về bộ sưu tập (ví dụ: kích thước và / hoặc nội dung hiện có của nó được đảm bảo không bao giờ thay đổi [một loại có thể hỗ trợ Addtrong khi vẫn đảm bảo rằng nội dung hiện tại sẽ không thay đổi]).
supercat

Ngoại trừ trong các trường hợp trong đó một loại có thể cần giữ một trong hai thứ hoàn toàn khác nhau ở các thời điểm khác nhau và các yêu cầu rõ ràng là loại trừ lẫn nhau (và do đó sử dụng các trường riêng biệt sẽ là dư thừa), tôi thường coi nhu cầu đúc thử là một dấu hiệu rằng các thành viên lẽ ra phải là một phần của giao diện cơ sở, thì không. Điều đó không có nghĩa là mã sử dụng giao diện nên bao gồm một số thành viên nhưng không nên sử dụng tính năng thử dùng để khắc phục sự thiếu sót của giao diện cơ sở, nhưng các giao diện cơ sở bằng văn bản đó sẽ giảm thiểu nhu cầu thử của khách hàng.
supercat

1

Có một ví dụ xuất hiện thường xuyên trong các phát triển trò chơi, đặc biệt là phát hiện va chạm, rất khó xử lý nếu không sử dụng một số hình thức kiểm tra loại.

Giả sử rằng tất cả các đối tượng trò chơi xuất phát từ một lớp cơ sở chung GameObject. Mỗi đối tượng có một cơ thể va chạm hình cứng nhắc CollisionShapemà có thể cung cấp một giao diện chung (nói vị trí truy vấn, định hướng, vv), nhưng tất cả các hình dạng va chạm thực tế sẽ được phân lớp bê tông như Sphere, Box, ConvexHullvv lưu trữ thông tin cụ thể cho loại đối tượng hình học (xem ở đây để biết ví dụ thực tế)

Bây giờ, để kiểm tra va chạm, tôi cần viết một hàm cho từng cặp hình dạng va chạm:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

có chứa toán học cụ thể cần thiết để thực hiện giao điểm của hai loại hình học đó.

Tại mỗi 'tick' của vòng lặp trò chơi của tôi, tôi cần kiểm tra các cặp đối tượng xem có va chạm không. Nhưng tôi chỉ có quyền truy cập vào GameObjects và CollisionShapes tương ứng của họ . Rõ ràng tôi cần biết các loại cụ thể để biết nên gọi chức năng phát hiện va chạm nào. Thậm chí không gửi đôi (mà về mặt logic thì không khác gì kiểm tra loại nào) có thể giúp ở đây *.

Trong thực tế trong tình huống này, các động cơ vật lý mà tôi đã thấy (Bullet và Havok) dựa vào thử nghiệm kiểu này hay dạng khác.

Tôi không nói rằng đây thực sự là một giải pháp tốt , chỉ là nó có thể là giải pháp tốt nhất trong số ít các giải pháp khả thi cho vấn đề này

* Về mặt kỹ thuật nó có thể sử dụng công văn đôi một cách khủng khiếp và phức tạp mà sẽ yêu cầu N (N + 1) / 2 kết hợp (trong đó N là số lượng các loại hình dạng bạn có) và sẽ chỉ xáo trộn những gì bạn đang thực sự làm mà đồng thời tìm ra các loại của hai hình dạng, vì vậy tôi không coi đây là một giải pháp thực tế.


1

Đôi khi bạn không muốn thêm một phương thức chung cho tất cả các lớp vì thực sự không có trách nhiệm thực hiện nhiệm vụ cụ thể đó.

Ví dụ: bạn muốn vẽ một số thực thể nhưng không muốn thêm mã vẽ trực tiếp vào chúng (điều này có ý nghĩa). Trong các ngôn ngữ không hỗ trợ nhiều công văn, bạn có thể kết thúc bằng mã sau:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Điều này trở nên có vấn đề khi mã này xuất hiện ở một số nơi và bạn cần sửa đổi nó ở mọi nơi khi thêm loại Thực thể mới. Nếu đó là trường hợp thì có thể tránh bằng cách sử dụng mẫu Khách truy cập nhưng đôi khi tốt hơn là chỉ giữ mọi thứ đơn giản và không áp đảo nó. Đó là những tình huống khi kiểm tra loại là OK.


0

Lần duy nhất tôi sử dụng là kết hợp với sự phản chiếu. Nhưng ngay cả khi đó là kiểm tra động chủ yếu, không được mã hóa cứng cho một lớp cụ thể (hoặc chỉ được mã hóa cứng cho các lớp đặc biệt như Stringhoặc List).

Bằng cách kiểm tra động tôi có nghĩa là:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

và không mã hóa cứng

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}

0

Kiểm tra loại và đúc loại là hai khái niệm rất liên quan. Liên quan chặt chẽ đến mức tôi cảm thấy tự tin khi nói rằng bạn không bao giờ nên làm một bài kiểm tra loại trừ khi mục đích của bạn là loại bỏ đối tượng dựa trên kết quả.

Khi bạn nghĩ về thiết kế hướng đối tượng lý tưởng, thử nghiệm kiểu (và đúc) sẽ không bao giờ xảy ra. Nhưng hy vọng rằng bây giờ bạn đã nhận ra rằng lập trình hướng đối tượng là không lý tưởng. Đôi khi, đặc biệt với mã cấp thấp hơn, mã không thể đúng với lý tưởng. Đây là trường hợp với ArrayLists trong Java; vì họ không biết trong thời gian chạy lớp nào đang được lưu trữ trong mảng, họ tạo ra Object[]các mảng và chuyển chúng thành đúng kiểu.

Nó đã được chỉ ra rằng một nhu cầu phổ biến để kiểm tra kiểu (và kiểu đúc) xuất phát từ Equalsphương thức, trong hầu hết các ngôn ngữ được cho là đơn giản Object. Việc thực hiện cần có một số kiểm tra chi tiết để thực hiện nếu hai đối tượng cùng loại, điều này đòi hỏi phải có khả năng kiểm tra chúng là loại gì.

Kiểm tra loại cũng xuất hiện thường xuyên trong sự phản ánh. Thường thì bạn sẽ có các phương thức trả về Object[]hoặc một số mảng chung khác và bạn muốn rút ra tất cả các Foođối tượng vì bất kỳ lý do gì. Đây là một sử dụng hoàn toàn hợp pháp của thử nghiệm loại và đúc.

Nói chung, kiểm tra loại là xấu khi nó không cần thiết kết hợp mã của bạn với cách thực hiện cụ thể được viết. Điều này có thể dễ dàng dẫn đến việc cần một thử nghiệm cụ thể cho từng loại hoặc kết hợp các loại, chẳng hạn như nếu bạn muốn tìm giao điểm của các đường, hình chữ nhật và hình tròn và hàm giao nhau có một thuật toán khác nhau cho mỗi kết hợp. Mục tiêu của bạn là đặt bất kỳ chi tiết cụ thể nào cho một loại đối tượng ở cùng một nơi với đối tượng đó, bởi vì điều đó sẽ giúp việc duy trì và mở rộng mã của bạn dễ dàng hơn.


1
ArrayListskhông biết lớp đang được lưu trữ trong thời gian chạy vì Java không có khái quát và khi cuối cùng chúng được giới thiệu, Oracle đã chọn cách tương thích ngược với mã không có tổng quát. equalscó cùng một vấn đề, và dù sao đó cũng là một quyết định thiết kế đáng ngờ; so sánh bình đẳng không có ý nghĩa cho mọi loại.
Doval

1
Về mặt kỹ thuật, các bộ sưu tập Java không truyền nội dung của chúng vào bất cứ thứ gì. Trình biên dịch sẽ đưa các dự báo vào vị trí của mỗi lần truy cập: vdString x = (String) myListOfStrings.get(0)

Lần cuối cùng tôi nhìn vào nguồn Java (có thể là 1.6 hoặc 1.5), có một diễn viên rõ ràng trong nguồn ArrayList. Nó tạo ra một cảnh báo trình biên dịch (bị triệt tiêu) vì lý do chính đáng nhưng dù sao cũng được cho phép. Tôi cho rằng bạn có thể nói rằng vì cách thức thực hiện thuốc generic, nó luôn luôn là Objectcho đến khi nó được truy cập bằng mọi cách; Generics trong Java chỉ cung cấp đúc ngầm được thực hiện an toàn bởi các quy tắc biên dịch.
meustrus

0

Bạn có thể chấp nhận trong trường hợp bạn phải đưa ra quyết định liên quan đến hai loại và quyết định này được gói gọn trong một đối tượng bên ngoài hệ thống phân cấp loại đó. Chẳng hạn, giả sử bạn đang lên lịch cho đối tượng nào sẽ được xử lý tiếp theo trong danh sách các đối tượng đang chờ xử lý:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Bây giờ hãy nói rằng logic kinh doanh của chúng tôi theo nghĩa đen là "tất cả các xe đều được ưu tiên hơn thuyền và xe tải". Thêm một thuộc Prioritytính vào lớp không cho phép bạn thể hiện logic kinh doanh này một cách sạch sẽ bởi vì bạn sẽ kết thúc với điều này:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

Vấn đề là bây giờ để hiểu thứ tự ưu tiên, bạn phải xem tất cả các lớp con, hay nói cách khác là bạn đã thêm khớp nối với các lớp con.

Tất nhiên bạn nên tạo các ưu tiên thành các hằng số và tự đặt chúng vào một lớp, điều này giúp giữ lịch trình logic kinh doanh cùng nhau:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Tuy nhiên, trong thực tế, thuật toán lập lịch là thứ có thể thay đổi trong tương lai và cuối cùng nó có thể phụ thuộc vào nhiều thứ hơn là chỉ loại. Ví dụ, nó có thể nói "xe tải có trọng lượng 5000 kg được ưu tiên đặc biệt hơn tất cả các phương tiện khác." Đó là lý do tại sao thuật toán lập lịch thuộc về lớp riêng của nó và đó là một ý tưởng tốt để kiểm tra loại để xác định loại nào nên đi trước:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Đây là cách đơn giản nhất để thực hiện logic kinh doanh và vẫn linh hoạt nhất với những thay đổi trong tương lai.


Tôi không đồng ý với ví dụ cụ thể của bạn, nhưng sẽ đồng ý với nguyên tắc có một tài liệu tham khảo riêng có thể chứa các loại khác nhau với các ý nghĩa khác nhau. Những gì tôi muốn đề xuất như một ví dụ tốt hơn sẽ là một lĩnh vực có thể giữ null, a Stringhoặc a String[]. Nếu 99% đối tượng sẽ cần chính xác một chuỗi, việc đóng gói mọi chuỗi trong một cấu trúc riêng biệt String[]có thể thêm chi phí lưu trữ đáng kể. Xử lý trường hợp chuỗi đơn bằng cách sử dụng tham chiếu trực tiếp đến Stringsẽ yêu cầu nhiều mã hơn, nhưng sẽ tiết kiệm dung lượng và có thể khiến mọi việc nhanh hơn.
supercat

0

Kiểm tra loại là một công cụ, sử dụng nó một cách khôn ngoan và nó có thể là một đồng minh mạnh mẽ. Sử dụng nó kém và mã của bạn sẽ bắt đầu có mùi.

Trong phần mềm của chúng tôi, chúng tôi đã nhận được tin nhắn qua mạng để đáp ứng yêu cầu. Tất cả các tin nhắn khử lưu lượng đã chia sẻ một lớp cơ sở chung Message.

Bản thân các lớp rất đơn giản, chỉ là tải trọng như đã nhập các thuộc tính và thói quen của C # để sắp xếp và sắp xếp chúng (Trên thực tế tôi đã tạo ra hầu hết các lớp bằng cách sử dụng các mẫu t4 từ mô tả XML của định dạng thông báo)

Mã sẽ giống như:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

Cấp, người ta có thể lập luận rằng kiến ​​trúc thông điệp có thể được thiết kế tốt hơn nhưng nó đã được thiết kế từ lâu và không dành cho C # vì vậy nó là như vậy. Ở đây kiểm tra loại đã giải quyết một vấn đề thực sự cho chúng tôi một cách không quá tồi tàn.

Đáng chú ý là C # 7.0 đang được khớp mẫu (trong nhiều khía cạnh là thử nghiệm loại trên steroid), nó không thể là tất cả ...


0

Lấy một trình phân tích cú pháp JSON chung. Kết quả của một phân tích thành công là một mảng, một từ điển, một chuỗi, một số, một giá trị boolean hoặc một giá trị null. Nó có thể là bất kỳ trong số đó. Và các phần tử của một mảng hoặc các giá trị trong từ điển có thể lại là bất kỳ loại nào trong số đó. Vì dữ liệu được cung cấp từ bên ngoài chương trình của bạn, bạn phải chấp nhận bất kỳ kết quả nào (đó là bạn phải chấp nhận nó mà không gặp sự cố; bạn có thể từ chối một kết quả không như bạn mong đợi).


Chà, một số trình giải mã JSON sẽ cố gắng khởi tạo một cây đối tượng nếu cấu trúc của bạn có thông tin kiểu cho "các trường suy luận" trong đó. Nhưng vâng. Tôi nghĩ rằng đây là hướng mà câu trả lời của Karl B hướng đến.
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.