Defining Data About the CodeAs with anything that is associated with data, there comes a time when an object's information will need to be persisted so it can be retrieved (and possibly updated) in the future. One approach is to have a client determine how the object's state should be saved. Listing 1-3 demonstrates one possible implementation.Listing 1-3. Saving a Country Objectprivate void btnSave_Click(object sender, System.EventArgs e) { if (this.mCountry != null) { TextWriter countryFile = File.CreateText(Application.StartupPath + @"\country.txt"); try { countryFile.WriteLine( "name:{0}", this.mCountry.Name); countryFile.WriteLine( "population:{0}", this.mCountry.Population); } finally { countryFile.Close(); } } }Figure 1-2 shows what a typical country.txt file would look like in Notepad.Figure 1-2. Persisting an ImprovedCountry objectHowever, this implementation is just one way to save the data to a file. What if another client application decides to save it to an XML file? What if the second client chooses to store the Population's property value first? Persistence strategies can vary widely among applications. The only chance of interoperability between these formats is solid documentation.One way to improve this approach is to create an interface that a class can implement so it can control the serialization process: public interface IPersist { void Load(Stream persistenceTarget); void Save(Stream persistenceTarget); }To illustrate how this interface could be implemented, we'll create a new class named PersistedCountry that has ImprovedCountry as its base class. It will also implement the IPersist interface. Listing 1-4 shows the implementation of PersistedCountry.Listing 1-4. Implementing PersistedCountrypublic class PersistedCountry : ImprovedCountry, IPersist{ private const string NAME_KEY = "name"; private const string POPULATION_KEY = "population"; public PersistedCountry(string name, long population) : base(name, population) { } public void Load(Stream persistenceTarget) { int totalBytes = (int)persistenceTarget.Length; Decoder dec = (new UnicodeEncoding()).GetDecoder(); byte[] storedInfo = new byte[totalBytes]; persistenceTarget.Read(storedInfo, 0, totalBytes); char[] storedChars = new char[dec.GetCharCount( storedInfo, 0, totalBytes)]; int totalDecodedChars = dec.GetChars(storedInfo, 0, totalBytes, storedChars, 0); string info = new string(storedChars); int nameStart = info.IndexOf(NAME_KEY); int populationStart = info.IndexOf(POPULATION_KEY); this.mName = info.Substring(nameStart + NAME_KEY.Length + 1, populationStart - (nameStart + NAME_KEY.Length)); this.mPopulation = Int32.Parse( info.Substring(populationStart + POPULATION_KEY.Length + 1)); } public void Save(Stream persistenceTarget) { Encoding enc = new UnicodeEncoding(); string nameInfo = string.Format("{0}:{1}", NAME_KEY, this.mName); byte[] nameInfoBytes = enc.GetBytes(nameInfo); persistenceTarget.Write(nameInfoBytes, 0, nameInfoBytes.Length); string populationInfo = string.Format("{0}:{1}", POPULATION_KEY, this.Population); byte[] populationInfoBytes = enc.GetBytes(populationInfo); persistenceTarget.Write(populationInfoBytes, 0, populationInfoBytes.Length); }}
In this case, there is no reason for an object to implement IPersist, because it does not need to customize the serialization process. Granted, object persistence is not as easy as this. For example, the Reflection code doesn't take into consideration deep object graphs; it just looks at the fields contained within a given object. Furthermore, some objects may have specialized needs that simple field iteration won't handle, so having the IPersist interface is a good thing. It should, however, not be a requirement for a class that wants its instances to be persistable to implement an interface. This is where using attributes can be beneficial. Attributes are useful to define public data about an assembly's members that can be used by object-related services to perform generalized tasks. Another example of where attributes are useful is when you need to mark either methods or entire classes as obsolete. If it were just one method, a developer could have the method throw a NotSupportedException, but clients of that object would get an unexpected surprise when they called that method. Also, there is no way to warn the client of future obsolescence. An attribute could be used on a method or an entire class to state that other paths should be used in the near future. Plus, an attribute can be constructed to provide fair warning without causing unexpected behavior.