TITLE: designing using get/set methods (Newsgroups: comp.lang.c++.moderated, 14 May 98) SINCERO: Arcadio A. Sincero Jr. : > In the book "Effective C++", Scott Meyers talks about something : > called "proxy objects". Although I've only skimmed the material (I : > didn't have the $40.00 on me to buy it at the time so I just read it : > right there in the book store), this concept looks like what I've : > been looking for. : > : > To recap the discussion: allowing direct access to an object's : > attributes by having an access method return a non-const reference : > to it is generally frowned upon because it violates the principle of : > encapsulation. Therefore, C++ programmers typically use the get/set : > idiom to provide access to an object's attributes. My _personal_ : > beef with this idiom, however, is that it is ugly. Something like this: : > : > recordObject.age() = 22; : > recordObject.weight() = 200; : > std::cout << recordObject.age() << std::endl << : > recordObject.weight() << std::endl; : > : > looks cleaner than something like this: : > : > recordObject.setAge(22); : > recordObject.setWeight(200); : > std::cout << recordObject.getAge() : > << recordObject.getWeight() << std::endl; SHAPTER: Douglas Shapter wrote: : [lots of stuff snipped] : : Why not overload the methods Age() and Weight()? Granted, they are : still "accessor" methods (I'm still not sure why that is such a Bad : Thing), but the syntax is cleaner: : : int Age() { return m_age; } : void Age(signed int a) { /*validate a here*/ m_age = a; } : : Thus: : : recordObject.Age(21); // in my dreams : cout << "I am " << recordObject.Age() << endl; : : You save yourself the messiness and maintainablity issues of proxy : objects, but you can't do the nifty things like Oleg mentioned earlier : in this thread (a.age() = b.age() = c.age()). ZABLUDA: Oleg Zabluda In practice, this is what I typically do. However, I typically return either the old value from the mutator or a reference to *this, depending on circumstances. To recap a little: there are many different ways to provide accessors and mutators. A simple fact that you have them does not mean that your class is not an abstraction, as long as you can demonstrate that you can come up with a significantly different implementation, within the same interface. Same is true if your accessors/mutators allow you to have a ayntax, like a.age() = 3. Let me try to provide a small catalog of possible accessor/mutator methods, with a small list of advantages and disadvantages. There is nothing new here, just a catalog of well-known conventions: 1. class Person { private: int age_; int weight_; public: int getAge() const { return age_; } void setAge(int age) { age_ = age; } void setWeight(int weight) { weight_ = weight; } }; - simple - efficient - verbose for users. - sometimes ugly: p.setAge(getAge()) - can not do f(Person().setAge(100).setWeight(70)); 2. class Person { private: int age_; int weight_; public: void getAge() const { return age_; } Person& setAge(int age) { age_ = age; return *this; } Person& setWeight(int weight) { weight_ = weight; return this;} }; - slightly less simple - slightly less efficient, but the compiler can potentially take care of it - still ugly, but more powerful: p.setAge(q.setAge(10).getAge()) - sometimes less verbose: a.setAge(30).setWeight(70); - can do f(Person().setAge(100).setWeight(70)); - can not do ``const int old_weight = p.setWeight(100);'' 3. class Person { private: int age_; int weight_; public: void getAge() const { return age_; } int setAge(int age) { const int old_age = age_; age_ = age; return old_age; } int setWeight(int weight) { const int old_weight = weight_; weight_ = weight; return old_weight; } }; Basically same as (2), but - less efficient. - even less simple. - can not do a.setAge(30).setWeight(70); - can not do f(Person().setAge(100).setWeight(70)); - can do `const int old_weight = p.setWeight(100);'' 4. Same as above, but don't use names setAge/getAge. Overload single name Age instead. Note that this decision is orthogonal to the choice between (1), (2) and (3). - Basically no different then above, but slightly less ugly, IMHO and fits better with English. The following sounds more like a normal English: const int a = p.Age(); While the following sounds less like a normal English: p.Age(3); But you don't notice it in just a little while. An additional advantage is that it follows the stdlib conventions. Note: I typically do 4 plus either 2 or 3, depending on intended use. 5. class Person { private: int age_; int weight_; public: int& age() { return age_; } int age() const { return age_; } int& weight() { return weight_; } int weight() const { return weight_; } }; - efficient. - non-verbose - cool short syntax a.age() = b.age() = c.age() = 5; - The biggest disadvantage is that if you'd want to change the implementation, you'd have to introduce massive changes, like proxies and stuff. However, if you are lucky, you might be able to automate the process: (not compiled) class Person { private: typedef Proxy<0, INT_MAX> Age_; // checks that int > 0 typedef Proxy<0, 1000> Weight_; // checks that 0 might look like this: template class Proxy { private: int& value_; public: Proxy(const int& value) : value_(check(value)) {} Proxy& operator = (int value) {value_ = check(value); return *this;} operator int () const { return check(value_); } public: int check(int value) { if (value < min || value > max) throw out_of_range(); else return value; } }; Basically, there is no One True Way. What you choose depends on the intended use. What I was trying to say is that the mere fact that you use any particular accessors/mutators doesn't automatically break the abstraction, if you are careful.