Lặp lại một lớp đại diện cho một bộ sưu tập: IEnumerable <T> vs phương thức tùy chỉnh


9

Tôi thường thấy mình cần phải thực hiện một lớp học là một bảng liệt kê / bộ sưu tập một cái gì đó. Xem xét cho chủ đề này ví dụ giả định trong IniFileContentđó là một bảng liệt kê / bộ sưu tập các dòng.

Lý do lớp này phải tồn tại trong cơ sở mã của tôi là vì tôi muốn tránh logic kinh doanh được lan truyền khắp nơi (= đóng gói where) và tôi muốn thực hiện theo cách có thể hướng đối tượng nhất.

Thông thường tôi sẽ thực hiện nó như sau:

public sealed class IniFileContent : IEnumerable<string>
{
    private readonly string _filepath;
    public IniFileContent(string filepath) => _filepath = filepath;
    public IEnumerator<string> GetEnumerator()
    {
        return File.ReadLines(_filepath)
                   .Where(l => !l.StartsWith(";"))
                   .GetEnumerator();
    }
    public IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Tôi chọn thực hiện IEnumerable<string>vì nó làm cho việc sử dụng của nó thuận tiện:

foreach(var line in new IniFileContent(...))
{
    //...
}

Tuy nhiên tôi đang tự hỏi nếu làm như vậy "bóng tối" ý định lớp học? Khi nhìn vào IniFileContentgiao diện, người ta chỉ thấy Enumerator<string> GetEnumerator(). Tôi nghĩ nó không rõ ràng về dịch vụ mà lớp đang thực sự cung cấp.

Hãy xem xét sau đó thực hiện thứ hai này:

public sealed class IniFileContent2
{
    private readonly string _filepath;
    public IniFileContent2(string filepath) => _filepath = filepath;
    public IEnumerable<string> Lines()
    {
        return File.ReadLines(_filepath)
                   .Where(l => !l.StartsWith(";"));
    }
}

Mà được sử dụng ít thuận tiện hơn (nhân tiện, nhìn thấy new X().Y()cảm giác như có gì đó không ổn với thiết kế lớp):

foreach(var line in new IniFileContent2(...).Lines())
{
    //...
}

Nhưng với một giao diện rõ ràng IEnumerable<string> Lines()làm rõ ràng những gì lớp này thực sự có thể làm.

Việc thực hiện nào bạn sẽ thúc đẩy và tại sao? Ngụ ý, nó có phải là một thực tiễn tốt để thực hiện IEnumerable để đại diện cho một liệt kê của một cái gì đó?

Tôi không tìm kiếm câu trả lời về cách:

  • đơn vị kiểm tra mã này
  • tạo một hàm tĩnh thay vì một lớp
  • làm cho mã này dễ bị tiến hóa logic kinh doanh trong tương lai
  • tối ưu hóa hiệu suất

ruột thừa

Đây là loại mã thực sự sống trong cơ sở mã của tôi, thực hiệnIEnumerable

public class DueInvoices : IEnumerable<DueInvoice>
{
    private readonly IEnumerable<InvoiceDto> _invoices;
    private readonly IEnumerable<ReminderLevel> _reminderLevels;
    public DueInvoices(IEnumerable<InvoiceDto> invoices, IEnumerable<ReminderLevel> reminderLevels)
    {
        _invoices = invoices;
        _reminderLevels = reminderLevels;
    }
    public IEnumerator<DueInvoice> GetEnumerator() => _invoices.Where(invoice => invoice.DueDate < DateTime.Today && !invoice.Paid)
                                                               .Select(invoice => new DueInvoice(invoice, _reminderLevels))
                                                               .GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


2
Bỏ phiếu để đóng câu hỏi này vì câu hỏi cập nhật đang hỏi ý kiến ​​về hai phong cách mã hóa và vì vậy bây giờ hoàn toàn dựa trên ý kiến.
David Arno

1
@DavidArno Tôi không đồng ý với ý nghĩa rằng việc hỏi liệu điều gì đó có phải là một thực tiễn tốt không không hoàn toàn dựa trên ý kiến ​​mà còn dựa trên sự kiện, kinh nghiệm và chuẩn mực.
Phát hiện

Cả hai mô hình đều không phù hợp với tôi: sự phụ thuộc vào các lớp cụ thể, sự phụ thuộc cứng vào hệ thống tệp, các quy ước xử lý mơ hồ (tôi tin rằng bạn có thể bị rò rỉ, thưa ngài), ngữ nghĩa trực quan của việc sử dụng newcho trường hợp sử dụng này, khó kiểm tra đơn vị, mơ hồ khi tôi / O ngoại lệ có thể xảy ra, vv Tôi xin lỗi, tôi không cố tỏ ra thô lỗ. Tôi nghĩ rằng tôi đã quá quen với những lợi ích của việc tiêm phụ thuộc .
John Wu

@JohnWu Xin vui lòng xem đây là một ví dụ giả định (vụng về tôi chọn thú nhận). Nếu bạn muốn trả lời câu hỏi này (không cố tỏ ra thô lỗ) hãy xem xét việc tập trung vào quyết định thiết kế của việc thực hiện IEnumerable hay không cho một lớp là một IniFileContent.
Phát hiện

Câu trả lời:


13

Tôi đang xem xét cách tiếp cận của bạn trước khi đề xuất một cách tiếp cận hoàn toàn khác. Tôi thích cách tiếp cận khác nhau nhưng có vẻ quan trọng để giải thích tại sao cách tiếp cận của bạn có sai sót.


Tôi chọn thực hiện IEnumerable<string>vì nó làm cho việc sử dụng của nó thuận tiện

Thuận tiện không nên vượt quá tính chính xác.

Tôi tự hỏi nếu MyFilelớp học của bạn sẽ chứa nhiều logic hơn thế này; bởi vì điều đó sẽ ảnh hưởng đến tính chính xác của câu trả lời này. Tôi đặc biệt quan tâm đến:

.Where(l => ...) //some business logic for filtering

bởi vì nếu điều này đủ phức tạp hoặc năng động, bạn đang ẩn logic đó trong một lớp có tên không tiết lộ rằng nó lọc nội dung trước khi phục vụ nó cho người tiêu dùng.
Một phần trong tôi hy vọng / giả định rằng logic bộ lọc này được dự định là mã hóa cứng (ví dụ: bộ lọc đơn giản bỏ qua các dòng nhận xét, ví dụ như cách các tệp .ini coi các dòng bắt đầu #là một nhận xét) và không phải là quy tắc dành riêng cho tệp.


public class MyFile : IEnumerable<string>

Có một cái gì đó thực sự đau buồn về việc có một số ít ( File) đại diện cho số nhiều ( IEnumerable). Một tập tin là một thực thể số ít. Nó bao gồm nhiều hơn chỉ là nội dung của nó. Nó cũng chứa siêu dữ liệu (tên tệp, phần mở rộng, ngày tạo, ngày sửa đổi, ...).

Một con người nhiều hơn tổng số con cái của nó. Một chiếc xe là nhiều hơn tổng số các bộ phận của nó. Một bức tranh không chỉ là một bộ sưu tập sơn và vải. Và một tập tin là nhiều hơn một bộ sưu tập các dòng.


Nếu tôi giả định rằng MyFilelớp của bạn sẽ không bao giờ chứa nhiều logic hơn chỉ liệt kê các dòng này (và Wherechỉ áp dụng một bộ lọc mã hóa tĩnh đơn giản), thì cái bạn đã có ở đây là cách sử dụng tên "tệp" khó hiểu và chỉ định của nó . Điều này có thể dễ dàng được sửa chữa bằng cách đổi tên lớp thành FileContent. Nó giữ lại cú pháp dự định của bạn:

foreach(var line in new FileContent(@"C:\Folder\File.txt"))

Nó cũng có ý nghĩa hơn từ quan điểm ngữ nghĩa. Nội dung của một tập tin có thể được chia thành các dòng riêng biệt. Điều này vẫn cho rằng nội dung của tệp là văn bản chứ không phải nhị phân, nhưng điều đó đủ công bằng.


Tuy nhiên, nếu MyFilelớp của bạn sẽ chứa nhiều logic hơn, tình huống sẽ thay đổi. Có một vài cách điều này có thể xảy ra:

  • Bạn bắt đầu sử dụng lớp này để thể hiện siêu dữ liệu của tệp, không chỉ nội dung của nó.

Khi bạn bắt đầu làm điều này, thì tệp đại diện cho tệp trong thư mục , không chỉ là nội dung của nó.
Cách tiếp cận chính xác ở đây là những gì bạn đã làm MyFile2.

  • Bộ Where()lọc bắt đầu có logic bộ lọc phức tạp không được mã hóa cứng, ví dụ: khi các tệp khác nhau bắt đầu được lọc khác nhau.

Khi bạn bắt đầu thực hiện việc này, các tệp bắt đầu có danh tính của riêng chúng, vì chúng có bộ lọc tùy chỉnh riêng. Điều này có nghĩa là lớp học của bạn đã trở thành FileTypenhiều hơn a FileContent. Hai hành vi cần được tách riêng hoặc kết hợp bằng cách sử dụng bố cục (có lợi cho MyFile2cách tiếp cận của bạn ) hoặc tốt nhất là cả hai (các lớp riêng biệt cho hành vi FileTypeFileContenthành vi, sau đó có cả hai thành phần trong MyFilelớp).


Một đề nghị hoàn toàn khác nhau.

Khi nó đứng, cả bạn MyFileMyFile2tồn tại hoàn toàn để cung cấp cho bạn một trình bao bọc xung quanh .Where(l => ...)bộ lọc của bạn . Thứ hai, bạn đang tạo một lớp hiệu quả để bao bọc một phương thức tĩnh ( File.ReadLines()), đây không phải là một cách tiếp cận tuyệt vời.

Ở một bên, tôi không hiểu lý do tại sao bạn chọn để tạo lớp học của bạn sealed. Nếu có bất cứ điều gì, tôi mong rằng tính kế thừa sẽ là tính năng lớn nhất của nó: các lớp dẫn xuất khác nhau với logic lọc khác nhau (giả sử rằng nó phức tạp hơn thay đổi giá trị đơn giản, vì không nên sử dụng kế thừa chỉ để thay đổi một giá trị)

Tôi sẽ viết lại cả lớp của bạn như sau:

foreach(var line in File.ReadLines(...).Where(l => ...))

Nhược điểm duy nhất của phương pháp đơn giản hóa này là bạn phải lặp lại Where()bộ lọc mỗi lần bạn muốn truy cập nội dung của tệp. Tôi đồng ý rằng đó là không mong muốn.

Tuy nhiên, có vẻ như quá mức khi bạn muốn tạo một Where(l => ...)câu lệnh có thể sử dụng lại , sau đó bạn cũng buộc lớp đó phải thực hiện File.ReadLines(...). Bạn đang bó nhiều hơn bạn thực sự cần.

Thay vì cố gắng bọc phương thức tĩnh trong một lớp tùy chỉnh, tôi nghĩ rằng nó phù hợp hơn nhiều nếu bạn bọc nó trong một phương thức tĩnh của chính nó:

public static IEnumerable<string> GetFilteredFileContent(string filePath)
{
    return File.ReadLines(filePath).Where(l => ...);
}

Giả sử bạn có các bộ lọc khác nhau, bạn có thể chuyển bộ lọc thích hợp làm tham số aa. Tôi sẽ chỉ cho bạn một ví dụ có thể xử lý nhiều bộ lọc, có thể xử lý mọi thứ bạn cần làm trong khi tối đa hóa khả năng sử dụng lại:

public static class MyFile
{
    public static Func<string, bool> IgnoreComments = 
                  (l => !l.StartsWith("#"));

    public static Func<string, bool> OnlyTakeComments = 
                  (l => l.StartsWith("#"));

    public static Func<string, bool> IgnoreLinesWithTheLetterE = 
                  (l => !l.ToLower().contains("e"));

    public static Func<string, bool> OnlyTakeLinesWithTheLetterE = 
                  (l => l.ToLower().contains("e"));

    public static IEnumerable<string> ReadLines(string filePath, params Func<string, bool>[] filters)
    {
        var lines = File.ReadLines(filePath).Where(l => ...);

        foreach(var filter in filters)
            lines = lines.Where(filter);

        return lines;
    }
}

Và cách sử dụng của nó:

MyFile.ReadLines("path", MyFile.IgnoreComments, MyFile.OnlyTakeLinesWithTheLetterE);

Đây chỉ là một ví dụ về máy nghiền có nghĩa là để chứng minh điểm mà các phương thức tĩnh có ý nghĩa hơn là tạo một lớp ở đây.

Đừng để bị cuốn vào các chi tiết cụ thể của việc triển khai các bộ lọc. Bạn có thể thực hiện chúng theo cách bạn muốn (cá nhân tôi chỉ thích tham số Func<>vì khả năng mở rộng vốn có và khả năng thích ứng của nó để tái cấu trúc). Nhưng vì bạn không thực sự là một ví dụ về các bộ lọc bạn định sử dụng, tôi đã đưa ra một giả định để cho bạn thấy một ví dụ khả thi.


nhìn thấy new X().Y()cảm giác như có gì đó không ổn với thiết kế lớp)

Theo cách tiếp cận của bạn, bạn có thể làm cho nó new X().Yít lưới hơn.

Tuy nhiên, tôi nghĩ rằng việc bạn không thích new X().Y()chứng minh điểm mà bạn cảm thấy như một lớp học không được bảo hành ở đây, nhưng một phương pháp là; mà chỉ có thể được biểu diễn mà không có lớp bằng cách tĩnh.


Tôi thực sự đánh giá cao phản hồi của bạn và chủ yếu là suy nghĩ của bạn dẫn bạn đổi tên lớp FileContent. Nó cho thấy ví dụ của tôi tệ như thế nào. Cũng thực sự tồi tệ khi tôi đưa ra câu hỏi của mình mà hoàn toàn thất bại trong việc thu thập loại phản hồi mà tôi mong đợi. Tôi đã chỉnh sửa nó với hy vọng làm cho ý định của tôi rõ ràng hơn.
Phát hiện

@Flater, ngay cả khi GetFilteredContent(string filename)được thực thi bằng mã với tên tệp hầu hết thời gian, tôi sẽ đặt phần chính của tác phẩm vào một phương thức lấy Streamhoặc TextReaderđể nó giúp việc kiểm tra dễ dàng hơn nhiều. Vì vậy, GetFilteredContent(string)sẽ là một bọc xung quanh GetFilteredContent(TextReader reader). Nhưng A đồng ý với đánh giá của bạn.
Berin Loritsch 17/07/18

4

Theo tôi, các vấn đề với cả hai phương pháp là:

  1. Bạn đang gói gọn File.ReadLines, điều này làm cho việc kiểm tra đơn vị khó hơn mức cần thiết,
  2. Một thể hiện lớp mới phải được tạo ra mỗi khi tệp được liệt kê, chỉ để lưu trữ đường dẫn dưới dạng _filepath.

Vì vậy, tôi khuyên bạn nên biến nó thành một phương thức tĩnh, được truyền IEnumerable<string>hoặc Streamđại diện cho nội dung tệp:

public static GetFilteredLines(IEnumerable<string> fileContents)
    => fileContents.Where(l => ...);

Sau đó, nó được gọi thông qua:

var filteredLines = GetFilteredLines(File.ReadLines(filePath));

Điều này tránh đặt tải không cần thiết lên heap và giúp đơn vị kiểm tra phương pháp dễ dàng hơn nhiều.


Tôi đồng ý với bạn, tuy nhiên đó hoàn toàn không phải là loại phản hồi mà tôi mong đợi (câu hỏi của tôi được đưa ra kém theo nghĩa đó). Xem câu hỏi chỉnh sửa của tôi.
Phát hiện

@ Phát hiện, wow, bạn có thực sự hạ thấp câu trả lời cho câu hỏi của bạn vì bạn đã hỏi sai câu hỏi? Đó là thấp.
David Arno

Có tôi đã làm cho đến khi tôi nhận ra vấn đề là câu hỏi của tôi. Ngoại trừ việc bây giờ tôi không thể hủy bỏ downvote của mình miễn là câu trả lời của bạn không được chỉnh sửa ...: - / Hãy chấp nhận lời xin lỗi của tôi (hoặc giả chỉnh sửa câu trả lời của bạn để tôi vui lòng xóa downvote của tôi).
Phát hiện

@ Phát hiện, vấn đề với câu hỏi của bạn bây giờ là bạn đang hỏi ý kiến ​​về hai phong cách mã hóa, làm cho câu hỏi lạc đề. Ngay cả với câu hỏi cập nhật của bạn, câu trả lời của tôi vẫn giống nhau: cả hai thiết kế đều thiếu sót vì hai lý do tôi chỉ định và do đó, đây không phải là một giải pháp tốt trong quan điểm của tôi.
David Arno

Tôi hoàn toàn đồng ý rằng thiết kế trong ví dụ của tôi là thiếu sót nhưng đó không phải là vấn đề của tôi (vì đây là một ví dụ giả định), nó chỉ là một cái cớ để giới thiệu cả hai cách tiếp cận này.
Phát hiện

2

Ví dụ trong thế giới thực mà bạn đã chứng minh, DueInvoicescho vay rất rõ với khái niệm rằng đây là một bộ sưu tập hóa đơn hiện đang đáo hạn. Tôi hiểu hoàn toàn làm thế nào các ví dụ giả tạo có thể khiến mọi người bị cuốn theo các thuật ngữ bạn đã sử dụng so với khái niệm bạn đang cố gắng truyền đạt. Bản thân tôi đã nhiều lần bực bội về điều đó.

Điều đó nói rằng, nếu mục đích của lớp hoàn toàn là một IEnumerable<T>, và không cung cấp bất kỳ logic nào khác, tôi phải đặt câu hỏi liệu bạn có cần cả một lớp hay chỉ đơn giản là có thể cung cấp một phương thức của một lớp khác. Ví dụ:

public class Invoices
{
    // ... skip all the other stuff about Invoices

