Là một giao diện phơi bày các chức năng không đồng bộ là một sự trừu tượng bị rò rỉ?


12

Tôi đang đọc cuốn sách Nguyên tắc, thực tiễn và mô hình tiêm phụ thuộc và tôi đọc về khái niệm trừu tượng rò rỉ được mô tả kỹ trong cuốn sách.

Ngày nay, tôi đang cấu trúc lại một cơ sở mã C # bằng cách sử dụng phép nội xạ phụ thuộc để các cuộc gọi không đồng bộ được sử dụng thay vì chặn các cuộc gọi. Làm như vậy tôi đang xem xét một số giao diện đại diện cho sự trừu tượng trong cơ sở mã của tôi và giao diện nào cần được thiết kế lại để có thể sử dụng các cuộc gọi không đồng bộ.

Ví dụ: xem xét giao diện sau đại diện cho kho lưu trữ cho người dùng ứng dụng:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

Theo định nghĩa của cuốn sách, một sự trừu tượng bị rò rỉ là một sự trừu tượng được thiết kế với một triển khai cụ thể, do đó một số chi tiết thực hiện "rò rỉ" thông qua chính sự trừu tượng hóa.

Câu hỏi của tôi là như sau: chúng ta có thể xem xét một giao diện được thiết kế với tâm trí không đồng bộ, chẳng hạn như IUserRep repository, như một ví dụ về Trừu tượng Leaky không?

Tất nhiên không phải tất cả các triển khai có thể có liên quan đến sự không đồng bộ: chỉ thực hiện ngoài quy trình (như triển khai SQL), nhưng kho lưu trữ trong bộ nhớ không yêu cầu không đồng bộ (thực tế triển khai phiên bản trong bộ nhớ của giao diện có lẽ nhiều hơn khó khăn nếu giao diện hiển thị các phương thức không đồng bộ, ví dụ, bạn có thể phải trả về một cái gì đó như Task.CompletedTask hoặc Task.FromResult (người dùng) trong việc triển khai phương thức).

Bạn nghĩ gì về điều này ?


@Neil có lẽ tôi đã nhận được điểm. Giao diện hiển thị các phương thức trả về Tác vụ hoặc Tác vụ <T> không phải là một bản tóm tắt bị rò rỉ trên mỗi se, chỉ đơn giản là một hợp đồng có chữ ký liên quan đến các tác vụ. Phương thức trả về Tác vụ hoặc Tác vụ <T> không có nghĩa là có triển khai async (ví dụ: nếu tôi tạo một tác vụ đã hoàn thành bằng cách sử dụng Task.CompletedTask Tôi không thực hiện triển khai async). Ngược lại, việc triển khai async trong C # yêu cầu loại trả về của phương thức async phải thuộc loại Nhiệm vụ hoặc Nhiệm vụ <T>. Nói cách khác, khía cạnh "rò rỉ" duy nhất trong giao diện của tôi là hậu tố async trong tên
Enrico Massone

@Neil thực sự có một hướng dẫn đặt tên trong đó nêu rõ rằng tất cả các phương thức async nên có tên kết thúc bằng "Async". Nhưng điều này không có nghĩa là một phương thức trả về một Tác vụ hoặc một Tác vụ <T> phải được đặt tên bằng hậu tố Async bởi vì nó có thể được thực hiện bằng cách không sử dụng các cuộc gọi không đồng bộ.
Enrico Massone

6
Tôi sẽ lập luận 'tính không đồng bộ' của một phương thức được biểu thị bằng thực tế là nó trả về a Task. Các hướng dẫn để thêm các phương thức async với từ async là để phân biệt giữa các lệnh gọi API giống hệt nhau (C # không thể gửi dựa trên loại trả về). Tại công ty chúng tôi, chúng tôi đã bỏ tất cả cùng nhau.
richzilla

Có một số câu trả lời và nhận xét giải thích tại sao bản chất không đồng bộ của phương pháp là một phần của sự trừu tượng hóa. Một câu hỏi thú vị hơn là làm thế nào một ngôn ngữ hoặc API lập trình có thể tách chức năng của một phương thức khỏi cách nó được thực thi, đến mức chúng ta không còn cần các giá trị trả về của tác vụ hoặc các dấu không đồng bộ? Những người lập trình chức năng dường như đã tìm ra điều này tốt hơn. Xem xét cách các phương thức không đồng bộ được xác định trong F # và các ngôn ngữ khác.
Frank Hileman

2
:-) -> "Người lập trình chức năng" ha. Async không bị rò rỉ nhiều hơn đồng bộ, có vẻ như vậy bởi vì chúng tôi được sử dụng để viết mã đồng bộ theo mặc định. Nếu tất cả chúng ta được mã hóa không đồng bộ theo mặc định, một chức năng đồng bộ có thể bị rò rỉ.
StarTrekRedneck

