开发者

Musing/open questions on OO philosophy

I'm going to use this opportunity to put down a number of related but different thoughts on object-oriented philosophy, as a sort of request for comments. If the admins see fit to close, I may make it a blog post instead. But I am asking questions, so I think it's a suitable community wiki.

Suppose I have an abstract class Bird. Supposing that we're taking a fairly typical OO philosophy here, Bird is an object that is capable of maintaining and changing state. The obvious example of a state for this simple model is whether or not our Bird is flying or not.

So, Bird has a method fly().

Or does it? I've seen this very simple example given in introductory books, and in my opinion, it's subtly misleading. If you're maintaining state, you don't need to tell an object to continue to be in a given state; it just is. That's what an object does. So really, you want Bird to have a method takeoff().

Suppose at some point we created our Bird object from another class, Egg. At some point, we might call a method Egg.hatch() which has the return type Bird. Assuming we're coding to an interface, Egg knows what type of bird it's an egg from, and the returned instance from hatch() knows what type of bird it is also. But we can't really say that there's anything in common between an Egg and a Bird that hatches from it. are we really saying that we want specific implementations of these classes to have a reference to an instance of some sort of BirdArchetype that represents the characteristics of all instances of that species, so that both Egg and Bird know their own kind, independently but consistently? Is there any well-known pattern for this kind of relationship?

Whose responsibility is it to ensure that the Egg changes state so that it's no longer capable of hatching again, or doing most of the other thin开发者_运维技巧gs that eggs normally do?

Philosophically, do we want to introduce the concept of a breakable object? Perhaps Egg should be coded so that it throws an exception if you try to do any of the things that change its state more than once. It's up to the calling code to keep track of the client object's lifecycle.

What else can we do with an egg? Perhaps we could cook it. If it were me, I'd code this so that calling cook() on an egg 'breaks' the Egg instance and returns an instance of a new object, FriedEgg. How is this different to telling a Bird to takeoff()? It's absurd to suggest that taking off makes it a fundamentally different bird. Yet the reasoning is the same; a bird that's flying (usually) can't do most of the other things that birds do, because it's busy. Arguably then, Bird is breakable as while it's in the flying state, calling some of its other methods won't work.

I suppose the only real distinction here is that the Bird can become un-broken. Is the concept of thermodynamic reversibility really that crucial to programming very simple models like this?


This is why I don't particularly care for modeling the real world and using that to explain/define OO philosophy.

Unless you're building a simulator (in which the answers to these questions become immediately obvious when one looks at the simulator context) it's far too easy to go off into the weeds and confuse yourself.


Perhaps having mutating state in an object is all wrong ;-)

Shouldn't Bird.takeoff() return a FlyingBird which can land()? (It clearly doesn't make sense for a bird which is not flying to land.)

Similarly, for an egg:

egg = Egg()
incubatedEgg = egg.incubate()
bird = incubatedEgg.hatch()

However, the return type of an operation might not always be limited to one value. For instance, during incubating the embryo could die.

egg = Egg()
egg.incubate() ->
 if "still alive" (aliveEgg) ->
   aliveEgg.hatch()
 else "died in incubation" (deadEgg) ->
   deadEgg.discard()

