Khi nào nên viết phương thức mở rộng cho các lớp của riêng bạn?


8

Gần đây tôi đã thấy một cơ sở mã có một lớp dữ liệu Addressđược xác định ở đâu đó và sau đó ở một nơi khác:

fun Address.toAnschrift() = let { address ->
    Anschrift().apply {
        // mapping code here...
    }
}

Tôi thấy khó hiểu khi không có phương pháp này trực tiếp trên địa chỉ. Có bất kỳ mô hình được thiết lập hoặc thực hành tốt nhất khi sử dụng các phương pháp mở rộng? Hay cuốn sách về điều đó vẫn phải được viết?

Lưu ý rằng mặc dù cú pháp Kotlin trong ví dụ của tôi, tôi quan tâm đến thực tiễn tốt nhất nói chung vì nó cũng sẽ áp dụng cho C # hoặc các ngôn ngữ khác có các khả năng đó.


4
Với "Anschrift" là một phiên bản tiếng Đức của "địa chỉ", đây dường như là một chuyển đổi cụ thể theo quốc gia của loại địa chỉ trung lập theo quốc gia. Muốn giữ mã quốc gia cụ thể ở một nơi khác có ý nghĩa. Đây không phải là một câu trả lời cho câu hỏi thực tế của bạn, nhưng nó giải thích tại sao họ đã làm nó trong ví dụ này .
R. Schmitz

1
»Các phương thức mở rộng cho phép bạn" thêm "các phương thức vào các loại hiện có mà không cần tạo loại dẫn xuất mới, biên dịch lại hoặc sửa đổi loại gốc« docs.microsoft.com/en-us/dotnet/csharp/programming-guide/ ,. »Đối với thư viện lớp mà bạn đã triển khai, bạn không nên sử dụng các phương thức mở rộng ... Nếu bạn muốn thêm chức năng quan trọng vào thư viện mà bạn sở hữu mã nguồn, bạn nên tuân theo các nguyên tắc .NET Framework tiêu chuẩn để tạo phiên bản lắp ráp« tl; dr nếu bạn sở hữu mã, không cần phương thức mở rộng.
Thomas Junk

Câu trả lời:


15

Các phương pháp mở rộng không cung cấp một cái gì đó không thể được thực hiện theo những cách khác. Chúng là đường cú pháp để làm cho sự phát triển đẹp hơn, mà không mang lại lợi ích kỹ thuật thực sự.


Phương pháp mở rộng tương đối mới. Các tiêu chuẩn và quy ước thường được quyết định bởi ý kiến ​​phổ biến (cộng với một số kinh nghiệm lâu dài về những gì kết thúc là một ý tưởng tồi), và tôi không nghĩ rằng chúng ta có thể vẽ một đường thẳng dứt khoát mà mọi người đều đồng ý.

Tuy nhiên, tôi có thể thấy một số lập luận, dựa trên những điều tôi đã nghe từ đồng nghiệp.

1. Chúng thường thay thế các phương thức tĩnh.

Các phương thức mở rộng phổ biến nhất dựa trên những thứ mà nếu không phải là phương thức tĩnh, không phải là phương thức lớp. Chúng trông giống như các phương thức lớp, nhưng chúng không thực sự.

Các phương thức mở rộng đơn giản không nên được coi là một phương án thay thế cho các phương thức lớp; nhưng đúng hơn là một cách để cung cấp cho các phương thức tĩnh một cách tổng hợp khác một giao diện "phương pháp đo lường lớp".

2. Khi phương thức dự định của bạn là dành riêng cho một dự án tiêu thụ nhưng không phải là thư viện nguồn.

Chỉ vì bạn phát triển cả thư viện và người tiêu dùng không có nghĩa là bạn sẵn sàng đưa logic vào bất cứ nơi nào phù hợp.

Ví dụ:

  • Bạn có một PersonDtolớp học đến từ DataLayerdự án của bạn .
  • WebUIDự án của bạn muốn có thể chuyển đổi PersonDtothành a PersonViewModel.

