Apakah antarmuka yang menampilkan fungsi async adalah abstraksi yang bocor?

13

Saya membaca buku Prinsip Ketergantungan Injeksi, Praktek, dan Pola dan saya membaca tentang konsep abstraksi bocor yang dijelaskan dengan baik dalam buku ini.

Hari ini saya refactoring basis kode C # menggunakan injeksi dependensi sehingga panggilan async digunakan alih-alih memblokir yang. Melakukannya saya sedang mempertimbangkan beberapa antarmuka yang mewakili abstraksi di basis kode saya dan yang perlu dirancang ulang agar panggilan async dapat digunakan.

Sebagai contoh, pertimbangkan antarmuka berikut yang mewakili repositori untuk pengguna aplikasi:

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

Menurut definisi buku, abstraksi bocor adalah abstraksi yang dirancang dengan implementasi spesifik dalam pikiran, sehingga beberapa detail implementasi "bocor" melalui abstraksi itu sendiri.

Pertanyaan saya adalah sebagai berikut: dapatkah kita mempertimbangkan antarmuka yang dirancang dengan async dalam pikiran, seperti IUserRepository, sebagai contoh Abstraksi Leaky?

Tentu saja tidak semua implementasi mungkin ada hubungannya dengan asynchrony: hanya implementasi proses yang keluar (seperti implementasi SQL) yang dilakukan, tetapi penyimpanan dalam memori tidak memerlukan asinkron (sebenarnya mengimplementasikan versi memori dari antarmuka mungkin lebih sulit jika antarmuka memperlihatkan metode async, misalnya Anda mungkin harus mengembalikan sesuatu seperti Task.CompletedTask atau Task.FromResult (pengguna) dalam implementasi metode).

Apa pendapatmu tentang itu ?

Enrico Massone
sumber
@Neil saya mungkin mengerti intinya. Antarmuka mengekspos metode pengembalian Tugas atau Tugas <T> bukan abstraksi bocor per se, hanyalah sebuah kontrak dengan tanda tangan yang melibatkan tugas. Metode yang mengembalikan Tugas atau Tugas <T> tidak menyiratkan memiliki implementasi async (misalnya jika saya membuat tugas yang diselesaikan dengan menggunakan Task.CompletedTask saya tidak melakukan implementasi async). Demikian pula sebaliknya, implementasi async dalam C # mensyaratkan bahwa tipe pengembalian metode async harus bertipe Tugas atau Tugas <T>. Dengan kata lain satu-satunya aspek "bocor" dari antarmuka saya adalah akhiran nama async
Enrico Massone
@ Neil sebenarnya ada pedoman penamaan yang menyatakan bahwa semua metode async harus memiliki nama yang berakhiran "Async". Tetapi ini tidak menyiratkan bahwa metode yang mengembalikan Tugas atau Tugas <T> harus dinamai dengan akhiran Async karena itu dapat diimplementasikan dengan tidak menggunakan panggilan async.
Enrico Massone
6
Saya berpendapat 'asyncness' dari suatu metode ditunjukkan oleh fakta bahwa ia mengembalikan a Task. Pedoman untuk suffixing metode async dengan kata async adalah untuk membedakan antara panggilan API yang identik (pengiriman C # cant berdasarkan jenis kembali). Di perusahaan kami, kami telah menjatuhkan semuanya.
richzilla
Ada sejumlah jawaban dan komentar yang menjelaskan mengapa sifat asinkron dari metode ini adalah bagian dari abstraksi. Pertanyaan yang lebih menarik adalah bagaimana bahasa atau pemrograman API dapat memisahkan fungsionalitas metode dari cara dijalankan, ke titik di mana kita tidak lagi memerlukan nilai-nilai pengembalian tugas, atau penanda async? Orang pemrograman fungsional tampaknya telah menemukan ini dengan lebih baik. Pertimbangkan bagaimana metode asinkron didefinisikan dalam F # dan bahasa lainnya.
Frank Hileman
2
:-) -> "Orang pemrograman fungsional" ha. Async tidak lebih bocor daripada sinkron, sepertinya begitu karena kita terbiasa menulis kode sinkronisasi secara default. Jika kita semua mengkodekan async secara default, fungsi sinkron mungkin tampak bocor.
StarTrekRedneck

Jawaban:

8

Seseorang dapat, tentu saja, menerapkan hukum abstraksi bocor , tetapi itu tidak terlalu menarik karena menyatakan bahwa semua abstraksi bocor. Seseorang dapat berdebat untuk dan menentang dugaan itu, tetapi tidak membantu jika kita tidak berbagi pemahaman tentang apa yang kita maksud dengan abstraksi , dan apa yang kita maksudkan dengan bocor . Oleh karena itu, saya akan mencoba menjelaskan bagaimana saya melihat masing-masing istilah ini:

Abstraksi

Definisi favorit saya tentang abstraksi diambil dari Robert C. Martin APPP :

"Sebuah abstraksi adalah penguatan yang esensial dan penghapusan yang tidak relevan."

Dengan demikian, antarmuka bukanlah abstraksi . Mereka hanya abstraksi jika membawa ke permukaan apa yang penting, dan menyembunyikan sisanya.

Bocor

Buku Prinsip, Pola, dan Praktek Injeksi Ketergantungan menentukan istilah abstraksi bocor dalam konteks Injeksi Ketergantungan (DI). Polimorfisme dan prinsip-prinsip SOLID memainkan peran besar dalam konteks ini.

Dari Prinsip Ketergantungan Pembalikan (DIP), berikut ini mengikuti, sekali lagi mengutip APPP, bahwa:

"klien [...] memiliki antarmuka abstrak"

Ini artinya bahwa klien (kode panggilan) mendefinisikan abstraksi yang mereka butuhkan, dan kemudian Anda pergi dan mengimplementasikan abstraksi itu.

Sebuah abstraksi bocor , dalam pandangan saya, adalah sebuah abstraksi yang melanggar DIP oleh entah termasuk beberapa fungsi yang klien tidak perlu .

Ketergantungan sinkron

Klien yang mengimplementasikan sepotong logika bisnis biasanya akan menggunakan DI untuk memisahkan dirinya dari detail implementasi tertentu, seperti, umumnya, database.

Pertimbangkan objek domain yang menangani permintaan reservasi restoran:

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);
    }
}

Di sini, IReservationsRepositoryketergantungan ditentukan secara eksklusif oleh klien, MaîtreDkelas:

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

Antarmuka ini sepenuhnya sinkron karena MaîtreDkelas tidak perlu asinkron.

Dependensi asinkron

Anda dapat dengan mudah mengubah antarmuka menjadi asinkron:

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

The MaîtreDkelas, bagaimanapun, tidak perlu metode-metode untuk menjadi asynchronous, jadi sekarang DIP ini dilanggar. Saya menganggap ini abstraksi bocor, karena detail implementasi memaksa klien untuk berubah. The TryAcceptMetode saat ini juga sudah menjadi asynchronous:

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);
}

Tidak ada dasar pemikiran untuk logika domain menjadi asinkron, tetapi untuk mendukung asinkron dari implementasi, ini sekarang diperlukan.

Pilihan yang lebih baik

Di NDC Sydney 2018 saya memberi ceramah tentang topik ini . Di dalamnya, saya juga menguraikan alternatif yang tidak bocor. Saya akan memberikan ceramah ini di beberapa konferensi pada tahun 2019 juga, tetapi sekarang diganti dengan judul baru injeksi Async .

Saya berencana untuk juga menerbitkan serangkaian posting blog untuk menemani pembicaraan. Artikel-artikel ini sudah ditulis dan duduk di antrian artikel saya, menunggu untuk diterbitkan, jadi tetap disini.

Mark Seemann
sumber
Bagi saya ini adalah niat. Jika abstraksi saya tampak seolah-olah harus berperilaku satu arah tetapi beberapa detail atau kendala mematahkan abstraksi seperti yang disajikan, itu abstraksi yang bocor. Tetapi dalam kasus ini, saya secara eksplisit menyampaikan kepada Anda bahwa operasinya tidak sinkron - bukan itu yang saya coba abstraksi. Itu berbeda dalam pikiran saya dari contoh Anda di mana saya (dengan bijak atau tidak) mencoba untuk mengabstraksi fakta bahwa ada database SQL dan saya masih mengekspos string koneksi. Mungkin itu masalah semantik / perspektif.
Semut
Jadi kita dapat mengatakan bahwa abstraksi tidak pernah bocor "per se", sebaliknya ia abstraksi jika beberapa detail dari satu implementasi spesifik bocor dari anggota yang terpapar dan memaksa konsumen untuk mengubah implementasinya, untuk memenuhi bentuk abstraksi. .
Enrico Massone
2
Cukup menarik, poin yang Anda soroti dalam penjelasan Anda adalah salah satu poin paling disalahpahami dari seluruh kisah injeksi ketergantungan. Kadang-kadang pengembang melupakan prinsip inversi ketergantungan dan mencoba mendesain abstraksi terlebih dahulu dan kemudian mereka mengadaptasi desain konsumen untuk mengatasi abstraksi itu sendiri. Sebaliknya, prosesnya harus dilakukan dalam urutan terbalik.
Enrico Massone
11

Sama sekali bukan abstraksi yang bocor.

Menjadi asinkron adalah perubahan mendasar pada definisi suatu fungsi - itu berarti tugas tersebut tidak selesai ketika panggilan kembali, tetapi itu juga berarti bahwa aliran program Anda akan berlanjut segera, bukan dengan penundaan yang lama. Fungsi asinkron dan sinkron yang melakukan tugas yang sama pada dasarnya adalah fungsi yang berbeda. Menjadi tidak sinkron bukanlah detail implementasi. Itu bagian dari definisi fungsi.

Jika fungsi membuka bagaimana fungsi itu dibuat tidak sinkron, itu akan bocor. Anda (tidak / tidak harus) peduli bagaimana penerapannya.

gnasher729
sumber
5

The asyncatribut dari metode adalah tag yang menunjukkan bahwa perawatan dan penanganan khusus diperlukan. Karena itu, perlu bocor ke dunia. Operasi asinkron sangat sulit untuk dikomposisikan dengan benar sehingga memberikan pengguna API yang penting adalah penting.

Sebaliknya, jika perpustakaan Anda mengelola dengan benar semua aktivitas asinkron dalam dirinya sendiri, maka Anda dapat membiarkan async"kebocoran" API.

Ada empat dimensi kesulitan dalam perangkat lunak: data, kontrol, ruang dan waktu. Operasi asinkron menjangkau keempat dimensi, dengan demikian, membutuhkan perhatian paling besar.

BobDalgleish
sumber
Saya setuju dengan sentimen Anda, tetapi "kebocoran" menyiratkan sesuatu yang buruk, yang merupakan maksud dari istilah "abstraksi bocor" - sesuatu yang tidak diinginkan dalam abstraksi. Dalam kasus async vs sync, tidak ada yang bocor.
StarTrekRedneck
2

abstraksi bocor adalah abstraksi yang dirancang dengan implementasi spesifik dalam pikiran, sehingga beberapa detail implementasi "bocor" melalui abstraksi itu sendiri.

Tidak terlalu. Abstraksi adalah hal konseptual yang mengabaikan beberapa elemen dari hal atau masalah yang lebih rumit (untuk membuat hal / masalah lebih sederhana, dapat ditelusuri, atau karena beberapa manfaat lainnya). Dengan demikian, ini tentu berbeda dari hal / masalah yang sebenarnya, dan dengan demikian akan bocor dalam beberapa bagian kasus (yaitu, semua abstraksi bocor, satu-satunya pertanyaan adalah sejauh mana - makna, di mana kasus adalah abstraksi berguna bagi kami, apa domain penerapannya).

Yang mengatakan, ketika datang ke abstraksi perangkat lunak, kadang-kadang (atau mungkin cukup sering?) Detail yang kami pilih untuk diabaikan sebenarnya tidak dapat diabaikan karena mereka mempengaruhi beberapa aspek perangkat lunak yang penting bagi kami (kinerja, rawatan, ...) . Jadi abstraksi yang bocor adalah abstraksi yang dirancang untuk mengabaikan detail tertentu (dengan asumsi bahwa itu mungkin dan bermanfaat untuk dilakukan), tetapi kemudian ternyata beberapa detail tersebut signifikan dalam praktiknya (tidak dapat diabaikan, sehingga mereka "keluar").

Jadi, antarmuka yang mengekspos detail implementasi tidak bocor per se (atau lebih tepatnya, antarmuka, dilihat secara terpisah, bukan dengan sendirinya abstraksi bocor); sebaliknya, kebocoran bergantung pada kode yang mengimplementasikan antarmuka (apakah ia benar-benar dapat mendukung abstraksi yang diwakili oleh antarmuka), dan juga pada asumsi yang dibuat oleh kode klien (yang berjumlah abstraksi konseptual yang melengkapi yang dinyatakan oleh antarmuka, tetapi tidak dapat dengan sendirinya dinyatakan dalam kode (mis. fitur bahasa tidak cukup ekspresif, jadi kami dapat menjelaskannya dalam dokumen, dll.)).

Filip Milovanović
sumber
2

Perhatikan contoh-contoh berikut:

Ini adalah metode yang menetapkan nama sebelum kembali:

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

Ini adalah metode yang menetapkan nama. Penelepon tidak dapat menganggap nama tersebut diatur sampai tugas yang dikembalikan selesai ( IsCompleted= benar):

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

Ini adalah metode yang menetapkan nama. Penelepon tidak dapat menganggap nama tersebut diatur sampai tugas yang dikembalikan selesai ( IsCompleted= benar):

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

T: Yang mana bukan milik dua lainnya?

A: Metode async bukan yang berdiri sendiri. Salah satu yang berdiri sendiri adalah metode yang mengembalikan batal.

Bagi saya, "kebocoran" di sini bukanlah asynckata kunci; itu fakta bahwa metode mengembalikan Tugas. Dan itu bukan kebocoran; itu bagian dari prototipe, dan bagian dari abstraksi. Metode async yang mengembalikan tugas membuat janji yang sama persis dibuat oleh metode sinkron yang mengembalikan tugas.

Jadi tidak, saya tidak berpikir pengenalan asyncbentuk abstraksi bocor dalam dan dari dirinya sendiri. Tetapi Anda mungkin harus mengubah prototipe untuk mengembalikan Tugas, yang "bocor" dengan mengubah antarmuka (abstraksi). Dan karena itu bagian dari abstraksi, itu bukan kebocoran, menurut definisi.

John Wu
sumber
0

Ini adalah abstraksi yang bocor jika dan hanya jika Anda tidak bermaksud semua kelas implementasi untuk membuat panggilan asinkron. Anda dapat membuat beberapa implementasi, misalnya, satu untuk setiap jenis basis data yang Anda dukung, dan ini akan baik-baik saja dengan asumsi Anda tidak perlu mengetahui implementasi yang tepat digunakan di seluruh program Anda.

Dan sementara Anda tidak bisa secara ketat menerapkan implementasi asinkron, namanya menyiratkan itu seharusnya. Jika keadaan berubah, dan itu mungkin panggilan serempak untuk alasan apa pun, maka Anda mungkin perlu mempertimbangkan perubahan nama, jadi saran saya adalah melakukan ini hanya jika Anda tidak berpikir ini akan sangat mungkin terjadi di masa depan.

Neil
sumber
0

Inilah sudut pandang yang berlawanan.

Kami tidak pergi dari kembali Fooke kembali Task<Foo>karena kami mulai menginginkan Taskbukan hanya Foo. Memang, kadang-kadang kita berinteraksi dengan Tasktetapi dalam kode dunia nyata kita mengabaikannya dan hanya menggunakan Foo.

Terlebih lagi, kita sering mendefinisikan antarmuka untuk mendukung perilaku async bahkan ketika implementasinya mungkin atau mungkin tidak sinkron.

Akibatnya, sebuah antarmuka yang mengembalikan sebuah Task<Foo>memberitahu Anda bahwa implementasi mungkin tidak sinkron apakah itu benar-benar atau tidak, meskipun Anda mungkin atau mungkin tidak peduli. Jika abstraksi memberi tahu kita lebih dari yang perlu kita ketahui tentang implementasinya, itu bocoran.

Jika implementasi kami tidak asinkron, kami mengubahnya menjadi asinkron, dan kemudian kami harus mengubah abstraksi dan semua yang menggunakannya, itu abstraksi yang sangat bocor.

Itu bukan penilaian. Seperti yang telah ditunjukkan orang lain, semua abstraksi bocor. Yang satu ini memang memiliki dampak yang lebih besar karena memerlukan efek riak async / menunggu seluruh kode kita hanya karena di suatu tempat di ujungnya mungkin ada sesuatu yang sebenarnya tidak sinkron.

Apakah itu terdengar seperti keluhan? Itu bukan maksud saya, tapi saya pikir ini pengamatan yang akurat.

Poin terkait adalah pernyataan bahwa "antarmuka bukan abstraksi." Apa yang dikatakan Mark Seeman dengan singkat telah disalahgunakan sedikit.

Definisi "abstraksi" bukanlah "antarmuka", bahkan dalam .NET. Abstraksi dapat mengambil banyak bentuk lainnya. Antarmuka dapat berupa abstraksi yang buruk atau dapat mencerminkan implementasinya sedemikian dekat sehingga dalam arti itu hampir tidak abstraksi sama sekali.

Tapi kami benar-benar menggunakan antarmuka untuk membuat abstraksi. Jadi untuk membuang "antarmuka bukan abstraksi" karena sebuah pertanyaan menyebutkan antarmuka dan abstraksi tidak mencerahkan.

Scott Hannen
sumber
-2

Apakah GetAllAsync()sebenarnya async? Maksud saya yakin "async" ada dalam nama, tapi itu bisa dihapus. Jadi saya bertanya lagi ... Apakah tidak mungkin untuk mengimplementasikan fungsi yang mengembalikan Task<IEnumerable<User>>yang diselesaikan secara serempak?

Saya tidak tahu secara spesifik Tasktipe . Net , tetapi jika tidak mungkin untuk mengimplementasikan fungsi secara serempak, maka pastikan itu adalah abstraksi yang bocor (dengan cara ini) tetapi sebaliknya tidak. Aku tidak tahu bahwa jika itu adalah IObservabledaripada Task, itu bisa diimplementasikan baik serentak atau async jadi tidak ada di luar fungsi tahu dan karena itu tidak bocor fakta tertentu.

Daniel T.
sumber
Task<T> berarti async. Anda mendapatkan objek tugas segera, tetapi mungkin harus menunggu urutan pengguna
Caleth
Mungkin harus menunggu bukan berarti itu async. Harus menunggu berarti async. Agaknya, jika tugas mendasar sudah dijalankan, Anda tidak perlu menunggu.
Daniel T.