Happy philosophizing :-)


  1. "Fly" reasonably means "begin flying!" (I would say it's used more often that way in natural speech). You don't have to interpret it to mean "sustain flight!" That's why we reasonably use "fly" and "land" (in natural english) as antonyms. (Evidence for the skeptical)

  2. You're assuming the egg is created ex nihilo in your model. That's not really a reasonable assumption. The bird came from an egg, but the egg came from a bird. Assuming a reasonable purpose for modelling an egg, it's to model the time-delay procreation of bird species. That means the baby bird is formed by the sexual reproduction of two other birds. The vehicle of generation of the baby bird is the egg. The egg doesn't need to know what kind of bird is in it; that bird was created by the parents and wrapped in a generic egg (that perhaps has variable properties based on the mother.

    In other words, your model isn't really well-formed with regard to this question.

  3. It's the egg's responsibility to see that it doesn't hatch again (a check in the hatch function). See the discussion of "imperatives" below.

  4. The concept of "breakable" is probably superfluous to your model. Are you concerned with the egg's breakability, or it's hatchability? If the breaking is a significant part of the data you are trying to model (for example, if anything in your model essentially depends upon the brokenness of the egg, then model it. Either way, hatching doesn't happen when the egg breaks, it happens when the egg hatches. One of the consequences of hatching is breaking, not the other way around.

  5. The reason egg.cook() seems wrong is because it is. Verb methods should take the object as the subject. Your statement "egg.cook()" is telling the egg to cook, which is rarely how we intend to use the verb "cook" in commands with respect to eggs. You probably really want a chef.cook(food) -- where the "food" arg is anything that derives from food (or better yet, has a Role (a la Moose) "isCookable".

  6. Remember, programming languages are imperative languages; interrogation, exhortation, and other "natural language" functions are inherently misplaced in programming languages, even if we achieve some version of it through commands (we emulate interrogation through commands; "TELL ME YOUR STATE!" is the imperative interpretation of the interrogative "What's your state?")

  7. A good program removes as much information as possible from it's models while meeting all functional specifications. That is, you have a specification (preferably in the form of a test suite). Meaningless information is information that isn't necessary to pass any of the tests. So, you implement one feature at a time, introducing the cookability of eggs when you have a test that quantifies that property via specification (again, read: tests). If you implement breakability of objects and realize that you can accomplish the same tests without it, you improve the program by removing it. Now, a really clever programmer is able to build his models in such a way as that they are extensible -- that is, that new properties can be added easily as new requirement specifications are produced.

    Note also, it is possible to have a bad specification. If you can't tell whether something is superfluous or not, the specs (read: tests) are either incorrect or incomplete with respect to that aspect of the program.


Seems more like an issue of naming. For example, in java you might query the bird's state as "getFly()", where you call it "fly()" as python would. Here, you're telling it to take off like you're changing the state of the bird, which is a little ambiguous to the reader of the code.

In the case of the Egg, I don't see any problem with the species of bird the egg handles as being part of the egg. As for who handles whether the egg is hatched, you could just keep a reference inside the egg to the bird that's hatched. An egg physically hatches, so it's up to me to see if it's hatched, so I don't stick an egg shell in an incubator for months expecting a chicken to pop out. In my opinion, it's up to the calling code to know who has hatched. If an egg hatches a bird, it's okay to keep a reference to that bird inside the egg if that helps keep your program organized. I would think it would be a convenience to anyone who uses that class.

There's no reason why you can't make an egg also be in a fried state, where any attempts to hatch it fail.

As to the absurdity of a different object for a bird in flight being a different bird than a grounded one, that's hard to justify. We may reason in our minds that it's the same bird, but in respect to object-oriented programming, we could certainly make them different objects if that meets the needs of the program. Is a bird more like an egg or a plane?

As to your Bird being breakable while it's in flight, is that the bird's fault? If my dog were in it's kennel, would it be broken if I told it to go fetch a ball?


Whose responsibility is it to ensure that the Egg changes state so that it's no longer capable of hatching again, or doing most of the other things that eggs normally do?

This depends on whether you want to keep around the egg remains in your model or consider the egg gone.

If the former, the hatch() method sets a private hatched flag to true and any of the egg's methods which depend on it being un-hatched (including hatch() itself) check that flag and fail (whether the failure is via return code or a raised exception, depends). The latter, might be doable with certain designs but don't really see any need to.

Or does it? I've seen this very simple example given in introductory books, and in my opinion, it's subtly misleading. If you're maintaining state, you don't need to tell an object to continue to be in a given state; it just is. That's what an object does. So really, you want Bird to have a method takeoff().

I think that your problem here is with imprecise word usage, not with software development. The authors of the fly() solution are usng "fly" verb in a sense of "start flight", e.g. as a synonym of "takeoff". Whether that's a 100% valid English usage or not is above my pay grade as ESL developer, but I certainly agree that "takeoff" is a significantly less ambiguous method name.

... so that both Egg and Bird know their own kind, independently but consistently? Is there any well-known pattern for this kind of relationship?

I'm not sure if there's an official "pattern", but having an egg constructor (or factory if you go with factory) implemented in a generic "Bird" class pass the bird type to a new "Egg" object and vice versa having "Egg" object pass its bird type to the constructor (or factory) of "Bird" is very widely used.

What else can we do with an egg? Perhaps we could cook it. If it were me, I'd code this so that calling cook() on an egg 'breaks' the Egg instance and returns an instance of a new object, FriedEgg. How is this different to telling a Bird to takeoff()

As a commenter already pointed out, the above is an OO design problem - the egg doesn't cook itself. Therefore you can't implement "cook" method on the egg in isolation (though some "set status to cooked" method might be needed for your model, if for no other reason than make it unhatcheable. However, the FriedEgg thing needs to be constructed (if needed) by a Cook object's "cook_egg" method - which also calls "set status to cooked" on an egg.


I don't think this really comes under OO philosophy. In my opinion, you're mixing business logic with design patterns.

To address your concerns in general, if you have a method that changes the state of the object, which can cause another method to throw an exception based on this new (changed) state, it is desirable to have a state-inspection method so that you don't have to explicitly handle the exception each time.

In your case, let's say you did egg.fry() (on another note, an egg cannot fry itself. So maybe you need a Cook class that takes an egg as an argument to a fry method and returns a FriedEgg instance), and after that you did egg.hatch(). The second call must return an exception. Instead of forcing the user of your class to put that call in an explicit try...catch block, you should provide a state-inspection method; something like isFried() or isHatchable(). Then instead of:

try {
  egg.hatch();
}

catch(UnhatchableException e) {
  ...
}

You have:

if(egg.isHatchable()) {
   egg.hatch();
}

So it is the responsibility of the calling code to check and see what state the object is in, before it performs an operation that can potentially throw an exception.

Now assuming you had a Cook class and a method called fry and you did Cook.fry(egg) which returns a FriedEgg instance, what would happen if you call hatch() on it? Common sense would tell you that a fried egg cannot hatch anything!

In this case, you should have an Egg interface with LiveEgg (i.e., an egg that can hatch) and FriedEgg both implementing the Egg interface. The difference however, is in the implementation of the hatch() method in FriedEgg; it would have to throw an UnsupportedOperationException because you cannot hatch a fried egg.

The problem with modeling most real-world scenarios (like cars, animals etc.) is that sometimes they don't help in adequately explaining the relationships. This is due to the fact that OO concepts are pretty abstract.

Anyway, hope this helps.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