การแยกการเข้าถึงข้อมูลใน ASP.NET MVC


35

ฉันต้องการตรวจสอบให้แน่ใจว่าฉันปฏิบัติตามมาตรฐานอุตสาหกรรมและแนวปฏิบัติที่ดีที่สุดกับการแตกที่แท้จริงครั้งแรกของฉันที่ MVC ในกรณีนี้คือ ASP.NET MVC โดยใช้ C #

ฉันจะใช้ Entity Framework 4.1 สำหรับโมเดลของฉันโดยมีวัตถุเป็นรหัสแรก (ฐานข้อมูลมีอยู่แล้ว) ดังนั้นจะมีวัตถุ DBContext สำหรับดึงข้อมูลจากฐานข้อมูล

ในการสาธิตที่ฉันได้อ่านผ่านเว็บไซต์ asp.net ผู้ควบคุมจะมีรหัสการเข้าถึงข้อมูลอยู่ สิ่งนี้ดูไม่ถูกต้องสำหรับฉันโดยเฉพาะอย่างยิ่งเมื่อทำตามแนวทางปฏิบัติของ DRY (อย่าทำซ้ำตัวเอง)

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

การกระทำหลายอย่างอาจใช้ ISBN และต้องการส่งคืนวัตถุ "หนังสือ" (โปรดทราบว่านี่อาจไม่ใช่รหัสที่ถูกต้อง 100%):

public class BookController : Controller
{
    LibraryDBContext _db = new LibraryDBContext();

    public ActionResult Details(String ISBNtoGet)
    {
        Book currentBook = _db.Books.Single(b => b.ISBN == ISBNtoGet);
        return View(currentBook);
    }

    public ActionResult Edit(String ISBNtoGet)
    {
        Book currentBook = _db.Books.Single(b => b.ISBN == ISBNtoGet);
        return View(currentBook);
    }
}

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

public partial class LibraryDBContext: DBContext
{
    public Book GetBookByISBN(String ISBNtoGet)
    {
        return Books.Single(b => b.ISBN == ISBNtoGet);
    }
}

public class BookController : Controller
{
    LibraryDBContext _db = new LibraryDBContext();

    public ActionResult Details(String ISBNtoGet)
    {
        return View(_db.GetBookByISBN(ISBNtoGet));
    }

    public ActionResult Edit(ByVal ISBNtoGet as String)
    {
        return View(_db.GetBookByISBN(ISBNtoGet));
    }
}

นี่เป็นชุดของกฎที่ถูกต้องที่จะปฏิบัติตามในการเข้ารหัสแอปพลิเคชันของฉันหรือไม่

หรือฉันคิดว่าคำถามเชิงอัตวิสัยจะเป็นดังนี้: "นี่เป็นวิธีที่ถูกต้องหรือไม่"

คำตอบ:


55

โดยทั่วไปคุณต้องการให้ผู้ควบคุมของคุณทำบางสิ่ง:

  1. จัดการคำขอที่เข้ามา
  2. มอบหมายการประมวลผลไปยังวัตถุทางธุรกิจบางอย่าง
  3. ส่งผลลัพธ์ของการประมวลผลทางธุรกิจไปยังมุมมองที่เหมาะสมสำหรับการเรนเดอร์

ไม่ควรมีใด ๆการเข้าถึงข้อมูลหรือเหตุผลทางธุรกิจที่ซับซ้อนในการควบคุม

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

ตัวควบคุมของคุณจะขึ้นอยู่กับ 'บริการ' เพื่อทำงานการประมวลผลจริง ในคลาสบริการของคุณคุณอาจทำงานโดยตรงกับแหล่งข้อมูลของคุณ (ในกรณีของคุณ DbContext) แต่อีกครั้งหากคุณพบว่าตัวเองเขียนกฎธุรกิจจำนวนมากนอกเหนือจากการเข้าถึงข้อมูลคุณอาจต้องการแยกธุรกิจของคุณออก ตรรกะจากการเข้าถึงข้อมูลของคุณ

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

สำหรับโครงการ MVC ทุกครั้งที่ฉันทำงานฉันมักจะลงเอยด้วยโครงสร้างที่ชอบ:

ตัวควบคุม

public class BookController : Controller
{
    ILibraryService _libraryService;

    public BookController(ILibraryService libraryService)
    {
        _libraryService = libraryService;
    }

    public ActionResult Details(String isbn)
    {
        Book currentBook = _libraryService.RetrieveBookByISBN(isbn);
        return View(ConvertToBookViewModel(currentBook));
    }

    public ActionResult DoSomethingComplexWithBook(ComplexBookActionRequest request)
    {
        var responseViewModel = _libraryService.ProcessTheComplexStuff(request);
        return View(responseViewModel);
    }
}

ธุรกิจบริการ

public class LibraryService : ILibraryService
{
     IBookRepository _bookRepository;
     ICustomerRepository _customerRepository;

     public LibraryService(IBookRepository bookRepository, 
                           ICustomerRepository _customerRepository )
     {
          _bookRepository = bookRepository;
          _customerRepository = customerRepository;
     }

