Tác giả có ý nghĩa gì khi truyền tham chiếu giao diện cho bất kỳ triển khai nào?


17

Tôi hiện đang trong quá trình cố gắng thành thạo C #, vì vậy tôi đang đọc Mã thích ứng thông qua C # của Gary McLean Hall .

Ông viết về các mẫu và chống mẫu. Trong phần triển khai so với phần giao diện, ông viết như sau:

Các nhà phát triển chưa quen với khái niệm lập trình cho giao diện thường gặp khó khăn trong việc buông bỏ những gì đằng sau giao diện.

Tại thời điểm biên dịch, bất kỳ ứng dụng khách nào của giao diện sẽ không có ý tưởng nào về việc triển khai giao diện mà nó đang sử dụng. Kiến thức như vậy có thể dẫn đến các giả định không chính xác khiến cho khách hàng thực hiện giao diện cụ thể.

Hãy tưởng tượng ví dụ phổ biến trong đó một lớp cần lưu một bản ghi trong bộ lưu trữ liên tục. Để làm như vậy, nó ủy quyền một cách chính xác cho một giao diện, ẩn các chi tiết của cơ chế lưu trữ liên tục được sử dụng. Tuy nhiên, sẽ không đúng khi đưa ra bất kỳ giả định nào về việc triển khai giao diện nào đang được sử dụng trong thời gian chạy. Ví dụ, truyền tham chiếu giao diện cho bất kỳ triển khai nào luôn là một ý tưởng tồi.

Nó có thể là rào cản ngôn ngữ hoặc thiếu kinh nghiệm của tôi, nhưng tôi không hiểu điều đó có nghĩa là gì. Đây là những gì tôi hiểu:

Tôi có một dự án vui vẻ thời gian rảnh để thực hành C #. Ở đó tôi có một lớp học:

public class SomeClass...

Lớp học này được sử dụng ở rất nhiều nơi. Trong khi học C #, tôi đọc rằng tốt hơn là trừu tượng hóa với một giao diện, vì vậy tôi đã thực hiện như sau

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Vì vậy, tôi đã đi vào tất cả các tài liệu tham khảo lớp và thay thế chúng bằng ISomeClass.

Ngoại trừ trong việc xây dựng, nơi tôi đã viết:

ISomeClass myClass = new SomeClass();

Tôi có hiểu chính xác rằng điều này là sai? Nếu có, tại sao vậy, và tôi nên làm gì thay thế?


25
Không ở đâu trong ví dụ của bạn, bạn đang chuyển một đối tượng của loại giao diện sang loại thực hiện. Bạn đang chỉ định một cái gì đó thuộc loại thực hiện cho một biến giao diện, điều này hoàn toàn tốt và chính xác.
Caleth

1
Ý bạn là gì "trong công cụ xây dựng nơi tôi đã viết ISomeClass myClass = new SomeClass();? Nếu bạn thực sự muốn nói điều đó, đó là đệ quy trong công cụ xây dựng, có thể không phải là điều bạn muốn. ?
Erik Eidt

@Erik: Vâng. Trong xây dựng. Bạn nói đúng. Sẽ sửa câu hỏi. Cảm ơn
Marshall

Sự thật thú vị: F # có một câu chuyện hay hơn C # về vấn đề đó - nó không có triển khai giao diện ngầm, vì vậy bất cứ khi nào bạn muốn gọi một phương thức giao diện, bạn cần phải cập nhật kiểu giao diện. Điều này cho thấy rất rõ ràng khi nào và cách bạn đang sử dụng các giao diện trong mã của mình và làm cho việc lập trình đến các giao diện ăn sâu hơn nhiều vào ngôn ngữ.
Scrwtp

