【问题标题】:Where do you like to put MVC view model data transformation logic?你喜欢把 MVC 视图模型数据转换逻辑放在哪里?
【发布时间】:2014-03-03 01:27:06
【问题描述】:

这是一个场景,我需要从多个 Web 服务调用返回的多个域​​对象中加载一个视图模型对象。将域模型对象转换为可理解的视图模型对象的代码是一些相当复杂的代码。我想过放的三个地方是:

  1. 控制器内部用于加载实例视图模型的内部方法。
  2. 视图模型类本身的静态 get 方法或属性,它返回已加载的视图模型实例。
  3. 完全独立的构建器或帮助器类,具有静态 get 方法、属性或重载构造函数,可返回加载的视图模型实例。

明确地说,我不想使用 AutoMapper 或类似的工具。从最佳实践的角度来看,我想知道这个逻辑应该去哪里,以及为什么。

编辑

这就是我目前所拥有的,这确实给了我“瘦”的控制器逻辑和关注点分离。我怎样才能让它变得更好呢?

    // ** Controller **
    public ActionResult Default()
    {

        var viewModel = MyViewModelBuilder.BuildViewModel(MarketType.Spot);

        return View(SpotViewUrl, viewModel);
    }

    // ** Builder **
    // Lives in MVC project under ViewModelBuilders folder
    public class MyViewModelBuilder
    {
        public static ChartsModel BuildViewModel(MarketType rateMarket)
        {
            var result = new ChartsModel
            {
                RateMarket = rateMarket,
                DateRange = new DateRange()
            };
            LoadGroupedRateLists(result, rateMarket);
            LoadCoorespondingRates(result);
            return result;
        }

        private static void LoadGroupedRateLists(ChartsModel model, RateMarket rateMarket)
        {
            var rateHistSvc = new RateHistoryService(RatesPrincipal.Current.Session.Token);
            var serviceResult = (rateMarket == RateMarket.Spot)
                                                                    ? rateHistSvc.GetSpotNationalRateHistory()
                                                                    : rateHistSvc.GetContractNationalRateHistory();

            // Break lists apart by category, and re-sort and trim.
            model.Cat1Rates = CategorizeTrimAndSort("cat1", false, serviceResult);
            model.Cat2Rates = CategorizeTrimAndSort("cat2", true, serviceResult);
            model.Cat3Rates = CategorizeTrimAndSort("cat3", false, serviceResult);
            model.Cat4Rates = CategorizeTrimAndSort("cat4", true, serviceResult);
            model.Cat5Rates = CategorizeTrimAndSort("cat5", false, serviceResult);
            model.Cat6Rates = CategorizeTrimAndSort("cat6", true, serviceResult);


            // Get Date range from results.
            var sortedRateMonths = serviceResultNational
                                    .Select(rate => rate.YearMonth)
                                    .OrderBy(ym => ym.Year)
                                    .ThenBy(ym => ym.Month);

            model.DateRange.Start = sortedRateMonths.First();
            model.DateRange.End = sortedRateMonths.Last();
        }   

        ...
    }

【问题讨论】:

  • 你看过类似automapper 这样的东西吗?这将允许你构建你正在远离控制器的映射,还允许将自动映射器注入控制器以实现可测试性。
  • 如上所述,我不想在这里使用 Automapper,因为它不适用于这种情况,在这种情况下有几个域对象被分割以进行展示。
  • Automapper 绝对适用于这种情况。您可以从多个对象映射到单个对象。
  • 可能是这样,但我真的很想为custom 表示转换代码找到最佳模式,因为实际上Automapper 无法解决每个转换/映射问题。
  • 我还没有遇到过 AM 无法解决的自定义转换/映射问题。不是试图说服你使用它,只是说它是使用 fluent API(.ForMember、.ConstructUsing 等)高度可定制的。

标签: asp.net-mvc transform viewmodel code-organization


【解决方案1】:

1 或 3,而不是 2。前提是如果您执行 #3,您实际上并没有让静态方法执行 Web 服务调用,而是让它执行映射。域对象输入,视图模型输出。优先使用扩展方法而不是重载的构造函数,如果对象不需要跟踪状态,则将其设为非静态没有任何好处。

为什么?

一旦你在模型上放置了一个逻辑方法,它就不再是一个 POCO。最佳实践是尽可能将视图模型视为无聊的数据桶。有些人还尝试在视图模型构造函数中进行映射,一旦您遇到任何类型的映射复杂性,这不是一个好主意。

