Mẫu cho một lớp chỉ làm một điều


24

Hãy nói rằng tôi có một quy trình thực hiện :

void doStuff(initalParams) {
    ...
}

Bây giờ tôi phát hiện ra rằng "làm công cụ" là một hoạt động compex. Thủ tục trở nên lớn, tôi chia nó thành nhiều thủ tục nhỏ hơn và chẳng mấy chốc tôi nhận ra rằng có một loại trạng thái nào đó sẽ hữu ích trong khi thực hiện công cụ, do đó tôi cần truyền ít tham số hơn giữa các thủ tục nhỏ. Vì vậy, tôi đưa nó vào lớp riêng của nó:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

Và sau đó tôi gọi nó như thế này:

new StuffDoer().Start(initialParams);

hoặc như thế này:

new StuffDoer(initialParams).Start();

Và đây là những gì cảm thấy sai. Khi sử dụng API .NET hoặc Java, tôi luôn không bao giờ gọi new SomeApiClass().Start(...);, điều này khiến tôi nghi ngờ rằng mình đang làm sai. Chắc chắn, tôi có thể đặt công cụ xây dựng của StuffDer'ser và thêm một phương thức trợ giúp tĩnh:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Nhưng sau đó tôi có một lớp có giao diện bên ngoài chỉ bao gồm một phương thức tĩnh, cũng cảm thấy kỳ lạ.

Do đó, câu hỏi của tôi: Có một mô hình được thiết lập tốt cho loại lớp này không

  • chỉ có một điểm vào và
  • không có trạng thái "có thể nhận ra bên ngoài", tức là trạng thái thể hiện chỉ được yêu cầu trong khi thực hiện một điểm nhập đó?

Tôi đã được biết là làm những việc như bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");khi tất cả những gì tôi quan tâm là phần thông tin cụ thể đó và các phương thức mở rộng LINQ không có sẵn. Hoạt động tốt, và phù hợp với một if()điều kiện mà không cần phải nhảy qua vòng. Tất nhiên, bạn muốn có một ngôn ngữ được thu gom rác nếu bạn viết mã như thế.
một CVn

Nếu đó là ngôn ngữ bất khả tri , tại sao lại thêm javac # ? :)
Steven Jeuris

Tôi tò mò, chức năng của 'một phương pháp' này là gì? Nó sẽ giúp rất nhiều trong việc xác định cách tiếp cận tốt nhất trong kịch bản cụ thể, nhưng dù sao câu hỏi thú vị!
Steven Jeuris

@StevenJeuris: Tôi đã vấp phải vấn đề này trong nhiều tình huống, nhưng trong trường hợp hiện tại, đó là phương pháp nhập dữ liệu vào cơ sở dữ liệu cục bộ (tải nó từ máy chủ web, thực hiện một số thao tác và lưu trữ trong cơ sở dữ liệu).
Heinzi

1
Nếu bạn luôn gọi phương thức khi bạn tạo cá thể, tại sao không biến nó thành logic khởi tạo bên trong hàm tạo?
NoChance

Câu trả lời:


29

Có một mẫu được gọi là Đối tượng Phương thức trong đó bạn tính ra một phương thức lớn, duy nhất có nhiều biến / đối số tạm thời thành một lớp riêng. Bạn làm điều này thay vì chỉ trích xuất các phần của phương thức thành các phương thức riêng biệt vì chúng cần truy cập vào trạng thái cục bộ (các tham số và biến tạm thời) và bạn không thể chia sẻ trạng thái cục bộ bằng cách sử dụng các biến đối tượng bởi vì chúng sẽ là cục bộ của phương thức đó (và các phương thức được trích xuất từ ​​nó) chỉ và sẽ không được sử dụng bởi phần còn lại của đối tượng.

Vì vậy, thay vào đó, phương thức trở thành lớp riêng của nó, các tham số và biến tạm thời trở thành biến thể hiện của lớp mới này và sau đó phương thức được chia thành các phương thức nhỏ hơn của lớp mới. Lớp kết quả thường chỉ có một phương thức cá thể công khai duy nhất thực hiện nhiệm vụ mà lớp gói gọn.


1
Điều này nghe giống như những gì tôi đang làm. Cảm ơn, ít nhất tôi có một cái tên cho nó bây giờ. :-)
Heinzi

2
Liên kết rất liên quan! Nó có mùi giống như một mô hình chống đối với tôi. Nếu phương thức của bạn dài như vậy, có lẽ các phần của nó có thể được trích xuất trong các lớp có thể sử dụng lại hoặc thường được tái cấu trúc theo cách tốt hơn? Tôi cũng chưa từng nghe về mô hình này trước đây, tôi cũng không thể tìm thấy nhiều tài nguyên khác khiến tôi tin rằng nó thường không được sử dụng nhiều.
Steven Jeuris


1
@StevenJeuris Tôi không nghĩ đó là một mô hình chống. Một mô hình chống sẽ làm lộn xộn các phương thức được tái cấu trúc với nhiều tham số thay vì lưu trữ trạng thái này là phổ biến giữa các phương thức này trong một biến thể hiện.
Alfredo Osorio

@AlfredoOsorio: Chà, nó làm lộn xộn các phương thức được cấu trúc lại với rất nhiều tham số. Chúng sống trong vật thể, vì vậy tốt hơn là truyền tất cả chúng xung quanh, nhưng điều tồi tệ hơn là tái cấu trúc thành các thành phần có thể tái sử dụng chỉ cần một tập hợp con giới hạn của các tham số.
Jan Hudec

13

Tôi muốn nói rằng lớp trong câu hỏi (sử dụng một điểm vào duy nhất) được thiết kế tốt. Thật dễ dàng để sử dụng và mở rộng. Khá RẮN.

Điều tương tự cho no "externally recognizable" state. Đó là sự đóng gói tốt.

Tôi không thấy bất kỳ vấn đề nào với mã như:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
Và tất nhiên, nếu bạn không cần phải sử dụng lại như vậy doer, điều đó sẽ giảm xuống var result = new StuffDoer(initialParams).Calculate(extraParams);.
một CVn

Tính toán nên được đổi tên thành doStuff!
shabunc

1
Tôi sẽ nói thêm rằng nếu bạn không muốn khởi tạo (mới) lớp của bạn nơi bạn sử dụng nó (có thể bạn muốn sử dụng Factory), bạn có tùy chọn tạo đối tượng ở một nơi khác trong logic nghiệp vụ của bạn và hơn là truyền trực tiếp các tham số theo phương pháp công khai duy nhất bạn có. Tùy chọn khác là sử dụng Factory và tạo cho nó một phương thức make để lấy các tham số và tạo đối tượng cho bạn với tất cả các tham số thông qua hàm tạo của nó. Hơn bạn gọi phương thức công khai của bạn mà không có thông số từ bất cứ nơi nào bạn muốn.
Patkos Csaba

2
Câu trả lời này là quá đơn giản. Là một StringComparerlớp học mà bạn sử dụng new StringComparer().Compare( "bleh", "blah" )cũng được thiết kế? Điều gì về việc tạo trạng thái khi hoàn toàn không cần thiết? Được rồi, hành vi được gói gọn (tốt, ít nhất là với người dùng của lớp, nhiều hơn về câu trả lời của tôi ), nhưng điều đó không làm cho nó trở thành 'thiết kế tốt' theo mặc định. Như ví dụ 'doer-result' của bạn. Điều này sẽ chỉ có ý nghĩa nếu bạn từng sử dụng doertách biệt result.
Steven Jeuris

1
Đó không phải là một lý do để tồn tại, đó là one reason to change. Sự khác biệt có vẻ tinh tế. Bạn phải hiểu điều đó có nghĩa là gì. Nó sẽ không bao giờ tạo một lớp phương thức
jgauffin

8

Từ quan điểm nguyên tắc RẮN câu trả lời của jgauffin có ý nghĩa. Tuy nhiên, bạn không nên quên các nguyên tắc thiết kế chung, ví dụ như ẩn thông tin .