3
Đây là một chủ đề hơi lạc đề, nhưng tôi nghĩ rằng tác giả đã chẩn đoán sai vấn đề mà mọi người mới biết đến khái niệm này. Theo tôi, vấn đề là những người mới sử dụng khái niệm này không biết cách tạo ra các giao diện tốt. Thật dễ dàng để tạo các giao diện quá cụ thể không thực sự cung cấp bất kỳ tính tổng quát nào (có thể xảy ra với nó ISomeClass), nhưng cũng dễ dàng tạo ra các giao diện quá chung chung mà không thể viết mã hữu ích vào thời điểm đó là các tùy chọn duy nhất là để suy nghĩ lại giao diện và viết lại mã hoặc để downcast.
Derek Elkins rời SE

Câu trả lời:


37

Tóm tắt lớp của bạn vào một giao diện là điều bạn nên xem xét nếu và chỉ khi bạn có ý định viết các triển khai khác của giao diện đã nói hoặc khả năng mạnh mẽ để làm như vậy trong tương lai tồn tại.

Vì vậy, có lẽ SomeClassISomeClasslà một ví dụ tồi, bởi vì nó sẽ giống như có một OracleObjectSerializerlớp và một IOracleObjectSerializergiao diện.

Một ví dụ chính xác hơn sẽ là một cái gì đó như OracleObjectSerializervà a IObjectSerializer. Nơi duy nhất trong chương trình của bạn nơi bạn quan tâm sử dụng triển khai nào là khi thể hiện được tạo. Đôi khi điều này được tách rời hơn nữa bằng cách sử dụng một mô hình nhà máy.

Ở mọi nơi khác trong chương trình của bạn nên sử dụng IObjectSerializerkhông quan tâm đến cách thức hoạt động của nó. Hãy giả sử trong một giây mà bạn cũng có một SQLServerObjectSerializertriển khai ngoài OracleObjectSerializer. Bây giờ, giả sử bạn cần đặt một số thuộc tính đặc biệt để đặt và phương thức đó chỉ có trong OracleObjectSerializer chứ không phải SQLServerObjectSerializer.

Có hai cách để đi về nó: cách không chính xác và cách tiếp cận nguyên tắc thay thế Liskov .

Cách không chính xác

Cách không chính xác, và chính ví dụ được đề cập trong cuốn sách của bạn, sẽ là lấy một thể hiện IObjectSerializervà đưa nó vào OracleObjectSerializervà sau đó gọi phương thức setPropertychỉ có sẵn trên OracleObjectSerializer. Điều này là xấu bởi vì mặc dù bạn có thể biết một trường hợp là một OracleObjectSerializer, nhưng bạn đang giới thiệu một điểm khác trong chương trình của bạn, nơi bạn quan tâm để biết thực hiện nó là gì. Khi việc triển khai đó thay đổi và có lẽ sớm hay muộn nếu bạn có nhiều triển khai, tình huống tốt nhất, bạn sẽ cần tìm tất cả các địa điểm này và thực hiện các điều chỉnh chính xác. Trường hợp xấu nhất, bạn đưa ra một IObjectSerializerví dụ cho a OracleObjectSerializervà bạn nhận được lỗi thời gian chạy trong sản xuất.

Phương pháp tiếp cận nguyên tắc thay thế Liskov

Liskov nói rằng bạn không bao giờ cần các phương thức như setPropertytrong lớp triển khai như trong trường hợp của tôi OracleObjectSerializernếu được thực hiện đúng cách. Nếu bạn trừu tượng một lớp OracleObjectSerializerthành IObjectSerializer, bạn nên bao gồm tất cả các phương thức cần thiết để sử dụng lớp đó và nếu bạn không thể, thì có điều gì đó không ổn với sự trừu tượng của bạn ( ví dụ cố gắng làm cho một Doglớp hoạt động như một IPersontriển khai).

Cách tiếp cận đúng sẽ là cung cấp một setPropertyphương thức IObjectSerializer. Phương pháp tương tự trong SQLServerObjectSerializerlý tưởng sẽ làm việc thông qua setPropertyphương pháp này . Tốt hơn hết, bạn tiêu chuẩn hóa các tên thuộc tính thông qua việc Enummỗi lần thực hiện chuyển enum thành tương đương với thuật ngữ cơ sở dữ liệu của chính nó.

