Friday, September 21, 2012

Speed through failure


I play freecell quite a bit; my wife has correctly said obsessively. I've been playing it since Windows NT and I've easily played 10,000+ games in that time (probably a significant under-estimate). My longest session was on a flight between London Heathrow and Dallas Fort Worth where I completed 138 games in a row, successfully (8 hours of play for anybody keeping tack). 

I've played two very different implementations of freecell. The original version I played was from Microsoft and it did not allow undo. When I switched to my first phone that could run it, I had a version that supported undo.

The difference between "with undo" and "without undo" is the difference between night and day; it's analogous to big design up front and test-driven development. 

Before I had undo, finishing 30 games successfully in a row was, at least for me, an amazing feat. My overall success rate was around 75%. More significantly, my play style was also very different. 

As a contrast, with undo I'm at 99.9%, having won about 1550 games out of 1551 on the current version I play (to be clear, I didn't lose a game, something happened during a reboot, but that's fine, one incorrect stitch and all that). It is not that I've gotten better at Freecell. In fact, by some measurements (metrics) I've gotten much worse.

Before undo, my first move would typically take 1 - 15 minutes. I'd look at the entire board, review a number of plans plans of attack, work ahead typically 10 - 15+ steps, while keeping track of the free cells using my fingers. The only time I could safely make a move was if I could get back to 4 free cells. Any less and it was risky, often quite necessary, but risk. It was easy to make one small mistake and that was it, game over. My average game time was probably 15-30 minutes, sometimes upwards of hours across days.

Contrast that with a version that supports undo. My current average time across 1500+ games is around 5 minutes and 20 seconds. My first move is typically 1 - 5 seconds rather than 1 - 15 minutes. Seemingly ironically, my longest game is when I have had the undo feature. I had one game that easily took 15+ hours to finish across something like 9 days.

Where does this difference come from? Again, I'm certain it is not that I've gotten better. I have recently tried playing a version without undo and I'm not any better at such a version. I think the difference comes from the option to keep trying without ending up stuck and failing; I just assume I can and will finish it and I stick to it. Without undo, the cost of failure is so great, I have trouble moving; mistakes are not OK. With undo, I can try things out.

My logest ever game resulted from this unbridled optimism. Since I "knew" I would finish it, I stuk to it. In fact, I was able to finish it with what I considered a rather exotic and certainly counterintuitive start that left me with no free cells for a number of turns. The start was one I would have never tried without undo for a few reasons: First, I would have given up long before I even gave it a try. Second, because of undo, the cost of such an experiment was minimal, wile the payback was a possible (and likely) success.

Most of the time things take much less time. I'm able to try out ideas quicker, keep less in my head since I can always back things out and even try more wild ideas that ultimately leave me at what I would consider too risky without the undo option, which seems to be required for some layouts.

What can I take away from this?
  • Being able to experiment and fail without it being too costly generally speeds up my final results.
  • Being able to experiment has opened me up to more, different kinds of starts and moves than I would have come across playing without that option.
  • Even so, because of the optimism, I need to make sure I'm not giving up at all or after too much sunk cost.

The big question is this: Does this observation reflect an innate nature of the game, or do I play the game as I live my life?

Friday, January 27, 2012

My Android Apps so Far

Here's a list of what I have installed as of 2012-JAN


NameNotes
Adobe Flash Player 11.1Because I want my battery to drain.
AldiokeReader - not sure what my favorite one is yet.
Amazon MP3
Anagram Word Scramble
Android Task Manager
Angry Birds
Apparatus Lite
Amazon Appstore
Cartoon Wars: Gunner+
Cool Reader
DirectTV Remote
Dropbox
Evernote
Extensive Notes Lite
Freecell
GO Keyboard
GO Keyboard Pad plugin
Google+
GunsNGlory WW2
JavaIDEdroid
LG TV Remote
Maps
Moon+ Reader
Music
MX Video Player
MX Video Player Codec (ARMv7)
Netflix
NVIDIA Globall Demo
Osmos HDGet this. Great game. Also on iOS, Linux, Mac, PC, Steam
Pandora
Perfect Viewer PDF Plugin
Pinball Deluxe
Practice English Grammer 2
RealCalc
Replica Island
SketchBook X
Skype
Skyrim Live Wallpaper
Slice It!
Solitaire
Speedx 3D
Sudoku Free
Terminal IDE
Torrent-fu
tTorrent
Twitter
U Connect
VI Reference
Whiteboard
Wind-up Knight
Writepad

