использование наследования в контроллерах и представлениях - PullRequest
0 голосов
/ 12 сентября 2018

Я опубликовал этот обзор на codereview.stackexchange.com некоторое время назад ... Я чувствую, что он может быть более подходящим для stackoverflow, так как это больше вопрос, чем обзор кода.

Это займет немного объяснений, Пожалуйста, потерпите меня .


Я занимаюсь разработкой сайта электронной коммерции в ASP.NET MVC. Пользователи могут размещать на сайте рекламу разных типов.

Я использую наследование для определения типов моих объявлений, и этот вопрос касается использования преимуществ иерархической структуры для удаления повторяющегося кода в контроллерах и представлениях.

У меня разные типы объявлений: SimpleAd, Car и RealEstateRental.

Каждое объявление получено из 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; }
}

Теперь другие объявления получены из этого базового класса:

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; }
}

Я использую Entity Framework для взаимодействия с базой данных и использую шаблоны единиц работы и репозитория:

У меня есть общий 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 контроллера: SimpleAdController, CarController и RealEstateRentalController:

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);
    }
}

CarController и RealEstateRentalController имеют одинаковый метод 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)

Опять же, я чувствую, что повторяю много кода в каждом представлении. Я попытался уменьшить количество повторяющихся кодов, поместив их в общие частичные представления. Я не уверен, есть ли лучший способ сделать это?

Ответы [ 3 ]

0 голосов
/ 18 сентября 2018

Технически это возможно.Для похожих объектов вы можете ввести перечисление и использовать его, чтобы указать, с каким типом объекта вы имеете дело в controller.Вы можете создать общий вид для обработки похожих объявлений (но, конечно, вам нужно будет показать / скрыть соответствующие элементы пользовательского интерфейса в зависимости от типа модельного объявления).это псевдокод для 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
    }
}

Но я должен отметить, что наследование в коде, связанном с пользовательским интерфейсом, в конечном итоге приводит к большему количеству проблем, чем выгод.Код становится все сложнее поддерживать и содержать в чистоте.Поэтому имеет смысл хранить все ваши Views и Controllers отдельно, даже если они имеют код, очень близкий друг к другу.Вы можете начать оптимизировать использование «повторного кода» под вашими сервисами DI (также известными как business logic) или аналогичным уровнем.

Проблема repeated code для уровня пользовательского интерфейса должна быть решена с помощью извлечения компонентов (aka controls, partial views, view components).Возможно наследование контроллера, но усложнить поддержку кода.

0 голосов
/ 19 сентября 2018

Больше абстракции -> больше утечек абстракции.

У меня есть полное решение, как генерировать контроллеры из определения модели EF, используя деревья выражений

Проверьте, как выглядит код контроллера после удаления всего «дублированного кода»:

https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/UsersController.cs

или это («Роли» могут быть созданы, когда «Пользователи» были импортированы из AD)

https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/RolesController.cs

Эти блоки при запуске настраивают полный контроллер с множеством функций (например, поддержка преобразования строк, анализаторы ошибок SQL Server и т. Д., Поддержка необработанных исключений «один ко многим», «многие ко многим»)

static ControllerMeta<User, int> meta = new ControllerMeta<User, int>(
            // how to find entity by "id"      
            findByIdExpression: id => e => e.UserId == id,
            // how to extract "id" from http responce      
            keyConverter: Converters.TryParseInt,
            // configure EF includes for Index page
            indexIncludes: chain => chain
                       .IncludeAll(e => e.UserPrivilegeMap)
            // ... and so on, try to read it

Но эти определения на самом деле являются своего рода новым внутренним DSL. На самом деле вы спрашиваете «как написать новый DSL, который определяет контроллеры / страницы большими блоками». Ответ - это легко, но есть причина, по которой люди придерживаются языков общего назначения. Это потому, что это «общее».

P.S. Одна деталь: если вы хотите, чтобы «полный контроллер» мог быть сокращен / сконфигурирован во время выполнения, поэтому вы вынуждены самостоятельно анализировать http-запросы - и игнорировать модель привязки параметров MS - это потому, что BindAttribute - важный модификатор привязки - может не быть настроенным во время выполнения простым способом. Для многих людей - даже когда они теряют «int id» в списке параметров - это слишком высокая цена. Даже если отказ от привязки параметров MS очень логичен: зачем вам сохранять магическую привязку параметров MS, когда вы собираетесь настраивать весь контроллер магическим образом?

0 голосов
/ 12 сентября 2018

Простите, если я неправильно понял, но при условии, что вы добавили родовое UOW, мне кажется, вы могли бы сделать что-то вроде этого: Я не понимаю, почему было бы плохо это делать

public class AdBaseController : ControllerBase
{
    private IUnitOfWork _unitOfWork;

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

    public ActionResult GetDisplayAction<TAd, TViewModel>(long id)
    {
        SimpleAd simpleAd = _unitOfWork<TAd>.GenericAdRepository.Get(id)
        var viewModel = Mapper.Map<TViewModel>(simpleAd);         
        return View(viewModel);
    }
}

public class SimpleAdController : ControllerBase
{    
    public SimpleAdController(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    [HttpGet]
    public ActionResult Display(long id)
    {
        return GetDisplayAction<AdType, ViewModelType>();
    }
}
...