Nói một cách đơn giản, sử dụng một ISomeClasschỉ là một nửa của nó. Bạn không bao giờ cần phải sử dụng nó ngoài phương thức chịu trách nhiệm cho việc tạo ra nó. Để làm như vậy gần như chắc chắn là một lỗi thiết kế nghiêm trọng.


1
Dường như với tôi rằng, nếu bạn đang đi để cast IObjectSerializerđến OracleObjectSerializervì bạn “biết” rằng đây là nó là gì, thì bạn nên thành thật với chính mình (và quan trọng hơn, với những người có thể duy trì mã này, có thể bao gồm tương lai của bạn tự) và sử dụng OracleObjectSerializertất cả các cách từ nơi nó được tạo ra đến nơi nó được sử dụng. Điều này làm cho nó rất công khai và rõ ràng rằng bạn đang giới thiệu một sự phụ thuộc vào một triển khai cụ thể và công việc và sự xấu xí liên quan đến việc đó trở thành một gợi ý mạnh mẽ rằng có gì đó không ổn.
KRyan

(Và, nếu vì một lý do nào bạn thực sự làm phải dựa vào một thực hiện cụ thể, nó trở nên rõ ràng hơn rất nhiều rằng đây là những gì bạn đang làm và rằng bạn đang làm việc đó với ý định và mục đích. Đây “nên” không bao giờ xảy ra tất nhiên, và 99% số lần có vẻ như nó xảy ra thực sự không phải vậy và bạn nên sửa chữa mọi thứ, nhưng không có gì là chắc chắn 100% hoặc theo cách mà mọi thứ nên diễn ra.)
KRyan

@KRyan Hoàn toàn đúng. Trừu tượng chỉ nên được sử dụng nếu bạn có nhu cầu. Sử dụng trừu tượng hóa khi không cần thiết chỉ phục vụ để làm cho mã khó hiểu hơn một chút.
Neil

29

Câu trả lời được chấp nhận là chính xác và rất hữu ích, nhưng tôi muốn giải quyết ngắn gọn cụ thể dòng mã bạn đã hỏi về:

ISomeClass myClass = new SomeClass();

Nói rộng ra, điều này không tệ. Điều nên tránh bất cứ khi nào có thể sẽ là làm điều này:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Trường hợp mã của bạn được cung cấp một giao diện bên ngoài, nhưng bên trong lại đưa nó vào một triển khai cụ thể, "Bởi vì tôi biết nó sẽ chỉ là triển khai đó". Ngay cả khi điều đó đã trở thành sự thật, bằng cách sử dụng một giao diện và chuyển nó sang một triển khai, bạn đang tự nguyện từ bỏ sự an toàn của loại thực để bạn có thể giả vờ sử dụng sự trừu tượng hóa. Nếu ai đó đã làm việc với mã sau này và thấy một phương thức chấp nhận tham số giao diện, thì họ sẽ cho rằng bất kỳ triển khai nào của giao diện đó là một tùy chọn hợp lệ để truyền vào. sau khi bạn quên rằng một phương thức cụ thể nằm ở những tham số cần thiết. Nếu bạn cảm thấy cần phải chuyển từ một giao diện sang một triển khai cụ thể, thì giao diện đó, việc thực hiện, hoặc mã tham chiếu đến chúng được thiết kế không chính xác và nên thay đổi. Ví dụ, nếu phương thức chỉ hoạt động khi đối số được truyền vào là một lớp cụ thể, thì tham số chỉ nên chấp nhận lớp đó.

Bây giờ, nhìn lại cuộc gọi nhà xây dựng của bạn

ISomeClass myClass = new SomeClass();