Friday, November 11, 2011

A Story About Too Much Power

A Story About Too Much Power


Note: In this article I talk about JMockIt. However, these comments generally apply to that class of mocking frameworks. This list includes (at least): JMockIt, Typemock Isolator, PowerMock. These are the current breed of mocking tools - all very powerful.

I gave a talk in Berlin November 10, 2011. The subject was ostensibly about legacy refactoring. It was really about a lot of things, the starting point being legacy refactoring. Rather than using problems out of a class on legacy refactoring, I wanted something fresh and shiny. In the past, I started the talk with the following contrived example:
Continue reading at my wikisite...

Monday, October 17, 2011

Sometimes you need the big guns

I'm a fan of using libraries to create test doubles. Having said that, I'm not going to use them all of the time and even when I do my primary use is a stub and sometimes a spy.

In C++ I have yet to try any libraries; I tend to hand-roll my test doubles. There are tools, I just haven't taken the time to try. I can say the same about Objective-C - though given the id type, the libraries are starting with the basic supported needed to go a great job.

In Java I prefer Mockito, while in C# I prefer Moq. These are light-weight tools that give you nice/loose mocks (my preferred place to start). There are more powerful tools to be sure.

For example, in Java there's JMockIt and in .Net there's a commercial product called Isloator.Net. These tools allow you to do things that verge on black magic. For example, do you want to override a static method? No problem. How about a sealed class? No problem.

In both Java and C#, these 4 libraries are using either proxies or dynamic byte code (IL) injection. In a strong sense, these tools are highly stylized forms of Aspect Oriented Programming. What the second two tools allow you to do is change the language semantics, on the fly, per test. That is exceedingly cool to me.

Unfortunately, I have a rule that guides me regarding cool things. If I genuinely think something is cool, then it's probably too much to use in general. That rule serves me well. However, like all rules, it will not apply at some times.

Years ago at Hertz I used Aspect Oriented Programming to solve a problem. The original design, which I was a part of creating, using a decorator, did not scale well. 2 - 3 years in we didn't want to change that. Had we started from scratch, a reflective object walker would have made more sense. Rather than rip out a deeply embedded design, I created a simple aspect that gave us the same semantics as the decorator and obviated the need to create error-prone, and mostly pass-through classes. The result is still in use years later and it's pretty much maintenance free.

Last week, I came across a case where using these more powerful test-doubling tools seems to make sense. I was working with a client and they have an API I'll call X. They have a number of tests they are calling unit tests, which are actually functional and integration tests rolled up into one. Those tests are somewhat fragile but they do add value. They could improve those tests by making a clear distinction between functional and integration tests, and I hope they will.

We decided to give a go at writing unit tests, or at least tests that were much closer to a unit orientation rather than an integrated or functional orientation. The first example was generally successful. The developer had already identified the place of change and, more importantly, generally knew the required changes. The underling object was a bit big and hard to make, so we captured a good example of an object in a file (moving slightly away from unit orientation) and used it to continue the test.

Previously, for a similar test, they would need to initialize "the application" in the form of a Project and several such things. Now we had a stand-alone test that worked in memory with very little setup required.

We then tried it on another part of the system that was more coupled with the X API. The object we needed to create was complex and required an initialized environment to create. Furthermore, in its creation, the objects upon which it depended were sealed. The type itself was also sealed.

We first tried to use reflection to call private constructors, but the underlying type proved a bit too complex and API X is highly coupled. Ultimately, we backed up a touch and did something similar to the previous example: extract an object, put it in a file. Even so, reading this file, unlike the first example, still required a fully-initialized environment.

While it might be possible to introduce interfaces and then write all code to depend on interfaces, essentially using an adaptor for every sealed class, the signal to noise ratio seemed low. The option we chose was to minimally initialize the system. However, this will require either running from a particular directory or doing some (necessary anyway) work to make the execution environment quite a bit less coupled to directories on the file system.

I think a more powerful tool, like Isolator.Net, makes sense in this case. It still seems a bit cool for my general use, but sometimes those handy rules are the problem and not the tool.

Thursday, September 15, 2011

Mockito using thenAnswer versus thenReturn

Mockito is a great tool for creating Test Doubles. Some time back I wrote a tutorial demonstrating Mockito. At the time, I did not need to use a feature of Mockito where the value returned by a stubbed method was evaluated later instead of immediately.

The week of 2011/9/11 I was working with a few people and we came across a need based on the design of an underlying domain class. I cannot share with you the real code, but the only thing that was important was the return type of the method in question:
package shoe.ex;

