XProgramming > XP Magazine > Eine Kleine Refaktorung
COLLECTED TOPICS: Adventures in C# | Documentation in XP | Book Reviews
Eine Kleine Refaktorung
Ron Jeffries
05/14/2005
Friedrich Brunzema offered a problem on the xp list, with a series of stories that he felt were noticeably less costly to do in one order than another. I've nothing to do tonight, so I'm going to work the problem -- more accurately a subset of it -- to see what happens. (Updated to include a bit more summary, and the code so far.)

The Problem

Here's what Friedrich says:

Recently, I was working on some functionality where I think the order execution of a story affected the cost. I will change the domain slightly to protect the innocent.

Please bear with as I am trying to explain the stories a bit. The customer has a dialog where they need to keep track of a set of specific directories. In each of the directories, there are a number of files. The customer wants to be able to choose a subset of the files in the directory, so that they can do something specific (like do a search) to the selected files. Pretend for a second that folders and files only have the containment relationship, nothing else. The dialog looks something like this:

+C:\temp\Folder1                        Add Button
|------File1.txt                Remove Button
+C:\temp\Folder2
|------File4.txt                OKButton  CancelButton

On the left side we have a populated tree control, on the right side an 'add' and a 'remove' button. Usual OK and Cancel to the expected things.

The add button brings up a file open dialog, that lets the user pick a text file. The remove Button deletes the currently highlighted node from the tree control according to some rules.

Here are some of the micro-level stories the customers/developers came up with.

  1. Add file. User clicks add, picks a file, system creates Folder Node, and file subnode.
  2. Don't add a duplicate file. Add file that is already shown in the tree control. System does not add a duplicate file.
  3. Don't add duplicate directory. User picks in a directory that already appears as a node, but the chosen file is different from any of the ones that may already be there. In this case, don't add a new duplicate folder node, but add it under the correct existing directory node.
  4. Remove Directory Node. User clicks on the directory node, then on Remove. System removes directory node and all child nodes beneath.
  5. Remove Directory Node if Last File Removed. User clicks to select the last file node under a directory node. User clicks Remove. The system removes both the file node, as well as the parent directory node.

Each of these micro-level stories incrementally add business value to the system. In the implementation, we used a model-view controller type concept. The model stores the data, and fires an event when a change has been made in the system. The event handler in the UI just re-populates the tree. The advantage of this type of setup is that we can easily unit- and story test the functionality.

Scenario 1.

Team is given these stories the given order, but only one story micro story at a time. The team must complete the UI functionality and pass the automated acceptance test for the story to be complete, and before being given the next story. [This is contrived, but does make a point, in our case all of the stories were given at once.]

Story 1.

Pair does the simplest thing possible, and avoids speculation. The pair implements the data structure within the model as an array [conceptually ArrayList or vector] of strings, with the implication that the even array indexes hold the Directory node information, and the odd hold the file nodes. As soon as a directory/file pair is added to the ArrayList, the model fires the ArrayList to the UI. The UI's event handler now walks the array, creating DirectoryNode/File node in sequence. Things work great, storytest passes, UI works as planned.

Story 2.

Pair has no trouble implementing "Don't Add duplicate file" ��e open file dialog gave them both a file name and directory name. The model is searched for a matching directory name. If the next index matches the file name, we've got a duplicate, so we don't add it. If the directory name matches, but the file does not, we do nothing, because we've not been asked to support multiple files under one directory node.

Story 3

OK, there's a problem with this one. Problem is that for the first two stories, the pair has assumed that a simple array list with alternating directory/file names was a sufficiently good model to store the data. This story shows that its not. When a new file comes in that is under an existing directory node, we don't have room for it, because of our implicit assumption that directory and file nodes alternate, and that there is only ever one file under each directory. But the problem is not only in the model. Remember that the UI has code to populate the tree. Change the model, and you also have to change the UI population code. Since we've done Unit and Storytest driven development, both our Unit as well as our Storytests carry the implicit assumption that directory and file nodes alternate. No problem, just refactor everything. Stories 4,5,6 After the refactoring is complete to a data structure such as in Scenario 2, no problems with the implementation.

Scenario 2.

Team is given all the stories at once. Pair looks at all the stories, and decides that all of them are best done by one pair, since multiple pairs would run into each other anyways coding on this. They see Story 3, and know right away that the structure in the model must be

