ใช้การจัดองค์ประกอบและการสืบทอดสำหรับ DTO


13

เรามี ASP.NET Web API ที่ให้บริการ REST API สำหรับแอปพลิเคชันหน้าเดียวของเรา เราใช้ DTOs / POCO เพื่อส่งผ่านข้อมูลผ่าน API นี้

ปัญหาคือตอนนี้ DTO เหล่านี้เริ่มใหญ่ขึ้นตามกาลเวลาดังนั้นตอนนี้เราต้องการที่จะปรับโครงสร้าง DTO อีกครั้ง

ฉันกำลังมองหา "วิธีปฏิบัติที่ดีที่สุด" วิธีการออกแบบ DTO: ขณะนี้เรามี DTO ขนาดเล็กที่ประกอบด้วยเฉพาะเขตข้อมูลประเภทค่าเช่น:

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

    public string Name { get; set; }
}

DTO อื่น ๆ ใช้ UserDto นี้ตามการจัดองค์ประกอบเช่น:

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

    public UserDto AssignedTo { get; set; }
}

นอกจากนี้ยังมี DTO เพิ่มเติมที่กำหนดโดยการสืบทอดจากผู้อื่นเช่น:

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

เนื่องจาก DTO บางตัวถูกใช้สำหรับจุดปลาย / วิธีการต่าง ๆ (เช่น GET และ PUT) จึงมีการขยายเพิ่มเติมในบางฟิลด์เมื่อเวลาผ่านไป และเนื่องจากการสืบทอดและการจัดองค์ประกอบ DTO อื่น ๆ ก็ใหญ่ขึ้นเช่นกัน

คำถามของฉันคือตอนนี้การสืบทอดและการจัดองค์ประกอบไม่ใช่แนวปฏิบัติที่ดี? แต่เมื่อเราไม่นำมันกลับมาใช้ใหม่มันให้ความรู้สึกเหมือนการเขียนรหัสเดียวกันหลาย ๆ ครั้ง มันเป็นการปฏิบัติที่ไม่ถูกต้องหรือไม่ที่จะใช้ DTO สำหรับจุดปลาย / วิธีการที่หลากหลายหรือควรมี DTO ที่แตกต่างกันซึ่งแตกต่างกันในความแตกต่างบางอย่างเท่านั้น?


6
ฉันไม่สามารถบอกคุณได้ว่าคุณควรทำอะไร แต่จากประสบการณ์ของฉันในการทำงานกับ DTO การสืบทอดและการเรียบเรียงโดยเร็วกว่าการตามล่าคุณเหมือนกาแฟที่ไม่ดี ฉันจะไม่ใช้ DTO ซ้ำอีกครั้ง เคย นอกจากนี้ฉันไม่พิจารณา DTO ที่คล้ายกันว่าเป็นการละเมิด DRY จุดปลายสองจุดที่ส่งกลับการเป็นตัวแทนเดียวกันสามารถใช้ DTO เดียวกันซ้ำได้ จุดปลายสองจุดที่ส่งกลับการเป็นตัวแทนที่คล้ายกันจะไม่ส่งกลับ DTO เดียวกันดังนั้นฉันจึงทำ DTO เฉพาะสำหรับแต่ละจุด ถ้าฉันถูกบังคับให้เลือกองค์ประกอบที่เป็นปัญหาน้อยกว่าในระยะยาว
Laiv

@Laiv นี่เป็นคำตอบที่ถูกต้องสำหรับคำถาม แต่อย่าเพิ่งทำ ไม่แน่ใจว่าทำไมคุณถึงแสดงความคิดเห็น
TheCatWhisperer

2
@Laiv: คุณใช้อะไรแทน จากประสบการณ์ของฉันคนที่ดิ้นรนกับเรื่องนี้เป็นเพียงการครุ่นคิด DTO เป็นเพียงที่เก็บข้อมูลและนั่นคือทั้งหมด
Robert Harvey

TheCatWhisperer เพราะข้อโต้แย้งของฉันจะอิงตามความเห็น ฉันยังคงพยายามที่จะแก้ไขปัญหาแบบนี้ในโครงการของฉัน @ RobertHarvey จริงไม่ทราบว่าทำไมฉันมักจะเห็นสิ่งที่ยากกว่าที่พวกเขาเป็นจริง ฉันยังคงแก้ปัญหาอยู่ ฉันค่อนข้างมั่นใจว่า HAL เป็นแบบจำลองที่มีไว้เพื่อแก้ไขปัญหาเหล่านี้ แต่การอ่านคำตอบของคุณฉันรู้ว่าฉันทำ DTO ด้วยเช่นกัน ดังนั้นฉันจะใช้แนวทางของคุณก่อน การเปลี่ยนแปลงจะมีความเร้าใจน้อยกว่าการเปลี่ยนมาใช้ HATEOAS อย่างสมบูรณ์
Laiv

