0

我有一个AddCustomer() 有四个参数 ( firName, lastName, email, companyId),如下所示。

public class CustomerService
    {

            public bool AddCustomer(
                    string firName, string lastName, 
                    string email, int companyId)
            {

           //logic: create company object based on companId

           //other logic including validation

           var customer = //create customer based on argument and company object 

           //save the customer

        }

    }

     public class Customer
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public Company Company { get; set; }

        public string EmailAddress { get; set; }

        //Other five primitive properties

    }

        public class Company
    {
        public int Id { get; set; }

        public string Name { get; set; }

    }

我的问题是应该将AddCustomer's参数更改为Customer对象,如下所示。请注意,该方法中仅使用了上面显示的四个字段。

 public bool AddCustomer(Customer customer){         
         }

更新

如果使用以下:

public bool AddCustomer(Customer customer)

问题:参数之一是 CompanyId。因此,使用 CompanyId 作为参数创建 Customer 构造函数可能不适用于所有情况。但是,如果没有构造函数,AdCustomer() 的客户端会混淆分配哪些属性。

更新 2

理想情况下,我想通过限制属性设置器来保护实体客户和公司的不变量。

4

3 回答 3

1

An answer very much depends on what the purpose and the responsibility of the CustomerService class and the Customer class is, and what they are intended to achieve.

From your question it would seem ("other logic including validation") that it is the responsibility of CustomerService to determine what constitutes a valid new Customer to be registered, whereas the Customer class itself is nothing more than a DTO without any behavior.

So consider the following hypothetical use cases: a customer's email changes; the Company the Customer works for changes; if the Company is bankrupt, the new Customer registration should be refused; if the Company produces a lot of sales for us, the Customer should be regarded as a Premium Customer. How would such cases be handled and what responsibilities are involved?

You might want to approach this differently, in the sense that you make both intent and behavior explicit, instead of having "AddCustomer", "UpdateCustomer", "DeleteCustomer" and "GetCustomer(Id)". The Customer service could be responsible for service coordination and infrastructure aspects, while the Customer class really focuses on the required domain behavior and customer related business rules.

I will outline one (a CQRS type approach) of several possible approaches to better break up responsibilities, to illustrate this:

Encode behavioral intent and decisions as Commands and Events respectively.

namespace CustomerDomain.Commands
{
    public class RegisterNewCustomer : ICommand
    {
        public RegisterNewCustomer(Guid registrationId, string firstName, string lastName, string email, int worksForCompanyId)
        {
            this.RegistrationId = registrationId;
            this.FirstName = firstName;
            // ... more fields
        }
        public readonly Guid RegistrationId;
        public readonly string FirstName;
        // ... more fields
    }
    public class ChangeCustomerEmail : ICommand
    {
        public ChangeCustomerEmail(int customerId, string newEmail)
        // ...
    }
    public class ChangeCustomerCompany : ICommand
    {
        public ChangeCustomerCompany(int customerId, int newCompanyId)
        // ...
    }
    // ... more commands
}

namespace CustomerDomain.Events
{
    public class NewCustomerWasRegistered : IEvent
    {
        public NewCustomerWasRegistered(Guid registrationId, int assignedId, bool isPremiumCustomer, string firstName /* ... other fields */)
        {
            this.RegistrationId = registrationId;
            // ...
        }
        public readonly Guid RegistrationId;
        public readonly int AssignedCustomerId;
        public readonly bool IsPremiumCustomer;
        public readonly string FirstName;
        // ...
    }
    public class CustomerRegistrationWasRefused : IEvent
    {
        public CustomerRegistrationWasRefused(Guid registrationId, string reason)
        // ...
    }
    public class CustomerEmailWasChanged : IEvent
    public class CustomerCompanyWasChanged : IEvent
    public class CustomerWasAwardedPremiumStatus : IEvent
    public class CustomerPremiumStatusWasRevoked : IEvent
}

This allows expressing intent very clearly, and including only the information that is actually needed to accomplish a specific task.

Use small and dedicated services to deal with the needs of your application domain in making decisions:

namespace CompanyIntelligenceServices
{
    public interface ICompanyIntelligenceService
    {
        CompanyIntelligenceReport GetIntelligenceReport(int companyId); 
        // ... other relevant methods.
    }

    public class CompanyIntelligenceReport
    {
        public readonly string CompanyName;
        public readonly double AccumulatedSales;
        public readonly double LastQuarterSales;
        public readonly bool IsBankrupt;
        // etc.
    }
}

Have the CustomerService implementation deal with infrastructure / coordination concerns:

public class CustomerDomainService : IDomainService
{
    private readonly Func<int> _customerIdGenerator;
    private readonly Dictionary<Type, Func<ICommand, IEnumerable<IEvent>>> _commandHandlers;
    private readonly Dictionary<int, List<IEvent>> _dataBase;
    private readonly IEventChannel _eventsChannel;
    private readonly ICompanyIntelligenceService _companyIntelligenceService;

    public CustomerDomainService(ICompanyIntelligenceService companyIntelligenceService, IEventChannel eventsChannel)
    {
        // mock database.
        var id = 1;
        _customerIdGenerator = () => id++;
        _dataBase = new Dictionary<int, List<IEvent>>(); 

        // external services and infrastructure.
        _companyIntelligenceService = companyIntelligenceService;
        _eventsChannel = eventsChannel;

        // command handler wiring.
        _commandHandlers = new Dictionary<Type,Func<ICommand,IEnumerable<IEvent>>>();
        SetHandlerFor<RegisterNewCustomerCommand>(cmd => HandleCommandFor(-1,
            (id, cust) => cust.Register(id, cmd, ReportFor(cmd.WorksForCompanyId))));
        SetHandlerFor<ChangeCustomerEmail>(cmd => HandleCommandFor(cmd.CustomerId, 
            (id, cust) => cust.ChangeEmail(cmd.NewEmail)));
        SetHandlerFor<ChangeCustomerCompany>(cmd => HandleCommandFor(cmd.CustomerId,
            (id, cust) => cust.ChangeCompany(cmd.NewCompanyId, ReportFor(cmd.NewCompanyId))));
    }
    public void PerformCommand(ICommand cmd)
    {
        var commandHandler = _commandHandlers[cmd.GetType()]; 
        var resultingEvents = commandHandler(cmd);
        foreach (var evt in resultingEvents)
            _eventsChannel.Publish(evt);
    }
    private IEnumerable<IEvent> HandleCommandFor(int customerId, Func<int, Customer, IEnumerable<IEvent>> handler)
    {
        if (customerId <= 0)
            customerId = _customerIdGenerator();
        var events = handler(LoadCustomer(customerId));
        SaveCustomer(customerId, events);
        return events;
    }
    private void SetHandlerFor<TCommand>(Func<TCommand, IEnumerable<IEvent>> handler)
    {
        _commandHandlers[typeof(TCommand)] = cmd => handler((TCommand)cmd);
    }
    private CompanyIntelligenceReport ReportFor(int companyId)
    {
        return _companyIntelligenceService.GetIntelligenceReport(companyId);
    }
    private Customer LoadCustomer(int customerId)
    { 
        var currentHistoricalEvents = new List<IEvent>();
        _dataBase.TryGetValue(customerId, out currentHistoricalEvents);
        return new Customer(currentHistoricalEvents);
    }
    private void SaveCustomer(int customerId, IEnumerable<IEvent> newEvents)
    {
        List<IEvent> currentEventHistory;
        if (!_dataBase.TryGetValue(customerId, out currentEventHistory))
            _dataBase[customerId] = currentEventHistory = new List<IEvent>();
        currentEventHistory.AddRange(newEvents);
    }
}

And then that allows you to really focus on the required behavior, business rules and decisions for the Customer class, maintaining only the state needed to perform decisions.

internal class Customer
{
    private int _id; 
    private bool _isRegistered;
    private bool _isPremium;
    private bool _canOrderProducts;

    public Customer(IEnumerable<IEvent> eventHistory)
    {
        foreach (var evt in eventHistory)
            ApplyEvent(evt);
    }