Câu trả lời:


8

Tất nhiên, người ta có thể viện dẫn luật trừu tượng bị rò rỉ , nhưng điều đó không đặc biệt thú vị bởi vì nó cho rằng tất cả các khái niệm trừu tượng đều bị rò rỉ. Người ta có thể tranh luận và chống lại phỏng đoán đó, nhưng nó không có ích gì nếu chúng ta không chia sẻ sự hiểu biết về ý nghĩa của chúng ta bằng cách trừu tượng hóa và những gì chúng ta muốn nói về sự rò rỉ . Do đó, trước tiên tôi sẽ cố gắng phân định cách tôi xem từng điều khoản sau:

Trừu tượng

Định nghĩa trừu tượng yêu thích của tôi bắt nguồn từ APPP của Robert C. Martin :

"Một sự trừu tượng là sự khuếch đại của thiết yếu và loại bỏ những thứ không liên quan."

Do đó, các giao diện không phải là bản thân trừu tượng . Chúng chỉ trừu tượng nếu chúng mang đến những gì quan trọng và che giấu phần còn lại.

Rò rỉ

Cuốn sách Nguyên tắc, mô hình và thực tiễn tiêm phụ thuộc định nghĩa thuật ngữ trừu tượng rò rỉ trong bối cảnh của Dependency Injection (DI). Đa hình và các nguyên tắc RẮN đóng một vai trò lớn trong bối cảnh này.

Từ Nguyên tắc đảo ngược phụ thuộc (DIP), sau đây, một lần nữa trích dẫn APPP, rằng:

"khách hàng [...] sở hữu các giao diện trừu tượng"

Điều này có nghĩa là các máy khách (mã gọi) xác định các trừu tượng mà chúng yêu cầu, sau đó bạn đi và thực hiện trừu tượng hóa đó.

Một sự trừu tượng bị rò rỉ , theo quan điểm của tôi, là một sự trừu tượng vi phạm DIP bằng cách nào đó bao gồm một số chức năng mà khách hàng không cần .

Phụ thuộc đồng bộ

Một khách hàng thực hiện một đoạn logic nghiệp vụ thường sẽ sử dụng DI để tự tách rời khỏi các chi tiết triển khai nhất định, chẳng hạn như, thông thường, cơ sở dữ liệu.

Xem xét một đối tượng miền xử lý yêu cầu đặt chỗ nhà hàng:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

Ở đây, sự IReservationsRepositoryphụ thuộc được xác định riêng bởi khách hàng, MaîtreDlớp:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

Giao diện này hoàn toàn đồng bộ vì MaîtreDlớp không cần nó không đồng bộ.

Phụ thuộc không đồng bộ

Bạn có thể dễ dàng thay đổi giao diện thành không đồng bộ:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

Các MaîtreDlớp, tuy nhiên, không cần các phương pháp đó là không đồng bộ, vì vậy bây giờ các DIP bị vi phạm. Tôi coi đây là một sự trừu tượng rò rỉ, bởi vì một chi tiết thực hiện buộc khách hàng phải thay đổi. Các TryAcceptphương pháp bây giờ cũng đã trở thành không đồng bộ:

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

Không có lý do cố hữu nào để logic miền không đồng bộ, nhưng để hỗ trợ tính không đồng bộ của việc triển khai, điều này hiện bắt buộc.

Lựa chọn tốt hơn

Tại NDC Sydney 2018 tôi đã nói chuyện về chủ đề này . Trong đó, tôi cũng phác thảo một giải pháp thay thế không bị rò rỉ. Tôi cũng sẽ có bài nói chuyện này tại một số hội nghị vào năm 2019, nhưng bây giờ được đổi thương hiệu với tiêu đề mới về tiêm Async .