Tôi thấy một số vấn đề với cách tiếp cận nhất định:

  • Như bạn đã chỉ ra, mọi người không mong đợi sử dụng từ khóa 'mới', khi đối tượng được tạo không quản lý bất kỳ trạng thái nào . Thiết kế của bạn phản ánh ý định của nó. Những người sử dụng lớp của bạn có thể bị nhầm lẫn về trạng thái mà nó quản lý và liệu các cuộc gọi tiếp theo đến phương thức có thể dẫn đến hành vi khác nhau hay không.
  • Từ quan điểm của người sử dụng lớp, trạng thái bên trong được ẩn giấu tốt, nhưng khi muốn sửa đổi lớp hoặc chỉ đơn giản là hiểu nó, bạn đang làm mọi thứ trở nên phức tạp hơn. Tôi đã viết rất nhiều về các vấn đề tôi thấy với các phương thức phân tách chỉ để làm cho chúng nhỏ hơn , đặc biệt là khi chuyển trạng thái sang phạm vi lớp. Bạn đang sửa đổi cách sử dụng API của mình để có các chức năng nhỏ hơn! Điều đó theo tôi chắc chắn là đưa nó đi quá xa.

Một số tài liệu tham khảo liên quan

Có thể một điểm chính của tranh luận nằm ở việc kéo dài bao xa Nguyên tắc Trách nhiệm duy nhất . "Nếu bạn đưa nó đến mức cực đoan đó và xây dựng các lớp có một lý do tồn tại, bạn có thể sẽ chỉ có một phương thức cho mỗi lớp. Điều này sẽ gây ra một lượng lớn các lớp cho ngay cả các quy trình đơn giản nhất, khiến hệ thống bị khó hiểu và khó thay đổi. "

Một tài liệu tham khảo có liên quan khác liên quan đến chủ đề này: "Chia các chương trình của bạn thành các phương thức thực hiện một nhiệm vụ có thể xác định được. Giữ tất cả các hoạt động trong một phương thức ở cùng một mức độ trừu tượng ." - Kent Beck Key ở đây là "cùng một mức độ trừu tượng". Điều đó không có nghĩa là "một điều", vì nó thường được giải thích. Mức độ trừu tượng này hoàn toàn tùy thuộc vào bối cảnh mà bạn đang thiết kế.

Vậy cách tiếp cận phù hợp là gì?

Nếu không biết trường hợp sử dụng cụ thể của bạn, thật khó để nói. Có một kịch bản trong đó đôi khi tôi (không thường xuyên) sử dụng một cách tiếp cận tương tự. Khi tôi muốn xử lý một tập dữ liệu, mà không muốn làm cho chức năng này có sẵn cho toàn bộ phạm vi lớp. Tôi đã viết một bài đăng trên blog về nó, làm thế nào lambdas thậm chí có thể cải thiện hơn nữa việc đóng gói . Tôi cũng bắt đầu một câu hỏi về chủ đề ở đây trên Lập trình viên . Sau đây là một ví dụ mới nhất về nơi tôi đã sử dụng kỹ thuật này.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Vì mã rất 'cục bộ' trong phạm vi ForEachkhông được sử dụng lại ở bất kỳ nơi nào khác, tôi chỉ có thể giữ nó ở vị trí chính xác nơi có liên quan. Phác thảo mã theo cách mà mã dựa vào nhau được nhóm mạnh mẽ với nhau làm cho nó dễ đọc hơn theo quan điểm của tôi.

Các lựa chọn thay thế có thể

  • Trong C #, bạn có thể sử dụng các phương thức mở rộng thay thế. Vì vậy, hoạt động dựa trên đối số trực tiếp mà bạn chuyển sang phương pháp 'một điều' này.
  • Xem liệu hàm này thực sự không thuộc về lớp khác.
  • Làm cho nó một hàm tĩnh trong một lớp tĩnh . Đây rất có thể là cách tiếp cận phù hợp nhất, cũng được phản ánh trong các API chung mà bạn đã đề cập.

Tôi tìm thấy một tên có thể cho mô hình chống này như được nêu trong một câu trả lời khác, Poltergeists .
Steven Jeuris