class TreeViewModel:
        ArrayList Directories  (of type DirectoryNode)

class DirectoryNode:
        ArrayList Files (of Type FileNode)

class FileNode:
        String Filename;

They pick story 3 as the first story, because it helps them drive the development of the system. Since story 3 is a superset of story 1, they get this one for 'free'. Not really free, but what they save is the refactoring time, time spent on changing the data structure model in scenario 1. Notice that they don't pick any of the "Remove" stories before the "Add" stories. I would assert that there is a logical order in the development that you really should to the "Add" before doing the remove.

Ron might argue that the cost between Scenario 1 and Scenario 2 is not all that different, if Scenario 1 was coded with the principles of simple design in mind. I beg to differ -based on my experience, I would argue that using Scenario 2 is significantly cheaper, since you don't have to refactor the basic data structure in the model and all of the test dependencies. This is also a really small example, where the total cost is small, and the cost of the refactoring is still relatively small.

Now for the killer question: Are people in Scenario 2 speculating?

Now I'm not going to have the time to work all of Friedrich's stories, and I wouldn't proceed with quite the same "micro-stories" as he chose in any case. Instead, I'm going to work mostly on the "model" side, and finesse the issue of filling in an actual TreeView object as long as I can. I'm interested in just one thing: does it matter whether I start on Story 3 or not? Let's find out.

Story 1

In Story 1, the user clicks a file name and the program adds that file and folder to the list of files to be processed.

Begin with a test ... in this case, with a test class, including my favorite first test, Hookup:

using System;
using NUnit.Framework;

namespace ActionListLibrary {
  [TestFixture] public class ActionListTest: Assertion {
  
    public ActionListTest() {
    }
    
    [SetUp] public void SetUp() {
    }
    
    [Test] public void Hookup() {
      Assert(true);
    }
  }
}

Fortunately, since I'm out of practice, this runs with little trouble. I had to reference NUnit, and I had to rename some of the files and the project. Now I'm green.

First real test, add a file to the list. I've decided to call the list of files to be processed an ActionList. Here's the test:

    [Test] public void AddOneFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Contents.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Contents.Count);
    }

Now frankly this is a pretty big bite for me, but I felt up to it. I'm assuming a class ActionList, and an AddFile method that takes a directory name and a file name, and a Contents property that returns an ArrayList of files to be processed. Right now, I'm just checking the count, and I plan to figure out what the list contents really are in a minute. First, to make the test run:

using System;
using System.Collections;

namespace ActionListLibrary {
  public class ActionList {
    ArrayList list;

    public ActionList() {
      list = new ArrayList();
    }

    public void AddFile(String directory, String filename) {
      list.Add(directory+filename);
    }

    public ArrayList Contents {
      get {return list;}
    }
  }
}

This is enough to get the test to go green. Now, as I understand it, my version of Story 1 is done, since I'm not connecting my list to the tree structure widget in the window. This may change things a bit, but on the other hand, I'm about 15 minutes into the work, so I have plenty of time to complete things. I think I'll add another file, just to show that I can. I'll update the same test, though there are those who would call me heretic for doing so:

    [Test] public void AddOneFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Contents.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Contents.Count);
      list.AddFile("dir", "file2");
      AssertEquals(2, list.Contents.Count);
    }

Heretic? Well, there are some folks who think that a given test should have only one Assert, because then when it fails, you know just what happened. There is value to this notion, and often I work in much that style. In this case, though, we are working with sequential behavior, and I see no big advantage to writing three tests to do the work of this one. But now, because I'm not working the tree control side of things, I need to go to the next story:

Story 2

Story 2 is: Don't add a duplicate file. If the same file is added twice, don't put it in the list twice. I'll use this test:

    [Test] public void AddDuplicateFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Contents.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Contents.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Contents.Count);
    }

We add the same file twice, and expect the count not to change. In fact, of course, it does change, so the test is red, as we expect. Time to make it run.

I'm feeling a bit nervous about my concatenation trick. It really won't work at all, of course, since I didn't even deal with delimiters. It's just a trick to get things going. I am inclined to create some kind of a "file description" object with a directory name and a file name, but I'm on a red bar and that seems like a bit of a jump.

I want to get the test green. I'll just save the concatenated value, check the list for containing it, and add it only if not there. (In some other language, I might use a Set, but C# doesn't seem to have one. There's probably some cool HashTable trick, but this will work just fine, I expect. Let's see ...

    public void AddFile(String directory, String filename) {
      String fileDescription = directory+filename;
      if (! list.Contains(fileDescription))
        list.Add(fileDescription);
    }

