2013-06-29 43 views
5

Tôi có F # chương trình sau đó lấy một trang web từ internet:Xử lý ngoại lệ Web đúng cách?

open System.Net 

[<EntryPoint>] 
let main argv = 
    let mutable pageData : byte[] = [| |] 
    let fullURI = "http://www.badaddress.xyz" 
    let wc = new WebClient() 
    try 
     pageData <- wc.DownloadData(fullURI) 
     () 
    with 
    | :? System.Net.WebException as err -> printfn "Web error: \n%s" err.Message 
    | exn -> printfn "Unknown exception:\n%s" exn.Message 

    0 // return an integer exit code 

này hoạt động tốt nếu URI là hợp lệ máy có kết nối internet web server đáp ứng đúng vv Trong một thế giới lập trình chức năng lý tưởng, kết quả của một hàm sẽ không phụ thuộc vào các biến bên ngoài không được chuyển thành các đối số (các tác dụng phụ).

Những gì tôi muốn biết là F # mẫu thiết kế thích hợp để đối phó với các hoạt động mà có thể yêu cầu các chức năng để đối phó với thu hồi lỗi bên ngoài là gì. Ví dụ: nếu trang web bị hỏng, có thể bạn muốn đợi 5 phút và thử lại. Các tham số có nên thử bao nhiêu lần để thử lại và trì hoãn giữa các lần thử lại được truyền một cách rõ ràng hoặc là OK để nhúng các biến này vào hàm?

Trả lời

6

Trong F #, khi bạn muốn xử lý có thể khôi phục lỗi, bạn hầu như muốn sử dụng loại option hoặc loại Choice<_,_>. Trong thực tế, sự khác biệt duy nhất giữa chúng là Choice cho phép bạn trả lại một số thông tin về lỗi trong khi option thì không. Nói cách khác, option là tốt nhất khi nó không quan trọng cách hoặc lý do tại sao một cái gì đó không thành công (chỉ vì nó không thành công); Choice<_,_> được sử dụng khi có thông tin về cách hoặc lý do tại sao điều gì đó không thành công là điều quan trọng. Ví dụ, bạn có thể muốn ghi thông tin lỗi vào nhật ký; hoặc có thể bạn muốn xử lý tình huống lỗi khác nhau dựa trên lý do tại sao điều gì đó không thành công - trường hợp sử dụng tuyệt vời cho việc này là cung cấp thông báo lỗi chính xác để giúp người dùng chẩn đoán sự cố.

Với ý nghĩ đó, dưới đây là cách tôi muốn Refactor mã của bạn để xử lý thất bại trong sạch, phong cách chức năng:

open System 
open System.Net 

/// Retrieves the content at the given URI. 
let retrievePage (client : WebClient) (uri : Uri) = 
    // Preconditions 
    checkNonNull "uri" uri 
    if not <| uri.IsAbsoluteUri then 
     invalidArg "uri" "The URI must be an absolute URI." 

    try 
     // If the data is retrieved successfully, return it. 
     client.DownloadData uri 
     |> Choice1Of2 
    with 
    | :? System.Net.WebException as webExn -> 
     // Return the URI and WebException so they can be used to diagnose the problem. 
     Choice2Of2 (uri, webExn) 
    | _ -> 
     // Reraise any other exceptions -- we don't want to handle them here. 
     reraise() 

/// Retrieves the content at the given URI. 
/// If a WebException is raised when retrieving the content, the request 
/// will be retried up to a specified number of times. 
let rec retrievePageRetry (retryWaitTime : TimeSpan) remainingRetries (client : WebClient) (uri : Uri) = 
    // Preconditions 
    checkNonNull "uri" uri 
    if not <| uri.IsAbsoluteUri then 
     invalidArg "uri" "The URI must be an absolute URI." 
    elif remainingRetries = 0u then 
     invalidArg "remainingRetries" "The number of retries must be greater than zero (0)." 

    // Try to retrieve the page. 
    match retrievePage client uri with 
    | Choice1Of2 _ as result -> 
     // Successfully retrieved the page. Return the result. 
     result 
    | Choice2Of2 _ as error -> 
     // Decrement the number of retries. 
     let retries = remainingRetries - 1u 

     // If there are no retries left, return the error along with the URI 
     // for diagnostic purposes; otherwise, wait a bit and try again. 
     if retries = 0u then error 
     else 
      // NOTE : If this is modified to use 'async', you MUST 
      // change this to use 'Async.Sleep' here instead! 
      System.Threading.Thread.Sleep retryWaitTime 

      // Try retrieving the page again. 
      retrievePageRetry retryWaitTime retries client uri 

