2015-05-25 24 views
12

Tôi hiện đang xây dựng một ứng dụng web và cố gắng thiết kế ứng dụng theo cấu trúc MVC và kiến ​​trúc hướng dịch vụ tốt.Tách trình xác thực và dịch vụ với các cuộc gọi API bên ngoài

Tuy nhiên, tôi đã nhấn một chút tường khi kết nối lớp trình bày (tức là bộ điều khiển của tôi) và dịch vụ back-end trong khi vẫn duy trì báo cáo lỗi/xác thực tốt báo cáo lại cho người dùng.

Tôi đã đọc một bài đăng SO thực sự tốt here về cách tách logic Xác thực khỏi lớp dịch vụ và phần lớn tất cả đều có ý nghĩa. Tuy nhiên, có một "lỗ hổng", nếu bạn có thể gọi nó, trong mô hình này đã cười khúc khích với tôi: Làm thế nào để tránh trùng lặp nỗ lực khi tra cứu các đối tượng được yêu cầu bởi cả trình xác nhận và dịch vụ?

Tôi nghĩ rằng nó sẽ được dễ dàng hơn để giải thích với một ví dụ khá đơn giản:

Hãy nói rằng tôi có một ứng dụng cho phép người dùng chia sẻ các đoạn mã xung quanh. Bây giờ, tôi đã quyết định thêm một tính năng mới cho phép người dùng đính kèm tài khoản GitHub của họ vào tài khoản của họ trên trang web của tôi (tức là xây dựng một hồ sơ). Với mục đích của ví dụ này tôi sẽ chỉ đơn giản giả định rằng tất cả người dùng của tôi đều đáng tin cậy và chỉ cố gắng thêm tài khoản GitHub của riêng họ, chứ không phải của ai khác :)

Làm theo bài viết SO đã đề cập ở trên một dịch vụ GitHub cơ bản để truy xuất thông tin người dùng GitHub.

interface IGitHubUserService { 
    GitHubUser FindByUserName(string username); 
} 

Việc triển khai cụ thể GitHubUserService thực hiện cuộc gọi tốn kém đến https://api.github.com/users/{0} để lấy thông tin người dùng. Một lần nữa, theo mô hình của bài viết tôi thực hiện lệnh sau để liên kết một tài khoản người dùng cho một người dùng GitHub:

// Command for linking a GitHub account to an internal user account 
public class GitHubLinkCommand { 
    public int UserId { get; set; } 
    public string GitHubUsername { get; set } 
}; 

validator của tôi cần phải xác nhận rằng tên người dùng nhập vào bởi người sử dụng là một tài khoản GitHub hợp lệ. Điều này rất đơn giản: gọi FindByUserName trên GitHubUserService và chắc chắn rằng kết quả không phải là null:

public sealed class GitHubLinkCommandValidator : Validator<GitHubLinkCommand> { 
    private readonly IGitHubUserService _userService; 

    public GitHubLinkCommandValidator(IGitHubUserService userService) { 
     this._userService = userService; 
    } 

    protected override IEnumerable<ValidationResult> Validate(GitHubLinkCommand command) { 
     try { 
      var user = this._userService.FindByUserName(command.GitHubUsername); 
      if (user == null) 
       yield return new ValidationResult("Username", string.Format("No user with the name '{0}' found on GitHub's servers.")); 
     } 
     catch(Exception e) { 
      yield return new ValidationResult("Username", "There was an error contacting GitHub's API."); 
     } 
    } 
} 

Được rồi đó là tuyệt vời! Trình xác thực thực sự đơn giản và có ý nghĩa. Bây giờ là lúc để làm cho GitHubLinkCommandHandler:

public class GitHubLinkCommandHandler : ICommandHandler<GitHubLinkCommand> 
{ 
    private readonly IGitHubUserService _userService; 

    public GitHubLinkCommandHandler(IGitHubUserService userService) 
    { 
     this._userService = userService; 
    } 

    public void Handle(GitHubLinkCommand command) 
    { 
     // Get the user details from GitHub: 
     var user = this._userService.FindByUserName(command.GitHubUsername); 

     // implementation of this entity isn't really relevant, just assume it's a persistent entity to be stored in a backing database 
     var entity = new GitHubUserEntity 
     { 
      Name = user.Login, 
      AvatarUrl = user.AvatarUrl 
      // etc. 
     }; 

     // store the entity: 
     this._someRepository.Save(entity); 
    } 
} 

