【问题标题】:Should all business logic be in domain models or?所有业务逻辑都应该在域模型中还是?
【发布时间】:2013-06-17 08:25:37
【问题描述】:

ASP.NET MVC4 - 基本上我曾经在我的控制器中拥有我的所有业务逻辑(我试图将其放入域模型中)。 但是我不太清楚我的所有业务逻辑是否应该放入域模型中,或者是否应该保留一些在控制器中?

例如,我得到了如下所示的控制器操作:

[HttpPost]
    public ActionResult Payout(PayoutViewModel model)
    {
        if (ModelState.IsValid)
        {
            UserProfile user = PublicUtility.GetAccount(User.Identity.Name);
            if (model.WithdrawAmount <= user.Balance)
            {
                user.Balance -= model.WithdrawAmount;
                db.Entry(user).State = EntityState.Modified;
                db.SaveChanges();

                ViewBag.Message = "Successfully withdrew " + model.WithdrawAmount;
                model.Balance = user.Balance;
                model.WithdrawAmount = 0;
                return View(model);
            }
            else
            {
                ViewBag.Message = "Not enough funds on your account";
                return View(model);
            }
        }
        else
        {
            return View(model);
        }
    }

现在是否应该将所有逻辑放入域模型中的方法中,使操作方法看起来像这样?

[HttpPost]
    public ActionResult Payout(PayoutViewModel model)
    {
        var model = GetModel(model);
        return View(model);
    }

或者你会怎么做?

【问题讨论】:

  • 我建议将所有代码放入域模型中。它使控制器更清洁。
  • @DarrenDavies 所以即使 ModelState.IsValid 也应该放入域模型中?
  • 不,ModelState.IsValid 是 MVC 的东西,我会将 if 条件部分放入域中返回模型或抛出异常。
  • 这是关于 FatControllers 的重要读物codebetter.com/iancooper/2008/12/03/the-fat-controller

标签: c# .net asp.net-mvc asp.net-mvc-4


【解决方案1】:

我们将应用程序和业务逻辑放在单独的层(csproj 文件)中,一个用于业务逻辑的领域层和一个用于应用程序逻辑的服务层。这将它们完全从 MVC 项目中抽象出来。这对我们有两大好处。首先是业务逻辑与可能改变的模式无关。几年前,我们没有人能想象到今天 MVC 的流行,而在几年后,我们不知道是否会出现一些新事物并取代 MVC,从而让您的绝大多数代码成为如果您想放弃 MVC 去做其他事情,那么“未绑定”到 MVC 会有所帮助。

第二个好处是它可以很容易地实现不同的表示层。因此,如果您想将您的业务逻辑呈现为 WCF 服务,您可以通过创建一个新的 WCF 项目并将其作为您的服务和域层的外观来非常容易地做到这一点。它使维护变得非常容易,因为您的 MVC 项目和 WCF 服务都将使用相同的业务逻辑类。

示例 下面是我会做的一些示例代码。这既快又脏,如果用户不保存回数据库等,应该有更多的东西,比如添加日志记录......

public enum PayoutResult
{
    UserNotFound,
    Success,
    FundsUnavailable,
    DBError
}

public class UserProfile
{
    public float Balance { get; set; }

    public string Username { get; set; }

    // other properties and domain logic you may have

    public bool Withdraw(PayoutModel model)
    {
        if (this.Balance >= model.Amount)
        {
            this.Balance -= model.Amount;
            return true;
        }

        return false;
    }
}


public class PayoutService
{
    IUserRepository userRepository;

    public PayoutService()
    {
        this.userRepository = new UserRepository();
    }

    public PayoutResult Payout(string userName, PayoutModel model)
    {
        var user = this.userRepository.GetAll().SingleOrDefault(u => u.Username == userName);
        if (user == null)
        {
            return PayoutResult.UserNotFound;
        }

        // don't set the model properties until we're ok on the db
        bool hasWithdrawn = user.Withdraw(model);
        if (hasWithdrawn && this.userRepository.SaveUser(user))
        {
            model.Balance = user.Balance;
            model.Amount = 0;

            return PayoutResult.Success;
        }
        else if (hasWithdrawn)
        {
            return PayoutResult.DBError;
        }

        return PayoutResult.FundsUnavailable;
    }
}

您的控制器现在看起来像这样

[HttpPost]
public ActionResult Payout(PayoutModel model)
{
    if (ModelState.IsValid)
    {
        var result = service.Payout(User.Identity.Name, model);
        // This part should only be in the MVC project since it deals with 
        // how things should be presented to the user
        switch (result)
        {
            case PayoutResult.UserNotFound:
                ViewBag.Message = "User not found";
                break;
            case PayoutResult.Success:
                ViewBag.Message = string.Format("Successfully withdraw {0:c}", model.Balance);
                break;
            case PayoutResult.FundsUnavailable:
                ViewBag.Message = "Insufficient funds";
                break;
            default:
                break;
        }               
    }

    return View(model);
}