ลองทำสิ่งนี้เพื่อการสนทนาที่ดีที่อาจช่วยคุณได้: stackoverflow.com/questions/6297322/…
johnny

คำตอบ:


10

เพื่อเป็นการปฏิบัติที่ดีที่สุดลองทำ DTO ของคุณให้กระชับที่สุด คืนสิ่งที่คุณต้องการคืนเท่านั้น ใช้สิ่งที่คุณต้องการใช้เท่านั้น ถ้านั่นหมายถึง DTO พิเศษสองสามอย่าง

ในตัวอย่างของคุณงานมีผู้ใช้ หนึ่งอาจไม่ต้องการวัตถุผู้ใช้เต็มรูปแบบที่นั่นอาจเป็นเพียงชื่อของผู้ใช้ที่ได้รับมอบหมายงาน เราไม่ต้องการคุณสมบัติที่เหลือของผู้ใช้

สมมติว่าคุณต้องการมอบหมายงานให้กับผู้ใช้รายอื่นซึ่งอาจถูกล่อลวงให้ส่งผู้ใช้แบบเต็มและวัตถุงานแบบเต็มในระหว่างการโพสต์การมอบหมายใหม่ แต่สิ่งที่จำเป็นจริงๆคือรหัสงานและรหัสผู้ใช้ สิ่งเหล่านี้เป็นข้อมูลเพียงสองชิ้นเท่านั้นที่จำเป็นสำหรับการมอบหมายภารกิจใหม่ดังนั้นให้ทำแบบจำลอง DTO ดังกล่าว ซึ่งมักจะหมายความว่ามี DTO แยกต่างหากสำหรับการโทรทุกครั้งหากคุณพยายามใช้ DTO แบบลีน

นอกจากนี้บางครั้งอาจต้องการมรดก / องค์ประกอบ สมมติว่าคนหนึ่งมีงานทำ งานมีหลายงาน ในกรณีนั้นการรับงานอาจส่งคืนรายการงานสำหรับงานด้วยเช่นกัน ดังนั้นจึงไม่มีกฎเกี่ยวกับการแต่งเพลง ขึ้นอยู่กับสิ่งที่เป็นแบบอย่าง


รัดกุมที่สุดเท่าที่เป็นไปได้ก็หมายความว่าฉันควรสร้าง DTO ใหม่หากพวกเขามีความแตกต่างในรายละเอียดบางอย่างใช่ไหม
เจ้าหน้าที่

1
@officer - โดยทั่วไปแล้วใช่
Jon Raynor

2

นอกจากว่าระบบของคุณจะยึดตามการทำงานของ CRUD อย่างเข้มงวด DTO ของคุณจะละเอียดเกินไป ลองสร้างจุดปลายที่รวบรวมกระบวนการทางธุรกิจหรือสิ่งประดิษฐ์ วิธีการนี้แมปกับเลเยอร์ตรรกะทางธุรกิจและ "การออกแบบที่ขับเคลื่อนด้วยโดเมนของ Eric Evans"

ตัวอย่างเช่นสมมติว่าคุณมีปลายทางที่ส่งคืนข้อมูลสำหรับใบแจ้งหนี้เพื่อให้สามารถแสดงบนหน้าจอหรือแบบฟอร์มไปยังผู้ใช้ปลายทางได้ ในรุ่น CRUD คุณจะต้องโทรหาปลายทางหลายครั้งเพื่อรวบรวมข้อมูลที่จำเป็น: ชื่อที่อยู่สำหรับเรียกเก็บเงินที่อยู่สำหรับจัดส่งสินค้ารายการโฆษณา ในบริบทของธุรกรรมทางธุรกิจ DTO เดียวจากจุดปลายเดียวสามารถส่งคืนข้อมูลทั้งหมดได้ในครั้งเดียว


Robert คุณหมายถึง Aggregate Root ที่นี่หรือไม่?
johnny

