Mẫu lặp - tại sao điều quan trọng là không để lộ đại diện bên trong?


24

Tôi đang đọc C # Design Design Essentials . Tôi hiện đang đọc về mẫu lặp.

Tôi hoàn toàn hiểu cách thực hiện, nhưng tôi không hiểu tầm quan trọng hoặc thấy trường hợp sử dụng. Trong cuốn sách, một ví dụ được đưa ra khi ai đó cần lấy danh sách các đối tượng. Họ có thể đã làm điều này bằng cách phơi bày một tài sản công cộng, chẳng hạn như IList<T>hoặc một Array.

Cuốn sách viết

Vấn đề với điều này là sự đại diện nội bộ trong cả hai lớp này đã được tiếp xúc với các dự án bên ngoài.

Đại diện nội bộ là gì? Thực tế đó là một arrayhay IList<T>? Tôi thực sự không hiểu tại sao đây là một điều tồi tệ cho người tiêu dùng (lập trình viên gọi đây) để biết ...

Cuốn sách sau đó nói rằng mô hình này hoạt động bằng cách phơi bày GetEnumeratorchức năng của nó , vì vậy chúng ta có thể gọi GetEnumerator()và hiển thị 'danh sách' theo cách này.

Tôi cho rằng mô hình này có một vị trí (giống như tất cả) trong một số tình huống nhất định, nhưng tôi không thấy được ở đâu và khi nào.



4
Bởi vì, việc triển khai có thể muốn thay đổi từ việc sử dụng một mảng, sang sử dụng một danh sách được liên kết mà không yêu cầu thay đổi trong lớp người tiêu dùng. Điều này sẽ là không thể nếu người tiêu dùng biết chính xác lớp trả về.
MTilsted

11
Đó không phải là một điều xấu cho người tiêu dùng biết , đó là một điều xấu cho người tiêu dùng dựa vào . Tôi không thích thuật ngữ ẩn thông tin vì nó khiến bạn tin rằng thông tin đó là "riêng tư" như mật khẩu của bạn thay vì riêng tư như bên trong điện thoại. Các thành phần bên trong được ẩn đi vì chúng không liên quan đến người dùng, không phải vì chúng là một loại bí mật. Tất cả những gì bạn cần biết là quay số ở đây, nói chuyện ở đây, nghe đây.
Thuyền trưởng Man

Giả sử chúng ta có một "giao diện" đẹp Fmà mang lại cho tôi những kiến thức (phương pháp) của a, bc. Tất cả đều tốt và tốt, có thể có nhiều điều khác biệt nhưng chỉ Fvới tôi. Hãy nghĩ về phương thức này là "các ràng buộc" hoặc các điều khoản của hợp đồng Fcam kết các lớp thực hiện. Giả sử rằng chúng ta thêm một dmặc dù, bởi vì chúng ta cần nó. Điều này thêm một ràng buộc bổ sung, mỗi lần chúng tôi làm điều này, chúng tôi áp đặt ngày càng nhiều hơn cho Fs. Cuối cùng (trường hợp xấu nhất) chỉ có một điều có thể là Fvì vậy chúng tôi cũng có thể không có nó. Và Fràng buộc rất nhiều chỉ có một cách để làm điều đó.
Alec Teal

@captainman, thật là một nhận xét tuyệt vời. Vâng, tôi hiểu tại sao phải 'trừu tượng' nhiều thứ, nhưng khúc mắc về việc biết và dựa vào là ... Thành thật mà nói .... Rực rỡ. Và một sự khác biệt quan trọng mà tôi đã không xem xét cho đến khi đọc bài viết của bạn.
MyDaftQuestions

Câu trả lời:


56

Phần mềm là một trò chơi của những lời hứa và đặc quyền. Không bao giờ là một ý tưởng tốt để hứa nhiều hơn bạn có thể cung cấp, hoặc nhiều hơn nhu cầu cộng tác viên của bạn.

