【问题标题】:Is "Value Validation in Getter/Setter" good style?“Getter/Setter 中的值验证”是好的风格吗?
【发布时间】:2012-01-01 00:42:07
【问题描述】:

我的 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 );
}

【问题讨论】:

  • 在大多数情况下这是一种非常好的编程风格。我强烈推荐。如果您使用数据库模型等也很好
  • 我不认为到处都有无效的对象是一种好的风格。将事物初始化为有效值要好得多。
  • 但是如果你用有效值初始化它们并忘记了 Setter() (例如在添加新成员之后 - 在克隆方法中,或在读入期间,或在复制构造函数中,赋值方法,等等)你会很晚才提到这个(而且大多数时候你不会知道这个错误来自哪里)。所以你会得到一个例外,而且它会很好地重现......

标签: c++ exception setter getter


【解决方案1】:

关于验证:Setter 验证类不变量是否受到尊重是非常常见的(实际上,与公共值相比,这是它们的大部分附加值)。这在 Getters 中是最不寻常的(延迟验证对于调试来说是地狱),但在 Debug 构建中,在那个阶段重新验证可能会很有趣,只是为了捕获更多错误。

但是,这不是您在这里所做的。您的类 Invariant 显然允许一个可选元素(如GroupId),但您禁止用户将此GroupId 设置为无效状态...为什么!?这是不规则的。

关于 Getter,有一个简单的解决方案:可选类型(如 Boost.Optional)。这样一来,您始终可以毫无例外地返回,但您可以提供一个值......或者不提供。

【讨论】:

    【解决方案2】:

    我会同意你的新团队成员。对我来说,这并不常见,但如果你必须这样做并且这是唯一的方法,你应该这样做。
    我会尝试制作填充有一致性数据的对象。
    您可以通过检查构造函数中的值并且不提供设置器来达到此目的。
    首先我为编写 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。所以没有例外,也没有OutOfBounds!
    希望我能提供帮助,如果您有其他问题或有什么问题,请与我联系,很高兴学习!
    我很乐意收到对我的想法的反馈!

    【讨论】:

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

    我认为这是一种常见的风格,它肯定是第一个想法中 getter/setter 的核心动机之一。但是,在您的特定示例中,我认为它们没有意义,而且我认为通常有更好的选择:

    如果调用GetGroupID 引发异常,如果HasGroupID 产生错误,那么这是一个程序员错误 - 所以你应该使用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)组 ID 是一个可选元素 - 所以它也可以不存在(参见HasGroupID())(3)我们初始化所有成员都无效,以避免访问未初始化的成员(--> 异常)
    • 使用内联方法创建非虚拟类 GroupID 的方法很棒(性能、内存占用),我经常使用它,但我无法为每个参数创建一个新类!
    • @Charly:处理带有异常的编程错误听起来很像我们不解决错误,我们将它们推到地毯下并希望它们不会爬出来。断言和异常的区别在于异常可以被捕获和忽略,但编程错误永远不应该被忽略。您应该重新考虑使用断言。
    • @Charly:对于可选元素,请考虑使用预打包的 optional 工具,例如 boost::optional,因为这将删除大量样板代码并使事情变得更简单维持。你会记得几个月后你的每个参数的值意味着不存在吗?你会一直有这样的价值吗? (注意:boost::optional 具有更大的内存和运行时成本,但语义更明确,因此更易于维护)
    【解决方案4】:

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

    通常,在最基本的情况下,这些都不是必需的,但后来会出现数量惊人的这些新芽,如果您一直在摆弄班级成员,那么有很多代码需要修复。如果您从一开始就到处使用简单的 getter/setter,那么现在您可以向它们添加任何您需要的东西。

    【讨论】:

    • 如果您从第一天就开始使用 getter 和 setter,但现在才添加抛出的验证检查,那么您就违反了之前的合同。已经存在的代码现在可能会被破坏。它仍然可以编译的事实并不意味着这是一件好事。
    • @R. Martinho Fernandes:如果一直记录(例如)Set 只能使用有效值调用,并且Get 不能在Set 之前调用,那么添加在始终被禁止的情况下抛出的异常不破坏之前的合同。因此,部分问题在于,合同的设计是否首先考虑了这些无效值。如果您想稍后再决定一个曾经有效(如果无用)的值现在无效,那就更难了。
    猜你喜欢
    • 2010-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-02
    • 1970-01-01
    • 2015-02-07
    相关资源
    最近更新 更多