import java.util.Enumeration;

public interface ProblemInterface {
  Enumeration<object> returnTypeHurts();
}

To understand this problem, let's look at a simple example:
package shoe.ex;

import java.util.ArrayList;
import java.util.Iterator;
import org.junit.Test;

public class WhenToUseThenReturn {  
  @Test
  public void thisWorksFine() {
    ArrayList<object> aList = new ArrayList<object>();
    aList.add("Brett");
    Iterator<object> iterator = aList.iterator();
    iterator.next();
  }
}
This first example works without fail. However, change the order of lines 12 and 13:
@Test
public void thisDoesNotWork() {
  ArrayList<object> aList = new ArrayList<object>();
  Iterator<object> iterator = aList.iterator();
  aList.add("Brett");
  iterator.next();
}
And you'll see the following failure:
java.util.ConcurrentModificationException at
java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at
java.util.AbstractList$Itr.next(AbstractList.java:343) at
shoe.ex.WhenToUseThenReturn.thisDoesNotWork(WhenToUseThenReturn.java:14)
<snip>

What is going on?
If you create the Iterator before you update the collection, then the Iterator becomes invalidated. This is also true of Enumeration (used in the interface above).

Why does the method signature of the interface cause problems?
In the underlying problem, we needed to create a stub class that returns hard-coded collections. Now by "hard-coded" I really mean hard-coded per test. That is, each @Test needs its own set of values, which is typical (or if not typical, why are you writing generic test setup?).

The initialization of the underlying stub object, using Mockito, was created in the @Setup. During the @Setup, the hard-coded return values are created by first creating empty collections, then associating those empty collections' Enumerations with the interface. Here's such an example:
package shoe.ex;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import org.junit.Before;
import org.junit.Test;

public class WhenToUseThenReturn {
  private ProblemInterface mock;
  private ArrayList<object> arrayList;

  @Before
  public void createMockObject() {
    arrayList = new ArrayList<object>();
    mock = mock(ProblemInterface.class);
    when(mock.returnTypeHurts()).thenReturn(
      Collections.enumeration(arrayList)); 
    }

  @Test
  public void seeTheProblem() {
   arrayList.add("Brett");
   Enumeration<object> i = mock.returnTypeHurts();
   i.nextElement();
  }
}

To make this work, the following call:
Collections.enumeration(arrayList)
Must happen after this call:
arrayList.add("Brett");
So one option is to add to the collection then execute this line of code:
when(mock.returnTypeHurts()).thenReturn(Collections.enumeration(arrayList));
Using thenReturn forces moving the statement out of the @Before method. More importantly, assuming there are many @Tests (and in this case there are), you'll have to remember to do that in each individual test. That's a violation of the DRY principle.

Alternatively, you can use thenReturn to have Mockito defer evaluation until later. The method thenReturn takes an anonymous inner class, so it's a few more moving parts:
when(mock.returnTypeHurts()).thenAnswer(new Answer<object>() { 
  public Object answer(InvocationOnMock invocation) throws Throwable {
    return Collections.enumeration(arrayList);
  }});
Rather than creating the Enumeration immediately, this instead creates an instance of an anonymous inner class that will be executed later. When the code uses the returnTypeHurts() method, this anonymous inner class instance will create a new Enumeration. Also, since this performed each time the returnTypeHurts() method is called it does so again. In fact, using theAnswer behaves semantically more like the underlying code so it is a better fit.

Should I always use thenAnswer?
I don't think so. While it will always work, it's not always necessary. I'd recommend using the simpler thenReturn unless it causes problems. In practice, I've not had to use it very often.

