Mẫu thiết kế cho hoạt động của chế độ trên đối tượng được phép, chỉ khi đối tượng ở trạng thái nhất định


8

Ví dụ:

Chỉ những đơn xin việc chưa được xem xét hoặc phê duyệt, mới có thể được cập nhật. Nói cách khác, một người có thể cập nhật biểu mẫu thiết bị công việc của mình cho đến khi HR bắt đầu xem xét nó, hoặc nó đã được chấp nhận.

Vì vậy, một đơn xin việc có thể ở 4 tiểu bang:

ÁP DỤNG (trạng thái ban đầu), IN_REVIEW, ĐƯỢC PHÊ DUYỆT, TUYÊN BỐ

Làm thế nào để tôi đạt được hành vi như vậy?

Chắc chắn tôi có thể viết phương thức update () trong lớp Ứng dụng, kiểm tra trạng thái Ứng dụng và không làm gì hoặc ném ngoại lệ nếu Ứng dụng không ở trạng thái bắt buộc

Nhưng loại mã này không làm cho nó rõ ràng là một quy tắc như vậy tồn tại, nó cho phép bất cứ ai gọi phương thức update () và chỉ sau khi khách hàng không biết hoạt động đó không được phép. Do đó, khách hàng cần lưu ý rằng một nỗ lực như vậy có thể thất bại, do đó, hãy thận trọng. Khách hàng nhận thức được những điều như vậy cũng có nghĩa là logic bị rò rỉ ra bên ngoài.

Tôi đã thử tạo các lớp khác nhau cho mỗi trạng thái (ApprovedApplication, v.v.) và đặt các hoạt động được phép lên chỉ các lớp được phép, nhưng cách tiếp cận này cũng cảm thấy sai.

Có một mẫu thiết kế chính thức, hoặc một đoạn mã đơn giản, để thực hiện hành vi đó không?


7
Những thứ này thường được gọi là StateMachines và việc triển khai chúng sẽ thay đổi một chút tùy thuộc vào yêu cầu của bạn và (các) ngôn ngữ bạn đang làm việc.
Telastyn

và, làm thế nào để bạn đảm bảo đúng phương pháp có sẵn trên đúng trạng thái?
uylmz

1
Nó phụ thuộc vào ngôn ngữ. Các lớp khác nhau là một triển khai phổ biến cho các ngôn ngữ phổ biến, mặc dù "ném nếu không ở trạng thái đúng" có lẽ là phổ biến nhất.
Telastyn

1
Đâu là vấn đề trong việc bao gồm phương pháp "canUpdate" và kiểm tra nó trước khi gọi Cập nhật?
Euphoric

1
this kind of code does not make it obvious such a rule exists- Đây là lý do tại sao mã có tài liệu. Những người viết mã tốt sẽ lấy lời khuyên của Euphoric và cung cấp một phương pháp để cho bên ngoài kiểm tra quy tắc trước khi thử phần cứng.
Blrfl

Câu trả lời:


4

Loại tình huống này xuất hiện khá thường xuyên. Ví dụ, các tệp chỉ có thể được thao tác trong khi mở và nếu bạn cố gắng làm gì đó với một tệp sau khi đã đóng, bạn sẽ có ngoại lệ thời gian chạy.

Mong muốn của bạn ( thể hiện trong câu hỏi trước của bạn ) là sử dụng hệ thống loại ngôn ngữ để đảm bảo rằng điều sai thậm chí không thể xảy ra là cao cả, vì các lỗi thời gian biên dịch luôn thích hợp hơn các lỗi thời gian chạy. Tuy nhiên, không có mẫu thiết kế nào mà tôi biết cho loại tình huống này, có lẽ vì nó sẽ gây ra nhiều vấn đề hơn nó sẽ giải quyết. (Nó sẽ không thực tế.)

Điều gần nhất với tình huống của bạn mà tôi biết là mô hình hóa các trạng thái khác nhau của một đối tượng tương ứng với các khả năng khác nhau thông qua các giao diện bổ sung, nhưng theo cách này, bạn chỉ giảm số lượng vị trí trong mã có thể xảy ra lỗi thời gian chạy không loại trừ khả năng xảy ra lỗi thời gian chạy.

