I am going to keep this tutorial short-and-sweet with most of the learning hopefully coming from looking at code and brief remarks about it. Hopefully you leave the thread feeling a bit better about interfaces than you did before reading it. So lets just start right away and design a basic set of interfaces.
The goal is to make a design that allows quick and easy loading/saving/reading of custom settings. Now here is a question that really highlights the importance of object oriented programming in general, and as a bonus, the value of interfaces. What settings format do we want to design our system for? Json? XML? Ini? Some settings format we do not even know we want yet? How about one that does not even exist yet? The answer is.. none of the above.
That is the beauty of core object oriented programming. Solid interface design allows us to not care about the questions above. You can change the implementation, not the design. Changing your entire design 10 years into the applications life is not an easy task. Changing the implementation of the way you load/save settings from JSON to XML is.
So lets go ahead and design it step by step.
Note, for all the classes here, the only namespaces you need to include are:
using System.Collections.Generic; using System.IO; using System.Text; using System.Xml.Serialization;
First, we need a way to load and save the settings and access its contents. Lets make this in of its self an inter-face as it is useful for a wide number of operations. We will define four basic operations, which the method names describe.
ISerializeTool
public interface ISerializeTool { string ExportToString<T>(T obj); T ImportFromString<T>(string obj); void ExportToFile<T>(T obj, string file); T ImportFromFile<T>(string path); }
Great, now we have defined a contract of sorts to handle the data we want.
Now lets design a basic interface to be used to manage a settings file. We use the <out T>, which is just using covariance, which allows you to use a more derived type than that specified by the generic parameter.
ISettings
public interface ISettings<out T> { T Current { get; } void Save(string target = ""); void Load(string location = ""); }
So there is our two interfaces. Now lets show how they can be used to implement a settings manager of sorts using the interfaces above.
Settings
public class Settings<T> : ISettings<T> { public T Current { get; set; } public string DefaultPath { get; set; } public ISerializeTool SettingsSerializer { get; set; } public void Save(string target = "") { if (string.IsNullOrEmpty(target)) target = DefaultPath; SettingsSerializer.ExportToFile(Current, target); } public void Load(string location = "") { if (string.IsNullOrEmpty(location)) location = DefaultPath; if(!File.Exists(location)) throw new FileNotFoundException("Mydata settings file does not exist."); Current = SettingsSerializer.ImportFromFile<T>(location); } }
Now lets tie them all together for something useful. Lets just say we are feeling XML today and that is what we want our settings to use. No problem, we just need to implement the ISerializeTool. Here is mine:
public class XmlSerializeTool : ISerializeTool { public string ExportToString<T>(T obj) { var serializer = new XmlSerializer(typeof (T)); using (var stringWriter = new StringWriter()) { serializer.Serialize(stringWriter, obj); return stringWriter.ToString(); } } public T ImportFromString<T>(string serializedObj) { var serializer = new XmlSerializer(typeof (T)); using (var stringWriter = new StringReader(serializedObj)) { return (T) serializer.Deserialize(stringWriter); } } public void ExportToFile<T>(T obj, string file) { using (var fileWriter = new StreamWriter(file, false, Encoding.UTF8)) { fileWriter.Write(ExportToString(obj)); } } public T ImportFromFile<T>(string file) { using (var fileReader = new StreamReader(file, Encoding.UTF8)) { return ImportFromString<T>(fileReader.ReadToEnd()); } } }
Now, for examples sake, here is a simple test you can use to try all the above.
public class RandomNameGeneratorSettings { public List<string> FirstNames { get; set; } public List<string> LastNames { get; set; } } public static class SettingsTest { public static ISettings<RandomNameGeneratorSettings> SettingsManager { get; set; } public static void RunTest() { // Make settings. var settings = new Settings<RandomNameGeneratorSettings> { SettingsSerializer = new XmlSerializeTool(), DefaultPath = "MyPath.xml", Current = new RandomNameGeneratorSettings { FirstNames = new List<string> {"Joe", "Bob"}, LastNames = new List<string> {"Johnson", ""} } }; // Set the ISettings above with our implementations. SettingsManager = settings; // Save the settings to a file and load them from that file. SettingsManager.Save(); SettingsManager.Load(); // Print first names in current settings. foreach (var setting in SettingsManager.Current.FirstNames) { Console.WriteLine(setting); } // Add a name to the current settings. SettingsManager.Current.FirstNames.Add("Josh"); // Save to (default) file again. SettingsManager.Save(); // Clear the current first names. SettingsManager.Current.FirstNames.Clear(); // Load the settings again from the default file, // which should give us the new dat that we changed just before. SettingsManager.Load(); // Ensure that happened. foreach (var setting in SettingsManager.Current.FirstNames) { Console.WriteLine(setting); } } }
I hope you have a better grasp on interfaces and C# after reading this. Good luck.