Мои бизнес-объекты имеют следующую архитектуру:
- проверка любых входящих данных вызывает исключение в установщике, если оно не соответствует бизнес-логике.
- свойство не может быть повреждено / несовместимо, если существующее значение по умолчанию / null недопустимо
- бизнес-объекты могут создаваться бизнес-модулем только через метод статического фабричного типа, который принимает реализацию интерфейса, которая используется совместно с бизнес-объектом, для копирования в бизнес-объект.
- Обеспечивает, чтобы контейнеры зависимостей, пользовательский интерфейс и персистентные слои не могли создавать недопустимый объект Model или передавать его куда-либо.
- Этот фабричный метод перехватывает все различные исключения проверки в словаре проверки, так что, когда попытки проверки завершены, словарь, предоставленный вызывающей стороной, заполняется именами полей и сообщениями, и возникает исключение, если какая-либо из проверок не произвела проходить.
- легко сопоставляется с полями пользовательского интерфейса с соответствующими сообщениями об ошибках
- В бизнес-объектах нет методов типа базы данных / персистентности
- необходимые поведения персистентности определяются через интерфейсы репозитория в бизнес-модуле
Пример интерфейса бизнес-объекта:
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; }
}
интерфейс хранилища бизнес-объектов:
/// <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);
}
Тогда фактический бизнес-объект выглядит следующим образом:
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);
}
Пример репозитория, с которым у меня возникают проблемы при написании изолированных тестов:
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();
}
}
Вопросы:
- Когда добавляется новый член, необходимо внести как минимум 8 мест (дБ, конструктор linq-to-sql, интерфейс модели, свойство модели, словарь копирования модели, пользовательский интерфейс, пользовательский интерфейс DTO, модульный тест
- Тестируемость
- тестирование методов БД, которые жестко запрограммированы на зависимость от точного типа, который не имеет общедоступного конструктора по умолчанию и должен проходить через другой метод, что делает тестирование изолированным либо невозможным, либо потребуется вмешательство в бизнес-объект для создания это более проверяемое.
- Использование InternalsVisibleTo, чтобы у BusinessModel.Tests был доступ к внутреннему конструктору, но мне нужно было бы добавить это для любого другого модуля тестирования слоя постоянства, что делает его очень плохо масштабируемым
- , чтобы сделать функцию копирования общей, бизнес-объектам требовались публичные сеттеры
- Я бы предпочел, чтобы объекты модели были неизменными
- DTO требуются для того, чтобы пользовательский интерфейс предпринял попытку проверки данных
Я стремлюсь к полному повторному использованию этого бизнес-уровня с другими механизмами персистентности и механизмами пользовательского интерфейса (windows forms, asp.net, asp.net mvc 1 и т. Д.). Кроме того, члены команды могут развиваться в соответствии с этим бизнес-уровнем / архитектурой с минимальными трудностями.
Есть ли способ применить неизменные проверенные объекты модели или обеспечить, чтобы ни пользовательский интерфейс, ни уровень персистентности не могли удержать недопустимый уровень без этих головных болей?