    public IEnumerable<Invoice> GetDueItems()
    {
         foreach(var line in File.ReadLines(_invoicesFile))
         {
             var invoice = ReadInvoiceFrom(line);
             if (invoice.PaymentDue <= DateTime.UtcNow)
             {
                 yield return invoice;
             }
         }
    }
}

Công yield returnviệc khi bạn không thể chỉ bọc một truy vấn LINQ hoặc nhúng logic sẽ dễ theo dõi hơn. Tùy chọn khác chỉ đơn giản là trả về truy vấn LINQ:

public class Invoices
{
    // ... skip all the other stuff about invoices

    public IEnumerable<Invoice> GetDueItems()
    {
        return from Invoice invoice in GetAllItems()
               where invoice.PaymentDue <= DateTime.UtcNow
               select invoice;
    }
}

Trong cả hai trường hợp này, bạn không cần một lớp bao bọc đầy đủ. Bạn chỉ cần cung cấp một phương thức và iterator về cơ bản được xử lý cho bạn.

Lần duy nhất mà tôi cần một lớp đầy đủ để xử lý phép lặp là khi tôi phải rút các đốm màu ra khỏi cơ sở dữ liệu trong một truy vấn chạy dài. Tiện ích này được trích xuất một lần để chúng tôi có thể di chuyển dữ liệu ở nơi khác. Có một số điều kỳ lạ tôi gặp phải với cơ sở dữ liệu khi tôi cố gắng truyền phát nội dung ra bằng cách sử dụng yield return. Nhưng điều đó đã biến mất khi tôi thực sự thực hiện tùy chỉnh của mình IEnumerator<T>để kiểm soát tốt hơn khi tài nguyên được dọn sạch. Đây là ngoại lệ chứ không phải là quy tắc.

Vì vậy, trong ngắn hạn, tôi khuyên bạn không nên thực hiện IEnumerable<T>trực tiếp nếu vấn đề của bạn có thể được giải quyết theo một trong những cách được nêu trong mã ở trên. Tiết kiệm chi phí tạo ra điều tra viên một cách rõ ràng khi bạn không thể giải quyết vấn đề theo bất kỳ cách nào khác.


Lý do tại sao tôi tạo một lớp riêng biệt InvoiceDtoxuất phát từ lớp kiên trì và do đó chỉ là một túi dữ liệu, tôi không muốn làm lộn xộn nó với phương pháp liên quan đến kinh doanh. Do đó việc tạo ra DueInvoiceDueInvoices.
Phát hiện

@ Phát hiện, Nó không phải là lớp DTO. Heck, nó có thể là một lớp tĩnh với các phương thức mở rộng. Tất cả những gì tôi đề nghị là trong hầu hết các trường hợp, bạn có thể giảm thiểu mã soạn sẵn của mình và làm cho API dễ tiêu hóa cùng một lúc.
Berin Loritsch 17/07/18

0

Hãy nghĩ về các khái niệm. Mối quan hệ giữa một tập tin và nội dung của nó là gì? Đó là mối quan hệ "có" , không phải là "có" .

Do đó, lớp tệp nên một phương thức / thuộc tính để trả về nội dung. Và điều đó vẫn thuận tiện để gọi:

public IEnumerable<string> GetFilteredContents() { ... }

foreach(string line in myFile.GetFilteredContents() { ... }

Bạn đã đúng về mối quan hệ giữa một tập tin và nội dung của nó. Ví dụ không được suy nghĩ kỹ. Tôi đã thực hiện một số chỉnh sửa cho câu hỏi ban đầu của mình để chính xác những suy nghĩ của tôi.
Phát hiện

Chấp nhận lời xin lỗi của tôi cho downvote không chính đáng, tuy nhiên tôi không thể xóa nó trừ khi bạn chỉnh sửa câu trả lời của mình. : - /
Phát hiện
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.