The test is green. Notice that I forecast the notion of the FileDescription with the name of my string variable. Let's go ahead and create that object. That will lead to something interesting: figuring out how to compare two of them! We'll just posit our new class with this code:

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (! list.Contains(fileDescription))
        list.Add(fileDescription);
    }

Which won't compile until we build the class:

using System;

namespace ActionListLibrary {
  public class FileDescription {
    String directory;
    String filename;

    public FileDescription(String directory, String filename) {
      this.directory = directory;
      this.filename = filename;
    }
  }
}

This compiles, as I expect, but the second test doesn't run. The comparison of the two FileDescriptions is saying they are not equal, because it isn't checking their contents, just their identity. After a little Help-searching to remind myself how to do this, I produce:

    public override bool Equals(Object obj) {
      if (obj == null || obj.GetType() != this.GetType())
        return false;
      FileDescription fd = (FileDescription) obj;
      return fd.directory == directory && fd.filename == filename;
    }

And now my tests are running again. I notice that I've left the two strings public. That's not my fashion, so we'll fix that:

namespace ActionListLibrary {
  public class FileDescription {
    private String directory;
    private String filename;

    public FileDescription(String directory, String filename) {
      this.directory = directory;
      this.filename = filename;
    }

    public override bool Equals(Object obj) {
      if (obj == null || obj.GetType() != this.GetType())
        return false;
      FileDescription fd = (FileDescription) obj;
      return fd.directory == directory && fd.filename == filename;
    }
  }
}

This leads to a surprise: the Equals method still works. I would have expected to have to produce accessors, but I guess that C# lets methods of a class access variables of another instance without objection. That's news to me, but the tests still run, so it must be so.

Hmm. I think my second story is running. I can add files, and detect duplicates.

Story 3

Now Friedrich's third story was this: Don't add a duplicate directory. User picks in a directory that already appears as a node, but the chosen file is different from any of the ones that may already be there. In this case, don't add a new duplicate folder node, but add it under the correct existing directory node.

He's envisioning an underlying data structure that echoes the shape of the tree control. Now the way our code works right now, we could add anything we want, and if it's unique in either filename or directory, it will be saved, and otherwise it won't. How many times the directory shows up in the window is a function of how we interface our ActionList to the tree control, not of its internal structure. I guess I need to learn how tree controls work, and see what we have to provide to one as output from our list. Back to the help files.

It appears that there is a class TreeView, and that what one does is to Clear() it, and then add instances of TreeNode to it. We'll want to add one TreeNode for each directory in our list, and one child TreeNode to that one for each filename in that directory. I'm thinking that the best way to do that is for a user to pass a TreeView to our object and have us fill it up. (The alternative is to have us have some kind of iterator, or return some other odd collection, and for the user of our ActionList to build the tree. That seems more like our job to me.

The protocol for loading a TreeView is typical Microsoft. You say things like

myTreeView.Nodes.Add(aTreeNode).
And to add to an inner node, you use this horrible kind of thing:

treeView1.Nodes[customerArray.IndexOf(customer2)].Nodes.Add(
           new TreeNode(customer2.CustomerName + "." + order1.OrderID));

That's right, you access the outer Nodes using the index of the node previously added. Who taught these people about objects??? I'm not sure whether you get the same one back that you sent in, or not. If you do, we could just hold on to it, as we build.

Now at this point, I'm thinking about our structure. It's flat. It would be possible to unwind it and send the right messages to the TreeView, but then we would have to decode more than we'd like to about the directory subtrees. However ... don't we have to anyway? Well, it's not clear. Friedrich's examples suggest that we don't have to deal with arbitrary nesting. I'll assume that for now.

So how can we build and test this. One way would be to build some kind of MockTreeView that we can send these weird Nodes and Add messages to, and then check in the mock whether we have it right. Another way, and I think it's a better one, would be to return a tree structure of our own invention, which can "clearly" be spun over to send messages loading a TreeView.

We are the point that Friedrich was concerned about. We're about to convert our structure to a tree-shaped one, because our flat structure won't hold up any more. (Our held up a little longer than his did, because we picked a slightly different structure. He was alternating folder name and file name in an ArrayList, and we created a little object. That made sense to me, because the FileDescription object expresses a key idea of the program. But even so, our ArrayList will no longer hold up. What do do?

Well, our new structure wants to contain two levels of objects, the first representing a directory, and the second representing a filename in that directory. I'm filled with fear that this may wreck my whole little FileDescription object, but we'll see: we're just here to see what happens. So when we come in with a FileDescription, let's put it away in a more structured object: we want to wind up with a collection of directories, each containing a collection of files. I'm not sure that I need another test: this is just a refactoring.

ActionList.AddFile() looks like this:

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (! list.Contains(fileDescription))
        list.Add(fileDescription);
    }

We want it to change to think of the list as a list of directories. If the directory is not already there, we'll add it. Then we'll add the file to it (not adding duplicates. Let's just code that by intention:

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      GetDirectory(fileDescription.Add(fileDescription));
    }

This seems straightforward enough. If we don't have the directory already, we add it. Then we get it, and add our new fileDescription to it. So how to code these methods. First HaveDirectory:

    private bool HaveDirectory(FileDescription fd) {
      foreach (ArrayList directory in directoryList) {
        FileDescription first = (FileDescription) directory[0];
        if (first.Directory == fd.Directory)
          return true;
      }
      return false;
    }

Looks right: we just search the directories, see if the first fileDescription directory name is what we need, and if it is, we have that directory. We will have to implement the Directory property on FileDescription, of course. Now for AddDirectory ... hmm, now that is interesting ... I foresee trouble. We can't just add a list with no files in it: if we do, then GetDirectory can't find it. With this implementation, each ArrayList has to have at least one file in it. We'll modify AddFile this way:

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      else
        GetDirectory(fileDescription.Add(fileDescription));
    }