Vì vậy, trong tình huống của bạn, bạn sẽ khai báo một số giao diện mô tả những gì có thể được thực hiện với đối tượng của bạn ở các trạng thái khác nhau và đối tượng của bạn sẽ trả về một tham chiếu đến giao diện bên phải khi chuyển trạng thái.

Vì vậy, ví dụ, approve()phương thức của lớp bạn sẽ trả về một ApprovedApplicationgiao diện. Giao diện sẽ được triển khai một cách riêng tư, (thông qua một lớp lồng nhau), do đó mã chỉ có tham chiếu đến Applicationkhông thể gọi bất kỳ ApprovedApplicationphương thức nào. Sau đó, mã thao tác một ứng dụng đã được phê duyệt nêu rõ ý định của nó để làm như vậy tại thời điểm biên dịch bằng cách yêu cầu ApprovedApplicationlàm việc với. Nhưng tất nhiên, nếu bạn lưu trữ giao diện này ở đâu đó, và sau đó bạn tiến hành sử dụng giao diện này sau khi decline()phương thức đã được gọi, bạn vẫn sẽ gặp lỗi thời gian chạy. Tôi không nghĩ có một giải pháp hoàn hảo cho vấn đề của bạn.


Là một lưu ý phụ, nên là application.approve (someoneWhoCanApprove) hoặc someoneWhoCanApprove.approve (application)? Tôi nghĩ rằng đây phải là lần đầu tiên vì "ai đó" có thể không có quyền truy cập vào các lĩnh vực ứng dụng để thực hiện các điều chỉnh cần thiết
uylmz

Tôi không chắc chắn, nhưng bạn cũng nên kiểm tra khả năng cả hai có thể không đúng. tức là if( someone.hasApprovalPermission( application ) ) { application.approve(); } Nguyên tắc phân tách mối quan tâm chỉ ra rằng cả ứng dụng và ai đó đều không nên quan tâm đến việc đưa ra quyết định liên quan đến quyền và bảo mật.
Mike Nakis

3

Tôi gật đầu với các câu trả lời khác nhau nhưng OP dường như vẫn có mối quan tâm về kiểm soát dòng chảy. Có quá nhiều để cố gắng kết hợp trong lời nói. Tôi sẽ chuyển sang một số mã - Mẫu trạng thái.


Tên tiểu bang như thì quá khứ

"In_Review" có lẽ không phải là trạng thái mà là quá trình chuyển đổi hoặc quá trình. Mặt khác, tên trạng thái của bạn phải nhất quán: "Áp dụng", "Phê duyệt", "Từ chối", v.v ... HOẶC cũng có "Đã đánh giá". Hay không.

Trạng thái Ứng dụng thực hiện chuyển đổi đánh giá và đặt trạng thái thành Đã đánh giá. Trạng thái được đánh giá thực hiện chuyển đổi phê duyệt và đặt trạng thái thành Đã phê duyệt (hoặc bị từ chối).


// Application class encapsulates state transition,
// the client is unable to directly set state.
public class Application {
    State currentState = null;

    State AppliedState    = new Applied(this);
    State DeclinedState   = new Declined(this);
    State ApprovedState   = new Approved(this);
    State ReviewedState   = new Reviewed(this);

    public class Application (ApplicationDocument myApplication) {
        if(myApplication != null && isComplete()) {
            currentState = AppliedState;
        } else {            
            throw new ArgumentNullException ("Your application is incomplete");
            // some kind of error communication would probably be better
        }
    }

    public apply()    { currentState.apply(); }
    public review()   { currentState.review(); }
    public approve()  { currentState.approve(); }
    public decline()  { currentState.decline(); }


    //These could be done via an enum. I like enums!
    protected void setSubmittingState() {}
    protected void setApproveState() {}
    // etc. ...
}