các vấn đề từ việc đúc không thực sự áp dụng. Không ai trong số này dường như bị phơi bày ra bên ngoài, vì vậy không có bất kỳ rủi ro cụ thể nào liên quan đến nó. Về cơ bản, chính dòng mã này là một chi tiết triển khai mà các giao diện được thiết kế trừu tượng để bắt đầu, vì vậy một người quan sát bên ngoài sẽ thấy nó hoạt động theo cùng một cách bất kể chúng làm gì. Tuy nhiên, điều này cũng không GAIN bất cứ điều gì từ sự tồn tại của một giao diện. Bạn myClasscó loại ISomeClass, nhưng nó không có bất kỳ lý do nào vì nó luôn được chỉ định thực hiện cụ thể,SomeClass. Có một số lợi thế tiềm năng nhỏ, chẳng hạn như có thể hoán đổi việc triển khai mã bằng cách thay đổi chỉ cuộc gọi của nhà xây dựng hoặc gán lại biến đó sau đó thành một triển khai khác, nhưng trừ khi có một nơi nào đó yêu cầu biến được nhập vào giao diện thay vì việc triển khai mẫu này làm cho mã của bạn trông giống như các giao diện chỉ được sử dụng bởi vẹt, không nằm ngoài sự hiểu biết thực tế về lợi ích của các giao diện.


1
Apache Math thực hiện điều này với Nguồn Cartesian3D , dòng 286 , có thể thực sự gây phiền nhiễu.
J_F_B_M

1
Đây là câu trả lời thực sự chính xác giải quyết câu hỏi ban đầu.
Benjamin Gruenbaum

2

Tôi nghĩ việc hiển thị mã của bạn với một ví dụ tồi tệ hơn:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

Vấn đề là khi ban đầu bạn viết mã, có lẽ chỉ có một triển khai giao diện đó, do đó, việc truyền vẫn sẽ hoạt động, chỉ là trong tương lai, bạn có thể triển khai một lớp khác và sau đó (như ví dụ của tôi cho thấy) thử và truy cập dữ liệu không tồn tại trong đối tượng bạn đang sử dụng.


Tại sao xin chào, thưa ngài. Bất cứ ai từng nói với bạn rằng bạn đẹp trai như thế nào?
Neil

2

Để rõ ràng, hãy xác định đúc.

Đúc là buộc phải chuyển đổi một cái gì đó từ loại này sang loại khác. Một ví dụ phổ biến là truyền một số dấu phẩy động thành một kiểu số nguyên. Một chuyển đổi cụ thể có thể được chỉ định khi truyền nhưng mặc định chỉ đơn giản là diễn giải lại các bit.

Đây là một ví dụ về việc truyền từ trang tài liệu Microsoft này .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Bạn có thể làm điều tương tự và chuyển một cái gì đó thực hiện giao diện sang một triển khai cụ thể của giao diện đó, nhưng bạn không nên vì điều đó sẽ dẫn đến lỗi hoặc hành vi không mong muốn nếu sử dụng một cách thực hiện khác mà bạn mong đợi.


"Đúc là chuyển đổi một cái gì đó từ loại này sang loại khác." - Không. Đúc là chuyển đổi rõ ràng một cái gì đó từ loại này sang loại khác. (Cụ thể, "cast" là tên của cú pháp được sử dụng để chỉ định chuyển đổi đó.) Chuyển đổi ngầm định không phải là phôi. "Một chuyển đổi cụ thể có thể được chỉ định khi truyền nhưng mặc định chỉ đơn giản là diễn giải lại các bit." -- Chắc chắn không. Có rất nhiều chuyển đổi, cả ẩn và rõ ràng, liên quan đến những thay đổi đáng kể đối với các mẫu bit.
hvd

@hvd Tôi đã thực hiện một điều chỉnh bây giờ liên quan đến nhân chứng đúc. Khi tôi nói mặc định chỉ đơn giản là diễn giải lại các bit, tôi đã cố gắng thể hiện rằng nếu bạn tự tạo kiểu cho riêng mình, thì trong trường hợp diễn viên được xác định tự động, khi bạn chuyển nó sang loại khác, các bit sẽ được giải thích lại . Trong Animal/ Giraffeví dụ ở trên, nếu bạn thực hiện Animal a = (Animal)g;các bit sẽ được giải thích lại, (mọi dữ liệu cụ thể của Hươu cao cổ sẽ được hiểu là "không phải là một phần của đối tượng này").
Ryan1729