Tôi dự định cũng sẽ xuất bản một loạt các bài đăng trên blog để đi kèm với bài nói chuyện. Các bài viết này đã được viết và ngồi trong hàng đợi bài viết của tôi, chờ đợi để được xuất bản, vì vậy hãy theo dõi.


Đối với tôi đây là một vấn đề của ý định. Nếu sự trừu tượng của tôi xuất hiện như thể nó sẽ hành xử theo một cách nhưng một số chi tiết hoặc ràng buộc phá vỡ sự trừu tượng như được trình bày, đó là một sự trừu tượng bị rò rỉ. Nhưng trong trường hợp này, tôi trình bày rõ ràng với bạn rằng hoạt động này không đồng bộ - đó không phải là điều tôi đang cố gắng trừu tượng hóa. Điều đó khác biệt trong suy nghĩ của tôi so với ví dụ của bạn, nơi tôi (thông minh hay không) đang cố gắng trừu tượng hóa thực tế là có một cơ sở dữ liệu SQL và tôi vẫn để lộ một chuỗi kết nối. Có lẽ đó là vấn đề ngữ nghĩa / quan điểm.
Ant P

Vì vậy, chúng ta có thể nói rằng một sự trừu tượng không bao giờ bị rò rỉ "per se", thay vào đó là một sự rò rỉ nếu một số chi tiết của một triển khai cụ thể bị rò rỉ từ các thành viên bị phơi bày và buộc người tiêu dùng thay đổi việc thực hiện nó, để đáp ứng hình dạng trừu tượng .
Enrico Massone

2
Thật thú vị, điểm mà bạn nhấn mạnh trong lời giải thích của bạn là một trong những điểm bị hiểu lầm nhất trong toàn bộ câu chuyện tiêm chích phụ thuộc. Đôi khi các nhà phát triển quên nguyên tắc đảo ngược phụ thuộc và cố gắng thiết kế trừu tượng trước và sau đó họ điều chỉnh thiết kế của người tiêu dùng để đối phó với chính sự trừu tượng đó. Thay vào đó, quá trình nên được thực hiện theo thứ tự ngược lại.
Enrico Massone

11

Nó hoàn toàn không phải là một sự trừu tượng.

Không đồng bộ là một thay đổi cơ bản đối với định nghĩa của hàm - điều đó có nghĩa là tác vụ chưa kết thúc khi cuộc gọi trở lại, nhưng điều đó cũng có nghĩa là luồng chương trình của bạn sẽ tiếp tục gần như ngay lập tức, không bị trì hoãn kéo dài. Một chức năng không đồng bộ và chức năng đồng bộ thực hiện cùng một nhiệm vụ về cơ bản là các chức năng khác nhau. Không đồng bộ không phải là một chi tiết thực hiện. Đó là một phần của định nghĩa của hàm.

Nếu chức năng tiếp xúc với cách thức chức năng được thực hiện không đồng bộ, điều đó sẽ bị rò rỉ. Bạn (không / không cần phải) quan tâm đến cách nó được thực hiện.


5

Các asyncthuộc tính của một phương pháp là một thẻ chỉ ra rằng chăm sóc và xử lý đặc biệt là bắt buộc. Như vậy, nó cần phải rò rỉ ra thế giới. Các hoạt động không đồng bộ là cực kỳ khó để soạn đúng, do đó, việc cung cấp cho người dùng API một cách chính xác là rất quan trọng.

Thay vào đó, nếu thư viện của bạn quản lý đúng cách tất cả hoạt động không đồng bộ trong chính nó, thì bạn có thể đủ khả năng để không async"rò rỉ" ra khỏi API.

Có bốn chiều khó khăn trong phần mềm: dữ liệu, kiểm soát, không gian và thời gian. Các hoạt động không đồng bộ trải dài cả bốn chiều, do đó, cần sự quan tâm nhất.


Tôi đồng ý với tình cảm của bạn, nhưng "rò rỉ" hàm ý điều gì đó tồi tệ, đó là mục đích của thuật ngữ "trừu tượng rò rỉ" - một điều không mong muốn trong sự trừu tượng. Trong trường hợp async vs sync, không có gì bị rò rỉ.
StarTrekRedneck

2

một sự trừu tượng bị rò rỉ là một sự trừu tượng hóa được thiết kế với một triển khai cụ thể, do đó một số chi tiết thực hiện "rò rỉ" thông qua chính sự trừu tượng hóa.