ขณะนี้ DTO ของเราได้รับการออกแบบมาเพื่อให้ข้อมูลทั้งหมดสำหรับฟอร์มเช่นเมื่อแสดงรายการสิ่งที่ต้องทำสิ่งที่ต้องทำ TodoListDTO มีรายการของ TaskDTO แต่เนื่องจากเรากำลังใช้ TaskDTO ซ้ำแต่ละคนก็มี UserDTO ดังนั้นปัญหาคือข้อมูลจำนวนมากที่ถูกสอบถามในแบ็กเอนด์และส่งผ่านสาย
เจ้าหน้าที่

จำเป็นต้องใช้ข้อมูลทั้งหมดหรือไม่
Robert Harvey

1

เป็นการยากที่จะสร้างแนวทางปฏิบัติที่ดีที่สุดสำหรับบางสิ่งที่ "ยืดหยุ่น" หรือเป็นนามธรรมในฐานะ DTO โดยพื้นฐานแล้ว DTO เป็นเพียงวัตถุสำหรับการถ่ายโอนข้อมูล แต่ขึ้นอยู่กับปลายทางหรือเหตุผลของการถ่ายโอนคุณอาจต้องการใช้ "แนวทางปฏิบัติที่ดีที่สุด" ที่แตกต่างกัน

ผมขอแนะนำให้อ่านรูปแบบของสถาปัตยกรรม Enterprise Application โดยมาร์ตินฟาวเลอร์ มีทั้งบทที่ทุ่มเทให้กับรูปแบบที่ DTO ได้รับส่วนที่มีรายละเอียดมาก

แต่เดิมพวกมันถูก "ออกแบบ" เพื่อใช้ในการโทรทางไกลที่มีราคาแพงซึ่งคุณน่าจะต้องการข้อมูลจำนวนมากจากส่วนต่าง ๆ ของตรรกะของคุณ DTOs จะทำการถ่ายโอนข้อมูลในสายเดียว

ผู้เขียนกล่าวว่า DTO ไม่ได้มีวัตถุประสงค์เพื่อใช้ในสภาพแวดล้อมท้องถิ่น แต่บางคนพบว่ามีประโยชน์สำหรับพวกเขา โดยปกติแล้วพวกมันจะถูกใช้เพื่อรวบรวมข้อมูลจาก POCO ที่แตกต่างกันในเอนทิตี้เดียวสำหรับ GUIs, API หรือเลเยอร์ที่แตกต่างกัน

ตอนนี้ด้วยการสืบทอดการใช้รหัสซ้ำเหมือนผลข้างเคียงของการสืบทอดมากกว่าวัตถุประสงค์หลัก ในทางกลับกันจะมีการนำโค้ดกลับมาใช้ใหม่เพื่อวัตถุประสงค์หลัก

บางคนแนะนำให้ใช้องค์ประกอบและการสืบทอดพร้อมกันโดยใช้จุดแข็งของทั้งคู่และพยายามลดจุดอ่อนของพวกเขา ต่อไปนี้เป็นส่วนหนึ่งของกระบวนการทางจิตของฉันเมื่อเลือกหรือสร้าง DTO ใหม่หรือคลาส / วัตถุใหม่สำหรับเรื่องนั้น:

  • ฉันใช้การสืบทอดกับ DTO ภายในเลเยอร์เดียวกันหรือบริบทเดียวกัน DTO จะไม่สืบทอดจาก POCO แต่อย่างใด BLL DTO จะไม่สืบทอดจาก DAL DTO เป็นต้น
  • หากฉันพบว่าตัวเองกำลังพยายามซ่อนเขตข้อมูลจาก DTO ฉันจะปรับโครงสร้างใหม่และอาจใช้องค์ประกอบแทน
  • ถ้าเขตข้อมูลที่แตกต่างกันน้อยมากจากฐาน DTO คือทั้งหมดที่ฉันต้องการฉันจะใส่ใน DTO สากล Universal DTOs ใช้ภายในเท่านั้น
  • POCO / DTO พื้นฐานแทบจะไม่ถูกใช้กับตรรกะใด ๆ วิธีการที่ฐานจะตอบสนองต่อความต้องการของลูก ๆ เท่านั้น ถ้าฉันจำเป็นต้องใช้ฐานฉันหลีกเลี่ยงการเพิ่มเขตข้อมูลใหม่ที่ลูก ๆ จะไม่ใช้

บางคนอาจไม่ใช่วิธีปฏิบัติที่ "ดีที่สุด" พวกเขาทำงานได้ดีสำหรับโครงการที่ฉันทำมา แต่คุณต้องจำไว้ว่าไม่มีขนาดที่เหมาะกับทุกคน ในกรณีของ DTO สากลที่คุณควรระวังลายเซ็นของฉันมีลักษณะเช่นนี้:

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

