Некоторые уточнения. Эти классы данных устанавливают один уровень выше типов примитивов, ограничивая данные, чтобы сделать их осмысленными.
На самом деле они располагаются чуть выше классов RegexConstrainedString
и ConstrainedNumber<T>
, и именно здесь мы говорим о рефакторинге кода проверки конструктора в отдельный метод.
Проблема с рефакторингом кода валидации заключается в том, что Regex, необходимый для валидации, существует только в подклассах RegexConstrainedString
, поскольку каждый подкласс имеет свой Regex
. Это означает, что данные проверки доступны только конструктору RegexConstrainedString
, а не его методам. Поэтому, если я исключу код проверки, абонентам потребуется доступ к Regex
.
public class Password: RegexConstrainedString
{
internal static readonly Regex regex = CreateRegex_CS_SL_EC( @"^[\w!""#\$%&'\(\)\*\+,-\./:;<=>\?@\[\\\]\^_`{}~]{3,20}$" );
public Password( string value ): base( value.TrimEnd(), regex ) {} //length enforced by regex, so no min/max values specified
public Password( Password original ): base( original ) {}
public static explicit operator Password( string value ) {return new Password( value );}
}
Таким образом, при чтении значения из базы данных или чтении пользовательского ввода конструктор Password перенаправляет Regex
в базовый класс для обработки проверки. Другой трюк заключается в том, что он автоматически обрезает конечные символы в случае, если тип базы данных - char, а не varchar, поэтому мне не нужно это делать. В любом случае, вот как выглядит главный конструктор для RegexConstrainedString:
protected RegexConstrainedString( string value, Regex subclasses_static_regex, int? min_length, int? max_length )
{
_value = (value ?? String.Empty);
if (min_length != null)
if (_value.Length < min_length)
throw new Exception( "Value doesn't meet minimum length of " + min_length + " characters." );
if (max_length != null)
if (_value.Length > max_length)
throw new Exception( "Value exceeds maximum length of " + max_length + " characters." );
value_match = subclasses_static_regex.Match( _value ); //Match.Synchronized( subclasses_static_regex.Match( _value ) );
if (!value_match.Success)
throw new Exception( "Invalid value specified (" + _value + "). \nValue must match regex:" + subclasses_static_regex.ToString() );
}
Поскольку вызывающим пользователям потребуется доступ к Regex
подкласса, я думаю, что лучше всего внедрить в подклассе метод IsValueValid
, который перенаправляет данные в метод IsValueValid
в базовом классе RegexConstrainedString
. Другими словами, я бы добавил эту строку в класс Password:
public static bool IsValueValid( string value ) {return IsValueValid( value.TrimEnd(), regex, min_length, max_length );}
Однако мне это не нравится, потому что я копирую код конструктора подклассов, и мне нужно помнить, что нужно снова обрезать строку и передавать ту же минимальную / максимальную длину, когда это необходимо. Это требование будет навязано всем подклассам RegexConstrainedString, и я не хочу этого делать. Такие классы данных, как Password, очень просты, потому что RegexConstrainedString выполняет большую часть работы, реализуя операторы, сравнения, клонирование и т. Д.
Кроме того, существуют другие сложности с выделением кода. Проверка включает запуск и сохранение соответствия Regex в экземпляре, поскольку некоторые типы данных могут иметь свойства, которые сообщают о конкретных элементах строки. Например, мой класс SessionID содержит свойства, такие как TimeStamp, которые возвращают сопоставленную группу из Match, хранящегося в экземпляре класса данных. Суть в том, что этот статический метод является совершенно другим контекстом. Поскольку он по существу несовместим с контекстом конструктора, конструктор не может его использовать, поэтому я бы в итоге повторил код еще раз.
Итак ... Я мог бы выделить код валидации, реплицировав его и настроив его для статического контекста и наложив требования на подклассы, или я мог бы упростить задачу и просто выполнить конструирование объекта. Относительная дополнительная выделенная память будет минимальной, так как в экземпляре хранятся только строка и ссылка Match. Все остальное, такое как Match и сама строка, все равно будет сгенерировано проверкой, поэтому нет никакого пути к этому. Я мог бы беспокоиться о производительности весь день, но мой опыт показывает, что правильность важнее, потому что правильность часто приводит к множеству других оптимизаций. Например, мне никогда не нужно беспокоиться о неправильно отформатированных или измеренных данных, проходящих через мое приложение, потому что используются только значимые типы данных, что вынуждает проверять точку входа в приложение с других уровней, будь то база данных или пользовательский интерфейс. 99% моего кода проверки были удалены как ненужный артефакт, и в настоящее время я проверяю только нулевые значения. Кстати, достигнув этой точки, я теперь понимаю, почему включение нулей было ошибкой в миллиард долларов . Кажется, это единственное, что я должен проверять больше, даже если они по существу не существуют в моей системе. Сложные объекты, имеющие эти типы данных в виде полей, не могут быть построены с пустыми значениями, но я должен обеспечить это в установщиках свойств, что раздражает, потому что в противном случае им никогда бы не понадобился код проверки ... только код, который выполняется в ответ на изменения значений .
UPDATE:
Я смоделировал вызовы функций CLR в обоих направлениях и обнаружил, что, когда все данные верны, разница в производительности составляет лишь доли миллисекунды на тысячу вызовов, что незначительно. Однако, когда примерно половина паролей недействительна, исключая исключения в версии «создания экземпляров», это на три порядка медленнее, что соответствует примерно 1 дополнительной секунде на 1000 вызовов. Разница в размерах, конечно, будет многократной, поскольку для нескольких столбцов в таблице выполняется несколько вызовов CLR, но для моего проекта это в 3-5 раз. Итак, приемлемы ли для меня дополнительные 3–5 секунд на 1000 обновлений в качестве компромисса за простоту и чистоту моего кода? Ну, это зависит от частоты обновления. Если бы мое приложение получало 1000 обновлений в секунду, задержка в 3 - 5 секунд была бы разрушительной. Если, с другой стороны, я получаю 1000 обновлений в минуту или час, это может быть вполне приемлемо. В моей ситуации я могу сказать вам, что это вполне приемлемо, поэтому я думаю, что я просто пойду с метод создания экземпляров и разрешить ошибки до конца. Конечно, в этом тесте я обрабатывал ошибки в CLR, а не позволял SQL Server их обрабатывать. Распространение информации об ошибках в SQL Server, а затем, возможно, обратно в приложение, может значительно замедлить процесс. Я предполагаю, что мне придется полностью реализовать это, чтобы получить реальный тест, но из этого предварительного теста, я почти уверен, что результаты будут.