Bạn không thể (và sẽ không muốn) thêm phương thức chuyển đổi này vào DataLayerdự án của mình . Vì phương thức này nằm trong phạm vi của WebUIdự án, nên nó nằm trong dự án đó.

Phương thức mở rộng cho phép bạn có phương thức này có thể truy cập toàn cầu trong dự án của bạn (và người tiêu dùng có thể có của dự án của bạn) mà không yêu cầu DataLayerthư viện triển khai.

3. Nếu bạn nghĩ rằng các lớp dữ liệu chỉ nên chứa dữ liệu.

Ví dụ, Personlớp của bạn chứa các thuộc tính cho một người. Nhưng bạn muốn có một vài phương thức định dạng một số dữ liệu:

public class Person
{
    //The properties...

    public SecurityQuestionAnswers GetSecurityAnswers()
    {
        return new SecurityQuestionAnswers()
        {
            MothersMaidenName = this.Mother.MaidenName,
            FirstPetsName = this.Pets.OrderbyDescending(x => x.Date).FirstOrDefault()?.Name
        };
    }
}

Tôi đã thấy nhiều đồng nghiệp ghét ý tưởng trộn dữ liệu và logic trong một lớp. Tôi không hoàn toàn đồng ý với những điều đơn giản như định dạng dữ liệu, nhưng tôi thừa nhận rằng trong ví dụ trên, cảm thấy bẩn Personkhi phải phụ thuộc vào SecurityQuestionAnswers.

Đặt phương thức này trong một phương thức mở rộng sẽ ngăn chặn lớp dữ liệu thuần túy khác.

Lưu ý rằng đối số này là một trong những phong cách. Điều này tương tự như các lớp một phần. Có rất ít hoặc không có lợi ích kỹ thuật để làm như vậy, nhưng nó cho phép bạn tách mã thành nhiều tệp nếu bạn thấy nó sạch hơn (ví dụ: nếu nhiều người sử dụng lớp nhưng không quan tâm đến các phương thức tùy chỉnh bổ sung của bạn).

4. Phương pháp trợ giúp

Trong hầu hết các dự án, tôi có xu hướng kết thúc với các lớp trợ giúp. Đây là các lớp tĩnh thường cung cấp một số tập hợp các phương thức để định dạng dễ dàng.

Ví dụ: một công ty tôi làm việc có định dạng datetime cụ thể mà họ muốn sử dụng. Thay vì phải dán chuỗi định dạng khắp nơi hoặc biến chuỗi định dạng thành biến toàn cục, tôi đã chọn phương thức mở rộng DateTime:

public static string ToCompanyFormat(this DateTime dt)
{
    return dt.ToString("...");
} 

Là tốt hơn so với một phương pháp trợ giúp tĩnh bình thường? Tôi nghĩ vậy. Nó làm sạch cú pháp. Thay vì

DateTimeHelper.ToCompanyFormat(myObj.CreatedOn);

Tôi có thể làm:

myObj.CreatedOn.ToCompanyFormat();

Điều này tương tự như một phiên bản lưu loát của cùng một cú pháp. Tôi thích nó hơn, mặc dù không có lợi ích kỹ thuật từ việc có cái này hơn cái kia.


Tất cả những lập luận này là chủ quan. Không ai trong số họ bao gồm một trường hợp có thể không bao giờ được bảo hiểm.

  • Mọi phương thức mở rộng có thể dễ dàng được viết lại thành một phương thức tĩnh thông thường.
  • Mã có thể được tách trong các tệp bằng các lớp một phần, ngay cả khi các phương thức mở rộng không tồn tại.

Nhưng một lần nữa, chúng ta có thể khẳng định rằng chúng không bao giờ cần thiết đến mức cực đoan:

  • Tại sao chúng ta cần các phương thức lớp, nếu chúng ta luôn có thể tạo các phương thức tĩnh với một tên chỉ rõ rằng nó dành cho một lớp cụ thể?