// could be an interface if we don't have any default or base behavior.
public abstract class State {   
    protected Application theApp;
    // maybe these return an object communicating errors / error state.
    public abstract void apply();
    public abstract void review();
    public abstract void accept();
    public abstract void decline();
}

public class Applied implements State {
    public Applied (Application newApp) {
        if(newApp != null)
            theApp = newApp;
        else
            throw new ArgumentNullException ("null application argument");
     }

    public override void apply() {
        // whatever is appropriate when already in "applied" state
        // do not do any work on behalf of other states!
        // throwing exceptions here is not appropriate, as others
        // have said.
      }

    public override void review() {
        if(recursiveBureaucracyBuckPassing())
            theApp.setReviewedState();
    }

    public override void decline() { // ditto  }
}

public class Reviewed implements State {}
public class Approved implements State {}
public class Declined implements State {}

Chỉnh sửa - Xử lý lỗi Nhận xét

Một bình luận gần đây:

... Nếu bạn đang cố gắng mượn một cuốn sách đã được cấp cho người khác, mô hình Sách sẽ chứa logic để ngăn trạng thái của nó thay đổi. Điều này có thể thông qua một giá trị trả về (ví dụ: yay / nay hoặc mã trạng thái boolean thành công) hoặc một ngoại lệ (ví dụ: IllegalStateChangeException) hoặc một số phương tiện khác. Bất kể các phương tiện được chọn, khía cạnh này không được đề cập như một phần của câu trả lời này (hoặc bất kỳ).

Và từ câu hỏi ban đầu:

Nhưng loại mã này không làm cho nó rõ ràng là một quy tắc như vậy tồn tại, nó cho phép bất cứ ai gọi phương thức update () và chỉ sau khi khách hàng không biết hoạt động đó không được phép.

Có nhiều công việc thiết kế để làm. Không có Unified Field Theory Pattern. Sự nhầm lẫn xuất phát từ việc giả định khung chuyển đổi trạng thái sẽ thực hiện các chức năng ứng dụng chung và xử lý lỗi. Điều đó cảm thấy sai vì nó là. Câu trả lời hiển thị được thiết kế để kiểm soát sự thay đổi trạng thái.


Chắc chắn tôi có thể viết phương thức update () trong lớp Ứng dụng, kiểm tra trạng thái Ứng dụng và không làm gì hoặc ném ngoại lệ nếu Ứng dụng không ở trạng thái bắt buộc

Điều này cho thấy có ba chức năng làm việc ở đây: Nhà nước, Cập nhật và tương tác của hai. Trong trường hợp Applicationnày không phải là mã tôi đã viết. Nó có thể sử dụng nó để xác định trạng thái hiện tại. Applicationcũng không phải là applicationPaperworkmột. Applicationkhông phải là sự tương tác của hai người, nhưng có thể là một StateContextEvaluatorlớp chung . Bây giờ Applicationsẽ phối hợp các tương tác thành phần này và sau đó hành động tương ứng, như phát ra một thông báo lỗi.

Kết thúc chỉnh sửa


Tui bỏ lỡ điều gì vậy? Điều này dường như cho phép gọi cả bốn phương thức, bất kể trạng thái, mà không có gợi ý về cách thiết lập này được sử dụng để liên lạc với các phương thức gọi mà cuộc gọi áp dụng () không thành công do đã áp dụng ví dụ.
kwah

1
cho phép gọi tất cả bốn phương thức, bất kể nhà nước Có. Nó phải. không có gợi ý về cách thiết lập này được sử dụng để giao tiếp với các phương thức gọi Xem bình luận trong hàm Applicationtạo nơi ném ngoại lệ. Có thể gọi điện AppliedState.Approve()có thể dẫn đến một thông báo người dùng "Ứng dụng phải được xem xét trước khi có thể được phê duyệt."
radarbob

1
... Cuộc gọi áp dụng () không thành công do đã áp dụng ví dụ . Đó là suy nghĩ sai lầm. Cuộc gọi thành công. Nhưng có hành vi khác nhau cho các trạng thái khác nhau. Đó là Mẫu trạng thái ...... Tuy nhiên, lập trình viên phải quyết định hành vi nào là phù hợp. Nhưng đó là suy nghĩ sai lầm rằng "OMG một lỗi !!! Chúng ta cần phải đi apoplectic và hủy bỏ chương trình!" Tôi hy vọng AppliedState.apply()sẽ nhẹ nhàng nhắc nhở người dùng rằng ứng dụng đã được gửi và đang chờ xem xét. Và chương trình tiếp tục.
radarbob

Giả sử mẫu trạng thái đang được sử dụng làm mô hình, "lỗi" phải được truyền đến giao diện người dùng. Ví dụ: nếu bạn đang cố gắng mượn một cuốn sách đã được cấp cho người khác, mô hình Sách sẽ chứa logic để ngăn trạng thái của nó thay đổi. Điều này có thể thông qua một giá trị trả về (ví dụ: yay / nay hoặc mã trạng thái boolean thành công) hoặc một ngoại lệ (ví dụ: IllegalStateChangeException) hoặc một số phương tiện khác. Bất kể các phương tiện được chọn, khía cạnh này không được đề cập như một phần của câu trả lời này (hoặc bất kỳ).
kwah

Cảm ơn chúa, ai đó đã nói điều đó. "Tôi cần hành vi khác nhau dựa trên trạng thái của một đối tượng. ... Vâng, vâng. Bạn muốn mô hình trạng thái ." ++ đậu già.
RubberDuck

1

Nói chung, những gì bạn đang mô tả là một quy trình công việc. Cụ thể hơn, các chức năng kinh doanh được thể hiện bởi các quốc gia như ĐÁNH GIÁ ĐƯỢC PHÊ DUYỆT hoặc QUYẾT ĐỊNH nằm trong tiêu đề "quy tắc kinh doanh" hoặc "logic kinh doanh".

Nhưng để rõ ràng, các quy tắc kinh doanh không nên được mã hóa thành các ngoại lệ. Để làm như vậy sẽ là sử dụng các ngoại lệ để kiểm soát luồng chương trình và có nhiều lý do chính đáng tại sao bạn không nên làm điều đó. Các ngoại lệ nên được sử dụng cho các điều kiện đặc biệt và trạng thái INVALID của ứng dụng hoàn toàn không có ngoại lệ theo quan điểm kinh doanh.

Sử dụng ngoại lệ trong trường hợp chương trình không thể phục hồi từ tình trạng lỗi mà không có sự can thiệp của người dùng (ví dụ: "không tìm thấy tệp").

Không có mẫu cụ thể để viết logic kinh doanh, ngoài các kỹ thuật thông thường để sắp xếp các hệ thống xử lý dữ liệu kinh doanh và viết mã để thực hiện các quy trình của bạn. Nếu quy tắc kinh doanh và quy trình công việc phức tạp, hãy xem xét sử dụng một số loại máy chủ quy trình công việc hoặc công cụ quy tắc kinh doanh.

Trong mọi trường hợp, các trạng thái ĐÁNH GIÁ, ĐƯỢC PHÊ DUYỆT, TUYÊN BỐ, v.v. có thể được biểu diễn bằng một biến riêng kiểu enum trong lớp của bạn. Nếu bạn sử dụng các phương thức getter / setter, bạn có thể kiểm soát xem setters có cho phép thay đổi hay không bằng cách kiểm tra giá trị của biến enum trước tiên. Nếu ai đó cố gắng ghi vào setter khi giá trị enum ở trạng thái sai, thì bạn có thể ném ngoại lệ.


Có một đối tượng, được gọi là "Ứng dụng", thuộc tính của nó chỉ có thể được thay đổi nếu "Trạng thái" của nó là "BAN ĐẦU". Đây không phải là một quy trình công việc lớn, giống như các tài liệu chảy từ bộ phận này sang bộ phận khác. Những gì tôi không làm là, để phản ánh hành vi này trong một ý nghĩa hướng đối tượng.
uylmz

Ứng dụng @Reek sẽ hiển thị giao diện đọc / ghi và logic lặp sẽ diễn ra ở cấp cao hơn. Cả ứng viên và nhân sự đều sử dụng cùng một đối tượng, nhưng có các đặc quyền khác nhau - đối tượng ứng dụng không nên quan tâm đến nó. Các ngoại lệ bên trong có thể được sử dụng để bảo vệ tích hợp hệ thống, nhưng tôi sẽ không phòng thủ (chỉnh sửa thông tin liên hệ có thể không cần thiết ngay cả đối với các ứng dụng được phê duyệt - chỉ cần mức truy cập cao hơn).
rùng mình

1

Applicationcó thể là một giao diện và bạn có thể có một triển khai cho từng trạng thái. Giao diện có thể có một moveToNextState()phương thức và điều này sẽ ẩn tất cả logic công việc.

Đối với nhu cầu của khách hàng, cũng có thể có một phương thức trả lại trực tiếp những gì bạn có thể làm và không (nghĩa là một bộ booleans), thay vì chỉ trạng thái, do đó bạn không cần "danh sách kiểm tra" trong máy khách (tôi giả sử dù sao thì máy khách cũng là bộ điều khiển MVC hoặc UI).

Tuy nhiên, thay vì ném một ngoại lệ, bạn không thể làm gì và ghi lại nỗ lực. Điều này là an toàn trong thời gian chạy, các quy tắc đã được thi hành và khách hàng có cách để ẩn các điều khiển "cập nhật".


1

Một cách tiếp cận cho vấn đề cực kỳ thành công này là hypermedia - sự thể hiện trạng thái của thực thể được kèm theo các điều khiển hypermedia mô tả các loại chuyển đổi hiện được phép. Người tiêu dùng truy vấn các điều khiển để khám phá những gì có thể được thực hiện.

Đó là một máy trạng thái, với một truy vấn trong giao diện của nó cho phép bạn khám phá những sự kiện nào bạn được phép phát sinh.

Nói cách khác: chúng tôi đang mô tả web (REST).

Một cách tiếp cận khác là lấy ý tưởng của bạn về các giao diện khác nhau cho các trạng thái khác nhau và cung cấp một truy vấn cho phép bạn phát hiện giao diện nào hiện có sẵn. Hãy suy nghĩ IUn Unknown :: QueryInterface hoặc bỏ xuống. Mã khách hàng đóng vai Mẹ tôi có thể cùng nhà nước tìm hiểu những gì được phép.

Về cơ bản, đó là cùng một mẫu - chỉ sử dụng một giao diện để thể hiện các điều khiển hypermedia.


Tôi thích điều này. Nó có thể được kết hợp với mẫu Trạng thái để trả về một tập hợp các Quốc gia hợp lệ có thể được chuyển sang. Chain of Command đến với tâm trí một cách.
RubberDuck

1
Tôi đoán là bạn không muốn "tập hợp các trạng thái hợp lệ" mà là "tập hợp các hành động hợp lệ". Hãy nghĩ đồ thị: bạn muốn nút hiện tại (trạng thái) và danh sách các cạnh (hành động). Bạn sẽ tìm ra trạng thái tiếp theo khi bạn chọn hành động của mình.
VoiceOfUnreason

Đúng. Bạn hoàn toàn đúng. Một tập hợp các hành động hợp lệ trong đó hành động đó thực sự là một chuyển đổi trạng thái (hoặc một cái gì đó kích hoạt một hành động).
RubberDuck

1

Đây là một ví dụ về cách bạn có thể tiếp cận điều này từ góc độ chức năng và cách nó giúp tránh những cạm bẫy tiềm ẩn. Tôi đang làm việc tại Haskell, nơi tôi cho rằng bạn không biết, vì vậy tôi sẽ giải thích chi tiết khi tôi đi cùng.

data Application = Applied ApplicationDetails |
                   InReview ApplicationDetails |
                   Approved ApplicationDetails |
                   Declined ApplicationDetails

Điều này xác định một kiểu dữ liệu có thể ở một trong bốn trạng thái tương ứng với trạng thái ứng dụng của bạn. ApplicationDetailsđược coi là một loại hiện có chứa thông tin chi tiết.

newtype UpdatableApplication = UpdatableApplication Application

Một bí danh loại cần chuyển đổi rõ ràng đến và từ Application. Điều này có nghĩa là nếu chúng ta xác định hàm sau chấp nhận và hủy bỏ một hàm UpdatableApplicationvà thực hiện một cái gì đó hữu ích với nó,

updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...

sau đó chúng ta phải chuyển đổi rõ ràng Ứng dụng thành ứng dụng UpdizableApplication trước khi chúng ta có thể sử dụng nó. Điều này được thực hiện bằng cách sử dụng chức năng này:

findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _               = Nothing

Ở đây chúng tôi làm ba điều thú vị:

  • Chúng tôi kiểm tra trạng thái của ứng dụng (sử dụng kết hợp mô hình, mà là thực sự tiện dụng cho các loại mã này), và
  • nếu nó có thể được cập nhật, chúng tôi gói nó trong một UpdatableApplication(chỉ liên quan đến một ghi chú kiểu biên dịch về sự thay đổi của loại được thêm vào, vì Haskell có một tính năng cụ thể để thực hiện loại thủ thuật cấp độ loại này, nó không mất gì khi chạy) và
  • chúng tôi trả về kết quả bằng "Có thể" (tương tự như Optiontrong C # hoặc Optionaltrong Java - đó là một đối tượng bao bọc một kết quả có thể bị thiếu).

Bây giờ, để thực sự kết hợp nó, chúng ta cần gọi hàm này và, nếu kết quả thành công, hãy chuyển nó sang hàm cập nhật ...

case findUpdatableApplication application of
    Just updatableApplication -> do
        storeApplicationInDatabase (updateApplication updatableApplication)
        showConfirmationPage
    Nothing -> do
        showErrorPage

updateApplicationhàm cần đối tượng được bao bọc, chúng ta không thể quên kiểm tra các điều kiện tiên quyết. Và bởi vì chức năng kiểm tra điều kiện tiên quyết trả về đối tượng được bọc bên trong một Maybeđối tượng, chúng ta không thể quên kiểm tra kết quả và trả lời tương ứng nếu thất bại.

Bây giờ ... bạn có thể làm điều này trong một ngôn ngữ hướng đối tượng. Nhưng nó không thuận tiện:

  • Không có ngôn ngữ OO nào tôi từng thử có một cú pháp đơn giản để tạo loại trình bao bọc an toàn loại, do đó, đó là bản tóm tắt.
  • Nó cũng sẽ kém hiệu quả hơn, vì ít nhất là đối với hầu hết các ngôn ngữ họ sẽ không thể loại bỏ loại trình bao bọc, vì nó sẽ được yêu cầu tồn tại và có thể phát hiện được trong thời gian chạy (Haskell không có kiểm tra loại thời gian chạy, tất cả các kiểm tra loại đều được thực hiện tại thời gian biên dịch).
  • Mặc dù một số ngôn ngữ OO có các loại tương đương với Maybechúng thường không có cách trích xuất dữ liệu thuận tiện và chọn đường dẫn để đi cùng một lúc. Kết hợp mẫu là thực sự hữu ích ở đây, quá.

1

Bạn có thể sử dụng mẫu «lệnh», sau đó yêu cầu Invoker cung cấp danh sách các hàm hợp lệ theo trạng thái của lớp người nhận.

Tôi đã sử dụng tương tự để cung cấp chức năng cho các giao diện khác nhau được gọi là mã của tôi, một số tùy chọn không khả dụng tùy thuộc vào trạng thái hiện tại của bản ghi, vì vậy invoker của tôi đã cập nhật danh sách và theo cách đó mọi GUI đều hỏi Invoker tùy chọn nào có sẵn và họ tự vẽ theo.

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.