Các tầng lớp tiện ích là xấu xa? [đóng cửa]


96

Tôi đã thấy chủ đề này

Nếu một lớp "Tiện ích" là xấu, tôi phải đặt mã chung của mình ở đâu?

và nghĩ tại sao các lớp tiện ích lại xấu xa?

Giả sử tôi có một mô hình miền sâu hàng chục lớp. Tôi cần có thể xml-ify thể hiện. Tôi có tạo một phương thức toXml trên cha mẹ không? Tôi có tạo lớp trợ giúp MyDomainXmlUtility.toXml không? Đây là trường hợp doanh nghiệp cần trải dài toàn bộ mô hình miền - nó có thực sự thuộc về một phương thức thể hiện không? Điều gì xảy ra nếu có một loạt các phương thức phụ trợ trên chức năng xml của ứng dụng?


27
Việc mất giá trị chữ “ác” thật là ác!
Matthew Lock

1
@matthew tôi gìn giữ các điều khoản của bài đăng trên mà câu hỏi của tôi được dựa ...;)
hvgotcodes

Các lớp tiện ích là một ý tưởng tồi vì những lý do tương tự như vậy.
CurtainDog

1
Cuộc tranh luận về việc có nên có một phương pháp toXML là một cuộc tranh luận tập trung vào các mô hình miền phong phú và miền thiếu máu. codeflow.blogspot.com/2007/05/anemic-vs-rich-domain-models.html
James P.

@james, toXML chỉ là một ví dụ ... còn một số chức năng regex được sử dụng khắp nơi thì sao? Giống như, bạn cần làm một số công cụ với các dây trên mô hình tên miền của bạn, nhưng bạn couldnt sử dụng subclassing do khác quan tâm trọng mà sử dụng một lớp cha của bạn (trong java)
hvgotcodes

Câu trả lời:


128

Các lớp tiện ích không hẳn là xấu, nhưng chúng có thể vi phạm các nguyên tắc tạo nên một thiết kế hướng đối tượng tốt. Trong một thiết kế hướng đối tượng tốt, hầu hết các lớp phải đại diện cho một thứ duy nhất và tất cả các thuộc tính và hoạt động của nó. Nếu bạn đang vận hành một thứ, thì phương thức đó có lẽ phải là một thành viên của thứ đó.

Tuy nhiên, đôi khi bạn có thể sử dụng các lớp tiện ích để nhóm một số phương thức lại với nhau - một ví dụ là java.util.Collectionslớp cung cấp một số tiện ích có thể được sử dụng trên bất kỳ Bộ sưu tập Java nào. Chúng không dành riêng cho một loại Bộ sưu tập cụ thể, nhưng thay vào đó, triển khai các thuật toán có thể được sử dụng trên bất kỳ Bộ sưu tập nào.

Thực sự, những gì bạn cần làm là suy nghĩ về thiết kế của mình và xác định vị trí phù hợp nhất để đặt các phương pháp. Thông thường, nó giống như các hoạt động bên trong một lớp. Tuy nhiên, đôi khi, nó thực sự là một lớp tiện ích. Tuy nhiên, khi bạn sử dụng một lớp tiện ích, đừng chỉ ném các phương thức ngẫu nhiên vào nó, thay vào đó, hãy sắp xếp các phương thức theo mục đích và chức năng.


quan hệ hợp tốt với bài viết này về kiểm tra lớp tiện ích / helper chống lại nguyên tắc oo RẮN cũng readability.com/articles/rk1vvcqy
melaos

8
Nếu ngôn ngữ không cung cấp bất kỳ cơ chế không gian tên nào ngoài các lớp, bạn không có lựa chọn nào khác ngoài việc lạm dụng một lớp mà không gian tên sẽ làm. Trong C ++, bạn có thể đặt một hàm tự do, không liên quan đến các hàm khác, vào một không gian tên. Trong Java, bạn phải đặt nó vào một lớp như một thành viên (lớp) tĩnh.
Unslander Monica

