Asp.net-Mvc

利用控制器和視圖中的繼承

  • January 23, 2019

不久前我在 codereview.stackexchange.com 上發布了這篇評論……我覺得它可能更適合 stackoverflow,因為它更像是一個問題而不是程式碼審查。

它需要一點解釋,請多多包涵


我正在用 ASP.NET MVC 開發一個電子商務網站。使用者可以在網站上發布不同類型的廣告。

我使用繼承來定義我的廣告類型,這個問題是關於利用層次結構來刪除控制器和視圖中的重複程式碼。

我有不同的廣告類型SimpleAdCarRealEstateRental

每個 Ad 都源自 AdBase,它具有所有共同屬性:

public abstract class AdBase
{
   public long AdBaseId { get; set; }
   public bool IsActive { get; set; }
   public long UserId { get; set; }
   public string Title { get; set; }
   public short AdDurationInDays { get; set; }
   public string PhotosFolder { get; set; }
}

現在其他 Ads 都是從這個基類派生的:

public class SimpleAd : AdBase
{
   public decimal Price { get; set; }
}

public class Car : AdBase
{
   public decimal Price { get; set; }
   public string Make { get; set; }
}

public class RealEstateRental : AdBase
{
   public decimal WeeklyRent { get; set; }
   public DateTime AvailableFrom { get; set; }
   public short NoOfBedrooms { get; set; }
   public short NoOfBathrooms { get; set; }
}

我正在使用實體框架與數據庫進行互動,並且我正在使用工作單元和儲存庫模式:

我有一個包含所有常見廣告方法的通用 AdBaseRepository:

public abstract class AdBaseRepository<TEntity> where TEntity : AdBase
{
   protected readonly ApplicationDbContext Context;

   public AdBaseRepository(ApplicationDbContext context)
   {
      Context = context; 
   }

   public TEntity Get(long adBaseId)
   {
       return Context.AdBase.OfType<TEntity>()
                 .Where(r => r.IsActive == true && r.AdBaseId == adBaseId)
                 .FirstOrDefault();
   }

   // more common methods here...
}

其他廣告儲存庫繼承自上述類:

public class SimpleAdRepository : AdBaseRepository<SimpleAd>
{
   public SimpleAdRepository(ApplicationDbContext context) : base(context)
   {
   }
}

public class CarRepository : AdBaseRepository<Car>
{
   public CarRepository(ApplicationDbContext context) : base(context)
   {
   }

   // methods which apply only to car here...
}

這是我的工作單元:

public class UnitOfWork
{
   protected readonly ApplicationDbContext Context;

   public UnitOfWork(ApplicationDbContext context)
   {
       Context = context;
       SimpleAd = new SimpleAdRepository(Context);
       RealEstateRental = new RealEstateRentalRepository(Context);
       Car = new CarRepository(Context);
   }

   public SimpleAdRepository SimpleAd { get; private set; }
   public RealEstateRentalRepository RealEstateRental { get; private set; }
   public CarRepository Car { get; private set; }

   public int SaveChanges()
   {
       return Context.SaveChanges();
   }

   public void Dispose()
   {
       Context.Dispose();
   }
}

到目前為止,我對一切都很滿意……但問題是我不知道如何在我的控制器和視圖中利用這種繼承層次結構。

目前,我有 3 個控制器SimpleAdControllerCarControllerRealEstateRentalController

public class SimpleAdController : ControllerBase
{
   private UnitOfWork _unitOfWork;

   public SimpleAdController(UnitOfWork unitOfWork)
   {
       _unitOfWork = unitOfWork;
   }

   [HttpGet]
   // display specific ad
   public ActionResult Display(long id)
   {
       SimpleAd simpleAd = _unitOfWork.SimpleAd.Get(id);
       /* 
        * I have not included my ViewModel Classes in this question to keep
        * it small, but the ViewModels follow the same inheritance pattern
        */
       var simpleAdDetailsViewModel = Mapper.Map<SimpleAdDetailsViewModel>(simpleAd);
       return View(simpleAdDetailsViewModel);
   }
}

CarControllerRealEstateRentalController具有相同的Display方法,除了廣告的類型不同(例如在CarController我有):

   public ActionResult Display(long id)
   {
       Car car = _unitOfWork.Car.Get(id);
       var carViewModel = Mapper.Map<CarViewModel>(car);
       return View(car);
   }

我想要實現的是創建一個AdBaseController將所有常用方法放入其中的方法,如下所示:

public class AdBaseController : ControllerBase
{
   private UnitOfWork _unitOfWork;

   public AdBaseController(UnitOfWork unitOfWork)
   {
       _unitOfWork = unitOfWork;
   }

   // Display for generic ad type
   [HttpGet]
   public ActionResult Display(long id)
   {
       // SimpleAd simpleAd = _unitOfWork.SimpleAd.Get(id);
       /* 
        * I need to replace the above line with a generic ad type... 
        * something like: _unitOfWork<TAd>.GenericAdRepository.Get(id)
        */

       // var simpleAdDetailsViewModel = Mapper.Map<SimpleAdDetailsViewModel>(simpleAd);
       // return View(simpleAdDetailsViewModel);
       /* 
        * similarly I have to replace the above 2 lines with a generic type
        */
   }
}

如果我執行上述操作,那麼我的廣告控制器可以從它繼承,並且我不需要在每個控制器中重複相同的顯示方法……但是我需要製作我的UnitOfWork通用……或者有 2 個 UoW(通用和非通用)…我不確定這是否是個好主意?**有什麼建議AdBaseController**嗎?