Điều này đặc biệt áp dụng cho các loại. Điểm của việc viết một bộ sưu tập lặp là người dùng của nó có thể lặp lại nó - không hơn, không kém. Phơi bày loại cụ thểArray thường tạo ra nhiều lời hứa bổ sung, ví dụ: bạn có thể sắp xếp bộ sưu tập theo chức năng do chính bạn chọn, chưa kể đến việc một người bình thường Arraycó thể sẽ cho phép cộng tác viên thay đổi dữ liệu được lưu trữ bên trong nó.

Ngay cả khi bạn nghĩ rằng đây là một điều tốt ("Nếu trình kết xuất thông báo rằng tùy chọn xuất mới bị thiếu, thì nó chỉ có thể vá nó ngay trong! Neat!"), Nói chung, điều này làm giảm sự gắn kết của cơ sở mã, làm cho nó khó hơn lý do về - và làm cho mã dễ dàng lý do là mục tiêu quan trọng hàng đầu của công nghệ phần mềm.

Bây giờ, nếu cộng tác viên của bạn cần quyền truy cập vào một số điều để họ được đảm bảo không bỏ lỡ bất kỳ trong số họ, bạn thực hiện một Iterablegiao diện và chỉ hiển thị những phương thức mà giao diện này tuyên bố. Bằng cách đó, vào năm tới khi cấu trúc dữ liệu tốt hơn và hiệu quả hơn xuất hiện trong thư viện tiêu chuẩn của bạn, bạn sẽ có thể tắt mã cơ bản và hưởng lợi từ nó mà không cần sửa mã khách hàng của mình ở mọi nơi . Có những lợi ích khác để không hứa hẹn nhiều hơn mức cần thiết, nhưng riêng cái này thì lớn đến mức trong thực tế, không cần cái nào khác.


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...- Rực rỡ. Tôi chỉ không nghĩ về nó. Vâng, nó mang lại một 'lần lặp' và chỉ, một lần lặp!
MyDaftQuestions

18

Che giấu việc thực hiện là một nguyên tắc cốt lõi của OOP và là một ý tưởng tốt trong tất cả các mô hình, nhưng nó đặc biệt quan trọng đối với các trình vòng lặp (hoặc bất cứ điều gì chúng được gọi bằng ngôn ngữ cụ thể đó) trong các ngôn ngữ hỗ trợ phép lặp lười biếng.

Vấn đề với việc phơi bày các loại lặp cụ thể - hoặc thậm chí các giao diện như IList<T>- không nằm trong các đối tượng phơi bày chúng, mà là trong các phương thức sử dụng chúng. Ví dụ: giả sử bạn có chức năng in danh sách Foos:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

Bạn chỉ có thể sử dụng chức năng đó để in danh sách foo - nhưng chỉ khi chúng thực hiện IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

Nhưng nếu bạn muốn in mọi mục được lập chỉ mục chẵn của danh sách thì sao? Bạn sẽ cần tạo một danh sách mới:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

Điều này khá dài, nhưng với LINQ, chúng ta có thể thực hiện nó thành một lớp lót, điều này thực sự dễ đọc hơn:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

Bây giờ, bạn đã nhận thấy .ToList()cuối cùng? Điều này chuyển đổi truy vấn lười biếng thành một danh sách, vì vậy chúng ta có thể chuyển nó đến PrintFoos. Điều này đòi hỏi phải phân bổ danh sách thứ hai và hai lần chuyển vào các mục (một trong danh sách đầu tiên để tạo danh sách thứ hai và một danh sách khác trong danh sách thứ hai để in). Ngoài ra, nếu chúng ta có điều này:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

Điều gì nếu fooscó hàng ngàn mục? Chúng tôi sẽ phải đi qua tất cả chúng và phân bổ một danh sách khổng lồ chỉ để in 6 trong số chúng!

Nhập số - Phiên bản C # của Mẫu lặp. Thay vì có chức năng của chúng tôi chấp nhận một danh sách, chúng tôi làm cho nó chấp nhận Enumerable:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

Bây giờ Print6Fooscó thể lặp đi lặp lại một cách lười biếng trong 6 mục đầu tiên của danh sách và chúng ta không cần phải chạm vào phần còn lại của danh sách.