1
Việc nhóm dưới dạng các phương thức có thể là chuẩn theo quan điểm OOP. Tuy nhiên, OOP hiếm khi là giải pháp (trong số các trường hợp ngoại lệ là kiến ​​trúc cấp cao và các bộ công cụ widget là một trong những trường hợp mà ngữ nghĩa cực kỳ bị hỏng của mã tái sử dụng theo kế thừa thực sự phù hợp) và nói chung các phương pháp làm tăng sự kết hợp và phụ thuộc. Ví dụ đơn giản: nếu bạn cung cấp các phương thức máy in / máy quét cho lớp của mình dưới dạng các phương thức, bạn ghép lớp với các thư viện máy in / máy quét. Ngoài ra, bạn cố định số lượng triển khai có thể thành một triển khai hiệu quả. Trừ khi bạn ném vào giao diện và tăng thêm sự phụ thuộc.
Jo So

Mặt khác, tôi đồng ý với quan điểm của bạn "nhóm theo mục đích và chức năng". Hãy xem lại ví dụ về máy in / máy quét một lần nữa và bắt đầu với một tập hợp các lớp hòa hợp, thiết kế cho một mục đích chung. Bạn có thể muốn viết một số mã gỡ lỗi và thiết kế một biểu diễn văn bản cho các lớp của bạn. Bạn có thể triển khai máy in gỡ lỗi của mình trong một tệp duy nhất phụ thuộc vào tất cả các lớp đó và thư viện máy in. Mã không gỡ lỗi sẽ không bị gánh nặng bởi các phụ thuộc của việc triển khai này.
Jo So

1
Các lớp Util và Helper là một tạo tác đáng tiếc của những ràng buộc như vậy, và những người không hiểu OOP hoặc không hiểu miền vấn đề đã tiếp tục viết mã thủ tục.
bilelovitch

57

Tôi nghĩ rằng sự đồng thuận chung là lớp tiện ích không phải là ác cho mỗi gia nhập . Bạn chỉ cần sử dụng chúng một cách thận trọng:

  • Thiết kế các phương thức tiện ích tĩnh để trở nên chung và có thể sử dụng lại. Đảm bảo rằng chúng không có trạng thái; tức là không có biến tĩnh.

  • Nếu bạn có nhiều phương thức tiện ích, hãy phân vùng chúng thành các lớp theo cách giúp các nhà phát triển dễ dàng tìm thấy chúng.

  • Không sử dụng các lớp tiện ích trong đó các phương thức tĩnh hoặc phiên bản trong một lớp miền sẽ là giải pháp tốt hơn. Ví dụ, hãy xem xét nếu các phương thức trong một lớp cơ sở trừu tượng hoặc một lớp trợ giúp có thể khởi tạo sẽ là một giải pháp tốt hơn.

  • Đối với Java 8 trở đi, "các phương thức mặc định" trong một giao diện có thể là một lựa chọn tốt hơn các lớp tiện ích. (Xem Giao diện với các phương thức mặc định so với lớp Tóm tắt trong Java 8 chẳng hạn.)


Một cách khác để xem xét Câu hỏi này là quan sát rằng trong Câu hỏi được trích dẫn, "Nếu các lớp tiện ích là" ác "" là một đối số rơm rạ. Nó giống như tôi hỏi:

"Nếu lợn có thể bay, tôi có nên mang theo ô không?".

Trong câu hỏi trên, tôi không thực sự nói rằng lợn có thể bay ... hoặc tôi đồng ý với mệnh đề rằng chúng có thể bay.

Câu nói điển hình "xyz is evil" là những biện pháp tu từ nhằm khiến bạn suy nghĩ bằng cách đưa ra một quan điểm cực đoan. Chúng hiếm khi (nếu đã từng) được dùng như những tuyên bố về sự thật theo nghĩa đen.


16

Các lớp tiện ích có vấn đề vì chúng không phân nhóm được trách nhiệm với dữ liệu hỗ trợ chúng.

Tuy nhiên, chúng cực kỳ hữu ích và tôi luôn xây dựng chúng như một cấu trúc cố định hoặc như một bước đệm trong quá trình tái cấu trúc kỹ lưỡng hơn.

Từ quan điểm của Clean Code, các lớp tiện ích vi phạm Trách nhiệm đơn lẻ và Nguyên tắc Đóng mở. Chúng có rất nhiều lý do để thay đổi và do thiết kế không thể mở rộng. Chúng thực sự chỉ nên tồn tại trong quá trình tái cấu trúc như một mảnh ghép trung gian.


