Насколько я могу судить, никто не представляет достаточно очевидное решение, которое воплощает в себе лучшее как одноэтапного, так и двухэтапного построения.
примечание: Этот ответ предполагает использование C #, но принципы могут применяться в большинстве языков.
Во-первых, преимущества обоих:
One-Stage
Преимущество одноступенчатого построения состоит в том, что мы не допускаем существования объектов в недопустимом состоянии, тем самым предотвращая все виды ошибочного управления состоянием и все ошибки, которые с ним связаны. Однако некоторые из нас чувствуют себя странно, потому что мы не хотим, чтобы наши конструкторы генерировали исключения, и иногда это то, что нам нужно делать, когда аргументы инициализации недопустимы.
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
}
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
}
Двухэтапный метод проверки
Двухэтапное конструирование дает нам преимущество, позволяя выполнять нашу проверку вне конструктора, и, следовательно, исключает необходимость создания исключений внутри конструктора. Однако это оставляет нам «недопустимые» экземпляры, что означает, что мы должны отслеживать состояние и управлять им для экземпляра, или мы выбрасываем его сразу после выделения кучи. Возникает вопрос: почему мы выполняем выделение кучи и, следовательно, сбор памяти для объекта, который мы даже не используем?
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public void Validate()
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(Name));
}
if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
}
}
Одноступенчатый с помощью частного конструктора
Так как же мы можем исключить исключения из наших конструкторов и не позволить себе выполнять выделение кучи для объектов, которые будут немедленно отброшены? Это довольно просто: мы делаем конструктор частным и создаем экземпляры с помощью статического метода, предназначенного для выполнения экземпляров и, следовательно, выделения кучи, только после проверки.
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
private Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public static Person Create(
string name,
DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
return new Person(name, dateOfBirth);
}
}
Async Single-Stage с помощью частного конструктора
Помимо вышеупомянутых преимуществ валидации и предотвращения выделения кучи, предыдущая методология дает нам еще одно замечательное преимущество: асинхронная поддержка. Это удобно при работе с многоэтапной аутентификацией, например, когда вам нужно получить токен на предъявителя перед использованием вашего API. Таким образом, вы не получите недопустимый «выписанный» клиент API, и вместо этого вы можете просто заново создать клиент API, если вы получили ошибку авторизации при попытке выполнить запрос.
public class RestApiClient
{
public RestApiClient(HttpClient httpClient)
{
this.httpClient = new httpClient;
}
public async Task<RestApiClient> Create(string username, string password)
{
if (username == null)
{
throw new ArgumentNullException(nameof(username));
}
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
var basicAuthValue = Convert.ToBase64String(basicAuthBytes);
var authenticationHttpClient = new HttpClient
{
BaseUri = new Uri("https://auth.example.io"),
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
}
};
using (authenticationHttpClient)
{
var response = await httpClient.GetAsync("login");
var content = response.Content.ReadAsStringAsync();
var authToken = content;
var restApiHttpClient = new HttpClient
{
BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Bearer", authToken)
}
};
return new RestApiClient(restApiHttpClient);
}
}
}
Недостатки этого метода, по моему опыту, немногочисленны.
Как правило, использование этой методологии означает, что вы больше не можете использовать класс в качестве DTO, потому что десериализация объекта без публичного конструктора по умолчанию трудна в лучшем случае. Однако, если вы использовали объект в качестве DTO, вам не следует проверять сам объект, а нужно делать недействительными значения объекта, когда вы пытаетесь их использовать, поскольку технически значения не являются «недействительными» в отношении в DTO.
Это также означает, что вы в конечном итоге создадите фабричные методы или классы, когда вам понадобится разрешить контейнеру IOC создавать объект, поскольку в противном случае контейнер не будет знать, как создать экземпляр объекта. Однако во многих случаях фабричные методы оказываются одним из Create
самих методов.