Một lần nữa, điều này có vẻ thực sự gọn gàng và đơn giản. Tuy nhiên có một vấn đề rõ ràng: Bản sao gọi tới IGitHubUserService::FindByUserName, một từ trình xác thực và một từ dịch vụ. Vào một ngày tồi tệ, cuộc gọi như vậy có thể mất 1-2 giây mà không có bộ nhớ đệm phía máy chủ, làm cho quá trình sao chép quá tốn kém để sử dụng mô hình kiến ​​trúc này.

Có ai khác gặp phải vấn đề như vậy khi viết trình xác thực/dịch vụ xung quanh các API bên ngoài và làm cách nào bạn giảm sự cố gắng sao chép bên ngoài triển khai bộ đệm trong lớp bê tông của bạn?

+2

Tôi thường muốn tách 'IGitHubUserService' khỏi ứng dụng của mình. Ở đó tôi sẽ đặt một bộ nhớ cache và thậm chí nếu nó thường hoạt động như ** Proxy ** (với một số _decoration_) nó cũng có thể trở thành ** Adapter ** (nếu giao diện GitHub thay đổi) hoặc thậm chí là ** ** ** (trong trường hợp bạn muốn làm cho nó đủ phổ biến để được sử dụng cũng với CodePlex, Google Code ...) –

+0

@AdrianoRepetti Đó là khá nhiều giải pháp duy nhất tôi đã đưa ra, nhưng nó không thực sự nhiều của một "giải pháp". Tôi có thể thấy nó hoạt động cho một dự án lớn, nhưng nó có vẻ như là một khoản đầu tư lớn cho một tính năng được thực hiện tầm thường như một ví dụ. –

+2

Proxy với bộ nhớ cache cho IGitHubService không phải là nhiều dòng mã hơn GitHubLinkCommandHandler (giả sử bạn cần để lộ chỉ FindUserByName) nhưng có, tôi đồng ý nó sẽ là tốt đẹp nếu có _something_ để tự động hóa mã boilerplate này (như chúng ta sẽ làm với AOP). –

Trả lời

1

Theo quan điểm của tôi, vấn đề là cả LinkCommandHandler lẫn LinkCommandValidator đều không nên truy xuất người dùng GitHub ngay từ đầu. Nếu bạn nghĩ về nguyên tắc trách nhiệm duy nhất, Trình xác thực có một công việc duy nhất, để xác thực sự tồn tại của người dùng và LinkCommandHanlder có một công việc duy nhất để nạp thực thể vào kho lưu trữ. Không ai trong số họ nên có công việc kéo Entity/User từ GitHub.

Tôi muốn cấu trúc mã của mình theo mẫu sau, mỗi mẫu đại diện cho một lớp có thể phân bổ. Mỗi lớp có thể nói chuyện với các lớp ở trên và lớp dưới đây nhưng nó không thể bỏ qua một lớp.

  1. Lớp dữ liệu - đây là nguồn dữ liệu như cơ sở dữ liệu hoặc dịch vụ thường bạn không viết mã cho điều này, bạn chỉ cần dùng nó.
  2. Lớp truy cập - số này đại diện cho mã tương tác với trình dữ liệu
  3. Lớp tồn tại - đây là mã để nhận các mục sẵn sàng cho cuộc gọi đến Lớp truy cập chẳng hạn như dịch dữ liệu, xây dựng thực thể từ dữ liệu hoặc nhóm nhiều cuộc gọi đến lớp truy cập thành một yêu cầu duy nhất để truy xuất dữ liệu hoặc lưu trữ dữ liệu. Ngoài ra, quyết định bộ nhớ cache và các cơ chế lưu bộ nhớ đệm và xóa bộ nhớ cache sẽ nằm trong lớp này.
  4. Lớp bộ xử lý - điều này thể hiện mã thực hiện logic nghiệp vụ. Đây cũng là nơi bạn sẽ sử dụng các trình xác nhận hợp lệ, các bộ xử lý, trình phân tích cú pháp khác, v.v.

Và sau đó tôi giữ tất cả các điều trên tách riêng khỏi lớp trình bày của tôi. Khái niệm là mã lõi và chức năng không nên biết nếu nó đang được sử dụng từ một trang web hoặc một ứng dụng máy tính để bàn, hoặc một dịch vụ WCF.

