Here is an example that we will develop in detail. We did a VS exercise where two threads of execution shared a "file" for reading and writing. The controller ensured that the file was never simultaneously used for both reading and writing. The example was structured like this:
The code in FileController remembered whether the TextFile was being used for reading or writing and used this information to do the reads and writes.
It is safe for multiple threads to read the same file simultaneously, and the threads can even read at different rates of speed, since no thread changes the file. We should modify the above design so that it allows multiple reader threads. How is the controller changed to handle multiple readers?
In our original VS solution, the controller remembered which line the reader thread was reading. It's too complicated for the controller to track the progress of multiple reader threads --- it's better to manufacture, for each reader thread, an object that remembers the progress of the reader thread. We use a factory method, makeReader(...), to manufacture TextFileReader objects:
When each ReaderForm thread wants to read the file, it calls the FileController's openRead() method, which checks if the status is Available or Reading. In these cases, the status is set to Reading, the numberOfReaders is increased by one, and a thefile.makeReader(...) manufactures a FileReader object. The object's handle is returned to the ReaderForm, which calls the object's read method to read the file's lines, one at a time. (The FileReader object holds an internal counter to remember how far the file has been read. Once the file is completely read, the close method is called to tell the controller that the reading is finished.)
The makeReader is a factory method --- it manufactures FileReaders.
When there are multiple ReaderForm threads, each one calls openRead() and obtains its own manufactured FileReader for reading the shared file.
The FileController is programmed to allow a WriterForm thread to write to the file only when the file's status is Available. (Then, the status is set to Writing, and the file can be written.) In the next section, we see how to define class FileWriter to do file writing.
Here are the relevant classes:
===================================================
// defines the controller's state:
public enum Mode {Available, Read, Write};
// controls access to a "File" that can be read or written
public class FileController {
private TextFile thefile; // handle to the file that is controlled
private Mode status = Mode.Available; // current mode of use
private int readers = 0; // number of active readers
public FileController() { }
// allows multiple readers; returns handle to new FileReader
// If file is busy writing, then returns null.
public TextFileReader openRead() {
lock(this){
TextFileReader r = null;
if (status == Mode.Available || status == Mode.Read) {
status = Mode.Read;
readers = readers + 1;
r = thefile.makeReader(closeRead); // see class TextFile below
}
return r;
}}
// closes file
public void closeRead() {
lock(this){
readers = readers - 1;
if (readers == 0 ) { status = Mode.Available; }
}}
// opens for writing; returns FileWriter. If file busy reading, returns null.
public TextFileWriter openWrite() {
lock(this){
TextFileWriter w = null;
if (status == Mode.Available) {
status = Mode.Write;
w = thefile.makeWriter(closeWrite);
}
return w;
}}
// closes file and resets mode to Mode.Available
public void closeWrite() {
lock (this) { status = Mode.Available; }
}
}
===================================================
===================================================
// delegate that defines type of method for closing file:
public delegate void CloseOperation();
public class TextFile {
private List<string> contents;
public TextFile() { reset(); }
public void reset() { contents = new List<string>(); }
public Tuple<bool, string> readAt(int i) {
String s = "EOF"; bool outcome = false;
if ( 0 <= i && i < contents.Count) {
return new Tuple<bool, string>(true, contents.ElementAt(i)); }
else { return new Tuple<bool, string>(false, "EOF"); }
}
public bool write(string s) { contents.Add(s); }
// factory method:
public TextFileReader makeReader(CloseOperation c) {
return new TextFileReader(this, c);
}
// factory method:
public TextFileWriter makeWriter(CloseOperation c) {
reset();
return TextFileWriter.newWriter(this, c);
}
===================================================
===================================================
// fields and methods for reading a shared textfile, one line at a time:
public class TextFileReader {
private Textfile myfile; // handle to the file to be read
private int count; // how many lines in file have been read
private CloseOperation closefile; // method that closes the file
public FileReader(TextFile t, CloseOperation c) {
myfile = t; count = 0; closefile = c;
}
// checks if the file has more lines to read
public bool more() {
return (count < myfile.Count);
}
// reads and returns next line of file. Returns "EOF" if no more lines.
public string read() {
string line = "EOF";
var pair = myfile.readAt(count);
if (pair.Item1) { // did read succeed?
line = pair.Item2
count = count + 1;
}
return line;
}
// closes file once reading finished
public void close() {
closefile();
}
}
===================================================
If you study Dr. Mizuno's notes and the Design Patterns book, you will see interfaces included, because this makes the pattern more general. (We will add interfaces, as well, below.)
Here is a side issue: when a Client is finished using the manufactured object, it might wish to "dispose" of it. The way to do this in C# is to call the object's Finalize() method and then erase all occurrences of its handle. Then, the background garbage-collector program will erase the unneeded object from heap storage.
An object that "counts through" the elements of a collection is called an iterator object. In the above example, each FileReader object is an iterator for the file.
An iterator object must have a method that returns the next item in a collection and a method that asks if there are any more items left to be returned. You can use an iterator object like FileReader with a loop like this:
TextFile file = ...;
TextFileReader r = controller.openRead();
if (r != null) { // OK to read?
while (r.more()) { // reading lines from file, one at a time:
string s = r.read();
Console.WriteLine(s);
}
}
The example looks like the iterator objects used in Java.
In the C# .NET library, there is an interface, IEnumerable, that defines
how you are supposed to code an interator class in C#:
===================================================
public interface IEnumerable {
// moves to the next element in the collection and makes it Current;
// returns true if successful; returns false if there is no next element.
public bool MoveNext();
// resets the counting to the front of the collection:
public void Reset();
// returns the value of the current element in the collection:
object IEnumerator.Current;
}
===================================================
You are also supposed to write a factory method, GetEnumerator(), which
constructs the iterator object. Here is a small example:
===================================================
// defines my collection of objects built from class C:
class MyCollection {
// holds data structure of objects constructed from class C:
C[] obs = ...;
public IEnumerable getEnumerator() {
return new CollectionIterator(obs);
}
class CollectionIterator: IEnumerable {
private MyCollection c; // handle to the data structure to be iterated
private int counter i;
public CollectionIterator(C[] obs) { c = obs; Reset(); }
public bool MoveNext() {
bool answer = false;
if ((i+1) < c.Count ) { i = i + 1; answer = true; }
return answer;
}
public void Reset() { i = -1; }
public C Current() { return c[i]; }
object IEnumerator.Current { get{ this.Current() }; }
}
===================================================
The C# compiler lets you use the objects that implement the IEnumerable interface
with a
foreach loop. Indeed, a foreach loop like this one:
MyCollection collection = ... ;
foreach (C c in collection) {
... c ...
}
is reformatted by the C# compiler into this
while loop that uses the iterator object:
IEnumerable iterator = collection.GetEnumerator();
iterator.Reset();
while (iterator.MoveNext()) {
C c = (C)(iterator.Current);
... c ...
}
Dr. Mizuno's CIS501 notes have several good examples of iterators in
Java and C#.
Whenever you define a data structure that holds a collection of objects --- a matrix, a tree, a linked list, etc. --- you should define an iterator that can traverse the structure and enumerate the objects. Then you can use the foreach loop to process the data structure.
The simplest form of "iterator pattern" looks like this:
(If you check Dr. Mizuno's notes or the Design Patterns book,
you will see an interface in the pattern to make it more general purpose.)
The AggregateIterator holds the algorithm for traversing the ItemAggregate, one Item at a time. This simplifies the coding of the latter and also makes possible multiple active iterators. On the negative side, the AggregateIterator holds only a handle to the ItemAggregate (and not a copy of all the Items in it). Therefore, it is dangerous and probably erroneous to change the contents of the ItemAggregate while an AggregateIterator is in use!
Here is the new class,
class FileWriter, written so that there is only one object constructed from it.
(The key is the private constructor method!)
===================================================
// constructs a single object for writing to a text file (list of strings)
public class TextFileWriter
// holds the handle to the "singleton" FileWriter object:
private static TextFileWriter writerOb = new TextFileWriter();
private static bool inUse; // remembers if writerOb is being used
private static Textfile myfile; // the file to write to
private static CloseOperation closefile; // method that closes the file
// the constructor is private !
private TextFileWriter() { inUse = false; }
public void write(string s) { myfile.write(s); }
public void close() { inUse = false; closefile(); }
// returns the handle to the FileWriter object, if it isn't inUse
public static TextFileWriter newWriter(TextFile f, CloseOperation c) {
TextFileWriter w = null;
if (!inUse) {
inUse = true; w = writerOb;
closefile = c;
myfile = f; myfile.reset();
}
return w;
}
}
===================================================
You use the singleton class like this:
TextFileWriter mywriter = TextFileWriter.newWriter(...,...);
It is illegal to say
TextFileWriter mywriter = new TextFileWriter()
because the constructor is private.
We see that the FileController can be used to control other kinds of resources besides textfiles. Maybe we use it with binary files or with a shared data buffer or with a hardware device with readable/writable data.
We can "cut" the above design into two, to expose how the FileController
might connect to other resources.
Here is the controller's part, where we insert interfaces on the outgoing arcs:
The interfaces are "plug-in" points for the file and the forms of reader/writer.
When we replug-in the classes we coded so far, we have the system we started with:
But we can reuse the controller with other forms of file:
When there are a set of interfaces that include factory methods, this is called an abstract factory. We plug into the interfaces a "concrete factory" of classes that "manufacture" a family of objects that form a subassembly. In the above example, we have a simple abstract factory for manufacturing readers and writers. There are two concrete factories --- one for text files and one for binary files.
The best-known example of an abstract factory is a collection of factory methods and interfaces for manufacturing graphics. We might have a set of Windows XP widget classes, a set of Windows 7 widget classes, and a set of Mac widget classes, all implementing an abstract factory of widget interfaces.
Here is a page from Design Patterns, by E. Gamma, et al. (Addison Wesley, 1995, copied under "fair use" laws) that explains the example well: