Gunakan komposisi dan warisan untuk DTO

13

Kami memiliki ASP.NET Web API yang menyediakan REST API untuk Aplikasi Satu Halaman kami. Kami menggunakan DTO / POCO untuk meneruskan data melalui API ini.

Masalahnya sekarang, bahwa DTO ini semakin besar dari waktu ke waktu, jadi sekarang kami ingin memperbaiki DTO.

Saya mencari "praktik terbaik" bagaimana merancang DTO: Saat ini kami memiliki DTO kecil yang hanya terdiri dari bidang tipe nilai, misalnya:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

DTO lain menggunakan UserDto ini dengan komposisi, misalnya:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Juga, ada beberapa DTO yang diperluas yang didefinisikan dengan mewarisi dari yang lain, misalnya:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Karena beberapa DTO telah digunakan untuk beberapa titik akhir / metode (misalnya GET dan PUT), mereka telah diperluas secara bertahap oleh beberapa bidang seiring waktu. Dan karena warisan dan komposisi DTO lainnya juga menjadi lebih besar.

Pertanyaan saya sekarang adalah apakah warisan dan komposisi bukan praktik yang baik? Tetapi ketika kita tidak menggunakannya kembali, rasanya seperti menulis kode yang sama beberapa kali. Apakah praktik yang buruk untuk menggunakan DTO untuk beberapa titik akhir / metode, atau haruskah ada DTO yang berbeda, yang hanya berbeda dalam beberapa nuansa?

petugas
sumber
6
Saya tidak bisa memberi tahu Anda apa yang harus Anda lakukan, tetapi menurut pengalaman saya bekerja dengan DTO, warisan dan komposisi lebih cepat daripada nanti memburu Anda seperti kopi yang buruk. Saya tidak akan pernah menggunakan kembali DTO lagi. Pernah. Ditambah lagi, saya tidak menganggap DTO serupa sebagai pelanggaran KERING. Dua titik akhir yang mengembalikan representasi yang sama dapat menggunakan kembali DTO yang sama. Dua titik akhir yang mengembalikan representasi serupa tidak mengembalikan DTO yang sama jadi saya membuat DTO khusus untuk masing-masing . Jika saya terpaksa memilih, komposisi ini akan menjadi masalah yang kurang dalam jangka panjang.
Laiv
@ Longv ini adalah jawaban yang benar untuk pertanyaan itu, hanya saja jangan. Tidak yakin mengapa Anda memberikannya sebagai komentar
TheCatWhisperer
2
@Laiv: Apa yang Anda gunakan? Dalam pengalaman saya, orang-orang yang berjuang dengan ini hanya terlalu memikirkannya. DTO hanyalah sebuah wadah untuk data, dan hanya itu yang ada.
Robert Harvey
TheCatWhisperer karena argumen saya terutama didasarkan pada pendapat. Saya masih mencoba untuk mengatasi masalah semacam ini di proyek saya. @RobertHarvey benar, tidak tahu mengapa saya cenderung melihat hal-hal yang lebih sulit daripada yang sebenarnya. Saya masih mengerjakan solusinya. Saya cukup yakin bahwa HAL adalah model yang dimaksudkan untuk menyelesaikan masalah-masalah ini, tetapi membaca jawaban Anda, saya menyadari bahwa saya melakukan DTO terlalu granulars juga. Jadi saya akan mempraktikkan pendekatan Anda terlebih dahulu. Perubahan akan menjadi kurang dramatis daripada bergeser sepenuhnya ke HATEOAS.
Laiv
Coba ini untuk diskusi yang baik yang mungkin dapat membantu Anda: stackoverflow.com/questions/6297322/…
johnny

Jawaban:

9

Sebagai praktik terbaik, cobalah dan buat DTO Anda sesingkat mungkin. Kembalikan hanya apa yang Anda butuhkan untuk kembali. Hanya gunakan apa yang perlu Anda gunakan. Jika itu berarti DTO tambahan, biarlah.

Dalam contoh Anda, tugas berisi pengguna. Seseorang mungkin tidak memerlukan objek pengguna penuh di sana, mungkin hanya nama pengguna yang ditugaskan tugas. Kami tidak membutuhkan properti pengguna lainnya.

Katakanlah Anda ingin menetapkan kembali tugas ke pengguna yang berbeda, seseorang mungkin tergoda untuk melewatkan objek pengguna penuh dan objek tugas penuh selama pos penugasan ulang. Tapi, yang benar-benar dibutuhkan hanyalah tugas Id dan ID pengguna. Itu adalah dua informasi yang diperlukan untuk menetapkan kembali tugas, jadi buat model DTO seperti itu. Ini biasanya berarti ada DTO terpisah untuk setiap panggilan istirahat jika Anda berjuang untuk model DTO ramping.

Juga, kadang-kadang seseorang mungkin membutuhkan warisan / komposisi. Katakanlah seseorang memiliki pekerjaan. Suatu pekerjaan memiliki banyak tugas. Dalam hal itu, mendapatkan pekerjaan juga dapat mengembalikan daftar tugas untuk pekerjaan itu. Jadi, tidak ada aturan terhadap komposisi. Itu tergantung pada apa yang dimodelkan.

Jon Raynor
sumber
Sesingkat mungkin juga berarti bahwa saya harus membuat ulang DTO jika hanya berbeda dalam beberapa detail - bukan?
Petugas
1
@officer - Secara umum, ya.
Jon Raynor
2

Kecuali jika sistem Anda hanya didasarkan pada operasi CRUD, DTO Anda terlalu terperinci. Coba buat titik akhir yang mewujudkan proses bisnis atau artefak. Pendekatan ini memetakan dengan baik ke lapisan logika bisnis, dan ke "desain berbasis domain" Eric Evans.

Misalnya, Anda memiliki titik akhir yang mengembalikan data untuk faktur sehingga dapat ditampilkan di layar atau formulir ke pengguna akhir. Dalam model CRUD, Anda perlu beberapa panggilan ke titik akhir Anda untuk mengumpulkan informasi yang diperlukan: nama, alamat penagihan, alamat pengiriman, item baris. Dalam konteks transaksi bisnis, DTO tunggal dari titik akhir tunggal dapat mengembalikan semua informasi ini sekaligus.

Robert Harvey
sumber
Robert, maksudmu dengan Agregat Root di sini?
johnny
Saat ini DTO kami dirancang untuk menyediakan seluruh data untuk suatu formulir, misalnya saat menampilkan daftar todo, TodoListDTO memiliki Daftar TaskDTO. Tetapi karena kita menggunakan kembali TaskDTO, masing-masing dari mereka juga memiliki UserDTO. Jadi masalahnya adalah jumlah besar data yang dipertanyakan di backend dan dikirim melalui kabel.
Petugas
Apakah semua data diperlukan?
Robert Harvey
1

Sulit untuk menetapkan praktik terbaik untuk sesuatu yang "fleksibel" atau abstrak sebagai DTO. Pada dasarnya, DTO hanya objek untuk transfer data tetapi tergantung pada tujuan atau alasan transfer, Anda mungkin ingin menerapkan "praktik terbaik" yang berbeda.

Saya merekomendasikan membaca Pola Arsitektur Aplikasi Perusahaan oleh Martin Fowler . Ada seluruh bab yang didedikasikan untuk pola, di mana DTO mendapatkan bagian yang sangat rinci.

Awalnya, mereka "dirancang" untuk digunakan dalam panggilan jarak jauh yang mahal, di mana Anda mungkin membutuhkan banyak data dari berbagai bagian logika Anda; DTO akan melakukan transfer data dalam satu panggilan.

Menurut penulis, DTO tidak dimaksudkan untuk digunakan di lingkungan lokal, tetapi beberapa orang menemukan kegunaannya. Biasanya mereka digunakan untuk mengumpulkan informasi dari POCO yang berbeda menjadi satu kesatuan untuk GUI, API, atau lapisan yang berbeda.

Sekarang, dengan pewarisan, penggunaan kembali kode lebih seperti efek samping dari warisan daripada tujuan utamanya; komposisi, di sisi lain, diimplementasikan dengan penggunaan kembali kode sebagai tujuan utama.

Beberapa orang merekomendasikan penggunaan komposisi dan pewarisan bersama, menggunakan kekuatan keduanya dan mencoba mengurangi kelemahan mereka. Berikut ini adalah bagian dari proses mental saya ketika memilih atau membuat DTO baru, atau kelas / objek baru dalam hal ini:

  • Saya menggunakan warisan dengan DTO di dalam lapisan yang sama atau konteks yang sama. DTO tidak akan pernah mewarisi dari POCO, DTO BLL tidak akan pernah mewarisi dari DAL DTO, dll.
  • Jika saya menemukan diri saya mencoba menyembunyikan bidang dari DTO, saya akan refactor dan mungkin menggunakan komposisi sebagai gantinya.
  • Jika sangat sedikit bidang berbeda dari DTO basis yang saya butuhkan, saya akan meletakkannya dalam DTO universal. DTO Universal hanya digunakan secara internal.
  • POCO / DTO dasar hampir tidak akan pernah digunakan untuk logika apa pun, dengan cara itu basis hanya menjawab kebutuhan anak-anaknya. Jika saya perlu menggunakan pangkalan, saya menghindari menambahkan bidang baru yang tidak akan pernah digunakan oleh anak-anaknya.

Beberapa dari mereka mungkin bukan praktik "terbaik", mereka bekerja cukup baik untuk proyek yang saya kerjakan tetapi Anda harus ingat bahwa tidak ada ukuran yang cocok untuk semua. Dalam hal DTO universal Anda harus berhati-hati, tanda tangan metode saya terlihat seperti ini:

public void DoSomething(BaseDTO base) {
    //Some code 
}

Jika ada metode yang membutuhkan DTO sendiri, saya melakukan pewarisan dan biasanya satu-satunya perubahan yang perlu saya lakukan adalah parameter, meskipun kadang-kadang saya perlu menggali lebih dalam untuk kasus-kasus tertentu.

Dari komentar Anda, saya mengerti bahwa Anda menggunakan DTO bersarang. Jika DTO bersarang Anda hanya terdiri dari daftar DTO lain, saya pikir hal terbaik untuk dilakukan adalah membuka daftar.

Bergantung pada jumlah data yang perlu Anda tampilkan atau kerjakan, mungkin merupakan ide bagus untuk membuat DTO baru yang membatasi data; misalnya, jika UserDTO Anda memiliki banyak bidang dan Anda hanya perlu 1 atau 2, mungkin lebih baik memiliki DTO dengan hanya bidang tersebut. Mendefinisikan layer, konteks, penggunaan dan utilitas DTO akan banyak membantu ketika mendesainnya.

IvanGrasp
sumber
Saya tidak dapat menambahkan lebih dari 2 tautan dalam jawaban saya, inilah beberapa info lebih lanjut tentang DTO lokal. Saya sadar bahwa ini informasi yang sangat lama tetapi saya pikir beberapa di antaranya mungkin masih relevan.
IvanGrasp
1

Menggunakan komposisi dalam DTO adalah praktik yang sangat bagus.

Warisan di antara jenis DTO konkret adalah praktik yang buruk.

Untuk satu hal, dalam bahasa seperti C #, properti yang diimplementasikan otomatis memiliki overhead pemeliharaan yang sangat kecil sehingga menduplikasi mereka (saya benar-benar benci duplikasi) tidak berbahaya seperti seringkali.

Salah satu alasan untuk tidak menggunakan warisan konkret di antara DTO adalah bahwa alat tertentu akan dengan senang hati memetakannya ke jenis yang salah.

Misalnya, jika Anda menggunakan utilitas basis data seperti Dapper (saya tidak merekomendasikannya tetapi ini populer) yang melakukan class name -> table nameinferensi, Anda bisa dengan mudah menyimpan tipe turunan sebagai tipe basis beton di suatu tempat dalam hierarki dan dengan demikian kehilangan data atau lebih buruk .

Alasan yang lebih dalam untuk tidak menggunakan warisan di antara DTO adalah bahwa itu tidak boleh digunakan untuk berbagi implementasi antara tipe yang tidak memiliki hubungan "is a" yang jelas. Menurut saya, TaskDetailtidak terdengar seperti subtipe dari Task. Itu bisa dengan mudah menjadi properti dari Taskatau, lebih buruk lagi, itu bisa menjadi tipe atas Task.

Sekarang, satu hal yang Anda khawatirkan adalah menjaga konsistensi antara nama dan jenis properti dari berbagai DTO terkait.

Sementara pewarisan tipe konkret akan, sebagai konsekuensinya, membantu memastikan konsistensi seperti itu, jauh lebih baik menggunakan antarmuka (atau kelas dasar virtual murni dalam C ++) untuk mempertahankan konsistensi semacam itu.

Pertimbangkan DTO berikut ini

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
Aluan Haddad
sumber