如果您必须在 Web 服务中公开付款(我在企业环境中工作,所以这对我来说经常发生)您执行以下操作...

public class MyWCFService : IMyWCFService
{
    private PayoutService service = new PayoutService();

    public PayoutResult Payout(string username, PayoutModel model)
    {
        return this.service.Payout(username, model);
    }
}

【讨论】:

  • 再次,更多关于将业务逻辑放在Service层中的话题???业务逻辑属于域?您的意思是从 MVC 的角度来看,它位于服务层中,因为这就是 MVC 所看到的全部内容?而实际上,它位于域中的服务层之下?
【解决方案2】:

对我来说,关注点分离是这些决策最重要的指导原则。因此,这取决于您的域有多复杂以及您从复杂化代码中获得什么好处。

无论如何,作为一般规则,我倾向于向控制器提出以下问题:

  1. 视图模型的实例化和映射(除非有大量映射)
  2. 查看模型验证

而且,我倾向于将模型(或服务)用于非特定于应用程序的领域知识:

  1. 可以取款
  2. 提现

所以,这就是我将如何拆分代码:

    [HttpPost]
    public ActionResult Payout(PayoutViewModel model)
    {
        if (ModelState.IsValid)
        {
            var account = accountRepository.FindAccountFor(User.Identity.Name);

            if (account.CanWithdrawMoney(model.WithdrawAmount))
            {
                account.MakeWithdrawal(model.WithdrawAmount);

                ViewBag.Message = "Successfully withdrew " + model.WithdrawAmount;
                model.Balance = account.Balance;
                model.WithdrawAmount = 0;
                return View(model);
            }

            ViewBag.Message = "Not enough funds on your account";
            return View(model); 
        }
        else
        {
            return View(model);
        }
    }

应用程序状态的保存,我通常用拦截器包裹起来。这样您就可以在整个请求周围加上 unit of work transaction

【讨论】:

  • 与我的方法类似,但我不得不说,我认为最好有一个验证方法,否则由于提款受到其他限制,您最终可能会遇到大量 if 语句在你的控制器中。
  • @DavinTryon 那么账户模型中有CanWithdrawMoney等方法吗?我想将实体与实际业务逻辑分开,所以也许我应该使用部分加入它们?
  • @JohnMayer 在帐户域实体(模型)中是。为什么要分离实体和业务逻辑?这是一种称为“贫血”域实体的反模式。领域模型实体应该同时具有数据和行为。
【解决方案3】:

我会将所有逻辑放在域模型中,并对域进行两次调用,一次用于验证,一次用于执行用例。

所以实体看起来像这样:

public class User 
{
    public double Balance { get;set; }

    public ValidationMessageCollection ValidatePayout(double withdrawAmount)
    {
        var messages = new ValidationMessageCollection();

        if (withdrawAmount > Balance)
        {
            messages.AddError("Not enough funds on your account");
        }

        return messages;
     }

     public void Payout(double withdrawAmount)
     {
         balance -= withdrawAmount;
     }
 }

你的控制器看起来像这样:

[HttpPost]
public ActionResult Payout(PayoutViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = PublicUtility.GetAccount(User.Identity.Name);
    var validationMessages = user.ValidatePayout(model.WithdrawAmount)

    if(validationMessages.Any())
    {
        ViewBag.Message = validationMessages.ToSummary();
        return View(model);
    }

    ViewBag.Message = "Successfully withdrew " + model.WithdrawAmount;
    model.Balance = user.Balance;
    model.WithdrawAmount = 0;
    return View(model);
}

我还会做其他事情,比如插入应用程序/服务层、使用 viewModel 并在 ViewModelBuilder/Mapper 或类似工具中重置 ViewModel,但这显示了基本思想。

【讨论】:

    【解决方案4】:

    我们遵循的方法需要包含在 ViewModel 中的业务案例(您的案例:PayoutViewModel)并通过方法公开,这些方法将在控制器操作中使用。此外,我们对模型和视图模型有明确的区分,其中视图模型引用模型。

    【讨论】:

      【解决方案5】:

      建议您在控制器上使用精简代码,最好在我使用过的其他层(例如 serviceLayer)中处理业务逻辑,然后返回您想要返回视图的视图模型/控制器。甚至在服务层类中定义您的 ajax 方法。这降低了代码复杂性和可维护性问题。甚至它更具可读性..

      在您的控制器中,您可以使用 DI 注入 serviceLayer 类或将其实例化为

        ServiceLayer test = new ServiceLayer() ; 
      

      然后在你的控制器中

        test.registerCustomer(model); // or
        test.registerCutomer(CustomerViewModel);
      

      【讨论】:

      • 业务逻辑应该在域中。请不要说它应该在服务层中来混淆人们。
      • 为什么它在域中,我不明白,所以你是说我们根本不需要 serviceLayer 吗?
      • 域是业务的模型,它的类代表业务中的实体,其中的代码代表业务逻辑,否则你最终会得到一个贫血的域反模式。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多