“Getter/Setter中的数值验证”是一种好的编程风格吗?

5
我的Getter/Setter方法会在设置/返回值之前检查该值。当值无效时,它们会抛出异常(BadArgumentException或IllegalStateException)。这是必要的,因为我们使用无效值初始化所有成员 - 这样我们就避免了使用这些无效值(==在其他地方出现错误/段错误/异常)。
好处包括:
  • 从模型接收成员值时,您知道它们是有效的
  • 有效性检查仅在模型对象中执行
  • 值范围由模型对象定义
这似乎非常不寻常,因为大多数新团队成员首先对此表示抱怨 - 即使我向他们解释后,他们也会同意。
问题:这是一种良好的编程风格吗?(即使浪费了一些性能) 示例代码:
inline bool MyClass::HasGroupID const { return m_iGroupID != 0; }

int MyClass::GetGroupID() const
{
    if( !HasGroupID() )
        throw EPTIllegalStateException( "Cannot access uninitialized group ID!" );

    return m_iGroupID;

}   // END GetGroupID() const

void MyClass::SetGroupID( int iGroupID )
{
    // negative IDs are allowed!
    if( iGroupID != 0 )
        throw EPTBadArgumentException( "GroupID must not be zero!", iGroupID );

    m_iGroupID = iGroupID;

}   // END SetGroupID( int )

使用方法:

// in serialization
if( myObject.HasGroupID() )
    rStream.writeAttribute( "groupid", GetGroupID() );