如果你只需要在一个控制器中做映射,你可以把它放在一个子程序中。请记住,如果您想单独测试 sub 并将其保留在内部,则您的项目必须具有 InternalsVisibleTo 您的测试项目。

更新

查看您的代码后,我有点倾向于agree with @C Sharper,它既不属于控制器、视图模型也不属于辅助类/方法。编写这个 ChartsModel 是非常有趣的代码,并且包含很多业务逻辑。它真的应该在一个单独的层中。您的控制器应该调用该层并将所有这些有趣且重要的代码委托给另一个抽象。正如@C Sharper 所说,该抽象应该返回一个域对象。是否将该域对象用作您的视图模型,或者将其 DTO 到不同的视图模型中,取决于您。这可能是这样的:

public class MyController : Controller
{
    private readonly IComposeChartData _chartDataComposer;

    public MyController(IComposeChartData chartDataComposer)
    {
        _chartDataComposer = chartDataComposer;
    }

    public ActionResult Default()
    {
        var chartComposition = new ChartCompositionSettings
        {
            MarketType = MarketType.Spot,
            Token = RatesPrincipal.Current.Session.Token,
        };
        var chartData = _chartDataComposer.ComposeChartData(chartComposition);
        var chartModel = Mapper.Map<ChartsModel>(chartData);
        return View(SpotViewUrl, chartModel);
    }
}

这是一个不错的精益控制器机身。抽象可能看起来像这样:

public class ChartDataComposer : IComposeChartData
{
    public ChartData ComposeChartData(ChartCompositionSettings settings)
    {
        // all of the interesting code goes here
    }
}

这样,您的视图模型不需要移出到单独的层,但您确实需要在该层中创建一个类似的对象 (ChartData)。该接口将您的控制器与其所需的数据解耦,并且它返回的对象与您的演示数据(视图模型)具有凝聚力。

我想我并没有真正将这段代码视为业务逻辑,而更像是 表示逻辑。为什么您将其视为业务逻辑?

将您的RateHistoryService 类视为供应商。您从中获得原材料,并将这些原材料转化为不同的东西,在此过程中创造价值。这就是企业所做的。

在这种情况下,图表可视化是您提供的价值。否则,您的客户必须自己筛选原始数据、修剪、分类、排序、分组等,然后才能创建类似的图表。

我可能应该早点解释一下,但是服务调用 已经到了我们自己的业务层,并返回领域层 业务对象。拥有不止一项业务对我来说似乎很奇怪 层。

您的业务层可以有自己的内部分层。在这种情况下,您可以创建一个RateChartingService,它使用RateHistoryService 返回一个RateChartingResult 业务对象。然后将其映射到ChartsModel(或者像我之前说的,直接将它用作您的视图模型)。

【讨论】:

  • 你建议了一个扩展方法,这个扩展方法将扩展什么,控制器(我有多个域对象进入?)。我只是在考虑构建器/帮助器上的公共静态方法,该方法具有每个域对象的参数,并返回视图模型。另外,当您说sub 时,我认为这意味着子程序,是吗?
  • 我想我并没有真正将这段代码视为业务逻辑,而更多地视为表示逻辑。为什么您将其视为业务逻辑?
  • 我可能早该解释这一点,但服务调用已经是我们自己的业务层,并返回领域层业务对象。拥有多个业务层对我来说似乎很奇怪。
【解决方案2】:

我会说不要在你的控制器中这样做。控制器应该尽可能“瘦”。我会这样做。

您的“数据层”将为域对象分配其属性和值。 然后您的后续层将其称为“业务层”,会将您的域对象转移到您的 ViewModel 中。您只需将视图模型传递给您的控制器,而无需控制器处理任何逻辑。

分离很重要。域对象应该远离控制器,控制器应该只关心视图模型。

【讨论】:

  • 我真的不想将我的视图模型放在单独的程序集中,除非它真的被认为是不好的做法。我感觉这是表现层逻辑,应该在那个层。请参阅我的最后一次编辑,它为我目前所处的位置添加了更多颜色。
猜你喜欢
  • 1970-01-01
  • 2017-04-19
  • 2013-09-04
  • 1970-01-01
  • 1970-01-01
  • 2010-10-09
  • 1970-01-01
  • 2011-12-07
  • 2018-06-14
相关资源
最近更新 更多