同樣,我在我的視圖中重複了很多程式碼。例如,這是顯示SimpleAdView

<div class="row">
   <div class="col-l">
       @*this partial view shows Ad photos and is common code for all ad types*@
       @Html.Partial("DisplayAd/_Photos", Model)
   </div>
   <div class="col-r">
       <div class="form-row">
           @*Common in all ads*@
           <h5>@Model.Title</h5>
       </div>

       @*showing ad specific fields here*@
       <div class="form-row">
           <h5 class="price">$@Model.Price</h5>
       </div>

       @*Ad heading is common among all ad types*@
       @Html.Partial("DisplayAd/_AdBaseHeading", Model)
   </div>
</div>
@*Ad Description is common among all ad types*@
@Html.Partial("DisplayAd/_Description", Model)

這是我的展示CarView

<div class="row">
   <div class="col-l">
       @*Common in all ads*@
       @Html.Partial("DisplayAd/_Photos", Model)
   </div>
   <div class="col-r">
       <div class="form-row">
           @*Common in all ads*@
           <h5>@Model.Title</h5>
       </div>

      @*Price and Make are specific to Car*@ 
       <div class="form-row">
           <h5 class="price">$@Model.Price</h5>
       </div>
       <div class="form-row">
           <h5 class="make">@Model.Make</h5>
       </div>

       @*Common in all ads*@ 
       @Html.Partial("DisplayAd/_AdBaseHeading", Model)
   </div>
</div>
@*Common in all ads*@
@Html.Partial("DisplayAd/_Description", Model)

再一次,我覺得我在每個視圖中重複了很多程式碼。我試圖通過將它們放在共同的部分視圖中來減少重複程式碼的數量。我不確定是否有更好的方法來做到這一點?

從技術上講是可能的。對於類似的實體,您可以引入列舉並使用它來指示您處理的實體類型controller。您可以創建通用視圖來處理類似的廣告(當然,您需要根據模型廣告類型顯示/隱藏相應的 UI 元素)。這是controller說明這個想法的虛擬碼:

using System.Threading.Tasks;
using AutoMapper;
using MyNamespace.Data;
using Microsoft.AspNetCore.Mvc;
using MyNamespace.ViewModels;

namespace MyNamespace
{
   public enum AdType
   {
       [Description("Simple Ad")]
       SimpleAd = 0,

       [Description("Car")]
       Car = 1,

       [Description("Real Estate Rental")]
       RealEstateRental = 2
   }

   public class AdController : Controller
   {
       private readonly ApplicationDbContext _context;
       private readonly IMapper _mapper;

       public AdController(
           ApplicationDbContext context,
           IMapper mapper)
       {
           _context = context;
           _mapper = mapper;
       }

       [HttpGet("Ad/{type}")]
       public IActionResult Index(AdType? type = AdType.SimpleAd)
       {
           switch (type)
           {
               case AdType.RealEstateRental:
                   return RedirectToAction("RealEstateRental");
               case AdType.Car:
                   return RedirectToAction("Car");
               case AdType.SimpleAd:
               default:
                   return RedirectToAction("SimpleAd");
           }
       }

       [HttpGet("Ad/Car")]
       public IActionResult Car()
       {
           return View("Index", AdType.Car);
       }

       [HttpGet("Ad/RealEstateRental")]
       public IActionResult RealEstateRental()
       {
           return View("Index", AdType.RealEstateRental);
       }

       [HttpGet("Ad/SimpleAd")]
       public IActionResult SimpleAd()
       {
           return View("Index", AdType.SimpleAd);
       }

       [HttpGet("Ad/List/{type}")]
       public async Task<IActionResult> List(AdType type)
       {
           // var list = ... switch to retrieve list of ads via switch and generic data access methods 
           return list;
       }

       [HttpGet("Ad/{type}/Details/{id}")]
       public async Task<IActionResult> Details(AdType type, int id)
       {
           var ad = // ... switch by type to retrieve list of ads via switch and generic data access methods
           if (ad == null) return NotFound($"Ad not found.");

           // for instance - configure mappings via Automapper from DB entity to model views
           var model = _mapper.Map<AdViewModel>(ad);

           // Note: view will have to detect the exact ad instance type and show/hide corresponding UI fields
           return View(model);
       }

       [HttpGet("Ad/{type}/Add/")]
       public IActionResult Add(AdType type)
       {
           var ad = // ... switch by type to validate/add new entity  

           return View(_mapper.Map<AdEditModel>(ad));
       }

       [HttpPost("Ad/{type}/Add/")]
       public async Task<IActionResult> Add(AdEditModel model)
       {
           // detect ad type and save 
           return View(model);
       }

       [HttpGet("Ad/{type}/Edit/{id}")]
       public async Task<IActionResult> Edit(AdType type, int id)
       {
           // similar to Add
           return View(model);
       }

       [HttpPost("Ad/{type}/Edit/{id}")]
       public async Task<IActionResult> Edit(AdEditModel model)
       {
           // similar to Add
           return View(model);
       }

       // And so on
   }
}

但我應該指出,UI 相關程式碼中的繼承最終導致的問題多於好處。程式碼變得更複雜,維護和保持清潔。因此,即使它們的程式碼彼此非常接近,將所有你的Views和分開的東西也更有意義。Controllers您可以開始優化 DI 服務(又名business logic)或類似層下方的“重複程式碼”使用。

UI 級別的repeated code問題應該通過提取組件(又名controls, partial views, view components)來解決。控制器繼承是可能的,但會使程式碼更難維護。

引用自:https://stackoverflow.com/questions/52284912