2
Tôi sẽ gọi nó là một vấn đề thực tế vì ví dụ như trong Java, bạn không thể tạo bất kỳ hàm nào không phải là phương thức trong một lớp. Trong một ngôn ngữ như vậy, "lớp không có biến và chỉ có phương thức tĩnh" là một thành ngữ đại diện cho không gian tên của các hàm tiện ích ... Khi đối mặt với một lớp như vậy trong Java, IMHO không hợp lệ và vô nghĩa khi nói về một lớp. , mặc dù classtừ khóa được sử dụng. Nó quacks giống như một namespace, nó đi như một không gian tên - đó là một ...
Unslander Monica

2
Tôi xin lỗi phải thẳng thắn, nhưng "phương pháp tĩnh không trạng thái [...] kỳ quặc trong hệ thống OOP [...] vấn đề triết học" là cực kỳ sai lầm. Thật TUYỆT VỜI nếu bạn có thể tránh trạng thái vì điều đó giúp bạn dễ dàng viết mã chính xác. Thật là BROKEN khi tôi phải viết x.equals(y)vì 1) thực tế là điều này thực sự không nên sửa đổi bất kỳ trạng thái nào không được truyền tải (và tôi không thể dựa vào nó khi không biết việc triển khai) 2) Tôi chưa bao giờ có ý định đưa xvào " vị trí được ưa thích "hoặc" được hành động theo "so với y3) Tôi không muốn xem xét" danh tính "của xhoặc ymà chỉ quan tâm đến giá trị của họ.
Jo So

4
Thực sự hầu hết các chức năng trong thế giới thực được thể hiện tốt nhất dưới dạng các hàm tĩnh, không trạng thái, toán học. Math.PI có phải là một đối tượng nên được đột biến? Tôi có thực sự phải khởi tạo một AbstractSineCalculator triển khai IAbstractRealOperation chỉ để lấy sin của một số không?
Jo So

1
@JoSo Tôi hoàn toàn đồng ý về giá trị của tính không trạng thái và tính toán trực tiếp. Naive OO phản đối điều này về mặt triết học và tôi nghĩ điều đó khiến nó trở nên thiếu sót sâu sắc. Nó khuyến khích sự trừu tượng hóa những thứ không cần nó (như bạn đã chứng minh) và không cung cấp những điều trừu tượng có ý nghĩa cho tính toán thực tế. Tuy nhiên, một cách tiếp cận cân bằng để sử dụng ngôn ngữ OO có xu hướng hướng tới tính bất biến và không trạng thái vì chúng thúc đẩy tính mô-đun và khả năng bảo trì.
Alain O'Dea

2
@ AlainO'Dea: Tôi nhận ra rằng tôi đã đọc nhầm bình luận của bạn là chống lại chức năng không trạng thái. Tôi xin lỗi vì giọng điệu của mình - tôi hiện đang tham gia vào dự án Java đầu tiên của mình (sau khi tôi tránh OOP trong phần lớn thời gian 10 năm lập trình của mình) và bị chôn vùi dưới những lớp thứ trừu tượng với trạng thái và ý nghĩa không rõ ràng. Và tôi chỉ cần một chút không khí trong lành :-).
Jo So

8

Tôi cho rằng nó bắt đầu trở nên xấu xa khi

1) Nó quá lớn (chỉ cần nhóm chúng thành các danh mục có ý nghĩa trong trường hợp này).
2) Có các phương thức không phải là phương thức tĩnh

Nhưng miễn là những điều kiện này không được đáp ứng, tôi nghĩ rằng chúng rất hữu ích.


"Các phương thức không nên là phương thức tĩnh hiện có." Sao có thể như thế được?
Koray Tugay

@KorayTugay Hãy xem xét tính không tĩnh Widget.compareTo(Widget widget)so với tĩnh WidgetUtils.compare(Widget widgetOne, Widget widgetTwo). So sánh là một ví dụ về điều gì đó không nên được thực hiện tĩnh.
ndm 13

5

Quy tắc ngón tay cái

Bạn có thể nhìn vấn đề này từ hai khía cạnh:

  • Nhìn chung *Util phương pháp thường là một gợi ý về thiết kế mã xấu hoặc quy ước đặt tên lười biếng.
  • Đây là giải pháp thiết kế hợp pháp cho các chức năng không trạng thái trên nhiều miền có thể tái sử dụng. Xin lưu ý rằng đối với hầu hết tất cả các vấn đề thông thường đều có các giải pháp hiện có.

Ví dụ 1. Cách sử dụng đúng các utillớp / mô-đun. Ví dụ về thư viện bên ngoài

Giả sử bạn đang viết ứng dụng quản lý các khoản vay và thẻ tín dụng. Dữ liệu từ các mô-đun này được hiển thị thông qua các dịch vụ web ở jsonđịnh dạng. Về lý thuyết, bạn có thể chuyển đổi thủ công các đối tượng thành chuỗi sẽ ở trong đó json, nhưng điều đó sẽ phát minh lại bánh xe. Giải pháp đúng sẽ là bao gồm cả thư viện bên ngoài mô-đun được sử dụng để chuyển đổi các đối tượng java sang định dạng mong muốn. (trong hình ảnh ví dụ tôi đã hiển thị gson )

nhập mô tả hình ảnh ở đây


Ví dụ 2. Cách sử dụng đúng các utillớp / mô-đun. Viết của riêng bạnutil mà không có lý do cho các thành viên khác trong nhóm

Như một trường hợp sử dụng, giả sử rằng chúng ta cần thực hiện một số tính toán trong hai mô-đun ứng dụng, nhưng cả hai đều cần biết khi nào có ngày nghỉ lễ ở Ba Lan. Về lý thuyết, bạn có thể thực hiện các tính toán này bên trong các mô-đun, nhưng sẽ tốt hơn nếu trích xuất chức năng này để tách mô-đun.

Đây là chi tiết nhỏ, nhưng quan trọng. Lớp / mô-đun bạn đã viết không được gọi HolidayUtil, nhưng PolishHolidayCalculator. Về mặt chức năng, nó là một utillớp, nhưng chúng tôi đã tránh được từ chung chung.

nhập mô tả hình ảnh ở đây


1
Tôi thấy một người hâm mộ yEd;) Ứng dụng tuyệt vời.
Sridhar Sarnobat

3

Các lớp tiện ích không tốt vì chúng có nghĩa là bạn đã quá lười biếng để nghĩ ra một cái tên tốt hơn cho lớp :)

Nói như vậy là tôi lười. Đôi khi bạn chỉ cần hoàn thành công việc và đầu óc của bạn trống rỗng .. đó là lúc các lớp "Tiện ích" bắt đầu len lỏi vào.


3

Bây giờ nhìn lại câu hỏi này, tôi muốn nói rằng các phương thức mở rộng C # hoàn toàn phá hủy nhu cầu về các lớp tiện ích. Nhưng không phải tất cả các ngôn ngữ đều có cấu trúc thiên tài như vậy.

Bạn cũng có JavaScript, nơi bạn có thể thêm một chức năng mới vào ngay đối tượng hiện có.

Nhưng tôi không chắc thực sự có cách giải quyết vấn đề này bằng một ngôn ngữ cũ hơn như C ++ ...

Mã OO tốt khá khó viết và rất khó tìm vì viết Mã OO tốt đòi hỏi nhiều thời gian / kiến ​​thức hơn so với viết mã chức năng tốt.

Và khi bạn tiết kiệm ngân sách, sếp của bạn không phải lúc nào cũng vui khi thấy bạn đã dành cả ngày để viết một loạt các lớp học ...


3

Tôi không hoàn toàn đồng ý rằng các lớp tiện ích là xấu.

Mặc dù một lớp tiện ích có thể vi phạm các nguyên tắc của OO theo một số cách, nhưng chúng không phải lúc nào cũng xấu.

Ví dụ: hãy tưởng tượng bạn muốn một hàm sẽ làm sạch một chuỗi tất cả các chuỗi con phù hợp với giá trị x .

stl c ++ (hiện tại) không hỗ trợ trực tiếp điều này.

Bạn có thể tạo một phần mở rộng đa hình của std::string .

Nhưng vấn đề là, bạn có thực sự muốn MỌI chuỗi bạn sử dụng trong dự án của bạn là lớp chuỗi mở rộng của bạn không?

Có những lúc OO không thực sự có ý nghĩa, và đây là một trong số đó. Chúng tôi muốn chương trình của mình tương thích với các chương trình khác, vì vậy chúng tôi sẽ gắn bó std::stringvà tạo ra một lớp StringUtil_(hoặc một cái gì đó).

Tôi muốn nói rằng tốt nhất là bạn nên sử dụng một lần sử dụng mỗi lớp. Tôi muốn nói rằng thật ngớ ngẩn khi có một lần sử dụng cho tất cả các lớp hoặc nhiều nút cho một lớp.


2