Bất chấp những gì hvd nói, mọi người rất thường sử dụng thuật ngữ "diễn viên" trong tham chiếu để chuyển đổi ngầm định; xem ví dụ: https://www.google.com.vn/search?q="implicit+cast"&tbm=bks . Về mặt kỹ thuật tôi nghĩ sẽ đúng hơn khi bảo lưu thuật ngữ "diễn viên" cho các chuyển đổi rõ ràng, miễn là bạn không bị nhầm lẫn khi người khác sử dụng nó theo cách khác.
ruakh

0

5 xu của tôi:

Tất cả những ví dụ đó là Ok, nhưng chúng không phải là ví dụ trong thế giới thực và không thể hiện ý định của thế giới thực.

Tôi không biết C # vì vậy tôi sẽ đưa ra ví dụ trừu tượng (trộn giữa Java và C ++). Hy vọng điều đó ổn.

Giả sử bạn có giao diện iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Bây giờ giả sử có rất nhiều triển khai:

  • DynamicArrayList - sử dụng mảng phẳng, nhanh để chèn và xóa ở cuối.
  • LinkedList - sử dụng danh sách liên kết kép, nhanh chóng để chèn ở phía trước và cuối.
  • AVLTreeList - sử dụng AVL Tree, nhanh chóng để làm mọi thứ, nhưng sử dụng nhiều bộ nhớ
  • SkipList - sử dụng SkipList, nhanh để làm mọi thứ, chậm hơn AVL Tree, nhưng sử dụng ít bộ nhớ hơn.
  • HashList - sử dụng HashTable

Người ta có thể nghĩ về rất nhiều triển khai khác nhau.

Bây giờ giả sử chúng ta có đoạn mã sau:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Nó cho thấy rõ ý định của chúng tôi mà chúng tôi muốn sử dụng iList. Chắc chắn rằng chúng tôi không còn có thể thực hiện DynamicArrayListcác hoạt động cụ thể, nhưng chúng tôi cần một iList.

Xem xét mã sau:

iList list = factory.getList();

Bây giờ chúng tôi thậm chí không biết việc thực hiện là gì. Ví dụ cuối cùng này thường được sử dụng trong xử lý ảnh khi bạn tải một số tệp từ đĩa và bạn không cần loại tệp của nó (gif, jpeg, png, bmp ...), nhưng tất cả những gì bạn muốn là thực hiện một số thao tác hình ảnh (lật, quy mô, lưu dưới dạng png ở cuối).


0

Bạn có một giao diện ISomeClass và một đối tượng myObject mà bạn không biết gì từ mã của mình ngoại trừ việc nó được khai báo để triển khai ISomeClass.

Bạn có một lớp someClass mà bạn biết thực hiện giao diện ISomeClass. Bạn biết rằng vì nó được tuyên bố là sẽ triển khai ISomeClass hoặc bạn tự thực hiện nó để triển khai ISomeClass.

Có gì sai khi truyền myClass sang someClass? Hai điều sai. Thứ nhất, bạn không thực sự biết rằng myClass là thứ gì đó có thể được chuyển đổi thành someClass (một thể hiện của someClass hoặc một lớp con của someClass), vì vậy diễn viên có thể gặp trục trặc. Hai, bạn không cần phải làm điều này. Bạn nên làm việc với myClass được khai báo là iSomeClass và sử dụng các phương thức ISomeClass.

Điểm mà bạn nhận được một đối tượng someClass là khi một phương thức giao diện được gọi. Tại một số điểm, bạn gọi myClass.myMethod (), được khai báo trong giao diện, nhưng có triển khai trong someClass và tất nhiên có thể trong nhiều lớp khác đang triển khai ISomeClass. Nếu một cuộc gọi kết thúc bằng mã someClass.myMethod của bạn, thì bạn biết rằng bản thân là một thể hiện của someClass và tại thời điểm đó, nó hoàn toàn ổn và thực sự chính xác để sử dụng nó làm đối tượng của một số đối tượng. Tất nhiên, nếu nó thực sự là một thể hiện của OtherClass chứ không phải là AnotherClass, thì bạn sẽ không đến được mã someClass.

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.