Richard, Thanks for laying out the different design options. Unfortunately we're not always in a position to be able to study every design option since we're constrained by our shipping schedule. Sometimes we need to trade off what we know our engineering teams can build in the time we have available with what we would like to do in an ideal world. In many ways the studies we do in the Visual Studio team aim to tip the balance in favor of the user experience. It's one of the differences between the research that we do in the product teams at Microsoft and the research that the teams in Microsoft Research do.
Anyway, the more interesting debate is how the opportunistic programmers would deal with the design options you laid out. On the factory design pattern, that has been studied also. This will likely make you even more terrified of using software built by other software engineers :), but take a look: http://www.cs.cmu.edu/~NatProg/papers/Ellis2007FactoryUsability.pdf On the fluent like initializer object style you demonstrated below, I don't believe that will solve all the usability issues we observe opportunistic programmers experiencing with required parameters. These developers are very exploratory in the way that they write code and are looking for shortcuts everywhere they go. They are task oriented, not API oriented. They think about the task they are writing code for, not the way that the APIs work. The success of the fluent style of API would depend on how many 'dots' the user would need to write before they were able to call the method that helped them with their task. If it is too deep, they might even give up exploring the class before they get that far. We don't have the luxury of dismissing these types of programmers. While it might strike you with terror that these programmers exist, they are successfully building applications in many different domains. They may work differently to you and many other programmers but that doesn't necessarily mean that the code they create is worthless. Within the Visual Studio team at Microsoft we've devoted efforts to attempting to make them successful by adapting to their workstyles when appropriate. There are a few blog posts and papers that describe these personas in more detail that might be worth reading if you're interested in how we use them. http://p.einarsen.no/programmer-personality-types-and-why-it-matters-at-all/ http://drops.dagstuhl.de/opus/volltexte/2007/1080/pdf/07081.ClarkeSteven.Paper.1080.pdf http://blogs.msdn.com/b/stevencl/archive/2010/08/19/making-effective-use-of-personas-in-design.aspx http://blogs.msdn.com/b/stevencl/archive/2010/12/22/climate-change-and-developer-personas.aspx From: john.m.daugh...@gmail.com [mailto:john.m.daugh...@gmail.com] On Behalf Of John Daughtry Sent: 28 March 2012 13:51 To: Richard O'Keefe Cc: Steven Clarke; Brad Myers; Raoul Duke; Ppig-Discuss-List Subject: Re: studies of naming? You introduced a third option. However, it is a deviation of the implementation behind the interface as opposed to an alternative interface. Are you suggesting that every API study should consider all of the limitless alternative implementations? Perhaps I misunderstood the design alternative you suggest as the third option. Your further discussion (e.g., initializer objects) supports the notion that any attempt to achieve both usability and robustness is tedious and laborious in dominant languages. John On Tue, Mar 27, 2012 at 11:10 PM, Richard O'Keefe <o...@cs.otago.ac.nz<mailto:o...@cs.otago.ac.nz>> wrote: On 27/03/2012, at 10:14 PM, Steven Clarke wrote: > Yes, you're right Richard. Our study was focused on languages like Java and > C# (and importantly, as they existed around 2006/2007). So as you say, we > wouldn't generalize the results to languages that have named parameters. We > described the two design choices we evaluated at the start of the paper: > > "There are two common design choices: provide only > constructors that require certain objects (a "required > constructor"). This option has the benefit of enforcing > certain invariants at the expense of flexibility. An > alternative design, "create-set-call," allows objects to > be created and then initialized." There are actually three design choices, and I've seen the third one too often for comfort. Please note that this is a *different* choice from the one you endorsed: Good Smalltalk design: Provide a variety of factory methods with keyword arguments, all of which provide fully initialised objects satisfying the class invariant; such objects often need little or no mutation afterwards. Good Eiffel design agrees in every respect except 'keyword arguments'. Good create-set-call design: Ensure that the default 'new' constructor returns a fully initialised object satisfying the class invariant and offering meaningful default behaviour; such objects almost always require adjustment to get them into the state you really want but all states are meaningful. IN ADDITION make sure that all 'initialisation phase' methods can safely be called AT ALL TIMES. Ensure that every public method is *tested* with a default- initialised object. Bad create-set-call design: Don't think about class invariants. Rely on default constructors that leave fields with default values (0, nil, &c) that satisfy types but not invariants. Allow objects to be named outside their class in partially initialised states that require 'initialisation phase' methods to be called before 'work phase' methods, but do not check that this has been done. Allow 'initialisation phase' methods to be called at any time without checking it it makes sense, allowing even well-initialised objects to subsequently be put into inconsistent states. Much as I love Smalltalk, a lot of the code that I see using the create-set-call pattern is actually doing the >bad< version. Here's an example that took only 2 minutes to find. In Pharo 1.1 (which is not the current version) Url new<cmd-P> which is the equivalent of System.out.println(new Url()) raises an exception. Url new created an uninitialised object. That's _almost_ fair enough: this is supposed to be an abstract class, but it should have been caught in #new. Go to a concrete subclass: FileUrl new<cmd-P> also raises an exception, trying to print the elements of a nil String. > However, I don't think our result is unsurprising, All I can say is that it didn't surprise _me_. Compare for example f = fopen(x, y); /*C*/ s := FileStream read: x. "ST" s = new FileStream(y, x); //C# Smalltalk: obvious what it does. C: which argument is which? Compiler can't help. C#: three different things it could be; the compiler can tell them apart, but it's not so easy for people. And when you get to new FileStream(String, FileMode, FileSystemRights, FileShare, Int32, FileOptions) this is obviously going to be a lot harder for people to read than FileStream read: string rights: rights share: share bufferSize: int32 options: options If you *could* do s = new FileStream() .FileName(string) .Rights(rights) .Share(share) .BufferSize(int32) .Options(options) .Open(); that would be a lot clearer. And that introduces a fourth design pattern, which the paper did not investigate, call it "initializer object". The general scheme for Initializer Object is class X has a static Maker() method returning an instance of X_Maker(). X_Maker() has methods like Facet(value) returning the same X_Maker() object and a completion method called something like Open() or Create() that returns a fully initialised instance of X. This requires a creation style like s = FileStream.maker() .FileName(string) &c as before .Open(); > You've highlighted the core of the debate when, referring to the people who > prefer the create-set-call pattern, you said " the idea of them writing any > code that might affect my life or the life of anyone known to me is not one > that's going to help me sleep at night ". This was also the initial reaction > of many people inside Microsoft when they heard the results of our study. > > Our response in this debate has always been that different programmers > require different APIs. That's the message we tried to communicate in the > paper when we described the different personas. These personas represent > different workstyles of the developers we have observed using the .Net > framework. They are a crucial tool in our ability to successfully design a > framework that is broadly usable by millions of developers. The paper didn't just say that these programmers didn't LIKE or weren't COMFORTABLE using full-initialisation constructors, but that they didn't really get the idea. Is it really a good idea to design a framework that is (ab)usable by people who probably shouldn't be programming in the first place? Less dismissively: would these people have got the idea of Initializer Object? > One of the hardest things to do in order to use these workstyles successfully > to design an API is for the API designer to let go of their own biases > towards how things should be done, and instead, design the API based on a > deep understanding of how the user expects things to be done. You can call it a bias if you want. I call it sheer terror based on seeing it done wrong (as in: programs crash) far too often. The paper did not suggest to me that a deep understanding of what those users thought was happening had been reached, or even sought. In particular, may I offer the MacDonalds analogy? You go to a MacDonalds, and you tell them what you want. As you say - I want a hamburger - I want the mighty angus - no onion or pickles do you see this? * a hamburger bun materialises in front of you * it's filled with the beef and trimmings * the onion and pickles are taken away No. They *take your order*, and then deliver a complete hamburger not entirely unlike the way you want it, and then you eat it. You can't eat an incompletely assembled hamburger, because they don't give you one. In the Initializer Object pattern, the initializer object is like the order the person behind the counter is filling out. When you say "that's it" and pay, _that's_ when they select or assemble the hamburger and deliver it to you. Up to that point, you can revise the order if you want to. Isn't it at least possible that what the people who use new _() and then fill out their order might prefer Initializer Object? Surely there's no shortage of Windows developers who have bought a hamburger... A fifth design pattern could be called "Lazy Biphasic Object". A Biphasic object is one with (at least) two distinct states: initialisation phase, where various facets can be set up, and operational phase(s), where the object does whatever you really wanted it to do. Methods may be classified as - initialisation only - operation only - multiphase Lazy Biphasic Object is where the object changes from initialisation phase to operational phase the first time an operation only method is called. The best known instance of this is C FILE objects, where setbuf() and setvbuf() are initialisation only methods, and getc() and putc() are operation only methods, and the first time you call getc() or putc() the initialisation process is only then completed. Are the programmers who are only comfortable with create-set-call really thinking in terms of lazy biphasic objects? Would the API be better if designed that way? Would they mind at all if the transition were explicit rather than implicit? > That doesn't mean that we always do everything the way that users expect them > to be done but it does mean that when we decide to do something that differs > from their expectations we do it consciously and deliberately. It also means that you need to know what it REALLY is that they are expecting. Maybe you found that out, but the paper didn't _say_. There is a big difference between create-set-call where the object is *always* ready for use and lazy biphasic object where it is an error to call an initialisation-only method in operational phase. > In this case, the understanding we had gained from the study reported in this > paper and from many other studies we had run internally (at one point we were > running API user experience studies on the .Net framework monthly) indicated > that we needed to design the API to accommodate the users preference for > initializing objects since the alternative was that many developers would > have a very difficult time using APIs that were designed differently to their > expectations. You offer users an *extremely* limited choice, and then talk about what they did as their *preference*? The paper didn't mention the Initialiser Object approach: how do you know they would not have preferred that? The paper didn't mention explicit biphasic object: how do you know they would not have preferred that? It didn't mention lazy biphasic object (with exceptions raised for out-of-phase invocations). How do you know they would not have preferred that? The paper did not mention single-point-of-construction with keyword arguments (or even passing a dictionary, as you might do in Python). How do you know people would not have preferred that? Oh, it's great that you did the experiment, and really great that you wrote it up, but the range of alternatives considered was _far_ too small to base any far-reaching decisions on. Again, I repeat: whatever you actually found out, the *paper* does not tell me what those users actually thought was happening or wanted to happen, only which notation fitted those thoughts less badly. > > You're right again in saying that we knew these workstyles existed before the > study. Our paper describes the different ways that developers exhibiting > these workstyles prefer to initialize objects. This was useful information > for us at Microsoft in determining at that time, how best to accommodate the > preferences of many of our customers. > > It's interesting to note that the opportunistic workstyle has since been > observed and studied by others, most notably by Scott Klemmer's group at > Stanford: http://hci.stanford.edu/research/opportunistic/ Let me quote that page: Opportunistic Programming is a method of software development that emphasizes speed and ease of development over code robustness and maintainability. Sheil had a "Power tools for programmers" paper in the early 1980s arguing for "exploratory programming" and how to support it. (Of course Stanford was in touch with work on Lisp and Smalltalk at Xerox for a long time, so this old topic should have been very well known at Stanford.) As a programmer, I want to go at top speed. As a user of other people's programs, I am heartily *sick* of programs where insufficient attention was paid to robustness. Just yesterday I was trying to help a student debug a program in a language not entirely unlike C# where if he started n copies of his program several seconds apart, all went well, but start them in a shell loop and a random copy would get a completely black window. I honestly could not find anything in _his_ code to justify this. This was _not_ a good learning experience for him. -- The Open University is incorporated by Royal Charter (RC 000391), an exempt charity in England & Wales and a charity registered in Scotland (SC 038302).