Không hẳn. Một sự trừu tượng là một điều khái niệm mà bỏ qua một số yếu tố của một vấn đề hoặc vấn đề cụ thể phức tạp hơn (để làm cho vấn đề / vấn đề đơn giản hơn, dễ xử lý hơn hoặc do một số lợi ích khác). Như vậy, nó nhất thiết phải khác với sự việc / vấn đề thực tế, và do đó nó sẽ bị rò rỉ trong một số tập hợp con (ví dụ, tất cả các tóm tắt đều bị rò rỉ, câu hỏi duy nhất là ở mức độ nào - nghĩa là, trong trường hợp nào là trừu tượng hữu ích cho chúng tôi, lĩnh vực ứng dụng của nó là gì).

Điều đó nói rằng, khi nói đến trừu tượng hóa phần mềm, đôi khi (hoặc có thể thường là đủ?) Các chi tiết chúng tôi chọn bỏ qua thực sự không thể bị bỏ qua vì chúng ảnh hưởng đến một số khía cạnh của phần mềm quan trọng đối với chúng tôi (hiệu suất, khả năng bảo trì, ...) . Vì vậy, một sự trừu tượng bị rò rỉ là một sự trừu tượng được thiết kế để bỏ qua một số chi tiết nhất định (theo giả định rằng nó có thể và hữu ích để làm như vậy), nhưng sau đó hóa ra một số chi tiết đó có ý nghĩa trong thực tế (vì vậy chúng không thể bị bỏ qua "rò rỉ ra ngoài").

Vì vậy, một giao diện phơi bày một chi tiết của một triển khai không bị rò rỉ mỗi lần (hay đúng hơn là một giao diện, được xem trong sự cô lập, bản thân nó không phải là một sự trừu tượng bị rò rỉ); thay vào đó, sự rò rỉ phụ thuộc vào mã thực hiện giao diện (nó có thực sự hỗ trợ sự trừu tượng được biểu thị bởi giao diện không) và cũng dựa trên các giả định được tạo bởi mã máy khách (tương đương với sự trừu tượng hóa khái niệm bổ sung cho giao diện được thể hiện bởi giao diện, nhưng bản thân nó không thể được thể hiện bằng mã (ví dụ: các tính năng của ngôn ngữ không đủ biểu cảm, vì vậy chúng tôi có thể mô tả nó trong các tài liệu, v.v.)).


2

Hãy xem xét các ví dụ sau:

Đây là một phương thức đặt tên trước khi trả về:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

Đây là một phương thức đặt tên. Người gọi không thể giả sử tên được đặt cho đến khi hoàn thành tác vụ được trả về ( IsCompleted= true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

Đây là một phương thức đặt tên. Người gọi không thể giả sử tên được đặt cho đến khi hoàn thành tác vụ được trả về ( IsCompleted= true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

Q: Cái nào không thuộc về hai cái kia?

A: Phương thức async không phải là phương thức độc lập. Phương thức đứng một mình là phương thức trả về void.

Đối với tôi, "rò rỉ" ở đây không phải là asynctừ khóa; thực tế là phương thức trả về một tác vụ. Và đó không phải là một rò rỉ; nó là một phần của nguyên mẫu và là một phần của sự trừu tượng. Một phương thức async trả về một tác vụ thực hiện cùng một lời hứa được thực hiện bởi một phương thức đồng bộ trả về một tác vụ.

Vì vậy, không, tôi không nghĩ rằng việc giới thiệu các asynchình thức trừu tượng bị rò rỉ trong chính nó. Nhưng bạn có thể phải thay đổi nguyên mẫu để trả về một Tác vụ, "rò rỉ" bằng cách thay đổi giao diện (tính trừu tượng). Và vì nó là một phần của sự trừu tượng, theo định nghĩa, nó không phải là một sự rò rỉ.


0

Đây là một sự trừu tượng bị rò rỉ khi và chỉ khi bạn không có ý định tất cả các lớp thực hiện để tạo một cuộc gọi không đồng bộ. Ví dụ, bạn có thể tạo nhiều triển khai cho một loại cơ sở dữ liệu mà bạn hỗ trợ và điều này sẽ hoàn toàn ổn khi giả sử bạn không cần biết triển khai chính xác được sử dụng trong suốt chương trình của mình.

Và mặc dù bạn không thể thực thi nghiêm túc việc triển khai không đồng bộ, nhưng tên này có nghĩa là nó phải như vậy. Nếu hoàn cảnh thay đổi và đó có thể là một cuộc gọi đồng bộ vì bất kỳ lý do gì, thì bạn rất cần phải xem xét thay đổi tên, vì vậy lời khuyên của tôi chỉ là làm điều này nếu bạn không nghĩ rằng điều này rất có thể xảy ra trong Tương lai.


0

Đây là một quan điểm đối lập.

Chúng tôi đã không đi từ trở về trở Foolại Task<Foo>bởi vì chúng tôi bắt đầu muốn Taskthay vì chỉ Foo. Cấp, đôi khi chúng ta tương tác với Tasknhưng trong hầu hết các mã trong thế giới thực, chúng ta bỏ qua nó và chỉ sử dụng Foo.

Hơn nữa, chúng tôi thường xác định các giao diện để hỗ trợ hành vi không đồng bộ ngay cả khi việc triển khai có thể hoặc không đồng bộ.

Trong thực tế, một giao diện trả về một thông báo Task<Foo>cho bạn biết rằng việc triển khai có thể không đồng bộ cho dù nó thực sự có hay không, mặc dù bạn có thể hoặc không quan tâm. Nếu một sự trừu tượng cho chúng ta biết nhiều hơn những gì chúng ta cần biết về việc thực hiện nó, thì nó bị rò rỉ.

Nếu việc triển khai của chúng tôi không đồng bộ, chúng tôi thay đổi nó thành không đồng bộ, và sau đó chúng tôi phải thay đổi sự trừu tượng và mọi thứ sử dụng nó, đó là một sự trừu tượng rất rò rỉ.

Đó không phải là một bản án. Như những người khác đã chỉ ra, tất cả trừu tượng rò rỉ. Điều này có tác động lớn hơn bởi vì nó đòi hỏi một hiệu ứng gợn của async / chờ trong mã của chúng tôi chỉ vì ở đâu đó ở cuối của nó có thể có một cái gì đó thực sự không đồng bộ.

Nghe có vẻ như là một khiếu nại? Đó không phải là ý định của tôi, nhưng tôi nghĩ đó là một quan sát chính xác.

Một điểm liên quan là sự khẳng định rằng "một giao diện không phải là một sự trừu tượng". Những gì Mark Seeman nói ngắn gọn đã bị lạm dụng một chút.

Định nghĩa của "trừu tượng" không phải là "giao diện", ngay cả trong .NET. Trừu tượng có thể có nhiều hình thức khác. Một giao diện có thể là một sự trừu tượng kém hoặc nó có thể phản ánh sự thực thi của nó chặt chẽ đến mức theo một nghĩa nào đó, nó hầu như không phải là một sự trừu tượng hóa.

Nhưng chúng tôi hoàn toàn sử dụng các giao diện để tạo ra sự trừu tượng. Vì vậy, để loại bỏ "giao diện không trừu tượng" bởi vì một câu hỏi đề cập đến giao diện và trừu tượng không được khai sáng.


-2

GetAllAsync()thực sự không đồng bộ? Tôi có nghĩa là chắc chắn "async" có trong tên, nhưng điều đó có thể được gỡ bỏ. Vì vậy, tôi hỏi lại ... Không thể thực hiện một chức năng trả về một chức năng Task<IEnumerable<User>>được giải quyết đồng bộ?

Tôi không biết chi tiết cụ thể về loại .Net Task, nhưng nếu không thể triển khai chức năng một cách đồng bộ, thì chắc chắn đó là một bản tóm tắt bị rò rỉ (theo cách này) nhưng nếu không thì không. Tôi làm biết rằng nếu nó là một IObservablechứ không phải là một nhiệm vụ, nó có thể được thực hiện một trong hai đồng bộ hoặc async nên không có gì ngoài chức năng biết và do đó nó không bị rò rỉ rằng thực tế cụ thể.


Task<T> có nghĩa là không đồng bộ . Bạn nhận được đối tượng tác vụ ngay lập tức, nhưng có thể phải chờ chuỗi người dùng
Caleth

thể phải chờ không có nghĩa là nó nhất thiết không đồng bộ. Sẽ chờ đợi có nghĩa là không đồng bộ. Có lẽ, nếu tác vụ cơ bản đã được chạy, bạn không phải chờ.
Daniel T.
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.