4

Tôi sẽ nói rằng nó khá tốt, nhưng API của bạn vẫn phải là một phương thức tĩnh trên một lớp tĩnh, vì đó là những gì người dùng mong đợi. Thực tế là phương thức tĩnh sử dụng newđể tạo đối tượng trợ giúp của bạn và thực hiện công việc là một chi tiết triển khai cần được ẩn khỏi bất kỳ ai đang gọi nó.


Ồ Bạn quản lý để có được cử chỉ của nó chính xác hơn rất nhiều so với tôi đã làm. :) Cảm ơn. (tất nhiên tôi đi xa hơn một chút nơi chúng tôi có thể không đồng ý, nhưng tôi đồng ý với tiền đề chung)
Steven Jeuris

2
new StuffDoer().Start(initialParams);

Có một vấn đề với các nhà phát triển không biết gì có thể sử dụng một thể hiện được chia sẻ và sử dụng nó

  1. nhiều lần (ok nếu thực hiện trước (có thể một phần) không làm hỏng thực thi sau)
  2. từ nhiều luồng (không ổn, bạn nói rõ ràng nó có trạng thái bên trong).

Vì vậy, điều này cần tài liệu rõ ràng rằng nó không phải là luồng an toàn và nếu nó nhẹ (tạo ra nó nhanh, không ràng buộc tài nguyên bên ngoài cũng như nhiều bộ nhớ) thì bạn có thể khởi động nhanh chóng.

Ẩn nó trong một phương thức tĩnh giúp với điều này, bởi vì sau đó sử dụng lại cá thể không bao giờ xảy ra.

Nếu nó có một số khởi tạo đắt tiền, thì có thể (không phải lúc nào) cũng có ích khi chuẩn bị khởi tạo nó một lần và sử dụng nó nhiều lần bằng cách tạo một lớp khác chỉ chứa trạng thái và làm cho nó có thể nhân bản. Khởi tạo sẽ tạo trạng thái sẽ được lưu trữ trong doer.start()sẽ sao chép nó và truyền nó cho các phương thức nội bộ.

Điều này cũng sẽ cho phép những thứ khác như trạng thái thực thi một phần. (Nếu mất nhiều thời gian và các yếu tố bên ngoài, ví dụ như sự cố cung cấp điện, có thể làm gián đoạn thực thi.) Nhưng những điều ưa thích bổ sung này thường không cần thiết, vì vậy không đáng.


Điểm tốt. Hy vọng bạn không bận tâm đến việc dọn dẹp.
Steven Jeuris

2

Bên cạnh câu trả lời khác được trau chuốt, cá nhân hóa hơn , tôi cảm thấy những quan sát sau đây xứng đáng có một câu trả lời riêng.

Có những dấu hiệu cho thấy bạn có thể đang theo mô hình chống Poltergeists .

Poltergeists là những lớp có vai trò rất hạn chế và vòng đời hiệu quả. Họ thường bắt đầu các quy trình cho các đối tượng khác. Giải pháp tái cấu trúc bao gồm việc phân bổ lại các trách nhiệm cho các đối tượng tồn tại lâu hơn để loại bỏ Poltergeists.

Triệu chứng và hậu quả

  • Đường dẫn điều hướng dự phòng.
  • Các hiệp hội thoáng qua.
  • Các lớp không quốc tịch.
  • Các đối tượng và lớp học tạm thời, ngắn hạn.
  • Các lớp hoạt động đơn lẻ chỉ tồn tại với các hạt giống khác, hoặc gọi các lớp khác thông qua các hiệp hội tạm thời.
  • Các lớp với các tên hoạt động giống như điều khiển của Viking như start_ process_alpha.

Giải pháp tái cấu trúc

Ghostbuster giải quyết Poltergeists bằng cách loại bỏ chúng khỏi hệ thống phân cấp lớp hoàn toàn. Tuy nhiên, sau khi loại bỏ chúng, chức năng được cung cấp bởi người điều khiển đã được thay thế. Điều này là dễ dàng với một điều chỉnh đơn giản để sửa kiến ​​trúc.


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.