Câu trả lời cho câu hỏi này và của bạn, vẫn như cũ:

  • Cú pháp Nicer
  • Tăng khả năng đọc và ít mã hơn (mã sẽ đi đến điểm bằng ít ký tự hơn)
  • Intellisense cung cấp các kết quả có ý nghĩa theo ngữ cảnh (các phương thức mở rộng chỉ được hiển thị trên đúng loại. Các lớp tĩnh có thể sử dụng ở mọi nơi).

Một đề cập thêm đặc biệt mặc dù:

  • Các phương thức mở rộng cho phép một phương thức mã hóa kiểu; gần đây đã trở nên phổ biến hơn, trái ngược với cú pháp gói phương thức vanilla hơn. Các ưu tiên cú pháp tương tự có thể được nhìn thấy trong cú pháp phương thức của LINQ.
  • Trong khi chuỗi phương thức có thể được thực hiện bằng cách sử dụng các phương thức lớp; Hãy nhớ rằng điều này không hoàn toàn áp dụng cho các phương thức tĩnh. Các phương thức mở rộng phổ biến nhất dựa trên những thứ mà nếu không phải là phương thức tĩnh, không phải là phương thức lớp.

5
Điểm 2. là điểm tôi thấy thuyết phục nhất và là lý do tôi đã tự mình làm điều này. Đôi khi bạn có một lớp được sử dụng rộng rãi trên toàn bộ cơ sở mã của bạn và một phương thức có vẻ như nó "nên" nằm trong lớp ngoại trừ việc nó liên quan đến một số thư viện dự án hoặc bên thứ ba mà bạn muốn gói gọn. Các phương thức mở rộng cho các lớp của riêng bạn là một cách hợp lý để xử lý việc này.
Astrid

@Astrid: Tôi nghĩ đó là lý do kỹ thuật nhất để làm điều đó; Đúng.
Flater

Câu trả lời rất hay. Một trường hợp sử dụng cụ thể của C # là các giao diện không có hành vi. Phương pháp mở rộng cho phép chúng tôi cung cấp cho họ phương pháp.
RubberDuck

"Lợi ích kỹ thuật" nghĩa là gì? Những thứ như hiệu suất? Khả năng đọc mã cũng quan trọng không kém, nếu không muốn nói là như vậy.
Robert Harvey

@RobertHarvey Theo kỹ thuật, ý tôi là bất cứ thứ gì không chỉ là đường tổng hợp. Ví dụ, trải một lớp một phần trên nhiều tệp trong cùng một cụm không có lợi thế kỹ thuật. Truyền bá một lớp và các phần mở rộng của nó trên các hội đồng khác nhau. Mã khả năng đọc tất nhiên vẫn còn vấn đề, nhưng không phải từ quan điểm kỹ thuật.
Flater

5

Tôi đồng ý với Flater rằng không có đủ thời gian để quy ước kết tinh, nhưng chúng ta có ví dụ về chính Thư viện lớp .NET Framework. Hãy xem xét rằng khi các phương thức LINQ được thêm vào .NET, chúng không được thêm trực tiếp vào IEnumerable, mà là các phương thức mở rộng.

Chúng ta có thể lấy gì từ điều này? LINQ là

  1. một bộ chức năng gắn kết không phải lúc nào cũng cần,
  2. được dự định sẽ được sử dụng theo kiểu xâu chuỗi phương thức (ví dụ A.B().C()thay vì C(B(A))) và
  3. không yêu cầu quyền truy cập vào trạng thái riêng tư của đối tượng

Nếu bạn có tất cả ba tính năng đó, các phương thức mở rộng có rất nhiều ý nghĩa. Trên thực tế, tôi sẽ lập luận rằng nếu một tập hợp các phương thức có tính năng # 3 và một trong hai tính năng đầu tiên, bạn nên xem xét việc biến chúng thành các phương thức mở rộng.