Then we'll make AddDirectory add the fileDescription itself.

Now I have to say here that this is way more code than I really like to write. I haven't had a green bar for over ten minutes, and that's not good for me. But I think I know what I'm up to, so I'll continue. I'm probably wrong ... let's find out. Here's AddDirectory:

    private void AddDirectory(FileDescription fd) {
      ArrayList newDirectory = new ArrayList();
      newDirectory.Add(fd);
      directoryList.Add(newDirectory);
    }

That seems sure to work, and now for GetDirectory:

    private ArrayList GetDirectory(FileDescription fd) {
      foreach (ArrayList directory in directoryList) {
        FileDescription first = (FileDescription) directory[0];
        if (first.Directory == fd.Directory)
          return directory;
      }
      return null;
    }

We note the duplication between this and HasDirectory, of course, especially if you were pairing with me when I cut and pasted the code. We'll fix that, but first we have to make our tests run. I expect one of the tests to run and one not, but I'm not confident. I'm not even sure we can compile. Let's find out.

Two messages. First, no Add in FileDescription. That's a syntax error.

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      else
        GetDirectory(fileDescription).Add(fileDescription);
    }

The second message informs us that we haven't overridden HashCode in FileDescription. We can ignore that for now. Now the tests: AddDuplicateFile passes, AddOneFile (bad name there) fails. Neither is working as it should, because of course now our list Contents method is bollixed. I think ... and this is risky, be sure to call me on it ... that I could just change the tests to send Count directly to the ActionList, and have it return the count of filenames. That seems reasonably safe, though one hates to change tests. We'll modify the tests like this:

    [Test] public void AddOneFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
      list.AddFile("dir", "file2");
      AssertEquals(2, list.Count);
    }

    [Test] public void AddDuplicateFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
    }

And implement Count in ActionList. If this works, I'll be feeling good enough to look around. If not, I am honor-bound to give up and accept that I've bitten off too big a bite. Let's see ...

    public int Count {
      get {
        return directoryList.Count;
      }
    }

This is supposed to be equivalent to what we had, so I expect just the same failures ... and I get them. Now let's do count for real:

    public int Count {
      get {
        int count = 0;
        foreach ( ArrayList list in directoryList ) {
          foreach ( FileDescription fd in list ) {
            count++;
          }
        }
        return count;
      }
    }

This is pretty obvious. The compiler whines because we don't use fd, which gives me a better idea:

    public int Count {
      get {
        int count = 0;
        foreach ( ArrayList list in directoryList ) {
          count = count + list.Count;
        }
        return count;
      }
    }