     public Book RetrieveBookByISBN(string isbn)
     {
          return _bookRepository.GetBookByISBN(isbn);
     }

     public ComplexBookActionResult ProcessTheComplexStuff(ComplexBookActionRequest request)
     {
          // Possibly some business logic here

          Book book = _bookRepository.GetBookByISBN(request.Isbn);
          Customer customer = _customerRepository.GetCustomerById(request.CustomerId);

          // Probably more business logic here

          _libraryRepository.Save(book);

          return complexBusinessActionResult;

     } 
}

กรุ

public class BookRepository : IBookRepository
{
     LibraryDBContext _db = new LibraryDBContext();

     public Book GetBookByIsbn(string isbn)
     {
         return _db.Books.Single(b => b.ISBN == isbn);
     }

     // And the rest of the data access
}

+1 คำแนะนำที่ดีโดยรวมแม้ว่าฉันจะตั้งคำถามว่าสิ่งที่เป็นนามธรรมที่เก็บมีค่าหรือไม่
MattDavey

3
@MattDavey ใช่ที่จุดเริ่มต้น (หรือแอพที่ง่ายที่สุด) ยากที่จะเห็นความต้องการพื้นที่เก็บข้อมูลเลเยอร์ แต่ทันทีที่คุณมีความซับซ้อนระดับปานกลางในตรรกะทางธุรกิจของคุณมันจะกลายเป็นเรื่องง่าย แยกการเข้าถึงข้อมูล มันไม่ง่ายเลยที่จะสื่อสิ่งนั้นในวิธีที่ง่าย
Eric King

1
@Billy เคอร์เนล IoC ไม่จำเป็นต้องอยู่ในโครงการ MVC คุณสามารถมีในโครงการของตัวเองซึ่งโครงการ MVC ขึ้นอยู่กับ แต่ในทางกลับกันขึ้นอยู่กับโครงการพื้นที่เก็บข้อมูล โดยทั่วไปฉันไม่ทำอย่างนั้นเพราะฉันไม่รู้สึกว่าจำเป็นต้องทำ ถึงกระนั้นถ้าคุณไม่ต้องการให้โครงการ MVC ของคุณเรียกคลาสที่เก็บข้อมูลของคุณ ... ก็อย่าทำเช่นนั้น ฉันไม่ได้เป็นแฟนตัวยงของเอ็นร้อยหวายตัวเองเพื่อที่ฉันจะสามารถป้องกันตัวเองจากความเป็นไปได้ของการฝึกเขียนโปรแกรมที่ฉันไม่น่าจะมีส่วนร่วม
Eric King

2
เราใช้รูปแบบนี้ทุกประการ: Controller-Service-Repository ฉันต้องการเพิ่มว่ามันมีประโยชน์มากสำหรับเราที่จะให้บริการ / พื้นที่เก็บข้อมูลใช้วัตถุพารามิเตอร์ (เช่น GetBooksParameters) และจากนั้นใช้วิธีการขยายบน ILibraryService เพื่อทำข้อผิดพลาดของพารามิเตอร์ ด้วยวิธีดังกล่าว ILibraryService มีจุดเริ่มต้นที่ง่ายต่อการใช้วัตถุและวิธีการขยายสามารถเป็นพารามิเตอร์ที่บ้าที่สุดเท่าที่จะเป็นไปได้โดยไม่ต้องเขียนอินเทอร์เฟซและคลาสทุกครั้ง (เช่น GetBooksByISBN / ลูกค้า / วันที่ / บริการ). คอมโบนั้นยอดเยี่ยมมาก
BlackjacketMack

1
@IsaacKleinman ฉันจำไม่ได้ว่าสักคนเขียนสักคน (Bob Martin?) แต่มันเป็นคำถามพื้นฐาน: คุณต้องการ Oven.Bake (pizza) หรือ Pizza.Bake (oven) และคำตอบก็คือ 'มันขึ้นอยู่กับว่า' โดยปกติเราต้องการบริการภายนอก (หรือหน่วยงาน) จัดการวัตถุหนึ่งชิ้นหรือมากกว่านั้น (หรือพิซซ่า!) แต่ใครจะบอกว่าวัตถุเหล่านั้นแต่ละคนไม่มีความสามารถในการตอบสนองต่อประเภทของเตาอบที่พวกเขากำลังถูกอบฉันชอบ OrderRepository.Save (สั่งซื้อ) เพื่อ Order.Save () อย่างไรก็ตามฉันชอบ Order.Validate () เพราะคำสั่งซื้อสามารถรู้ได้ว่ามันเป็นรูปแบบในอุดมคติของตัวเอง บริบทและส่วนบุคคล
BlackjacketMack

2

นี่คือวิธีที่ฉันทำแม้ว่าฉันจะฉีดผู้ให้บริการข้อมูลเป็นอินเทอร์เฟซบริการข้อมูลทั่วไปเพื่อให้ฉันสามารถสลับการใช้งานได้

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


ใช่ฉันอ่านเกี่ยวกับการใช้ "ส่วนต่อประสานบริการ" สำหรับผู้ให้บริการข้อมูลเพราะช่วยในการทดสอบหน่วย
scott.korin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.