从视图模型调用业务逻辑是否是良好实践

5
我正在处理一个大型ASP.NET MVC项目(大约有15个单独的项目)。我们使用外观设计模式来调用业务逻辑以及其他项目。
问题:在MVC应用程序中,从ViewModel调用Facade是最佳实践吗?
我使用单个Facade实例来调用所有函数。我为每个操作创建一个ViewModel,并从ViewModel内部填充数据。这使得ViewModel变得更大,但控制器操作变得更薄,因为我们现在在ViewModel中完成了工作。在ViewModel构造函数中,我传递Facade实例并从业务逻辑层取出需要的数据。
public class MyViewModel
{
    private Facade _Facade;
    public IEnumerable<SomeModel> Collection { get; set; }
    public IEnumerable<SelectListItem> Years { get; set; }
    public IEnumerable<SelectListItem> Quarters { get; set; }
    public int SelectedYear { get; set; }
    public int SelectedQuarter { get; set; }


     public BottomUpForecastViewModel(EXFacade facade)
    {
        this._Facade = facade;
        this.Years = GetFinancialYears();
        this.Quarters = GetFinancialQuarters();
        this.SelectedYear = DateTime.Now.Year;
        this.SelectedQuarter = TimePeriods.GetQuarterNoForDate(DateTime.Now);
        Collection = GetMonthlyCollection(SelectedYear, SelectedQuarter);// Take data     from the _Facade(call facade)

    }

}

  public class MyController : Controller
  {


    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult BottomUpForecast()
    {

        return View(new MyViewModel(facade));
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult BottomUpForecast(MyViewModel model)
    {

        return View();

    }

}

这个做法好吗?如果我们不需要担心依赖性,你有更好的方案建议吗?
更新:我找到了一篇有趣的文章,介绍如何让控制器轻盈 "Put them on a diet": http://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/**

你可以参考我的回答。我不会在控制器中填充ViewModels。这会增加控制器中的代码量,我认为这通常是不好的。相反,只需从控制器调用业务层,并让业务层填充您的VM即可。我的控制器代码如下... MyViewModel vm = new BusinessLayer().BottomForecastVM(); return view(vm); https://dev59.com/Unzaa4cB1Zd3GeqPMi0Y#21609548 - CSharper
4个回答

12

你的想法是正确的。从View Model调用业务逻辑是完全可以接受的。我经常这样做。

不幸的是,你目前的实现与具体类型耦合度很高。你可以进行一些抽象重构:

相反,在你的业务层中,创建一个接口IEXFacade,将其绑定到对象并传递给ViewModel:

public interface IEXFacade
{
   public IEnumerable<SomeModel> GetMonthlyCollection(int SelectedYear, int SelectedQuarter);
   public IEnumerable<SelectListItem> GetFinancialYears();
   public IEnumerable<SelectListItem> GetFinancialQuarters();
   public int getSelectedYear();
   public int getSelectedQuarter(DateTime dateTime);
}

你的EXFacade定义可能类似于:

public class EXFacade : IEXFacade
{
   private TimePeriods _timePeriods = new TimePeriods();

   public int getSelectedYear()
   {
       return DateTime.Now.Year;
   }

   public int getSelectedQuarter (DateTime dateTime)
   {
       return _timePeriods.GetQuarterNoForDate(dateTime);
   }


   public IEnumerable<SomeModel> GetMonthlyCollection()
   {
           ....
           return MonthlyCollection;
   }

   public IEnumerable<SelectListItem> GetFinancialYears();
   {
           ....
           return MonthlyCollection;
   }

   public IEnumerable<SelectListItem> GetFinancialQuarters();
   {
           ....
           return MonthlyCollection;
   }

}
现在您的视图模型将采用IEXFacade并且更加容忍变化。
public class MyViewModel
{
     MyViewModel(IEXFacade facade)
     {
        Years = facade.GetFinancialYears();
        Quarters = facade.GetFinancialQuarters();
        SelectedYear = facade.getSelectedYear();
        SelectedQuarter = facade.getSelectedQuarter (DateTime.Now);
        Collection = facade.GetMonthlyCollection(SelectedYear, SelectedQuarter);
    }


    //Keeping the Facade Object seems extraneous (unless I'm missing something)
    //private Facade _Facade;
    public IEnumerable<SomeModel> Collection { get; set; }
    public IEnumerable<SelectListItem> Years { get; set; }
    public IEnumerable<SelectListItem> Quarters { get; set; }
    public int SelectedYear { get; set; }
    public int SelectedQuarter { get; set; }
}
目标是通过传递接口来解耦对EXFacade类的特定实现的依赖关系,这样您的EXFacade方法逻辑可以更改而不会破坏视图模型。只要接口(属性和签名)保持不变即可。

结论:

我不偏向于直接从我的ViewModel中调用逻辑,而不是从我的Controller中调用。但是,它通常更方便,因为它节省了一步。相反,将逻辑直接注入到您的模型中比将其合并到控制器中要不明显得多。但是,“Fat Controllers”与“Fat Models”的论点非常平衡,我认为两者都没有更正确的答案。

更重要的是要理解Facade Pattern旨在成为您的“喋喋不休”逻辑层和演示层之间的接口。出于抽象和解耦的考虑,该模式需要一个接口。一旦使用接口抽象您的Facade,您就可以进一步解耦,通过使用IOC容器(如NInject)将您的Facade注入到控制器或模型中。

我强烈建议在这样的大型项目中使用依赖注入模式。


@unique,希望我能有所帮助。我稍微更新了我的答案。我相信你走在正确的道路上,但使用一个IOC容器来实现依赖注入会更有帮助 ;) - Dave Alperovich
同样需要注意的是,在视图模型中实现逻辑的方式,也要在视图模型中添加默认构造函数。如果不添加默认构造函数,则在 POST 请求上会出现 ASP.NET MVC 绑定程序的问题。 - Patrick Peters
@Worthy7,我并不是想在VM中使用DI。我是在考虑让VM调用服务。这些服务将由DI进行管理。看看我的例子:VM具有实现Facade接口的服务属性。不确定我是否解释清楚了。 - Dave Alperovich
我理解你的方法,就是先将服务注入到控制器中,然后将实例化的服务传递给模型。 有趣的方法。我已经创建了“视图编排器”,使用各种服务和存储库为我构建某些复杂视图。 - Worthy7
我突然想到,我的当前视图模型创建方法是一个异步方法。这种情况有点必须这样,因为它本身调用了异步方法。如果我将此业务逻辑移动到视图模型的构造函数中,则无法再使用async/await。构造函数不能是异步的。这是一个致命问题。为了找到最佳平衡点,我现在考虑构建一个唯一目的是创建视图模型的对象。它将注入所有我的facade,并且可以使用这些facade来创建需要来自多个facade的信息的视图模型。有更好的建议吗?我洗耳恭听。 - Jay
显示剩余4条评论