    public IEnumerable<IEvent> Register(int id, RegisterNewCustomerCommand cmd, CompanyIntelligenceReport report)
    {
        if (report.IsBankrupt)
            yield return ApplyEvent(new CustomerRegistrationWasRefused(cmd.RegistrationId, "Customer's company is bankrupt"));
        var isPremium = IsPremiumCompany(report);
        yield return ApplyEvent(new NewCustomerWasRegistered(cmd.RegistrationId, id, isPremium, cmd.FirstName, cmd.LastName, cmd.Email, cmd.WorksForCompanyID));
    }
    public IEnumerable<IEvent> ChangeEmail(string newEmailAddress)
    {
        EnsureIsRegistered("change email");
        yield return ApplyEvent(new CustomerEmailWasChanged(_id, newEmailAddress));
    }
    public IEnumerable<IEvent> ChangeCompany(int newCompanyId, CompanyIntelligenceReport report)
    {
        EnsureIsRegistered("change company");
        var isPremiumCompany = IsPremiumCompany(report);
        if (!_isPremium && isPremiumCompany)
            yield return ApplyEvent(new CustomerWasAwardedPremiumStatus(_id));
        else 
        {
            if (_isPremium && !isPremiumCompany)
                yield return ApplyEvent(new CustomerPremiumStatusRevoked(_id, "Customer changed workplace to a non-premium company"));
            if (report.IsBankrupt)
                yield return ApplyEvent(new CustomerLostBuyingCapability(_id, "Customer changed workplace to a bankrupt company"));
        }
    }
    // ... handlers for other commands
    private bool IsPremiumCompany(CompanyIntelligenceReport report)
    {
        return !report.IsBankrupt && 
            (report.AccumulatedSales > 1000000 || report.LastQuarterSales > 10000);
    }
    private void EnsureIsRegistered(string forAction)
    {
        if (_isRegistered)
            throw new DomainException(string.Format("Cannot {0} for an unregistered customer", forAction));
    }
    private IEvent ApplyEvent(IEvent evt)
    {
        // build up only the status needed to take domain/business decisions.
        // instead of if/then/else, event hander wiring could be used.
        if (evt is NewCustomerWasRegistered)
        {
            _isPremium = evt.IsPremiumCustomer;
            _isRegistered = true;
            _canOrderProducts = true;
        }
        else if (evt is CustomerRegistrationWasRefused)
            _isRegistered = false;
        else if (evt is CustomerWasAwardedPremiumStatus)
            _isPremium = true;
        else if (evt is CustomerPremiumStatusRevoked)
            _isPremium = false;
        else if (evt is CustomerLostBuyingCapability)
            _canOrderProducts = false;
        return evt;
    }
}

An added benefit is that the Customer class in this case is completely isolated from any infrastructure concerns can be easily tested for correct behavior and the customer domain module can be easily changed or extended to accommodate new requirements without breaking existing clients.

于 2013-07-14T08:20:33.823 回答
0

是的....如果创建具有这4个属性的客户是有效的....理想情况下,您将拥有一个具有这4个属性的构造函数。这样,​​创建责任与客户对象一起存在,而客户服务不需要知道关于它,它只处理“客户”。

于 2013-07-13T23:16:39.860 回答
0

如何使用生成器模式导致代码有点像这样:

var customer = new CustomerBuilder()
                     .firstName("John")
                     .lastName("Doe")
                     .email("john.doe@example.com")
                     .companyId(6)
                     .createCustomer();

customerService.AddCustomer(customer);

然后,您可以让您的构建器类在调用 createCustomer 时处理查找公司对象,并且参数的顺序不再重要,您可以方便地放置逻辑以选择合理的默认值。

这也为您提供了一个方便的验证逻辑位置,因此您一开始就无法获得无效的 Customer 实例。

或者另一种可能的方法是让 AddCustomer 返回一个命令对象,以便您的客户端代码可以执行此操作:

customerService.AddCustomer()
    .firstName("John")
    .lastName("Doe")
    .email("john.doe@example.com")
    .companyId(6)
    .execute();
于 2013-07-13T23:45:01.363 回答