Vì vậy, trong ví dụ của bạn, tôi sẽ có một đối tượng GitHubLinkProcessor một phương thức gọi là LinkUser (tên người dùng chuỗi). Trong lớp đó, tôi sẽ khởi tạo lớp GitHubPeristenceLayer của mình và gọi phương thức FindUserByName (tên người dùng chuỗi) của nó. Tiếp theo, chúng tôi tiến hành khởi tạo một lớp GitHubUserValidator để xác thực người dùng không phải là null và tất cả các dữ liệu cần thiết đều có mặt. Một xác thực được thông qua, một đối tượng LinkRepositoryPersistence được khởi tạo và truyền GitHubUser cho persistence vào AccessLayer.

Nhưng tôi muốn lưu ý rằng đây chỉ là cách tôi sẽ làm điều đó, và không có nghĩa là tôi muốn ngụ ý rằng các phương pháp khác là ít hơn.

EDIT:

tôi sẽ cho một câu trả lời đơn giản vì tôi sợ câu trả lời của tôi đã quá dài và nhàm chán. =) Tôi sẽ chia tóc ở đây một lúc, vì vậy hãy chịu với tôi. Với tôi, bạn không xác nhận người dùng bằng cách gọi Git. Bạn đang kiểm tra sự tồn tại của một tài nguyên từ xa, có thể hoặc không thể thất bại. Một sự tương tự có thể là bạn có thể xác nhận rằng (800) 555-1212 là một định dạng hợp lệ cho một số điện thoại của Hoa Kỳ, nhưng không phải là số điện thoại hiện có và thuộc về đúng người. Đó là một quá trình riêng biệt. Như tôi đã nói, đó là chia tách lông, nhưng bằng cách làm như vậy nó cho phép các mẫu mã tổng thể tôi mô tả.

Vì vậy, giả sử đối tượng người dùng cục bộ của bạn có thuộc tính cho Tên người dùng và Email không thể rỗng. Bạn sẽ xác thực cho những người đó và chỉ chuyển sang kiểm tra tài nguyên nếu xác thực đó là chính xác.

public class User 
{ 
    public string UserName { get; set; } 
    public string Email { get; set; } 

    //git related properties 
    public string Login { get; set; } 
    public string AvataUrl { get; set; } 
} 

//A processor class to model the process of linking a local system user 
//to a remote GitHub User 
public class GitHubLinkProcessor() 
{ 
    public int LinkUser(string userName, string email, string gitLogin) 
    { 
      //first create our local user instance 
      var myUser = new LocalNamespace.User { UserName = userName, Email = email }; 

     var validator = new UserValidator(myUser); 
     if (!validator.Validate()) 
      throw new Exception("Invalid or missing user data!"); 

     var GitPersistence = new GitHubPersistence(); 

     var myGitUser = GitPersistence.FindByUserName(gitLogin); 
     if (myGitUser == null) 
      throw new Exception("User doesnt exist in Git!"); 

     myUser.Login = myGitUser.Login; 
     myUser.AvatorUrl = myGitUser.AvatarUrl; 

     //assuming your persistence layer is returning the Identity 
     //for this user added to the database 
     var userPersistence = new UserPersistence(); 
     return userPersistence.SaveLocalUser(myUser); 

     } 
} 

public class UserValidator 
{ 
    private LocalNamespace.User _user; 

    public UserValidator(User user) 
    { 
     this._user = user; 
    } 

    public bool Validate() 
    { 
     if (String.IsNullOrEmpty(this._user.UserName) || 
      String.IsNullOrEmpty(this._user.Email)) 
     { 
      return false; 
     } 
    } 
} 
+0

Lý thuyết là âm thanh, nhưng tôi đã thử một giải pháp như vậy trước đây và nó có một nhược điểm nghiêm trọng: Bạn * luôn luôn * truy xuất người dùng GitHub, thậm chí trước khi thực hiện xác nhận cơ bản nhất (tức là tất cả các trường biểu mẫu hiện diện?). Bỏ qua một lớp bộ nhớ đệm, đây là rất nhiều công việc dư thừa. Có rất nhiều xác nhận "rẻ" có thể thất bại trước khi thậm chí cần phải thực hiện một cuộc gọi API tới GitHub và nó sẽ hiệu quả hơn để chạy điều đó trước tiên. –

+0

Cách duy nhất tôi có thể thấy xung quanh nó (sử dụng kiến ​​trúc của bạn) là phá vỡ quá trình xác nhận thành nhiều phương thức, để 'GitHubLinkProcessor' vẫn có thể chịu trách nhiệm về luồng, trong khi vẫn có thể thất bại sớm trong quá trình xác thực quá trình. –

+0

Xin chào Jason, bạn đúng trong giả định đó. Đó là lý do tại sao tôi thêm một chút để trả lời của tôi để giúp làm rõ nó một chút. –

Các vấn đề liên quan