Does it work? No, not quite. I promised to quit, but I know what the problem is: I didn't suppress duplicates in the Add. Look at this:

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      else
        GetDirectory(fileDescription).Add(fileDescription);
    }

That line adds unconditionally, even if the files match. I beg my pair to let me fix it, even though he is telling me that we have to quit. Please, just this one more thing!

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      else
        AddNonDuplicate(fileDescription);
    }

    private void AddNonDuplicate(FileDescription fd) {
      ArrayList directory = GetDirectory(fd);
      if (!directory.Contains((fd)))
        directory.Add(fd);
    }
                                

I swear this is going to work. Let's see. Yes!!! It does! But that was scary: let's reflect.

Reflection

First of all, that last step was a big one. We wrote a bunch of methods, all by intention, and were fortunate to get them right. If we hadn't, we would have had to back everything out. So that was very risky practice. I was confident that I was on the right track, because I've done a lot of nested list work in my life, but still it was pretty scary.

What if we had had to back it out, what then? Well, I think I would have tried the same approach, only testing each method one at a time. To do that, I would either have had to make more methods in ActionList public, which doesn't bother me, or I would have created a new object for ActionList to hold on to.

That would be odd, because the new object would be exactly what ActionList needs to be. Then Friedrich would jump on me, saying that I had duplicated effort, and he'd probably be right. As it stands, though, we haven't actually backed out much work at all ... we have merely added.

That makes me want to suggest that if we had done the nested list sooner, we wouldn't have saved much time ... most of the time has gone into making these last few methods work. Up until then everything was just add and count, and the writing of tests that we need anyway.

So this is far from proven, but at this moment I think I have the data structure that I need, subject to some more tests perhaps. And although that last step was a big one, I haven't replaced much code. I'm not convinced at this moment that starting with Story 3 would have been faster.

We'll see what Friedrich thinks, when he reads this.

The current "tree" code is pretty straightforward, but it contains some duplication, and may need other improvements. We'll look at that shortly. And we haven't actually done story three, but we are now ready to do it. I actually suspect that it will work just fine.

Discussion ...

Certainly all questions are not answered. I'm suggesting that because I was able to move forward only, not replacing big chunks of code, doing the stories in 1, 2, 3 order doesn't look terribly costly so far. But I haven't actually shown that #3 works, and the implementation as it stands is clunky.

Even if one agrees that the line I took isn't very much affected by adding the single-level tree capability a ways in, it's easy to see that other lines of implementation, like the alternating-list approach that Friedrich describes, might be more difficult. I'm not sure whose position that truth supports: perhaps both. Here's an imaginary conversation between Order, someone who thinks story order is important, and Chaos, someone like me, who is less concerned:

Chaos:

I think I've "shown" that this story order is not inherently worse than starting with Story 3.

Order:

I object: it was easy because you just happened to pick an approach that encapsulated the list, and because you created that data object early on to hold the file description.

Chaos:

This actually proves my point: if we do incremental development "well", we are not likely to get stuck.

Order:

It looks to me like you were lucky. Another person, not moving so quickly to encapsulate those ideas, would have had a bigger refactoring.

Chaos:

My point exactly! Here's why:

  • With an ArrayList more exposed, we probably would have had more trouble. We would have had no place to add methods like HasDirectory() and the like. We must always move quickly to our own abstractions.
  • The same is true with the FileDescription. As soon as we become aware of the difference between directory and filename, we need to reflect that in the code.

When we reflect our ideas well in the code, incremental development works better, and we are able to be more indifferent to the order of incoming stories.

Order:

OK ... but you are saying that incremental development requires very high skill in recognizing the need for new abstractions. You got away with it this time, so let's pretend that I bow to your "lee7 sk1llz". What about the rest of us simple h4x0rz?

Seriously, wouldn't it still be better for people less skilled than your exalted self, to be looking ahead at all the stories? Wouldn't it be better as well if they chose them in an order that is more likely to bring out key abstractions?

Chaos:

Well that's at least two questions. First of all, I very much like to look ahead at all the stories. In fact, I always try to imagine what stories might be coming along later, even if the Customer hasn't mentioned them. Sometimes, though this is risky because it can give people hard ideas, I'll even ask about the future. So I definitely want to know about what's coming up.

But ...I try hard not to build in anything based on what's coming up, until it does come up. Still, I think that the knowledge of what might be coming sort of "informs" my design: I am aware of what's coming and maybe it helps me to lean in the right direction, or reminds me to build in certain abstractions.

