Summary:
Bjarne Stroustrup talks with Bill Venners about the perils of staying too low level and venturing too object-oriented in C++ programming style.
The ability to add new comments in this discussion is temporarily disabled.
Most recent reply: February 22, 2004 10:11 AM by
|
> So, how would you refactor JFrame in Java, or > CWnd in MFC, for example? Or at least, what > general approach would you take? > > This is something that came up with Scott Meyer's > interview and some others, if I remember. I've been > trying to get Bill to follow up by asking about > refactoring a specific example of a well-known class with > a ton of methods, such as either of the two mentioned > above.
That's one heck of a question! I don't even know where you would begin. Either of those classes do a lot of things and handle some pretty complex tasks.
Seriously, how useful would a 10 method windowing class be? I think you have to defer to Einstein on this one and 'make everything as simple as possible, but not simpler'. To which one could always retort with 'Any fool can make things bigger, more complex, and more violent. It takes a touch of genius-and a lot of courage-to move in the opposite direction.' I think it would take more than a touch of genius to effectively refactor the aforementioned classes.
Anyway, refactoring CWnd or JFrame is still one heck of a question. On the surface you're left with either get rid of a lot of functionality or reduce the number of methods and increase the number of classes that compose the main class.
Ripping out functionality would limit what you can do in either case and probably isn't a realistic option. The other option, composing the main class of a bunch of smaller classes, would reduce the number of methods, but then anytime you needed to do anything interesting, you would have to get a reference to the particular piece of the window and work with that particular object. That has its own pitfalls. So instead of needing to know a bunch of method calls, you would need to know all of the classes that make up the main class and all of those contituent class' method calls.
This would increase the number of things you would need to know and I think runs afoul a bit of the generally accepted principle of eliminating get/set methods as much as possible. I don't see much of a difference between get/set and needing to get a constituent object and calling a method or setting a property on it. You're still doing something that affects the state of the main class. It's not as bad as needing to poll the class for its state and then setting something, but it's certainly not clean. The main class would have to poll its contituent objects before doing anything. This would all be hidden from the end user, but it doesn't strike me as elegant or efficent.
Does anybody else have any ideas on this one? It's a heck of a question (I'm guessing why it doesn't get asked, or gets dodged when it does), although I think I've mentioned that already...
|
|
|
This is nearly exactly my question. http://www.artima.com/forums/flat.jsp?forum=32&thread=10110I don't see anyone giving really good answers. Helpful suggestions, yes, but not full-blown answers. The question is inherently complex. You have to understand what all of the responsibilities/concerns are, and what the usage patterns are. This is not a trivial task for user interface development. Though it's significantly related to my interests in managing software complexity, I have too many other interests right now to tackle it with the amount of diligence that would be required to do a good job. Darn day job. :->
|
|
|
> > So, how would you refactor JFrame in Java, or > > CWnd in MFC, for example? Or at least, what > > general approach would you take?
Let's face it, CWnd wraps an awful API. Even the much cleaner WTL version, CWindow, has a bunch of gunk. (JFrame is much better, IMHO, but still has gunk).
Where does the gunk come from? The gunk comes from trying to take on too much responsibility at once. These windowing classes are responsible for registering messages, handling timers, resizing themselves, attaining focus, changing style, interacting with peers, tracking their children, repainting, bitblting and self-disposal.
The fix is to strip down the Window class to the bare minimum of what a Window is (rectangle) and let other classes (or libraries) handle the extraneous or unrelated. WindowStylist - in charge of decorating windows, WindowPlanner - handles windows events, WindowGrimReaper - in charge of managing the window life cycle*.
Suddenly the CWnds of the world look a lot simpler. Other classes are shouldering some of the burden. But wait, this might seem ugly because it's kind of procedural.
But there's more to the ugliness than that. Developers entertain mixed metaphors when it comes to windows. On one hand, a window is a rectangle. On the other hand, a window is a metaphor for the Application as a whole.
So developers want to see the simple setTitle(), resize(), close() API that is Window As Rectangle, but also expect the kitchen sink interface that properly reflects Window As Whole Enchilada.
In a way, the WAWE metaphor provides a nice Facade pattern implementation to everything. But it's unsettling when we look for the simplier interface. My answer to refactoring: change the name of the class from JFrame to JVisualKitchenSink and CWnd to CVisualWholeEnchiladaApplication
*(I have a whole version with private delegates for defaults in my mind).
|
|
|
> This is nearly exactly my question. I had asked it before (last year when Bill interviewed Scott Meyers, on the subject of "Minimal and Complete Interfaces"), as well: http://www.artima.com/forums/flat.jsp?forum=32&thread=3031&message=10386&redirect=true&hilite=true&q=CWnd
|
|
|
He isn't lying when he says he's been advocating this approach to design for a while.
I guess I was hoping for juicer questions:
1.) Would you quit programming if you were forced to program in java?
2.) Who would win in a fight: Larry Wall, Yukihiro Matsumoto or Ken Thompson...
Actually, I would have really liked to know what kind of projects he has been working on and what other languages he uses besides C++.
|
|
|
Continuing on the topic of refactoring of a CWnd kind of a class, Why cant the splitting the functionality of the class into multiple classes and making them friends of each other can be done ? How bad/ good an approach is this ? Or is it that the OOP-purists would shy away from friend classes ?
|
|
|
> This is the kind of thinking that's reflected in a > language like Java for instance, but a lot of things > don't fit into class hierarchies. An integer shouldn't > be part of a class hierarchy. It doesn't need to. It > costs you to put it there. And it's very hard to do > elegantly.
First of all, the Java int primitive isn't part of a class hierarchy. Arguably, it should be. There are several justifications for making everything an object:
- you can store anything in a collection - you can define methods like toString and finalize and know everything will have them - you can define operations that all numbers have in common and build applications that work with a range of different numbers, including integers, bigints, floating point, decimal, etc.
The .NET approach where everything, including integers, are objects seems more uniform and elegant than the Java primitives. Wrapper classes are an acknowledgement that integer-as-object is needed.
I assume the "costs" Bjarne Stroustrup mentions are performance costs, which can be optimized away.
I think integer-as-primitive is inelegant compared to integer-as-object.
What am I missing?
|
|
|
I agree, Paul. Bill recently interviewed Matz, the creator of Ruby, who I'm sure doesn't agree with Bjarne on this! In Ruby (and Python and many others) integers are even more like objects than in .NET, where they look more like objects than they do in Java, but still need the boxing and unboxing.
The only reason to have a special primitive data type is performance. On C++, since it is still C, with additions, that is important, because people expect to be able to get "close to the metal." Many other languages -- Java, Python, Ruby and so on -- have C extensions for that purpose (and for access to close-to-the-metal things). Most of the time, it is a premature optimization to be overly concerned about the speed of integer operations.
Hardware has come a long way in the last fifteen or twenty years: in those days, luxuries like everything being an object were not as feasible. Around 1989 or 1990, I worked at a place that a product written in Smalltalk -- it was so slow that it was completely unusable on a Windows 3.1 desktop. They were pondering porting the whole thing to C++, just to boost the performance. I don't think that would happen these days.
|
|
|
Hi Paul,
I mostly agree with you but just wanted to point out a couple of small things...
> First of all, the Java int primitive isn't part of > a class hierarchy. > > Arguably, it should be. There are several justifications > for making everything an object: > > - you can store anything in a collection
C++ doesn't need this, since it has generics--and soon Java and C# will too...
> - you can define methods like toString and finalize and > know everything will have them > - you can define operations that all numbers have in > common and build applications that work with a range of > different numbers, including integers, bigints, floating > point, decimal, etc. > > The .NET approach where everything, including integers, > are objects seems more uniform and elegant than the Java > primitives. Wrapper classes are an acknowledgement that > integer-as-object is needed.
I think it's a distinction worth noting that .NET integers aren't objects; they're primitives that masquerade as objects through automatic boxing and unboxing. The only difference between this and Java is who is doing the wrapping: the programmer or the compiler. (And with 1.5, Java gets autoboxing as well... I think?)
> I assume the "costs" Bjarne Stroustrup mentions are > performance costs, which can be optimized away.
The costs are quite high for many applications. I don't think you can so easily optimize them away. I wouldn't be surprised if the performance penalty for many numerical applications was 10X or greater. (Not that I have any data to back that up...)
> I think integer-as-primitive is inelegant compared to > integer-as-object.
How about integer-as-primitive-with-autoboxing?
Personally I think Java and .NET strike a very practical balance by having all classes share a common ancestor, but having primitive data types. Stroustrup seems not to condone the former...?
|
|
|
>> I agree, Paul. Bill recently interviewed Matz, the creator of Ruby, who I'm sure doesn't agree with Bjarne on this! In Ruby (and Python and many others) integers are even more like objects than in .NET, where they look more like objects than they do in Java, but still need the boxing and unboxing. << I like Ruby and I'm glad integers are objects in Ruby. However, Java is appropriate for many classes of applications that Ruby (and Python, et al) would not be at all suitable for, especially where performance is a concern. My understanding is that for number-intensive applications, Java and C# can be comparable to C/C++ in speed (once the IL has been jitted)... whereas Ruby and Python are easily an order of magnitude or two slower. See how big the difference is in matrix multiplication: http://www.bagley.org/~doug/shootout/bench/matrix/Heck, Ruby doesn't even use native threads and Python's interpreter requires instructions to be executed totally serially. These languages are clearly not designed for apps where high performance is a requirement, since (overgeneralizing here) they don't gain any speed from multiple processors. Just as people expect certain levels of performance from C, there are people out there who expect certain levels of performance from Java... Java is just at a different place on the performance/ease-of-development continuum from C/C++ or Ruby/Python/Smalltalk. It seems to be at least performant enough and easy-to-use enough for millions of developers to happily develop on it.
|
|
|
Ah, but you are missing the crucial point to languages like Ruby and Python: use the best tool for the job. You do most of the coding in a nice programmner-friendly and very productive language and use libraries (eg. NumPy http://www.python.org/topics/scicomp/numpy.html), or write C extensions for just those parts that need the blazing performance. That matrix multiplication comparison is entertaining, but not of any real value. I believe there are plenty scientists using Python for high-performance numerical tasks, but they would be silly to write it all in pure Python code; instead they use the NumPy module (and/or other other similar modules).
|
|
|
> See how big the difference is in matrix multiplication: > http://www.bagley.org/~doug/shootout/bench/matrix/A more up-to-date version is available at: http://dada.perl.it/shootout/> Java is just at a different place > on the performance/ease-of-development continuum from > C/C++ or Ruby/Python/Smalltalk. Smalltalk performance is more like Java than Ruby/Python. Smalltalk implementations have long used what we now know as JIT compilation. As you said of Java, in general Smalltalks fast enough. http://www.lissett.com/ben/exp/bench1.htm
|
|
|
Joe says:
> > - you can store anything in a collection > > C++ doesn't need this, since it has generics--and soon > Java and C# will too...
Generics work for strongly-typed collections, not heterogeneous ones. I suppose in C++ you could always have a collection of void pointers, which amounts to the same thing but somehow seems uglier to me.
> I think it's a distinction worth noting that .NET integers > aren't objects; they're primitives that masquerade as > objects through automatic boxing and unboxing. The only > difference between this and Java is who is doing the > wrapping: the programmer or the compiler. (And with 1.5, > Java gets autoboxing as well... I think?)
Isn't the whole point of any high level language to get things automated for you?
This automated treatment is crucial - it means the value types *are* objects in the sense that they have methods and can be stored in collections. It's not just a matter of housekeeping, it means you can treat everything uniformly when it makes sense.
While boxing is of course necessary to use a value type in a collection, I doubt it's needed to simply call a method. I believe Java 1.5 will add boxing for collections, but you still won't be able to call methods through a primitive. Can anyone confirm that?
I wouldn't use "primitive" to describe an integer in .NET, because it has connotations of a fixed set of special-case types. .NET uses the phrase "value type" which includes not just boolean, char and numbers, but enums and structs too.
> The costs are quite high for many applications. I don't > think you can so easily optimize them away. I wouldn't be > surprised if the performance penalty for many numerical > applications was 10X or greater. (Not that I have any > data to back that up...)
I don't have hard numbers either, but I doubt it. When you just do arithmetic operations with an integer, it should map straight to an integer in your processor. Is Bjarne performance concern about pure OO systems like Smalltalk where there are no value types? If so, it's curious that the language he mentioned several times to contrast with C++ was Java.
|
|
|
One additional thought on the "too hot, too cold, just right" issue:
Bjarne describes how using C++ in a low-level, C-style way is as suboptimal as using it in an über-OO, Java-style way. (BTW, I suppose they go very well together, often code that is designed Java-style needs to be implemented C-style.)
It could be inferred that he considers well-used C++ to be superior to C as well as Java - and of course he'd be absolutely correct with that ;-) - but an important point is IMHO that each misuse C++ is a lot more cumbersome than using the respective style in the original language:
Getting a low-level, C-style solution to work in C++ is far more difficult than creating a real C solution. E.g. a custom memory management system in C is not trivial, but quite straightforward. In C++ it is no longer so (you have to consider POD's, copying, destructors, stuff ...).
A "pure-OO" language can also work very well. I won't comment on Java, but e.g. in Ruby the fact that every datatype is derived from a root class never gets in your way. In Ruby, this is just an implementation detail that never contorts your design - true multiparadigm programming is possible.
In C++, trying to be "pure-OO" is really bad because it means working against the language. Wether it is a bad idea in general is a secondary, almost moot, consideration.
"When in Rome, do as the Romans do."
If you are lucky - like I am - you happen to find the true C++ way of multiparadigm design a natural - as good as or even better than "pure OO" - way to think about design and programming.
|
|
|
> > Thanks for the gentle bruising with the clue stick :-) > > AFAIT, the article got updated to include the correct > > library name and reference since yesterday, no, no, > > honestly... > > > Yes, a reader emailed me suspecting it was a typo. Bjarne > agreed. I fixed it and added the link in the resources > section.
BTW, there's also a new signal/slot library added to Boost. And not mentioning Boost when talking about C++ libraries which offer what the standard library doesn't is a severe shortcoming, IMHO.
|
|