หากวิธีใดต้องการ DTO ของตัวเองฉันจะทำการสืบทอดและโดยปกติการเปลี่ยนแปลงเดียวที่ฉันต้องทำคือพารามิเตอร์แม้ว่าบางครั้งฉันต้องขุดลึกลงไปสำหรับกรณีเฉพาะ

จากความคิดเห็นของคุณฉันได้รับว่าคุณกำลังใช้ DTO ที่ซ้อนกัน หาก DTO ที่ซ้อนกันของคุณมีเพียงรายการของ DTO อื่น ๆ ฉันคิดว่าสิ่งที่ดีที่สุดที่ต้องทำคือการแกะรายการออก

ขึ้นอยู่กับจำนวนข้อมูลที่คุณต้องการแสดงหรือทำงานด้วยอาจเป็นความคิดที่ดีที่จะสร้าง DTO ใหม่ที่ จำกัด ข้อมูล ตัวอย่างเช่นถ้า UserDTO ของคุณมีฟิลด์จำนวนมากและคุณต้องการเพียง 1 หรือ 2 มันอาจจะดีกว่าถ้าคุณมี DTO ที่มีเฉพาะฟิลด์เหล่านั้น การกำหนดเลเยอร์บริบทการใช้งานและยูทิลิตี้ของ DTO จะช่วยได้มากเมื่อออกแบบ


ฉันไม่สามารถเพิ่มลิงก์มากกว่า 2 ลิงก์ในคำตอบของฉันนี่เป็นข้อมูลเพิ่มเติมเกี่ยวกับ DTO ในท้องถิ่น ฉันรู้ว่ามันเป็นข้อมูลเก่ามาก แต่ฉันคิดว่ามันอาจจะเกี่ยวข้อง
IvanGrasp

1

การใช้การจัดองค์ประกอบใน DTO นั้นเป็นการฝึกฝนที่สมบูรณ์แบบที่สุด

การสืบทอดในรูปแบบ DTOs ที่เป็นรูปธรรมถือเป็นการปฏิบัติที่ไม่ดี

สำหรับสิ่งหนึ่งในภาษาเช่น C # คุณสมบัติที่ดำเนินการโดยอัตโนมัติมีค่าใช้จ่ายในการบำรุงรักษาน้อยมากดังนั้นการทำซ้ำพวกเขา (ฉันเกลียดการทำซ้ำอย่างแท้จริง) ไม่เป็นอันตรายอย่างที่มันเป็นบ่อยครั้ง

เหตุผลหนึ่งที่ไม่ควรใช้การสืบทอดที่เป็นรูปธรรมในกลุ่ม DTO คือเครื่องมือบางอย่างจะแมปพวกเขาผิดประเภท

ตัวอย่างเช่นถ้าคุณใช้ยูทิลิตีฐานข้อมูลเช่น Dapper (ฉันไม่แนะนำ แต่มันก็เป็นที่นิยม) ซึ่งทำการclass name -> table nameอนุมานคุณสามารถทำการบันทึกประเภทที่ได้มาในรูปแบบฐานคอนกรีตที่ไหนสักแห่งในลำดับชั้นและทำให้สูญเสียข้อมูลหรือแย่ลง .

เหตุผลที่ลึกกว่าในการไม่ใช้การสืบทอดใน DTOs คือไม่ควรใช้การแบ่งปันการใช้งานระหว่างประเภทที่ไม่มีความสัมพันธ์ "เป็น" ที่ชัดเจน ในใจของฉันไม่ได้เสียงเหมือนชนิดย่อยของTaskDetail Taskมันได้อย่างง่ายดายเพียงอาจเป็นทรัพย์สินของหนึ่งTaskหรือแย่ลงก็อาจจะเป็น supertype Taskของ

ตอนนี้สิ่งหนึ่งที่คุณอาจกังวลคือการรักษาความมั่นคงระหว่างชื่อและประเภทของคุณสมบัติของ DTO ที่เกี่ยวข้องต่างๆ

ในขณะที่การสืบทอดของชนิดที่เป็นรูปธรรมจะช่วยให้มั่นใจในความสอดคล้องดังกล่าวได้ดีกว่ามากที่จะใช้อินเทอร์เฟซ (หรือคลาสพื้นฐานเสมือนจริงใน C ++) เพื่อรักษาความมั่นคงชนิดนั้น

พิจารณา DTO ต่อไปนี้

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
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.