Architectures - Domain Driven Design vs strict business logic enforcement
My business objects are coded with the following architecture:
- validation of any incoming data throws an exception in the setter if it doesn't fit business logic.
- property can not be corrupt/inconsistent state unless the existing default/null is invalid
- business objects can only be created by the business module via a static factory type method that accepts an interface implementation that is shared with the business object for copying into the business object.
- Enforces that the dependency container, ui, and persistence layers can not create an invalid Model object or pass it anywhere.
- This factory method catches all the different validation exceptions in a validation dictionary so that when the validation attempts are complete, the dictionary the caller provided is filled with field names and messages, and an exception is thrown if any of the validations did not pass.
- easily maps back to UI fields with appropriate error messages
- No database/persistence type methods are on the business objects
- needed persistence behaviors are defined via repository interfaces in the business module
Sample Business object interface:
public interface IAmARegistration
{
string Nbk { get; set; } //Primary key?
string Name { get; set; }
string Email { get; set; }
string MailCode { get; set; }
string TelephoneNumber { get; set; }
int? OrganizationId { get; set; }
int? OrganizationSponsorId { get; set; }
}
business object repository interface:
/// <summary>
/// Handles registration persistance or an in-memory repository for testing
/// requires a business object instead of interface type to enforce validation
/// </summary>
public interface IAmARegistrationRepository
{
/// <summary>
/// Checks if a registration record exists in the persistance mechanism
/// </summary>
/// <param name="user">Takes a bare NBK</param>
/// <returns></returns>
bool IsRegistered(string user); //Cache the result if so
/// <summary>
/// Returns null if none exist
/// </summary>
/// <param name="user">Takes a bare NBK</param>
/// <returns></returns>
IAmARegistration GetRegistration(string user);
void EditRegistration(string user,ModelRegistration registration);
void CreateRegistration(ModelRegistration registration);
}
Then an 开发者_高级运维actual business object looks as follows:
public class ModelRegistration : IAmARegistration//,IDataErrorInfo
{
internal ModelRegistration() { }
public string Nbk
{
get
{
return _nbk;
}
set
{
if (String.IsNullOrEmpty(value))
throw new ArgumentException("Nbk is required");
_nbk = value;
}
}
... //other properties omitted
public static ModelRegistration CreateModelAssessment(IValidationDictionary validation, IAmARegistration source)
{
var result = CopyData(() => new ModelRegistration(), source, false, null);
//Any other complex validation goes here
return result;
}
/// <summary>
/// This is validated in a unit test to ensure accuracy and that it is not out of sync with
/// the number of members the interface has
/// </summary>
public static Dictionary<string, Action> GenerateActionDictionary<T>(T dest, IAmARegistration source, bool includeIdentifier)
where T : IAmARegistration
{
var result = new Dictionary<string, Action>
{
{Member.Name<IAmARegistration>(x=>x.Email),
()=>dest.Email=source.Email},
{Member.Name<IAmARegistration>(x=>x.MailCode),
()=>dest.MailCode=source.MailCode},
{Member.Name<IAmARegistration>(x=>x.Name),
()=>dest.Name=source.Name},
{Member.Name<IAmARegistration>(x=>x.Nbk),
()=>dest.Nbk=source.Nbk},
{Member.Name<IAmARegistration>(x=>x.OrganizationId),
()=>dest.OrganizationId=source.OrganizationId},
{Member.Name<IAmARegistration>(x=>x.OrganizationSponsorId),
()=>dest.OrganizationSponsorId=source.OrganizationSponsorId},
{Member.Name<IAmARegistration>(x=>x.TelephoneNumber),
()=>dest.TelephoneNumber=source.TelephoneNumber},
};
return result;
}
/// <summary>
/// Designed for copying the model to the db persistence object or ui display object
/// </summary>
public static T CopyData<T>(Func<T> creator, IAmARegistration source, bool includeIdentifier,
ICollection<string> excludeList) where T : IAmARegistration
{
return CopyDictionary<T, IAmARegistration>.CopyData(
GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
}
/// <summary>
/// Designed for copying the ui to the model
/// </summary>
public static T CopyData<T>(IValidationDictionary validation, Func<T> creator,
IAmARegistration source, bool includeIdentifier, ICollection<string> excludeList)
where T : IAmARegistration
{
return CopyDictionary<T, IAmARegistration>.CopyData(
GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
}
Sample repository method that I'm having trouble writing isolated tests for:
public void CreateRegistration(ModelRegistration registration)
{
var dbRegistration = ModelRegistration.CopyData(()=>new Registration(), registration, false, null);
using (var dc=new LQDev202DataContext())
{
dc.Registrations.InsertOnSubmit(dbRegistration);
dc.SubmitChanges();
}
}
Issues:
- When a new member is added there are a minimum of 8 places a change must be made (db, linq-to-sql designer, model Interface, model property, model copy dictionary, ui, ui DTO, unit test
- Testability
- testing the db methods that are hard coded to depend on an exact type that has no public default constructor, and needs to pass through another method, makes testing in isolation either impossible, or will need to intrude on the business object to make it more testable.
- Using InternalsVisibleTo so that the BusinessModel.Tests has access to the internal contructor, but I would need to add that for any other persistence layer testing module, making it scale very poorly
- to make the copy functionality generic the business objects were required to have public setters
- I'd prefer if the model objects were immutable
- DTOs are required for the UI to attempt any data validation
I'm shooting for complete reusability of this business layer with other persistence mechanisms and ui mechanisms (windows forms, asp.net, asp.net mvc 1, etc...). Also that team members can develop against this business layer/architecture with a minimum of difficulty.
Is there a way to enforce immutable validated model objects, or enforce that neither the ui or persistance layer can get a hold of an invalid one without these headaches?
This looks very complicated to me.
IMO, Domain Objects should be POCOs that protect their invariants. You don't need a factory to do that: simply demand any necessary values in the constructor and provide valid defaults for the rest.
Whereever you have property setters, protect them by calling a validation method like you already do. In short, you must never allow an instance to be in an incosistent state - factory or no factory.
Writing an immutable object in C# is easy - just make sure that all fields are declared with the readonly
keyword.
However, be aware that if you follow Domain-Driven Design, Domain objects tend to fall in three buckets
- Entities. Mutable objects with long-lived identities
- Value Objects. Immutable objects without identity
- Services. Stateless
According to this defintion, only Value Objects should be immutable.
I have followed a different approach in the past. Rather than protecting property setters with validation exceptions, I adopt the convention that A) all domain objects provide a way to validate themselves on-demand (e.g. a Validate method), and B) repositories assert the validation of all objects on which persistence operations are requested (e.g. by invoking the Validate method and throwing an exception if it fails). This means that to use a repository is to trust that the repository upholds this convention.
The advantage of this, in the .NET world at least, is an easier-to-use domain surface and more succinct and readable code (no huge try-catch blocks or roundabout exception handling mechanisms). This also integrates well with ASP.NET MVC validation approach.
I will say that in non-property operations exposed by domain objects (such as those implied by the Law of Demeter over composed collections), I've tended to return a ValidationResult that provides immediate validation feedback. This feedback is also returned by the Validate methods. The reason I do this is that there is no detrimental impact to the structure or readability of consuming code. If the caller doesn't want to inspect the return value, they don't have to; if they do want to, they can. Such a benefit is not possible with property setters without also impacting the structure of the consuming code (with try-catch blocks). Likewise, domain services return ValidationResults. Only repositories throw ValidationExceptions.
Overall, this does allow you to put domain objects into invalid states, but they are isolated to the caller's execution context (the caller's "workspace") and cannot be persisted to the system at large.
精彩评论