You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DRY原则抉择:继承类长度校验放基类还是子类?代码重构咨询

Let's break down your questions one by one, with practical, developer-focused insights:

1. Will someone reading only the Email class know length validation happens in the base class?

Short answer: Nope, not without extra clues. Right now, the Email class just passes a Length constant to the base constructor, but there's no obvious hint that this triggers a length check. A developer who doesn't peek at the RequiredString code might assume Length is just some arbitrary value being passed up, not a validation rule. They'd only figure out the length check exists if they hit a TextLengthTooLongException or dig into the base class.

2. Is the current design reasonable, or should each subclass implement length validation?

This design is absolutely reasonable—it's a perfect application of the DRY principle. If you duplicated length validation code across dozens of subclasses, you'd create a maintenance nightmare: imagine having to update the error message format or fix an edge case (like trimming whitespace before checking length) in every single class. That's a recipe for bugs and wasted time.

The real issue here isn't the DRY approach itself—it's that the validation logic is too hidden from someone reading only the subclass. We just need to fix that transparency problem without giving up the benefits of code reuse.

3. How to refactor for better simplicity and clarity?

Here are a few actionable tweaks to make your code cleaner and more self-documenting:

a. Rename constants for instant clarity

Swap the vague Length constant in subclasses for MaxLength—it immediately tells anyone reading the code what that value is for:

public sealed class Email : RequiredString { 
    private const int MaxLength = 255; 
    private const string FieldName = "Email"; // More explicit than just "Name"
    public Email(string text) : base(FieldName, text, MaxLength) { 
        try { 
            new MailAddress(_text); 
        } catch (FormatException ex) { 
            throw new EmailFormatException($"'{_text}' is not a valid email address", ex); 
        } 
    }
    public static implicit operator Email(string text) { 
        return new Email(text); 
    } 
}

Now, base(FieldName, text, MaxLength) makes it obvious we're enforcing a maximum length rule, even without checking the base class.

b. Add XML docs to the base class

Document the base class constructors so IDE tooltips can tell developers what validation is happening, no digging required:

/// <summary>
/// Base class for required string fields with built-in validation.
/// </summary>
abstract class RequiredString { 
    protected readonly string _text; 

    /// <summary>
    /// Creates a required string field, validating it's not empty or whitespace.
    /// </summary>
    /// <param name="fieldName">Name of the field to use in error messages.</param>
    /// <param name="text">The string value to validate and store.</param>
    /// <exception cref="TextEmptyException">Thrown if text is empty or only whitespace.</exception>
    public RequiredString(string fieldName, string text) { 
        if (string.IsNullOrWhiteSpace(text)) throw new TextEmptyException($"The '{fieldName}' field is required"); 
        _text = text; 
    } 

    /// <summary>
    /// Creates a required string field with a maximum length constraint.
    /// </summary>
    /// <param name="fieldName">Name of the field to use in error messages.</param>
    /// <param name="text">The string value to validate and store.</param>
    /// <param name="maxLength">Maximum allowed length for the text.</param>
    /// <exception cref="TextEmptyException">Thrown if text is empty or only whitespace.</exception>
    /// <exception cref="TextLengthTooLongException">Thrown if text exceeds the maximum length.</exception>
    public RequiredString(string fieldName, string text, int maxLength): this(fieldName, text) { 
        if (_text.Length > maxLength) throw new TextLengthTooLongException($"The '{fieldName}' field length is too long ({text.Length}/{maxLength})"); 
    } 

    public static implicit operator string(RequiredString field) { 
        return field._text; 
    } 
}

Now, when someone hovers over the base(...) call in Email, their IDE will pop up a tooltip explaining exactly what validation logic is running in the base class.

c. Eliminate repetitive implicit conversions (optional)

If every subclass uses the same implicit conversion pattern (converting a string to the subclass type), you can use a generic base class to avoid repeating this code everywhere. Here's how:

First, make the base class generic:

/// <summary>
/// Generic base class for required string fields with built-in validation.
/// </summary>
abstract class RequiredString<T> where T : RequiredString<T> { 
    protected readonly string _text; 

    protected RequiredString(string fieldName, string text) { 
        if (string.IsNullOrWhiteSpace(text)) throw new TextEmptyException($"The '{fieldName}' field is required"); 
        _text = text; 
    } 

    protected RequiredString(string fieldName, string text, int maxLength): this(fieldName, text) { 
        if (_text.Length > maxLength) throw new TextLengthTooLongException($"The '{fieldName}' field length is too long ({text.Length}/{maxLength})"); 
    } 

    public static implicit operator string(RequiredString<T> field) { 
        return field._text; 
    } 

    // Generic implicit conversion from string to the subclass type
    public static implicit operator T(string text) { 
        // Use reflection to create an instance of the subclass—note: tiny runtime cost here
        return (T)Activator.CreateInstance(typeof(T), text); 
    } 
}

Then, your Email class gets much simpler—no need to redefine the implicit conversion:

public sealed class Email : RequiredString<Email> { 
    private const int MaxLength = 255; 
    private const string FieldName = "Email"; 

    public Email(string text) : base(FieldName, text, MaxLength) { 
        try { 
            new MailAddress(_text); 
        } catch (FormatException ex) { 
            throw new EmailFormatException($"'{_text}' is not a valid email address", ex); 
        } 
    } 
    // Implicit conversion from string to Email is inherited!
}

This cuts down on boilerplate code across all your subclasses. Just keep in mind that Activator.CreateInstance has a minor performance hit—if you're working in a high-throughput system, you might stick with manual conversions, but for most apps, this is a great simplification.

内容的提问来源于stack exchange,提问作者kevinob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 03:59:42