Không để lộ đại diện nội bộ là chìa khóa ở đây. Khi Print6Foosđược chấp nhận một danh sách, chúng tôi phải đưa ra một danh sách - thứ gì đó hỗ trợ truy cập ngẫu nhiên - và do đó chúng tôi phải phân bổ một danh sách, vì chữ ký không đảm bảo rằng nó sẽ chỉ lặp đi lặp lại trên đó. Bằng cách ẩn việc thực hiện, chúng ta có thể dễ dàng tạo ra một Enumerableđối tượng hiệu quả hơn , chỉ hỗ trợ những gì chức năng thực sự cần.


6

Phơi bày đại diện nội bộ hầu như không bao giờ là một ý tưởng tốt. Nó không chỉ làm cho mã khó hơn để lý do, mà còn khó bảo trì hơn. Hãy tưởng tượng bạn đã chọn bất kỳ sự bồi thường nội bộ nào - giả sử một IList<T>- và tiết lộ nội bộ này. Bất kỳ ai sử dụng mã của bạn đều có thể truy cập Danh sách và mã có thể dựa vào biểu diễn bên trong là Danh sách.

Vì bất kỳ lý do gì, bạn đang quyết định thay đổi đại diện nội bộ sang IAmWhatever<T>một thời điểm nào đó sau này. Thay vào đó, chỉ cần thay đổi phần bên trong của lớp, bạn sẽ phải thay đổi mọi dòng mã và phương thức dựa trên biểu diễn bên trong thuộc loại IList<T>. Điều này là cồng kềnh và dễ bị lỗi nhất, khi bạn là người duy nhất sử dụng lớp, nhưng nó có thể phá vỡ mã người khác sử dụng lớp của bạn. Nếu bạn chỉ hiển thị một tập hợp các phương thức công khai mà không để lộ bất kỳ nội bộ nào, bạn có thể đã thay đổi biểu diễn bên trong mà không có bất kỳ dòng mã nào bên ngoài lớp của bạn nhận thông báo, hoạt động như thể không có gì thay đổi.

Đây là lý do tại sao đóng gói là một trong những khía cạnh quan trọng nhất của bất kỳ thiết kế phần mềm không cần thiết nào.


6

Bạn càng ít nói, bạn càng ít phải nói.

Bạn càng ít hỏi, bạn càng ít phải nói.

Nếu mã của bạn chỉ lộ ra IEnumerable<T>, chỉ hỗ trợ GetEnumrator()thì nó có thể được thay thế bằng bất kỳ mã nào khác có thể hỗ trợ IEnumerable<T>. Điều này thêm linh hoạt.

Nếu mã của bạn chỉ sử dụng, IEnumerable<T>nó có thể được hỗ trợ bởi bất kỳ mã nào thực hiện IEnumerable<T>. Một lần nữa, có thêm sự linh hoạt.

Tất cả các ví dụ linq-to-object, chỉ phụ thuộc vào IEnumerable<T>. Mặc dù nó xử lý nhanh một số cách triển khai cụ thể của IEnumerable<T>nó thực hiện điều này theo cách thử nghiệm và sử dụng luôn có thể dự phòng chỉ sử dụng GetEnumerator()IEnumerator<T>thực hiện trả về. Điều này cho phép nó linh hoạt hơn nhiều so với khi nó được xây dựng trên đầu các mảng hoặc danh sách.


Đẹp trả lời rõ ràng. Rất thích hợp ở chỗ bạn giữ nó ngắn gọn, và do đó hãy làm theo lời khuyên của bạn trong câu đầu tiên. Bravo!
Daniel Hollinrake

0

Một từ ngữ tôi thích sử dụng gương Nhận xét của @CaptainMan: "Sẽ tốt cho người tiêu dùng biết chi tiết nội bộ. Thật tệ khi khách hàng cần biết chi tiết nội bộ."

Nếu một khách hàng phải nhập arrayhoặc IList<T>trong mã của họ, khi những loại đó là chi tiết nội bộ, người tiêu dùng phải làm cho họ đúng. Nếu họ sai, người tiêu dùng có thể không biên dịch, hoặc thậm chí nhận được kết quả sai. Điều này mang lại các hành vi tương tự như các câu trả lời khác của "luôn ẩn các chi tiết triển khai vì bạn không nên phơi bày chúng", nhưng bắt đầu khai thác những lợi thế của việc không phơi bày. Nếu bạn không bao giờ tiết lộ một chi tiết triển khai, khách hàng của bạn không bao giờ có thể đặt mình vào một ràng buộc bằng cách sử dụng nó!