Phương pháp mở rộng:

  1. Cho phép bạn tránh làm ô nhiễm API cốt lõi của một lớp,
  2. Không chỉ cho phép kiểu tạo chuỗi phương thức, mà còn cho phép nó xử lý linh hoạt hơn nullcác giá trị do một phương thức mở rộng được gọi từ một nullgiá trị chỉ đơn giản nhận được nullđối số đầu tiên của nó,
  3. Ngăn bạn vô tình truy cập / sửa đổi trạng thái riêng tư / được bảo vệ theo phương pháp không nên truy cập vào trạng thái đó

Các phương thức mở rộng có thêm một lợi ích: chúng vẫn có thể được sử dụng như một phương thức tĩnh và có thể được truyền trực tiếp dưới dạng giá trị cho hàm bậc cao hơn. Vì vậy, nếu MyMethodlà một phương thức mở rộng, thay vì nói myList.Select(x => x.MyMethod()), bạn chỉ cần sử dụng myList.Select(MyMethod). Tôi thấy rằng đẹp hơn.


4
Lý do LINQ được triển khai như một tập hợp các phương thức mở rộng là vì nó dự định là một tập hợp hoạt động chung trên một giao diện và bạn không thể đặt việc triển khai trên các giao diện. Nó thực sự không liên quan gì đến điểm của bạn (1) và (2). Điều này trái ngược với giải pháp của Java cho cùng một vấn đề, đó là cho phép triển khai trong các định nghĩa giao diện (mở ra cánh cửa đến một thế giới lạm dụng giao diện).
Ant P

"Và bạn không thể đặt việc triển khai trên các giao diện". Chưa, nhưng tuyên bố đó sẽ không còn đúng khi C # 8 tấn công vào máy của chúng tôi :)
David Arno

@DavidArno thật sao? Tôi đã rời khỏi thế giới C # được một thời gian. Thật là xấu hổ. Có vẻ như một bước lùi với tôi ...
Ant P

4

Động lực đằng sau các phương thức mở rộng là khả năng thêm chức năng bạn cần cho tất cả các lớp hoặc giao diện thuộc một loại nhất định bất kể chúng được niêm phong hay trong một tổ hợp khác. Bạn có thể thấy bằng chứng về điều này với mã LINQ, thêm chức năng cho tất cả IEnumerable<T>các đối tượng, cho dù chúng là danh sách hay ngăn xếp, v.v. Khái niệm này là để chứng minh các chức năng trong tương lai để nếu bạn đưa ra chính mình, IEnumerable<T>bạn có thể tự động sử dụng LINQ với nó.

Điều đó nói rằng, tính năng ngôn ngữ đủ mới để bạn không có bất kỳ hướng dẫn nào về thời điểm sử dụng hoặc không sử dụng chúng. Tuy nhiên, chúng cho phép một số điều trước đây không thể:

  • API thông thạo (xem Xác nhận thông thạo để biết ví dụ về cách thêm API trôi chảy vào bất kỳ đối tượng nào)
  • Tích hợp các tính năng (NetCore Asp.NET MVC sử dụng các phương thức mở rộng để kết nối các phụ thuộc và các tính năng trong mã khởi động)
  • Bản địa hóa tác động của một tính năng (ví dụ của bạn trong câu mở đầu)

Công bằng mà nói, chỉ có thời gian mới có thể nói được bao xa là quá xa, hoặc tại thời điểm nào các phương thức mở rộng trở thành một trở ngại hơn là một lợi ích. Như đã đề cập trong một câu trả lời khác, không có gì trong một phương thức mở rộng không thể thực hiện được bằng một phương thức tĩnh. Tuy nhiên, khi được sử dụng một cách thận trọng, chúng có thể làm cho mã dễ đọc hơn.

Điều gì về việc cố ý sử dụng các phương thức mở rộng trong cùng một hội đồng?