Why now?
This example has an interface that returns a generated value. Furthermore, that generated value is only good so long as the underlying collection does not change. And finally, we need to perform this setup for every test (there's only one here, but there will be more in the real system), and each test requires unique collection contents. This combination makes it necessary to defer creation of the Enumeration until after the collection has been updated. The collection needs to be updated in the test, therefore we need to either create the Enumeration in the test or later. By using thenAnser we manage to do it later.

Thursday, May 5, 2011

Bambo is a tough teacher

I needed to do some yard work. It’s nearly always windy in Oklahoma where I live, so when I got up Friday morning I noticed a distinct lack of wind and thought it would be a good idea to get started on the yard work. I was tired of getting dust, dirt and other unmentionable things in my eyes and ears and nose.

Things started as normal, but since I got started so early I took the time to clean up the west side of the house. Then I managed to rake and clean up a bit more. I pulled some weeds. Much of the work I did was not visible most of the time but it bothered me just a little. Then I got to the part of the job that was visible. I took a little extra time raking, sweeping, mowing, etc. I was “done” earlier than I had expected and then I made a crucial decision, it was time to take care of the bamboo.

3 years ago I planted an 8 foot by 2 foot plot of bamboo. When I did that I dug down 2 feet into the ground. Not knowing how to dig like a professional, it took me a long time - total planting time was 13 hours. 32 cubic feed of dirt and clay takes a lot of work to remove. I needed to dig so deep to put a protective lining around the bamboo. There are two kinds of bamboo you might plant. One kind stays in clumps and doesn’t invade everything. That’s not the kind I planted. I plated the kind that takes over. Thus, I needed to dig 2 feet into the ground and surround the bed with a thick plastic lining. I did all the prep work but then I decided I would let it move into my yard but not into the neighbor’s yard. So I left the front of the bed open.

Fast forward 3 years and notice that the bamboo moved more quickly than I imagined. I’ve been meaning to get to this outside job, but I’ve managed to put it off. I decided to dig a 2-foot deep trench in the front of the bed and add in the missing plastic lining. It took me about 6 hours. Here it is two days later and my hands and forearms are still quite sore. Much of the digging at the top was easy, but as I got deeper, the digging got more difficult. Then I hit clay. That was even worse. I really didn’t have the right tool for the job. I needed a trench digger, but I had a regular shovel, a gardening shovel and a standard spade. These were ok at the top but not as I worked deeper into the ground.

Finally, needing a break, I went to buy more lawn supplies including: pete moss, top soil, steaks, fencing, grass seed, lime, oh, and a 4 inch trench digger.

I was about 20 inches into the ground. The last inch had cost me plenty. The 2 inch bruise on the palm of my right hand (yes, I wore gloves) is one example. That last inch took me maybe an hour. 4 more inches would take me longer than another 4 hours. The trench digger, however, made it more like 15 minutes.

When I started digging I knew I needed to start wider at the top because the trench would naturally get narrower as I worked my way into the ground. This might not be the case if either a: I knew what I was doing, or b: I had the right tools for the job. So I started much wider than “necessary” to give me a little slack later.

That decision was key. Even though I eventually bought the right tool for the job, a 4 inch trench digger, if the trench was too narrow to use it, then I still would have been out of luck. So this decision made it possible for me to use the right tool later.

I managed to finish the trench, line the bed, replace the soil and then get about 100 square feet of yard reseeded.

Other than sore muscles, bruises and the satisfaction of getting something off my well-aged to-do list, what other take aways do I have from this experience?

  • Adoption of best practices
  • Leave a little slack
  • Right tool for the job

Adoption of Best Practices


There’s a great story of a manager of a Coca-cola plant who’s numbers were far better than his peers. When asked what his “secret” was, he said simply that rather than take a best practice and modify it to meet what the plant did, he instead modified the plant to match the best practice. His secret was not trying to be too clever.

I knew that I should have lined the bed three years ago. I knew that bamboo can spread. There was a well-established best practice on planting bamboo. It was easy to follow. I had everything I needed to follow the practice. I had a 2 foot deep hole just begging to be fully lined. I though I was more clever and decided to just line the back of the bed. So 3 years later I managed to spend 6 hours doing something that would have been an additional 2 minutes. If I had just followed the best practice recommended by people who were experts, I would have avoided most of the work I did and all of the bruises, scratches, etc.

The best practice when adoption best practices is to adopt the best practice. If you decide to modify a best practice to your local conditions before having direct experience with the practice, the very problems that lead you to consider adopting the practice will be the same thing that will mange to remove all the teeth out of the best practice.

Does this mean you don’t adapt the practice? No. It just means you need to fully understand the practice and more importantly what are its intentions before you start to modify it. When you have direct experience with it, that, coupled with the knowledge of your current challenges, might give you a better chance of coming up with something that works even better. Or, you might just keep with the practice as is.

I had another recent experience with this regarding Uncle Bob’s TPP. From my reading of his example, he also practiced TDD as if you mean it. I talked to him about this and sure enough he confirmed it. I was working on a “known” problem - a kata. I resisted doing what he had done because it wasn’t what I had done in the past. I even recognized this and mentioned it to him. I decided to just see what happens. Well I ate crow that evening because when I did it, the kata moved quite smoothly for me. In fact, better than it had in the past. It didn’t require as much planning ahead.

What can you do about this? If you can figure out how you tend to resist change, and then notice when you are doing so, you might be able to just work through it. In my case I could tell I was resisting change rather than rejecting the idea - if I slow down a bit and just pay attention I can make that determination, something I learned from practicing Yoga. As for how you can start to learn this, here’s one exercise you could try. Another idea is to try Yoga - or - try pausing whenever you find yourself resisting something and ask yourself the question: am I resisting the idea or change. If you’re not sure, then give it a try (and assume you’re resisting the change).

Leave a Little Slack


I stared digging by marking a line where I wanted to put the new edge. I then dug a shallow trench about a foot wide. The extra width was to give me enough room to work. While it might seem like more work at first, in fact it gave me enough rom to move around, attack the dirt at different angles and I used the “extra” dirt to fill a hole dug by my dogs in the bamboo (I hope it recovers).

As I got deeper, I used different kinds of shovels, and the trench became narrower because of the angle I was using to dig, which was related to the tools I used. When I finally hit clay, the larger shovels were too hard to work with both because of the angle but also because of the density of the clay and the amount I was trying to move. I started using a hand-shovel I use for gardening.

When I bought the right tool it was possible to use because there was enough room for it. Unlike the other shovels that are 10 inches wide or wider, this one was 4 inches wide. It worked perfectly. I was able to take out about 1 - 2 inches laterally and 3 - 4 inches deep at a time. While it might seem like slow going, it was blazing fast compared to my previous attempts.

Right Tools


Growing up on a farm in Iowa, my dad often reminded me of the idea of using the right tool for a job. You might be able to use another tool, but if you use the right tool for the job, it will just go smoother.

My wife bough a few pruning tools last fall. One in particular was much nicer than I might have bought. In the past I would just use the shovel to try and break the roots. This, as you might imagine, is hard work. It will eventually work, but as the roots get bigger, the work grows at least by the square of the diameter (I think the difficulty is NP hard (grin) but that’s because I don’t do this work on a regular basis). When I started the trench this time, I used that tool instead of the shovel. To be sure, it required a touch more work. I had to find it in the garage (it was where I though it would be), I had to carry it to the back yard. When I finished the job I had to put it away.

However, it was exactly the right thing to cut the roots that I came across. Trying to use a shovel to cut those roots in the past versus using a proper pruning tool is a great way to have a kinesthetic experience on the use of the right tool for the job.

As I used the pruning tool, I was reminded of this “right tool for the job.” When I took a break to buy more stuff to do more work on the yard, I looked for and found the trench digger. It was a bit expensive. I’ll probably use it for this one job and never again. Even so, within 2 minutes of using it, I did not regret the money spent versus the time saved. This initial impression was confirmed when I moved so much more quickly than I had been moving in more difficult situations. If I never use that tool again it was still the right thing for me to do. If I ever need to dig a trench again, I’m ready for it.

The danger is that you buy a tool for the problem you think you have but while it solves the apparent problem, the actual problem is something else. Case in point, scripting tools for ui-based checking.

I am not against using scripting tools, far from it. However, the secret of checking the UI is to design the code so that most of the logic is unit checkable, here's one such example. The scripting tool seems to be solving the problem of manual checking but that’s a surface problem. A well-design system makes it possible to easily look at it in ways that let you know if it works as expected. We’re used to thinking about headless checking, but what about body-less checking?

Conclusion


When adopting a practice, consider embracing it without change to get an actual read on the practice. Consider not being too clever. Once you have actual experience, then consider adapting the practice. I think being able to do this requires similar work to design. Often we think of the work we do and how we do that work as so tightly coupled that they cannot be separated. To be sure, this is sometimes the case. Even so, trying to treat the what and the how as separate can lead to a better understanding of where things are separable and where they are not.

There's a cost to switching tools. Your current tool may have costs that are either hidden or are accepted to the point that these costs are effectively hidden. As with practices, look around and see what tools other people are using. If you decide to try using a tool, use that tool. Don't adapt the use of the tool to your environment, conform to how the tool is designed to be used. Once you have an idea of how the tool is meant to be used, then consider adapting.

Making room to try a practice or tool as it is intended before adapting is a form of slack. Doing this may appear to take longer. In the short term this might be the case. Often what we see as a short term think becomes "the" thing and the decisions we made in the original context cost us more when we suddenly realize we're running a marathon. Introducing a practice or tool is introducing change. Most people resist change, so maybe applying this idea about adopting practices is a way to preemptively respond to change. Consider giving yourself some slack and try to embrace the tool or practice long enough to have an opinion not too clouded by your current reality. You might be surprised what happens. Try this a few times to see if it works for you. If it doesn't then consider adapting how your adopt changes.

Wednesday, July 7, 2010

Over design and the “but” exercise

Do you suffer from “over-design”? Do you even know if you over design? Is there any way you might be able to self assess? (Are you asking, "What is this "over-design" of which you speak?")

Here’s an exercise you can try on yourself (you can also try this with a team, but I’ll have more to say about that below).

The exercise seems simple. Count the number of times you say “but” in a day. Simply track it. Don’t do anything with the number, just count it. That’s it. You can get a bit more sophisticated and add other words and expression such as:
  • However
  • That won’t work
  • ... because

You might want to consider working up to that.

Warning, you might be thinking it as well. Count those if you can. Or, maybe work up to those as well.

Ultimately, what you are trying to do is simply observe the number of times throughout the day you reject an idea. That’s often what you do when you use the word but.

In an of itself, rejecting an idea is not a problem. Even a pattern of rejection is not a pattern. Observations like this are data. You might try to find patterns or correlations such as:
  • I tend to reject Andy more than I reject Mary
  • I seem to reject ideas in the morning or during lunch
  • Initially I reject ideas from my wife that require me to do work. I tend to come around (this is one of my patterns).

Once you discover those patterns, you might ask yourself if they fit for you. If they do, great. Maybe you got a little personal understanding. You probably became more keenly aware of how often you tend to reject ideas.

You can take this one step further to find times when you might be rejection and idea and are not even aware of doing it. Try the exercise again. This time, rather than counting the number of times, instead make the observation that you are saying “but” (or some other such expression). As soon as you do, and this is key, try to figure out where in your body you hold that feeling. It might be in your stance, hands on your waist. It could be in your celiac plexus, shoulders or your neck. If you are not used to making these kinds of observations, it will probably take many tries before you notice. You've probably become so used to manifesting a physical response to a given situation that it’s like fish in water.

As you become able to make these observations, then next thing to look for is patterns. Maybe:
  • When I talk to women and reject their ideas, I stand in a particular way
  • I tend to get hungry when I have to say no

As with the previous exercise, it’s data. Does the habit fit? If so, great. If not, then maybe try to find a way to break the habit. What you are doing is turing unconscious decisions or habituated behaviors, into conscious ones. You are giving yourself more choice. Really, you’re taking control of yourself from patterns you’ve picked up over the years.

There are several ways to do so. You might simply:
  • Smile in that situation
  • Breath (through the nose into the pit of the stomach)
  • Put a micro-bend in your knees
  • Tapping (the NLP variety, though if you have the shoes...)

The interesting thing is that making these kinds of observations will mostly likely result in some kind of change. Awareness tends to lead to change. If a particular behavior fits, then the observation will not likely change a given behavior. However, it might still make you aware and give you other options.

What about trying this in teams?

Danger, danger.

It’s certainly OK to try this with a group. One thing I’d be concerned about is applying a metric to the result of the count. For example:
Brett is the most negative, I counted how often he said “but” and it was the highest

This is a personal exercise. It’s easy for it to turn into a competition. While you might ask someone to help you notice something you are doing, it’s easy to turn a simple exercise like this into an ongoing battle.

How does this have anything to do with over design?

Have you ever noticed that moving from the design of a solution to the design of a framework is made with one “...but what about this...” at a time?

I’m certainly guilty of this.

On the one hand, implementing with too little context can lead to a myopic design. On the other hand, considering too much context often leads to over generalization (framework-itis). There’s certainly a balance somewhere in there.

Over design is a problem when you’ve designed for A but you really needed a design for B. Over design typically is more like, "I’ve introduced flexibility for A, B, C, D, E, F, G but you needed a subset of F".

How can this “but” exercise help?

When you are working through requirements on your way to design, simply track how often you ask the questions in a form such as “...yes, but what about...”. Every time you do, a kitten dies. Are you missing the forrest due to all of the trees? I've noticed this in myself and I think it's a pretty common characteristic in developers.

Extreme Test Driven Development is another way you can avoid over-design at the possible expense of myopic design. As with most things, there are many good paths somewhere in the middle.

In any case, if you think you might be a culprit of over design, or maybe even being a bit of a Negative Nancy, maybe this exercise could help you determine patterns driving your behavior. With that information, you might consider trying on different patterns to see if they fit better.