Tôi thích nghĩ về nó trong ánh sáng này bởi vì nó mở ra khái niệm che giấu việc thực hiện theo sắc thái ý nghĩa, thay vì một cách hà khắc "luôn che giấu chi tiết thực hiện của bạn." Có rất nhiều tình huống trong đó phơi bày việc thực hiện là hoàn toàn hợp lệ. Hãy xem xét trường hợp của trình điều khiển thiết bị thời gian thực, trong đó khả năng bỏ qua các lớp trừu tượng trong quá khứ và chọc byte vào đúng chỗ có thể là sự khác biệt giữa việc tạo thời gian hay không. Hoặc xem xét một môi trường phát triển nơi bạn không có thời gian để tạo ra "API tốt" và cần sử dụng mọi thứ ngay lập tức. Thường thì việc phơi bày các API cơ bản trong các môi trường như vậy có thể là sự khác biệt giữa việc đưa ra thời hạn hay không.

Tuy nhiên, khi bạn bỏ lại những trường hợp đặc biệt đó và xem xét những trường hợp tổng quát hơn, nó bắt đầu trở nên ngày càng quan trọng hơn để che giấu việc thực hiện. Phơi bày chi tiết thực hiện giống như dây thừng. Được sử dụng đúng cách, bạn có thể làm tất cả mọi thứ với nó, như leo lên những ngọn núi cao. Được sử dụng không đúng cách, và nó có thể buộc bạn trong một chiếc thòng lọng. Bạn càng ít biết về cách sản phẩm của bạn sẽ được sử dụng, bạn càng phải cẩn thận.

Nghiên cứu điển hình mà tôi thấy hết lần này đến lần khác là một nhà phát triển tạo ra một API không cẩn thận trong việc che giấu các chi tiết triển khai. Nhà phát triển thứ hai, sử dụng thư viện đó, thấy rằng API thiếu một số tính năng chính họ muốn. Thay vì viết một yêu cầu tính năng (có thể có thời gian quay vòng hàng tháng), thay vào đó họ quyết định lạm dụng quyền truy cập của họ vào các chi tiết triển khai ẩn (mất vài giây). Bản thân điều này không tệ, nhưng thường thì nhà phát triển API không bao giờ lên kế hoạch cho việc đó là một phần của API. Sau này, khi họ cải thiện API và thay đổi chi tiết cơ bản đó, họ thấy họ bị xiềng xích với cách triển khai cũ, bởi vì ai đó đã sử dụng nó như một phần của API. Phiên bản tồi tệ nhất của điều này là nơi ai đó đã sử dụng API của bạn để cung cấp tính năng lấy khách hàng làm trung tâm và giờ là công ty của bạn ' tiền lương được gắn với API thiếu sót này! Đơn giản bằng cách tiết lộ chi tiết triển khai khi bạn không biết đủ về khách hàng của mình đã chứng tỏ là đủ dây để buộc một chiếc thòng lọng xung quanh API của bạn!


1
Re: "Hoặc xem xét một môi trường phát triển nơi bạn không có thời gian để tạo ra 'API tốt' và cần sử dụng mọi thứ ngay lập tức": Nếu bạn thấy mình trong một môi trường như vậy và vì một lý do nào đó, bạn không muốn bỏ ( hoặc không chắc chắn nếu bạn làm thế), tôi khuyên bạn nên đọc cuốn sách Death March: The Complete Developer 'Guide Guide để sống sót trong các dự án' Nhiệm vụ bất khả thi ' . Nó có lời khuyên tuyệt vời về chủ đề này. (Ngoài ra, tôi khuyên bạn nên thay đổi suy nghĩ về việc không bỏ cuộc.)
ruakh

