Seebs wrote: > On 2009-11-17, Marnen Laibow-Koser <marnen / marnen.org> wrote: >> I didn't say the same object -- I said the same *thing*. >> a = Stat.new(15, -2) >> b = Stat.new(15, -2) >> It doesn't matter in the slightest whether a.object_id == b.object_id. >> Regardless, a and b are *the same thing* to all intents and purposes -- >> and you can tell that, in part, precisely because the object_id is >> irrelevant. > > Perhaps, but (15, -2) and (14, -1) are also *equal*, even though they're > obviously different things. > > Two fives equal a ten. When I am talking about money, all I care about > is the net value, not the internal representation (coins or bills). > Unless > I have to deal with a vending machine. > > But 99% of the time, 10==10 is the correct interpretation of a > comparison > between two fives and a ten. a = Cash.new(Array.new(2, Banknote.new(5, :usd))) b = Cash.new([Banknote.new(10, :usd)) a.to_money = b.to_money a.banknotes != b.banknotes a ?== b # depends on your definition of equality. > >> call. You're confusing the method with its return value. The >> difference is subtle but important. > > Hmm. > > Oh, heyyyyy. > > Okay, so one option would be: > def str > @str.to_i > end > > In short, just return the value rather than the object I'm using to > model > it. That would be the appropriate solution if you never wanted to expose the Stat object. > > The problem is, I sort of want to be able to do things like: > john.str.add_modifier("drunk", -3) > > Because really, the fact that str is a stat *is* part of the intended > published interface. Then your clients will know that it's a Stat object, and expect to call to_i on it. I don't see anything wrong with that; you can overload Stat + Fixnum and so on as we discussed earlier in this thread. But if you really want automatic, you can have it: class Stat def to_int self.to_i end end (According to http://www.rubyfleebie.com/to_i-vs-to_int/ , to_int is called automatically as necessary and will make your class act as an Integer. to_i, of course, will not do that.) [...] >> Why? There's no point to doing so as far as I can see. Even if it were >> a Fixnum, it would already be yielding different objects every time it >> changed. The client should make no assumptions about >> john.strength.object_id. > > Not as a specified interface that people should rely on, but rather, as > an > implementation choice -- it seems crazy to me to regenerate whole > object-trees > frequently. For your use case, you're probably right. For real value objects, it's a different story. > >>> This sounds extremely expensive for an extremely commonplace thing. In >>> an ordinary turn of a game, I might have thirty or forty modifiers which >>> change, meaning that I'd be duplicating-but-modifying non-trivial object >>> trees thirty or forty times. > >> It may sound expensive, but it really is the best way of handling value >> objects. This is how classes like Fixnum, Date, and BigDecimal work. >> (Actually, Fixnum uses the Flyweight pattern under the hood; the others >> might also.) > > None of those classes are complicated trees containing five or ten or > thirty other objects, though, are they? Fixnum probably isn't. Date probably contains about 5 other objects (totally guessing here -- haven't looked at the implementation). The BigDecimal library I wrote for Rubinius contained about 3-5 other objects, but I don't know if MRI or JRuby does it the same way. The Address class I wrote for Quorum ( http://quorum2.sourceforge.net ) contains about 6 fields in an immutable value object. Of course, I rarely only need to modify one field in someone's address. > >>> History of previous values (in some cases), and the modifiers aren't >>> fixnums; they're name/value pairs > >> Really? You're not just doing >> @modifiers = {:racial => 2, :class => 1, :armor => -1}? > > Modifier is a class too, which can handle things like counting down its > duration, etcetera. > > And a stat has a handful of them. OK...*these* are your value object candidates, perhaps. > >>> probably plus other traits such as >>> a duration (some expire, some don't). > >> Does that really belong in the values itself? I'd tend to think not; >> rather, that probably belongs in your event system. > > It seems to me that the knowledge that a modifier expires is best > handled > by embedding it in the modifier. It might be. > >> But if you want history, then the StatValue object or whatever you wind >> up using will probably have to be immutable anyway, so you can be sure >> your history is accurate! > > Ahh, but I don't necessarily care whether it's accurate, just whether I > have a record. If it's not accurate, then there's no point keeping a record. > > For a concrete example: A lot of games would have, say, a strength > stat, > and things that can lower your strength. Then you find a "potion of > restore strength", which restores your strength to its highest previous > value. > > To my mind, this is the kind of thing that should be handled entirely > by the stat object. It's not john's job to know what his highest score > was for a given stat; it's the stat's job to know what its highest score > was. That depends. A good argument could be made for having the Character store its own history, and just having the Stats be values. In either case, though, for this sort of functionality you don't necessarily need a full-fledged history. All you need is something like class Stat def value=(new_value) @max_value = [new_value, @value].max @value = new value end end If you're ambitious, you could put together something like ActiveRecord's before_save (which is probably what I'd use for this in a Rails app). > >> Then refactor your design at that time. Design for what you have now, >> rather than predicting the future. Remember YAGNI. > > In this case, though, I really do know that I need a fair bit of this. > The ability to damage and restore stats is pretty much essential to a > roguelike. Well, then you'll run into the design issues when you're ready for them. For now, though, and at every future step, try to http://c2.com/cgi/wiki?DoTheSimplestThingThatCouldPossiblyWork -- that is, work with the state of the app *at that time*. I know premature generalization is tempting, but it's not usually a good idea. > >> Since you don't know yet, don't design yet for them (your future plans >> could easily change). Design a model that works for what you actually >> have, and be ready to change it as necessary. > > Yeah, but changing this into an immutable object is a lot MORE work. So > I'm not sure I should put extra work into making the object less > flexible > on the off chance that I won't later not need to have done that work. Right. It's now clear that your stat object is too complex to be a simple immutable value object, although some of its components might well be. > >> Largely correct, although I can think of exceptions. You'll probably >> want to break this down into a composed object containing one or more >> value objects. > > I think so. > >> No need. The former is a lot friendlier. You can always redefine >> Character#strength= . > > True. > >> That strikes me as silly. There's no reason that the client class >> should care whether your strength= method *really* does an assignment >> operation. > > True. > > Hmm. > > So yeah, I think at this point, I really do want a complicated composite > object, which happens to delegate to a value object which has traits > which > are useful for my purposes -- in this case, almost certainly a fixnum. I don't think delegation will work here -- for one thing, you may not need to store the total value in any actual instance variable. Using to_int or possibly coerce will work much better. > > -s Best, -- Marnen Laibow-Koser http://www.marnen.org marnen / marnen.org -- Posted via http://www.ruby-forum.com/.