// in de-serialization
try {
    // required attribute - throw exception if value is invalid
    myObject.SetGroupID( rStream.GetIntegerAttribute( "groupid" );
} catch( EPTBadArgumentException& rException )
{
    throw EPTBadXmlAttribute( fileName, rException );
}

这是大多数情况下非常好的编程风格。我强烈推荐使用。如果您与数据库模型等工作,这也是很好的选择。 - galchen
我认为在程序中到处都有无效对象并不是一个好的编程风格。最好将事物初始化为有效值。 - R. Martinho Fernandes
但是,如果您使用有效值初始化它们并忘记Setter()(例如,在克隆方法中添加新成员后,在读入期间或在复制构造函数、赋值方法等中),则会非常晚地提到这一点(大多数情况下您不会知道错误来自哪里)。因此,您将收到异常,并且大多数情况下可以很好地重现... - Charly
4个回答

5
我认为这是一种常见的风格,肯定是 getter/setter 最初想法中的核心动机之一。然而,在你的特定示例中,我认为它们没有意义,并且通常有更好的替代方案:

如果调用 GetGroupIDHasGroupID 返回 false 时引发异常,则这是程序员错误 - 因此您应该使用 assert。实际上,为什么会出现没有组 ID 的对象?这听起来像是一个稍微不太优雅的设计。

此外,如果只有非零 ID 是有效的,则应尽早使用专用类型强制执行此限制。这样,处理组 ID 的所有代码都可以确保 ID 是有效的(事实上,编译器强制执行此操作!),并且您不可能忘记在某个地方进行检查。请考虑以下内容:

class GroupID {
public:
    explicit GroupID( int id ) : m_id( id ) {
        if ( m_id == 0 ) {
            throw EPTBadArgumentException( "GroupID must not be zero!", m_id );
        }
    }

    // You may want to allow implicit conversion or rather use an explicit getter
    operator int() const { return m_id; }
};

使用这个,你只需要写
void MyClass::SetGroupID( GroupID id )
{
    // No error checking needed; we *know* it's valid, because it's a GroupID!
    // We can also use a less verbose variable name without losing readability
    // because the type name reveals the semantics.
    m_iGroupID = id;

} // END SetGroupID( int )

事实上,由于组ID现在是专门的类型,您可以将函数重命名为Set并使用重载。因此,您有MyClass::Set(UserID)MyClass::Set(GroupID)等。


感谢您的回答。以下是一些注释:(1)在我的团队中,我们禁止使用断言,因为我们想用异常来处理程序员错误。(2)group-ID是可选元素,因此它也可以不存在(请参见HasGroupID())。 (3)我们将所有成员初始化为无效状态,以避免访问未初始化的成员(--> 异常)。 - Charly
创建一个非虚拟类GroupID并使用内联方法的方法非常好(性能,内存占用),我经常使用它,但我不能为每个参数创建一个新类! - Charly
2
@Charly:使用异常处理编程错误听起来很像“我们不解决错误,而是把它们藏起来,希望它们不会爬出来”。断言和异常的区别在于异常可以被捕获并忽略,但是编程错误永远不应该被忽略。你应该重新考虑使用断言的方式。 - David Rodríguez - dribeas
1
@Charly:对于可选元素,请考虑使用预打包的optional工具,例如boost::optional,这将消除大量样板代码并使维护更简单。几个月后,您是否会记得每个参数的值意味着“不存在”?您是否总是有这样的价值?(注意:boost::optional具有更大的内存和运行时成本,但在语义上更明确,因此更易于维护) - David Rodríguez - dribeas

1

本质上,这是getter/setter思想最重要的目的之一。仅仅封装本身并不舒适——编写直接访问成员变量的简单代码更容易。但通过封装,您可以限制对变量的访问:检查值的合理性,通过互斥体同步访问,将更改绑定到触发器,记录它们等等。

通常,在最基本的情况下,这些都是不必要的,但在以后的时间里,这些东西会突然出现很多,如果您一直在摆弄类成员,那么需要修复的代码就会很多。如果您从一开始就在所有地方使用简单的getter/setter,现在您可以添加任何需要的内容。


如果你从一开始就使用getter和setter,但是现在才添加抛出验证检查异常的功能,那么你正在违反你以前的契约。已经存在的代码现在可能已经失效了。它仍然可以编译并不表示这是一件好事。 - R. Martinho Fernandes
@R. Martinho Fernandes:如果一直有记录(例如),即 Set 必须只调用有效值,而在 Set 之前不能调用 Get,那么在始终禁止的情况下添加异常不会破坏先前的合同。因此,这部分是一个问题,即是否一开始设计了这些无效值的合同。如果您想随后过来并决定以前有效(但无用)的值现在已失效,那就更难了。 - Steve Jessop

0
关于验证:Setter 验证类不变量是否被遵守是非常普遍的(实际上,这是它们相对于公共值的主要价值所在)。Getter 中很少见到这种情况(懒惰验证会导致调试困难),但在 Debug 构建中重新验证可能很有趣,以便更好地捕获错误。
然而,这并不是你在这里做的事情。你的类不变量明显允许可选元素(如 GroupId),但你禁止用户将此 GroupId 设置为无效状态... 为什么?这是不规则的。
关于 Getter,有一个简单的解决方案:Optional 类型(如 Boost.Optional)。这样,你总是返回而不抛出异常,同时可以提供一个值...或者不提供。

0
我同意你的新团队成员的观点。对我来说这不是很常见,但如果你必须这样做并且这是唯一的方法,那么你应该去做。
我会尝试创建填充有一致数据的对象。
你可以通过在构造函数中检查值并不提供setter来实现这一点。
首先,我为写C#而道歉,因为我的C++有点生疏 :-)
class MyClass{

    private int groupId; 

    public MyClass(int groupId) {
        if(groupId ==0){
            throw new ArgumentOutOfRangeException("groupId",groupId,"Group Id must be >0");
        }
        this.groupId = groupId;
    }

    public int GroupId{ 
        get{ 
             return groupId;
        }
    }
}

现在,您可以确信只有在所有属性都符合要求时才会初始化 MyClass 类。您可以实现一个构造函数,它接受一个 MyClass 实例并克隆所有值以创建克隆对象。
如果您需要更改属性,请创建一个接口并定义一个 MyClassChangeable。
public class Programm {
    static void main(string[] args) {
        IMyClass myClassChangable = new MyClassChangable(-123);
        try {
            MyClass correctMyClass = new MyClass(myClassChangable);
            useThisClass(correctMyClass);

        } catch (Exception ex) {
            //Display to user or something
            Debug.WriteLine(ex);
        }
    }

    static void useThisClass(MyClass myClass) {
        //this is alway correct!
        int groupId = myClass.GroupId;

    }
}

interface IMyClass {
    int GroupId { get; }
}

class MyClass : IMyClass {
    private int groupId;

    public MyClass(int groupId) {
        if (groupId == 0) {
            throw new ArgumentOutOfRangeException("groupId", groupId, "Group Id must be >0");
        }
        this.groupId = groupId;
    }

    public MyClass(IMyClass instance)
        : this(instance.GroupId) {
    }

    #region Implementation of IMyClass

    public int GroupId {
        get { return groupId; }
    }

    #endregion
}


public class MyClassChangable : IMyClass {
    private int groupId = 0;

    public MyClassChangable() {

    }

    public MyClassChangable(int groupId) {
        this.groupId = groupId;
    }

    #region Implementation of IMyClass

    public int GroupId {
        get { return groupId; }
        set { groupId = value; }
    }

    #endregion
}

现在您可以按照自己的需求使用一个类,然后只需在一个点上“检查”它(只有一个异常点)。在“BusinessCore”中,您正在使用具有始终正确值的MyClass。因此,没有异常和越界!
希望我能够帮助到您,请随时联系我以获取更多问题或如果有任何错误,我很乐意学习!
我很享受对我的想法的反馈!


我同意 - 但如果GroupID是可选的呢?因此,有些有效的对象在HasGroupID()上返回false。 - Charly
如果您的类有两个或更多成员,则您的观点将更加明显。因此,更改一个成员,您将把该类复制到MyClassChangeable中,然后再将其复制回MyClass中?第二个副本将进行评估...所有这些都是为了更改一个参数吗?因为对于最初设置成员,您不需要MyClassChangeable。 - Charly
如果一个参数是可选的:我认为你应该为这个参数创建一个新的类/接口,这样你就可以确保这个参数被正确设置。例如:类“汽车”有属性“引擎”,如果“引擎”== null,则方法“启动()”将失败。因为你不知道哪些参数必须被设置,任何其他方法也可能失败。所以“汽车”类的使用者必须检查每个属性是否设置正确。这会导致消费者代码中的许多检查。解决方案是一个没有属性“引擎”和没有方法“启动”的“汽车主体”类,以及从“汽车主体”继承的“汽车”类。 - oberfreak
消费者只接受类型为"Car"的实例,因此可以确保方法"start"不会因为缺少属性而抛出异常。 - oberfreak
问题在于,您是否必须更改BusinessCore中此实例的参数,为什么不创建MyClass的新实例。在您的方法中更改实例的属性,也会更改每个其他线程或甚至进程中的属性。例如:该实例可以用于“for”循环,并突然被您的方法更改计数。从我的角度来看:我不希望方法通过在引用上隐式更改属性而更改属性。如果方法必须更改某个属性,则可以返回具有更改属性的新实例。 - oberfreak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接