คำเตือน:เนื่องจากยังไม่มีคำตอบที่ดีฉันจึงตัดสินใจโพสต์ส่วนหนึ่งจากบล็อกโพสต์ที่ยอดเยี่ยมที่ฉันอ่านเมื่อสักครู่ที่ผ่านมาโดยคัดลอกเกือบทุกคำ คุณสามารถค้นหาโพสต์บล็อกเต็มรูปแบบที่นี่ นี่คือ:
เราสามารถกำหนดสองอินเทอร์เฟซต่อไปนี้:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
IQuery<TResult>
ระบุข้อความที่กำหนดแบบสอบถามที่เฉพาะเจาะจงกับข้อมูลที่จะส่งกลับมาใช้TResult
ประเภททั่วไป ด้วยอินเทอร์เฟซที่กำหนดไว้ก่อนหน้านี้เราสามารถกำหนดข้อความค้นหาดังนี้:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
คลาสนี้กำหนดการดำเนินการเคียวรีด้วยพารามิเตอร์สองตัวซึ่งจะทำให้เกิดอาร์เรย์ของอUser
อบเจ็กต์ คลาสที่จัดการกับข้อความนี้สามารถกำหนดได้ดังนี้:
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
private readonly NorthwindUnitOfWork db;
public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
}
}
ตอนนี้เราสามารถให้ผู้บริโภคพึ่งพาIQueryHandler
อินเทอร์เฟซทั่วไปได้:
public class UserController : Controller
{
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;
public UserController(
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
{
this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.findUsersBySearchTextHandler.Handle(query);
return View(users);
}
}
UserController
ทันทีแบบนี้จะช่วยให้เรามีความยืดหยุ่นมากเพราะตอนนี้เราสามารถตัดสินใจว่าจะฉีดเข้าไปใน เราสามารถฉีดการนำไปใช้งานที่แตกต่างไปจากเดิมอย่างสิ้นเชิงหรือการใช้งานที่ครอบคลุมการนำไปใช้งานจริงโดยไม่ต้องทำการเปลี่ยนแปลงUserController
(และผู้บริโภคอื่น ๆ ทั้งหมดของอินเทอร์เฟซนั้น)
IQuery<TResult>
อินเตอร์เฟซที่ช่วยให้เราสนับสนุนเวลารวบรวมเมื่อระบุหรือฉีดIQueryHandlers
ในรหัสของเรา เมื่อเราเปลี่ยนเป็นการFindUsersBySearchTextQuery
ส่งคืนUserInfo[]
แทน (โดยการใช้งานIQuery<UserInfo[]>
) UserController
จะไม่สามารถรวบรวมได้เนื่องจากข้อ จำกัด ประเภททั่วไปในIQueryHandler<TQuery, TResult>
จะไม่สามารถที่จะแมปไปFindUsersBySearchTextQuery
User[]
การฉีด IQueryHandler
อินเทอร์เฟซเข้าสู่ผู้บริโภคมีปัญหาที่ชัดเจนน้อยกว่าที่ยังต้องได้รับการแก้ไข จำนวนการพึ่งพาของผู้บริโภคของเราอาจใหญ่เกินไปและอาจนำไปสู่การสร้างตัวสร้างมากเกินไป - เมื่อตัวสร้างรับข้อโต้แย้งมากเกินไป จำนวนคิวรีที่คลาสรันสามารถเปลี่ยนแปลงได้บ่อยซึ่งจะต้องมีการเปลี่ยนแปลงจำนวนอาร์กิวเมนต์คอนสตรัคเตอร์อย่างต่อเนื่อง
เราสามารถแก้ไขปัญหาที่ต้องฉีดIQueryHandlers
นามธรรมเพิ่มอีกชั้นมากเกินไป เราสร้างคนกลางที่อยู่ระหว่างผู้บริโภคและตัวจัดการแบบสอบถาม:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
IQueryProcessor
เป็นอินเตอร์เฟซที่ไม่ทั่วไปด้วยวิธีการทั่วไปหนึ่ง ดังที่คุณเห็นในนิยามอินเตอร์เฟสIQueryProcessor
ขึ้นอยู่กับIQuery<TResult>
อินเทอร์เฟซ IQueryProcessor
นี้ช่วยให้เราได้รับการสนับสนุนรวบรวมเวลาในการบริโภคของเราที่ขึ้นอยู่กับ มาเขียนใหม่UserController
เพื่อใช้ใหม่IQueryProcessor
:
public class UserController : Controller
{
private IQueryProcessor queryProcessor;
public UserController(IQueryProcessor queryProcessor)
{
this.queryProcessor = queryProcessor;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
UserController
ตอนนี้ขึ้นอยู่กับIQueryProcessor
ว่าสามารถจัดการทั้งหมดของคำสั่งของเรา UserController
's SearchUsers
วิธีการเรียกIQueryProcessor.Process
วิธีการผ่านในวัตถุแบบสอบถามที่เริ่ม เนื่องจากFindUsersBySearchTextQuery
ใช้IQuery<User[]>
อินเทอร์เฟซเราสามารถส่งต่อไปยังทั่วไปได้Execute<TResult>(IQuery<TResult> query)
วิธีการได้ ด้วยการอนุมานประเภท C # คอมไพเลอร์สามารถกำหนดประเภททั่วไปได้และช่วยให้เราไม่ต้องระบุประเภทอย่างชัดเจน ชนิดของProcess
วิธีการส่งคืนยังเป็นที่รู้จัก
ตอนนี้มันเป็นความรับผิดชอบของการดำเนินการที่จะหาที่เหมาะสมIQueryProcessor
IQueryHandler
สิ่งนี้ต้องใช้การพิมพ์แบบไดนามิกและอาจเลือกใช้เฟรมเวิร์ก Dependency Injection และสามารถทำได้ด้วยโค้ดเพียงไม่กี่บรรทัด:
sealed class QueryProcessor : IQueryProcessor
{
private readonly Container container;
public QueryProcessor(Container container)
{
this.container = container;
}
[DebuggerStepThrough]
public TResult Process<TResult>(IQuery<TResult> query)
{
var handlerType = typeof(IQueryHandler<,>)
.MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
QueryProcessor
ระดับโครงสร้างที่เฉพาะเจาะจงIQueryHandler<TQuery, TResult>
ประเภทขึ้นอยู่กับชนิดของอินสแตนซ์แบบสอบถามที่จัดมาให้ ประเภทนี้ใช้เพื่อขอให้คลาสคอนเทนเนอร์ที่ให้มาเพื่อรับอินสแตนซ์ของประเภทนั้น น่าเสียดายที่เราจำเป็นต้องเรียกHandle
ใช้เมธอดโดยใช้การสะท้อน (โดยใช้คีย์เวิร์ด dymamic C # 4.0 ในกรณีนี้) เนื่องจาก ณ จุดนี้เป็นไปไม่ได้ที่จะส่งอินสแตนซ์ตัวจัดการเนื่องจากTQuery
อาร์กิวเมนต์ทั่วไปไม่สามารถใช้งานได้ในเวลาคอมไพล์ อย่างไรก็ตามเว้นแต่ไฟล์Handle
วิธีการเปลี่ยนชื่อหรือได้รับอาร์กิวเมนต์อื่นการเรียกนี้จะไม่ล้มเหลวและหากคุณต้องการการเขียนแบบทดสอบหน่วยสำหรับคลาสนี้ทำได้ง่ายมาก การใช้เงาสะท้อนจะทำให้ลดลงเล็กน้อย แต่ก็ไม่มีอะไรน่าเป็นห่วง
เพื่อตอบข้อสงสัยของคุณ:
ดังนั้นฉันกำลังมองหาทางเลือกอื่นที่ห่อหุ้มข้อความค้นหาทั้งหมด แต่ก็ยังมีความยืดหยุ่นเพียงพอที่คุณจะไม่เพียง แต่แลกเปลี่ยนที่เก็บสปาเก็ตตี้เพื่อเพิ่มคลาสคำสั่ง
ผลที่ตามมาของการใช้การออกแบบนี้คือในระบบจะมีคลาสขนาดเล็กจำนวนมาก แต่การมีคลาสขนาดเล็ก / เน้นจำนวนมาก (ที่มีชื่อชัดเจน) เป็นสิ่งที่ดี วิธีนี้ดีกว่าอย่างเห็นได้ชัดเมื่อมีการโอเวอร์โหลดจำนวนมากพร้อมกับพารามิเตอร์ที่แตกต่างกันสำหรับวิธีการเดียวกันในที่เก็บเนื่องจากคุณสามารถจัดกลุ่มสิ่งเหล่านั้นในคลาสเคียวรีเดียวได้ ดังนั้นคุณยังคงได้รับคลาสเคียวรีน้อยกว่าเมธอดในที่เก็บ