@ruakh Tôi thấy điều quan trọng là phải hiểu cách làm việc trong môi trường mà bạn không có thời gian để tạo API tốt. Thành thật mà nói, nhiều như chúng ta yêu thích cách tiếp cận tháp ngà, mã thực sự là những gì thực sự trả các hóa đơn. Chúng ta càng sớm thừa nhận rằng, chúng ta càng sớm có thể bắt đầu tiếp cận phần mềm như một sự cân bằng, cân bằng chất lượng API với mã sản xuất, để phù hợp với môi trường chính xác của chúng ta. Tôi đã thấy quá nhiều nhà phát triển trẻ bị mắc kẹt trong mớ hỗn độn của lý tưởng, không nhận ra họ cần phải uốn cong chúng. (Tôi cũng đã từng là nhà phát triển trẻ .. vẫn đang phục hồi sau 5 năm thiết kế "API tốt")
Cort Ammon - Tái lập lại

1
Mã thực trả các hóa đơn, vâng. Thiết kế API tốt là cách bạn đảm bảo rằng bạn sẽ tiếp tục có thể tạo mã thực. Mã crappy ngừng thanh toán cho chính nó khi một dự án trở nên không thể nhận ra và không thể sửa lỗi mà không tạo ra các lỗi mới. (Và dù sao, đây là một vấn đề nan giải giả. Mã tốt yêu cầu không phải là thời gian nhiều như xương sống .)
ruakh

1
@ruakh Điều tôi đã tìm thấy là có thể tạo mã tốt mà không cần "API tốt". Nếu có cơ hội, các API tốt là tốt, nhưng để coi chúng là một điều cần thiết gây ra nhiều xung đột hơn giá trị của nó. Hãy xem xét: API cho cơ thể con người là khủng khiếp, nhưng chúng tôi vẫn nghĩ rằng chúng là những chiến công nhỏ khá tốt về kỹ thuật =)
Cort Ammon - Phục hồi Monica

Một ví dụ khác mà tôi đưa ra là một trong đó là nguồn cảm hứng cho tranh luận về việc sắp xếp thời gian. Một trong những nhóm tôi làm việc cùng có một số công cụ trình điều khiển thiết bị họ cần để phát triển, với các yêu cầu khó khăn trong thời gian thực. Họ đã có 2 API để làm việc. Một là tốt, một là ít như vậy. API tốt không thể tạo thời gian vì nó che giấu việc thực hiện. API ít hơn cho thấy việc triển khai và khi làm như vậy, cho phép bạn sử dụng các kỹ thuật dành riêng cho bộ xử lý để tạo ra một sản phẩm hoạt động. API tốt đã được đưa lên kệ một cách lịch sự và chúng tiếp tục đáp ứng các yêu cầu về thời gian.
Cort Ammon - Phục hồi Monica

0

Bạn không nhất thiết phải biết đại diện bên trong của dữ liệu của mình là gì - hoặc có thể trở thành trong tương lai.

Giả sử bạn có một nguồn dữ liệu mà bạn biểu diễn dưới dạng một mảng, bạn có thể lặp lại nó với một vòng lặp for thông thường.

Bây giờ nói rằng bạn muốn tái cấu trúc. Sếp của bạn đã yêu cầu nguồn dữ liệu là một đối tượng cơ sở dữ liệu. Hơn nữa, anh ta muốn có tùy chọn lấy dữ liệu từ API, hoặc hashmap, hoặc danh sách được liên kết, hoặc trống từ tính quay, hoặc micrô, hoặc số lần cạo yak thời gian thực được tìm thấy ở Ngoại Mông.

Chà, nếu bạn chỉ có một vòng lặp for, bạn có thể dễ dàng sửa đổi mã của mình để truy cập đối tượng dữ liệu theo cách mà nó dự kiến ​​sẽ được truy cập.

Nếu bạn có 1000 lớp đang cố truy cập vào đối tượng dữ liệu, thì bây giờ bạn có một vấn đề tái cấu trúc lớn.

Trình lặp là một cách tiêu chuẩn để truy cập vào nguồn dữ liệu dựa trên danh sách. Đó là một giao diện phổ biến. Sử dụng nó sẽ làm cho mã nguồn dữ liệu của bạn bất khả tri.

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.