9

如果您将业务逻辑放在ViewModel中,那么您将违反MVC模式。构建视图是控制器的工作,而不是通过接收依赖项来构建视图本身。

ViewModel应该无知于其他层(View和Controller),从而促进松散耦合的架构。

如果您的ViewModel变得太大,可以创建帮助方法或类来仅用于构造ViewModel。

public class MyController : Controller
{
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult BottomUpForecast()
    {
        return View(this.GetMyViewModel());
    }


    private MyViewModel GetMyViewModel()
    {
        var viewModel = new MyViewModel()
        {
            Years = this.facade.GetFinancialYears();
            Quarters = this.facade.GetFinancialQuarters();
            SelectedYear = DateTime.Now.Year;
            SelectedQuarter = this.facade.TimePeriods.GetQuarterNoForDate(DateTime.Now);
            Collection = this.facade.GetMonthlyCollection(SelectedYear, SelectedQuarter);
        }

        return viewModel;
    }
}

// Thin ViewModel
public class MyViewModel
{
    public IEnumerable<SomeModel> Collection { get; set; }
    public IEnumerable<SelectListItem> Years { get; set; }
    public IEnumerable<SelectListItem> Quarters { get; set; }
    public int SelectedYear { get; set; }
    public int SelectedQuarter { get; set; }
}

关于这个话题的有趣讨论在这里:https://dev59.com/rXM_5IYBdhLWcg3wPAjT#1464030


非常感谢您的评论。那么,在ASP.NET MVC应用程序中,模型的用途是什么? - unique
1
@unique - 大多数情况下,它们是相同的。关键区别在于,模型代表一个领域实体,ViewModel则针对包含下拉菜单等特定视图进行了定制。例如:PersonModel vs EditPersonViewModel。 - Yorro
2
继续Yorro的回答。你有三个层次,对吧,数据、业务和展示。数据层将调用数据库并填充“领域实体”,业务将简单地将领域实体转化为视图模型。业务层只需将视图模型传递给控制器,而不需要让控制器处理任何填充过程。 - CSharper

8

视图模型是视图的模型。它应该包括数据(模型)和将该数据移入和移出视图所需的任何逻辑。它不应该知道任何其他层的内容。它甚至不应该依赖于控制器,更不要说在它之下的任何东西了。填充视图模型是控制器的工作,因此调用业务逻辑也是控制器的工作。


但在这种情况下,从控制器操作结果视图填充视图模型的控制器操作非常庞大,不是吗? - unique
2
当然可以,但那是控制器的工作。将该逻辑推入视图模型中会创建额外的依赖关系。 - jmcilhinney
1
ViewModel 是一种横切层,必须充当“数据传输对象”。如果您将业务逻辑访问放在 ViewModel 中,就会破坏单一职责原则,并为该层添加额外的工作。 - Andrey
@Andrey 按照你在 aspnet/mvc 应用程序中概述的模式进行了跟随。ViewModel 不是 DTO。回想起来,我宁愿调用 ViewModel.AddPost() 而不是 businessLayer.Posts.Add(post)。如果将行为(委托给业务层)添加到 ViewModel 中(假设其中没有逻辑),是否存在任何现实的缺点?请记住,MVVM 不是 MVC,而且 ViewModel 可能负责许多不相关的 DTO!实际上,这将是一个声明,即 ViewModel 实际上只是另一层,可以稍微清理控制器。 - jwize
1
@jwize 从ViewModel调用业务层会增加软件的耦合性。我同意MVVM不是MVC,但在使用ASP.NET MVC框架时,你无法逃脱它;)。控制器应该负责将数据从业务层翻译成ViewModel(我建议使用AutoMapper来完成这个肮脏的工作)。 - Andrey

1

补充一下@Yorro的帖子,MVVM实际上使用这种模式,其中VM负责所有这些活动。在MVC中执行此类操作时,最好使用Controller。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接