[<EntryPoint>] 
let main argv = 
    /// WebClient used for retrieving content. 
    use wc = new WebClient() 

    /// The amount of time to wait before re-attempting to fetch a page. 
    let retryWaitTime = TimeSpan.FromSeconds 2.0 

    /// The maximum number of times we'll try to fetch each page. 
    let maxPageRetries = 3u 

    /// The URI to fetch. 
    let fullURI = Uri ("http://www.badaddress.xyz", UriKind.Absolute) 

    // Fetch the page data. 
    match retrievePageRetry retryWaitTime maxPageRetries wc fullURI with 
    | Choice1Of2 pageData -> 
     printfn "Retrieved %u bytes from: %O" (Array.length pageData) fullURI 

     0 // Success 
    | Choice2Of2 (uri, error) -> 
     printfn "Unable to retrieve the content from: %O" uri 
     printfn "HTTP Status: (%i) %O" (int error.Status) error.Status 
     printfn "Message: %s" error.Message 

     1 // Failure 

Về cơ bản, tôi chia mã của bạn ra thành hai chức năng, cộng với bản gốc main:

  • Một chức năng cố truy xuất nội dung từ URI được chỉ định.
  • Một hàm chứa logic để thử lại các lần thử; điều này 'kết thúc tốt đẹp' hàm đầu tiên thực hiện các yêu cầu thực tế.
  • Chức năng chính ban đầu hiện chỉ xử lý 'cài đặt' (bạn có thể dễ dàng lấy từ app.config hoặc web.config) và in kết quả cuối cùng. Nói cách khác, nó không biết gì về logic thử lại - bạn có thể sửa đổi một dòng mã với câu lệnh match và sử dụng hàm yêu cầu không thử lại thay vào đó nếu bạn muốn.

Nếu bạn muốn kéo nội dung từ nhiều URI VÀ chờ một số lượng đáng kể thời gian (ví dụ, 5 phút) giữa retries, bạn nên sửa đổi logic thử lại để sử dụng một hàng đợi ưu tiên hoặc một cái gì đó thay vì sử dụng Thread.Sleep hay Async.Sleep.

Plug Shameless: thư viện ExtCore chứa một số điều giúp cuộc sống của bạn dễ dàng hơn khi xây dựng một cái gì đó như thế này, đặc biệt là nếu bạn muốn làm cho nó không đồng bộ. Quan trọng nhất, nó cung cấp một luồng công việc asyncChoicecollections functions designed to work with it.

Đối với câu hỏi của bạn về việc truyền tham số (như thời gian chờ thử lại và số lần thử lại) - Tôi không nghĩ rằng có quy tắc cứng và nhanh để quyết định có chuyển chúng hay mã hóa chúng trong chức năng. Trong hầu hết các trường hợp, tôi thích chuyển chúng vào, mặc dù nếu bạn có nhiều hơn một vài thông số để truyền vào, bạn nên tạo một bản ghi để giữ tất cả chúng và thay vào đó. Một cách tiếp cận khác mà tôi đã sử dụng là tạo các tham số option giá trị, trong đó mặc định được lấy từ tệp cấu hình (mặc dù bạn sẽ muốn kéo chúng từ tệp một lần và gán chúng cho một số trường riêng tư để tránh phân tích cú pháp lại tệp cấu hình mỗi khi hàm của bạn được gọi); điều này giúp bạn dễ dàng sửa đổi các giá trị mặc định mà bạn đã sử dụng trong mã của mình, nhưng cũng cung cấp cho bạn tính linh hoạt khi ghi đè chúng khi cần thiết.