Rất dễ để gắn nhãn một thứ gì đó là một tiện ích đơn giản vì nhà thiết kế không thể nghĩ ra một nơi thích hợp để đặt mã. Thường có rất ít "tiện ích" thực sự.

Theo nguyên tắc chung, tôi thường giữ mã trong gói nơi nó được sử dụng lần đầu tiên và sau đó chỉ cấu trúc lại đến một nơi chung chung hơn nếu tôi thấy rằng sau này nó thực sự cần thiết ở nơi khác. Ngoại lệ duy nhất là nếu tôi đã có một gói thực hiện chức năng tương tự / liên quan và mã phù hợp nhất ở đó.


2

Các lớp tiện ích có chứa các phương thức tĩnh không trạng thái có thể hữu ích. Đây thường là những bài kiểm tra đơn vị rất dễ dàng.


1

Với Java 8, bạn có thể sử dụng các phương thức tĩnh trong các giao diện ... vấn đề đã được giải quyết.


Nó không giải quyết các vấn đề được nêu trong stackoverflow.com/a/3340037/2859065
Luchostein

Điều đó có gì khác so với việc đưa chúng vào một lớp và nhập?
wilmol

1

Hầu hết các lớp Util đều không tốt vì:

  1. Chúng mở rộng phạm vi của các phương pháp.Họ công khai mã mà nếu không sẽ là riêng tư. Nếu phương thức sử dụng được nhiều người gọi trong các lớp riêng biệt cần và ổn định (tức là không cần cập nhật), theo ý kiến ​​của tôi, tốt hơn hết là sao chép và dán các phương thức trợ giúp riêng vào lớp gọi. Khi bạn hiển thị nó dưới dạng một API, bạn sẽ khó hiểu điểm vào công khai của một thành phần jar là gì (bạn duy trì một cây có cấu trúc được gọi là phân cấp với một phương thức cha trên mỗi phương thức. Điều này dễ dàng hơn để phân tách thành các thành phần khó hơn khi bạn có các phương thức được gọi từ nhiều phương thức cha).
  2. Chúng dẫn đến mã chết. Các phương thức Util theo thời gian trở nên không được sử dụng khi ứng dụng của bạn phát triển và bạn sẽ nhận được mã không sử dụng làm ô nhiễm cơ sở mã của bạn. Nếu nó vẫn ở chế độ riêng tư, trình biên dịch của bạn sẽ cho bạn biết phương thức không được sử dụng và bạn chỉ có thể xóa nó (mã tốt nhất là không có mã nào cả). Một khi bạn thực hiện một phương pháp không riêng tư như vậy, máy tính của bạn sẽ không thể giúp bạn xóa mã không sử dụng. Nó có thể được gọi từ một tệp jar khác mà máy tính biết.

Có một số tương tự giữa thư viện tĩnh và thư viện động.


0

Khi tôi không thể thêm một phương thức vào một lớp (giả sử, Accountbị khóa trước các thay đổi bởi Jr. Developers), tôi chỉ cần thêm một vài phương thức tĩnh vào lớp Tiện ích của mình như sau:

public static int method01_Account(Object o, String... args) {
    Account acc = (Account)o;
    ...
    return acc.getInt();
}  

1
Có vẻ như bạn là Jr đối với tôi ..: P Cách không ác để làm điều này là lịch sự yêu cầu "Jr Developer" của bạn mở khóa Accounttệp. Hoặc tốt hơn, đi với hệ thống điều khiển nguồn không khóa.
Sudeep

1
Tôi cho rằng ý của anh ấy là Accounttệp đã bị khóa để các nhà phát triển Jr. không thể thực hiện thay đổi đối với nó. Là một Jr. Developer, để vượt qua nó, anh ấy đã làm như trên. Điều đó nói rằng, vẫn tốt hơn là chỉ có quyền ghi vào tệp và thực hiện đúng cách.
Doc

Thêm 1 điều này vì downvotes vẻ khắc nghiệt khi không ai có bối cảnh đầy đủ các lý do tại sao bạn cung cấp câu trả lời này
joelc

0

Các lớp tiện ích không phải lúc nào cũng xấu xa. Nhưng chúng chỉ nên chứa các phương thức phổ biến trên nhiều chức năng. Nếu có các phương thức chỉ sử dụng được trong một số lớp giới hạn, hãy xem xét việc tạo một lớp trừu tượng làm lớp cha chung và đặt các phương thức vào đó.

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.