Tôi nghĩ rằng có giá trị trong việc chức năng cốt lõi được kiểm tra và tin cậy tốt, và sau đó không gây rối với nó cho đến khi bạn hoàn toàn phải làm. Mã mới phụ thuộc vào nó, nhưng cần nó để cung cấp chức năng chỉ vì lợi ích của mã mới, có thể sử dụng Phương thức mở rộng để thêm bất kỳ tính năng nào để hỗ trợ mã mới. Các hàm này chỉ được quan tâm cho mã mới và phạm vi của các phương thức mở rộng được giới hạn trong không gian tên mà chúng được khai báo bên trong.

Tôi sẽ không loại trừ một cách mù quáng việc sử dụng các phương thức mở rộng, nhưng tôi sẽ xem xét liệu chức năng mới có thực sự được thêm vào mã lõi hiện có được kiểm tra tốt hay không.


1
+1 cho apis trôi chảy.
Robert Harvey

@RobertHarvey, Đoạn cuối cùng của tôi dưới tiêu đề "trong cùng một hội đồng"? Nếu bạn có mã nguồn cho hội đồng đó, bạn có thể làm bất cứ điều gì bạn muốn với nó, nhưng một khi nó được biên dịch, nó được niêm phong.
Berin Loritsch

Xóa bình luận của tôi, vì rõ ràng tôi không truyền đạt quan điểm của mình một cách hiệu quả.
Robert Harvey

1

Có bất kỳ mô hình được thiết lập hoặc thực hành tốt nhất khi sử dụng các phương pháp mở rộng?

Một trong những lý do phổ biến nhất để sử dụng các phương thức mở rộng là phương tiện "mở rộng, không sửa đổi" giao diện. Thay vì có IFooIFoo2, trong đó phương thức sau giới thiệu một phương thức mới Bar, Barđược thêm vào IFoothông qua một phương thức mở rộng.

Một trong những lý do Linq sử dụng các phương thức mở rộng cho Where, Selectv.v ... là để cho phép người dùng xác định phiên bản riêng của các phương thức này, sau đó cũng được sử dụng bởi cú pháp truy vấn Linq.

Ngoài ra, cách tiếp cận mà tôi sử dụng, có vẻ phù hợp với những gì tôi thường thấy trong .NET framework ít nhất là:

  1. Đặt các phương thức liên quan trực tiếp đến lớp trong chính lớp đó,
  2. Đặt tất cả các phương thức không liên quan đến chức năng cốt lõi của lớp trong các phương thức mở rộng.

Vì vậy, nếu tôi có một Option<T>, tôi muốn đưa những thứ như HasValue, Value, ==, vv trong vòng loại của chính nó. Nhưng một cái gì đó giống như một Maphàm, chuyển đổi đơn nguyên từ T1sang T2, tôi sẽ đưa vào một phương thức mở rộng:

public static Option<TOutput> Map<TInput, TOutput>(this Option<TInput> input, 
                                                   Func<TInput, TOutput> f) => ...

1
One of the reasons Linq uses extension methods for Where, Select etc is in order to allow users to define their own versions of these methods, which are then used by the Linq query syntax too. -- Bạn có chắc chắn về điều đó không? Cú pháp Linq sử dụng rộng rãi các biểu thức lambda (tức là các hàm hạng nhất), do đó không cần thiết phải viết các phiên bản SelectWhere. Tôi biết không có trường hợp nào người ta tự lăn các phiên bản của các chức năng này.
Robert Harvey

Điều đó nói rằng, tôi thấy câu trả lời của bạn hấp dẫn nhất trong tất cả các câu trả lời cho đến nay. Các phương thức mở rộng là một cách tuyệt vời để phát triển giao diện, đó chính là lý do tại sao chúng được tạo ra cho Linq.
Robert Harvey


@RobertHarvey Nếu bạn chuyển if (!predicate(item))mã trong đó sang if (predicate(item)), kết quả sẽ thay đổi khi sử dụng phiên bản của tôi Where.
David Arno

Tôi hiểu. Nhưng không ai sẽ làm điều này; họ chỉ đơn giản là thay đổi vị ngữ. Đó là toàn bộ quan điểm của việc có các vị từ: thay đổi điều kiện mà không thay đổi chức năng.
Robert Harvey
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.