In the implementation we just looked at, I started right off with tests that knew about directory and filename. If we had started with just a single string containing both, things might not have gone so well. So my knowledge of what's in the future may be influencing what I do. In any case, I certainly agree with you that I'd rather know.

You also asked whether choosing the stories in a specific order might be better. Friedrich called these "micro-stories". Since this whole game has been a couple of hours' work, counting the article, they probably weren't real stories.

With real stories, I think that things go best if we let the customer have free range in choosing the order of things. That doesn't mean that I wouldn't offer guidance: I would. And it doesn't mean that I wouldn't ask for a specific order if I felt it was really important: I would.

But I do try hard not to need to do that, and the point of this article and discussion is to suggest that while story order might be important sometimes, it is probably not as important as we often fear.

Order:

So you are saying that skill in incremental development is necessary to accepting stories in any order, and that the higher your skill the better you can do at changing things without high cost?

Chaos:

Exactly. I think that the way to bet is that we should think about everything we know, but put it into the code only when it's needed. Instead, make sure that our code runs all the tests, contains no duplication, is expressive, and minimizes entities. Kent Beck wrote that in his first edition XP book, and I've found it to be one of the most powerful little sets of rules I've ever seen.

Order:

Cool. I can see that. Let's take a break! Leave the listings in case anyone wants to see where we are.

Chaos:

Next time, if we choose to go further, we should remove some duplication in the ActionList, and look at the remaining stories, especially Story 3. I expect that one to "just work", but you know how that kind of expectation can go. Here's where we stand. Things are working, but not beautiful:

The Code So Far

ActionListTest

using System;
using System.Collections;
using NUnit.Framework;

namespace ActionListLibrary {
  [TestFixture] public class ActionListTest: Assertion {
  
    public ActionListTest() {
    }
    
    [SetUp] public void SetUp() {
   }

    [Test] public void AddOneFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
      list.AddFile("dir", "file2");
      AssertEquals(2, list.Count);
    }

    [Test] public void AddDuplicateFile() {
      ActionList list = new ActionList();
      AssertEquals(0, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
      list.AddFile("dir", "file");
      AssertEquals(1, list.Count);
    }

  }
}

ActionList

using System;
using System.Collections;

namespace ActionListLibrary {
  public class ActionList {
    ArrayList directoryList;

    public ActionList() {
      directoryList = new ArrayList();
    }

    public void AddFile(String directory, String filename) {
      FileDescription fileDescription = new FileDescription(directory, filename);
      if (!HaveDirectory(fileDescription)) {
        AddDirectory(fileDescription);
      }
      else
        AddNonDuplicate(fileDescription);
    }

    private void AddNonDuplicate(FileDescription fd) {
      ArrayList directory = GetDirectory(fd);
      if (!directory.Contains((fd)))
        directory.Add(fd);
    }

    private bool HaveDirectory(FileDescription fd) {
      foreach (ArrayList directory in directoryList) {
        FileDescription first = (FileDescription) directory[0];
        if (first.Directory == fd.Directory)
          return true;
      }
      return false;
    }

    private void AddDirectory(FileDescription fd) {
      ArrayList newDirectory = new ArrayList();
      newDirectory.Add(fd);
      directoryList.Add(newDirectory);
    }

    private ArrayList GetDirectory(FileDescription fd) {
      foreach (ArrayList directory in directoryList) {
        FileDescription first = (FileDescription) directory[0];
        if (first.Directory == fd.Directory)
          return directory;
      }
      return null;
    }

    public int Count {
      get {
        int count = 0;
        foreach ( ArrayList list in directoryList ) {
          count = count + list.Count;
        }
        return count;
      }
    }
  }
}

FileDescription

using System;

namespace ActionListLibrary {
  public class FileDescription {
    private String directory;
    private String filename;

    public FileDescription(String directory, String filename) {
      this.directory = directory;
      this.filename = filename;
    }

    public override bool Equals(Object obj) {
      if (obj == null || obj.GetType() != this.GetType())
        return false;
      FileDescription fd = (FileDescription) obj;
      return fd.directory == directory && fd.filename == filename;
    }

    public String Directory {
      get {
        return directory;
      }
    }
  }
}

XProgramming > XP Magazine > Eine Kleine Refaktorung
COLLECTED TOPICS: Adventures in C# | Documentation in XP | Book Reviews