代码之家  ›  专栏  ›  技术社区  ›  Junior

如何重构我的asp。net mvc 5控制器来抽象掉所有这些依赖关系?

  •  0
  • Junior  · 技术社区  · 7 年前

    我有一个应用程序,它是在ASP的顶部使用c#编写的。NET MVC 5框架。我已安装并安装 Unity.Mvc 允许我进行依赖注入的包。

    我的一个控制器中有以下代码。虽然这段代码对我来说似乎很干净,但有几件事困扰着我,我正在努力改进。

    查看以下内容 FlagsController 您将正确地注意到所有这些必须注入到我的控制器中的讨厌的依赖项。其次,我觉得我的控制器应该只需要依赖FlagService、Flash和Mapper。其余的都是我的ViewModels需要的依赖项,以使它们具有可展示性。

    // Here is my current code 
    public class FlagsController : Controller
    {
        protected IProductService ProductService;
        protected IClientService ClientService;
        protected IUserPassport Passport;
        protected ITaskFlagService FlagService;
        protected IFlashManager Flash;
        protected IMapper Mapper;
    
        public FlagsController (
            IProductService productService,
            IClientService clientService,
            IUserPassport passport,
            ITaskFlagService flagService,
            IFlashManager flash,
            IMapper mapper)
        {
            ProductService = productService;
            ClientService = clientService;
            Passport = passport;
            FlagService = flagService;
            Flash = flash;
            Mapper = mapper;
        }
    
        public ActionResult Index(ListFlagsViewModel viewModel)
        {
            viewModel.Flags = FlagService.GetPagedRecords(viewModel, 25);
    
            viewModel.SetMenuItems(ProductService, ClientService, Passport);
            return View(viewModel);
        }
    
        public ActionResult Create()
        {
            var viewModel = new CreateFlagViewModel();
            viewModel.SetMenutItems(ClientService, Passport);
    
            return View(viewModel);
        }
    
        [HttpPost, ValidateAntiForgeryToken]
        public ActionResult Create(CreateFlagViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    FlagService.Add(viewModel);
                    Flash.AddSuccess("New flag has been added.", 3);
    
                    return RedirectToAction("Index");
                }
                catch (Exception e)
                {
                    Flash.AddError(e.Message);
                }
            }
    
            viewModel.SetMenutItems(ClientService, Passport);
    
            return View(viewModel);
        }
    
    
        public ActionResult Edit(int? id)
        {
            var flag = FlagService.Get(id);
            if (flag == null)
            {
                return RedirectToAction("Index");
            }
    
            var viewModel = Mapper.Map<EditFlagViewModel>(flag);
            viewModel.SetMenutItems(ClientService, Passport, flag.ClientId, ProductService);
    
            return View(viewModel);
        }
    
        [HttpPost, ValidateAntiForgeryToken]
        public ActionResult Edit(EditFlagViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                var flag = FlagService.Get(viewModel.Id);
                if (flag == null)
                {
                    return RedirectToAction("Index");
                }
    
                TaskFlag updatedFlag = Mapper.Map(viewModel, flag);
                FlagService.Update(updatedFlag);
                Flash.AddSuccess("Flag has been updated.", 3);
    
                return RedirectToAction("Index");
            }
    
            viewModel.SetMenutItems(ClientService, Passport, viewModel.Client.Id, ProductService);
    
            return View(viewModel);
        }
    
        public ActionResult Details(int? id)
        {
            var flag = FlagService.Get(id);
            if (flag == null)
            {
                return RedirectToAction("Index");
            }
    
            var viewModel = Mapper.Map<DisplayFlagViewModel>(flag);
    
            return View(viewModel);
        }
     }
    

    现在,我正试图严格地将视图模型作为数据传输对象。但是为了让我的视图模型将实体模型转换为视图就绪的html表单,它需要一些服务来从数据库中提取数据以用于下拉菜单或其他事情。因此,我的视图模型不会有任何业务逻辑,但它需要调用不同的服务,以便将所有部分放在一起,以便为视图做好准备。为了更好地解释这一点,请看一下我的 CreateFlagViewModel 下面列出的类别。

    public class CreateFlagViewModel
    {
        [Required, MaxLength(50)]
        public string Title { get; set; }
    
        public OptionalClientMenuViewModel Client { get; set; }
        public OptionalProductMenuViewModel Product { get; set; }
    
        [MaxLength(255), DataType(DataType.MultilineText)]
        public string Description { get; set; }
    
        public CreateFlagViewModel()
        {
            Client = new OptionalClientMenuViewModel();
            Product = new OptionalProductMenuViewModel();
        }
    
        public void SetMenutItems(IClientService clientService, IUserPassport passport)
        {
            Client.SetOptions(clientService, passport);
        }
    }
    

    您会注意到,我在viewmodel中使用了子视图模型,以允许重用代码,并为每个子视图模型提供了EditorTemplate,以消除到处编写的重复代码。这是子视图模型aka OptionalClientMenuViewModel 负责显示出现在上述视图模型aka中的客户端列表 CreateFlagViewModel

    public class OptionalClientMenuViewModel
    {
        public int? Id { get; set; }
        public IEnumerable<SelectListItem> Options { get; set; }
    
        public OptionalClientMenuViewModel()
        {
            Options = new List<SelectListItem>();
        }
    
        public void SetOptions(IClientService service, IUserPassport passport, bool isActive = true)
        {
            Options = service.GetClientItemForUser(passport.User, isActive);
        }
    }
    

    如您所见 CreateFlagViewModel 视图模型类非常简洁。但是它有那么恶心 SetMenuItems 需要2个依赖项的方法。这些依赖关系迫使任何使用者“在本例中是控制器”必须传递它们。我觉得如果我能以某种方式摆脱 设置菜单项 方法,我的控制器将被清理,我的视图模型将看起来更干净。

    既然我使用的是依赖项注入,为什么我不能将代码传输到如下内容

    首先将子视图模型中的依赖项从方法交换到构造函数中。

    public class OptionalClientMenuViewModel
    {
        protected IClientService ClientService;
        protected IUserPassport Passport;
    
        public int? Id { get; set; }
        public IEnumerable<SelectListItem> Options { get; set; }
    
        public OptionalClientMenuViewModel(IClientService clientService, IUserPassport passport)
        {
            Options = new List<SelectListItem>();
        }
    
        public void SetOptions(bool isActive = true)
        {
            Options = ClientService.GetClientItemForUser(Passport.User, isActive);
        }
    }
    

    然后创建一个接口来自动调用 SetMenutItems() 每次渲染视图时使用。 公共接口IHaveMenuSetter { void SetMenutItems(); }

    最后,实现 IHaveMenuSetter 契约到我的视图模型中,并将依赖项注入构造函数中,以使 设置菜单项 方法参数较少。

    public class CreateFlagViewModel : IHaveMenuSetter
    {
        [Required, MaxLength(50)]
        public string Title { get; set; }
    
        public OptionalClientMenuViewModel Client { get; set; }
        public OptionalProductMenuViewModel Product { get; set; }
    
        [MaxLength(255), DataType(DataType.MultilineText)]
        public string Description { get; set; }
    
        public CreateFlagViewModel(OptionalClientMenuViewModel client)
        {
            Client = client;
            Product = new OptionalProductMenuViewModel();
        }
    
        public void SetMenutItems()
        {
            Client.SetOptions();
        }
    }
    

    最后,我的控制器看起来是这样的“假设其余的viewModels被重构为相同的代码模式”

    public class FlagsController : Controller
    {
        protected ITaskFlagService FlagService;
        protected IFlashManager Flash;
        protected IMapper Mapper;
        protected CreateFlagViewModel CreateViewModel;
    
        public FlagsController (
            ITaskFlagService flagService,
            IFlashManager flash,
            IMapper mapper, 
            CreateFlagViewModel createViewModel)
        {
            FlagService = flagService;
            Flash = flash;
            Mapper = mapper;
            CreateViewModel = createViewModel;
        }
    
        public ActionResult Index(ListFlagsViewModel viewModel)
        {
            viewModel.Flags = FlagService.GetPagedRecords(viewModel, 25);
    
            // This line could be even eliminated and auto called using `ActionFilterAttribute` by utilizing a new contract 
            viewModel.SetMenuItems();
    
            return View(viewModel);
        }
    
        public ActionResult Create()
        {
            // This line could be even eliminated and auto called using `ActionFilterAttribute` by utilizing a new contract
            CreateViewModel.SetMenutItems(); 
    
            return View(CreateViewModel);
        }
    
        [HttpPost, ValidateAntiForgeryToken]
        public ActionResult Create(CreateFlagViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    FlagService.Add(viewModel);
                    Flash.AddSuccess("New flag has been added.", 3);
    
                    return RedirectToAction("Index");
                }
                catch (Exception e)
                {
                    Flash.AddError(e.Message);
                }
            }
    
            // This line could be even eliminated and auto called using `ActionFilterAttribute` by utilizing a new contract
            CreateViewModel.SetMenutItems(); 
    
            return View(viewModel);
        }
    
    
        public ActionResult Edit(int? id)
        {
            var flag = FlagService.Get(id);
            if (flag == null)
            {
                return RedirectToAction("Index");
            }
    
            var viewModel = Mapper.Map<EditFlagViewModel>(flag);
    
            // This line could be even eliminated and auto called using `ActionFilterAttribute` by utilizing a new contract
            viewModel.SetMenutItems(); 
    
            return View(viewModel);
        }
    
        [HttpPost, ValidateAntiForgeryToken]
        public ActionResult Edit(EditFlagViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                var flag = FlagService.Get(viewModel.Id);
                if (flag == null)
                {
                    return RedirectToAction("Index");
                }
    
                 TaskFlag updatedFlag = Mapper.Map(viewModel, flag);
                FlagService.Update(updatedFlag);
                Flash.AddSuccess("Flag has been updated.", 3);
    
                return RedirectToAction("Index");
            }
    
            // This line could be even eliminated and auto called using `ActionFilterAttribute` by utilizing a new contract
            viewModel.SetMenutItems(); 
    
            return View(viewModel);
        }
    
        public ActionResult Details(int? id)
        {
            var flag = FlagService.Get(id);
            if (flag == null)
            {
                return RedirectToAction("Index");
            }
    
            var viewModel = Mapper.Map<DisplayFlagViewModel>(flag);
    
            return View(viewModel);
        }
     }
    

    清洁工可能但是上面的代码会给我一个错误,因为我的视图模型没有默认构造函数。当asp。净mvc DefaultModelBinder 尝试在 HttpPost 要求

    为了使设置正常工作,我必须覆盖 DefaultModelBinder 因此,它从IoC容器解析视图模型,而不是使用默认构造函数,这是我犹豫要做的事情。

    问题

    我当前代码的状态看起来可以接受吗?那么我提议的代码更改呢,提议的代码不是更好地分离了关注点吗?是否有更好的方法来清理此代码?

    1 回复  |  直到 7 年前
        1
  •  3
  •   StriplingWarrior    7 年前

    我看到你提出的解决方案的主要实际问题是 CreateFlagViewModel 需要作为操作参数传递,因此MVC需要能够构造它,考虑到它有一个构造函数参数,并且该构造函数参数将服务作为构造函数参数,MVC可能很难进行构造。可能有一些变通方法可以让这项工作奏效,但这表明你可能在错误的方向上思考。

    现在,我正试图严格地将视图模型作为数据传输对象。

    好主意。让我们试着将代码进一步推向这个方向,去掉如下方法 SetOptions

    但是为了让我的视图模型将实体模型转换为视图就绪的html表单,它需要一些服务来从数据库中提取数据以用于下拉菜单或其他事情。因此,我的视图模型不会有任何业务逻辑,但它需要调用不同的服务,以便将所有部分放在一起,以便为视图做好准备。

    实际上,我建议从ViewModel中去掉这一责任。如何创建viewmodel映射服务来完成SetOptions和SetMenuItems等方法中定义的工作?

    而不是:

        viewModel.SetMenutItems(ClientService, Passport);
    

    使用:

        FlagsModelMapper.SetMenutItems(viewModel);
    

    这样,您的服务就可以抽象出ClientService和Passport的注入。它保持独立的单元可测试性,就像您的控制器一样。而且ViewModel仍然易于反序列化。

    这与我发现的几乎总是最好遵循的模式是一致的,这是马克·希曼等专家所支持的。将类分为两类:服务和模型。模型应该尽可能地愚蠢,如果它们有任何逻辑,那么它应该只作用于模型本身内部可用的数据。服务使用构造函数注入来注入它们需要的其他服务。不要将模型注入服务,也不要在模型中使用服务。