This is going to be a longer example, and I'm just dealing with the conversion logic here because I think it's the most instructive. It's also not a "real world" example in the sense that the specific case you provided doesn't justify anything close to this much structure, but you could very easily find yourself heading in this direction in a production-scale app.
The code I've provided can be used to handle the input and the desired factor.
See the conversion and multiple codes here
Key Points
- Again, this is a "teaching example". As a beginner you should not expect your code to look anything like this. One step at a time :)
- Related data belongs together in the same type. The multiple doesn't make sense without it's prefix name.
Classes!
It's generally a good idea to create types that represents sets of values that belong together. The SI multiples are example of this. The name "nano" is associated with its factor. Because they all form a composite entity or idea, they make the most sense in a type (struct or class), which is the Multiple type in the code I provided.
public struct Multiple { public Multiple(string name, int exponent) { Name = name; Exponent = exponent; } public string Name { get; } // Read-Only Property public int Exponent { get; } // Read-Only Property public double Multipler => Math.Pow(10, Exponent); public double ConvertToBaseValue(double inputValue) { return inputValue/Multipler; } public double ConvertFromBaseValue(double inputValue) { return inputValue*Multipler; } public UnitValue CreateValue(double inputValue) { var newBaseValue = ConvertToBaseValue(inputValue); return new UnitValue(this, newBaseValue); } public static Multiple Singular => new Multiple(null, 0); public static Multiple Milli => new Multiple("Milli", -3); public static Multiple Micro => new Multiple("Micro", -6); public static Multiple Nano => new Multiple("Nano", -9); public static Multiple Pico => new Multiple("Pico", -12); }
Similarly, your input with two questions (what is the value? what unit do I want to see it in?) then forms the basis of the UnitValue type. If you separate the two from each other they don't make a lot of sense.
public class UnitValue { public UnitValue(Multiple multiple, double baseValue) { Multiple = multiple; BaseValue = baseValue; } public Multiple Multiple { get; } public double PrefixedValue { get { return Multiple.ConvertFromBaseValue(BaseValue); } set { BaseValue = Multiple.ConvertToBaseValue(value); } } public double BaseValue { get; set; } public UnitValue ConvertTo(Multiple multiple) { return new UnitValue(multiple, BaseValue); } }
Static/Class Methods for Default Values
I've added factory static methods to the Multiple to standardize the values. You'd want all of the standard definitions built into the type, or closely nearby.
// Members of Multiple type. public static Multiple Singular => new Multiple(null, 0); public static Multiple Milli => new Multiple("Milli", -3); public static Multiple Micro => new Multiple("Micro", -6); public static Multiple Nano => new Multiple("Nano", -9); public static Multiple Pico => new Multiple("Pico", -12); // Allows you to get multiples like so. var m = Multiple.Milli; var u = Multiple.Micro;
Don't Repeat Yourself!
Everyone will say this over and over again. Here I think it's demonstrated by avoiding typing in tedious values with all those zeroes. It also avoids bugs where I had 0.0001 for the milli prefix.
// Risky public static Multiple Pico => new Multiple("Pico", 0.000000000001); // Safer public static Multiple Pico => new Multiple("Pico", -12);
Property Accessors
Instead of making the user use a special method to update the base value or prefixed value, I've made the decision to make the BaseValue property an auto-property ( which means I don't need to write boilerplate code for the gettor and settors), and had the PrefixedValue wired up to it. This way you can make the lives of the users of the type somewhat easier, at the price of making them responsible to know when to use the different values and why.
public double PrefixedValue { get { return Multiple.ConvertFromBaseValue(BaseValue); } set { BaseValue = Multiple.ConvertToBaseValue(value); } }
Get Only Properties
Because the Multiple data types belong together, it doesn't make any sense for the client to update them. Therefore to prevent data corruption I've made them get only properties that are set at the time the object is instantiated.
// Constructor assigns the values when instantiated. public Multiple(string name, int exponent) { Name = name; Exponent = exponent; } public string Name { get; } // Can only be set when instantiated. public int Exponent { get; } // Can only be set when instantiated.
Summary
These are some of the things you'd want to consider as you move forward. I've skipped other things that are important to understand, but they can wait until you have a lot more experience:
- Program to the interface, not to the implementation.
- Using inheritance to specialize (if I wanted to handle imperial weights).
- Using reflection to iterate over all the predefined multiples so you could change the defined multiples and your UI would update automatically.
- Unit Testing your code to validate design.
I hope that wasn't too scary! If anyone has any feedback I'd love to hear it.
Edit 1 : Adding Usage Example
I've amended my gist here to include an example of the Main program, if it were using the classes I've written. I didn't go through a full refactoring exercise there except to remove duplication, and to push the specifics of logging into the Log method.
Why Replace Console.WriteLine()? It's a Platform Function
Indeed it is. But you're still repeating it all over the place. Imagine if you wanted to change the way you log things? Or add a standard format?
If you keep the implementation away from the users of the logging function, then you can change it whenever you want. If you wanted to replace the console logging function with a log file, you could change it in one place. Without this abstraction you'd be updating literally dozens of references.
// "Brittle" Code Console.WriteLine("Bob"); // In one file Console.WriteLine("Says"); // In another file Console.WriteLine("Hi!"); // In a third file // Abstracted Code Log("Bob"); Log("Bob"); private static void Log(string message) { Console.WriteLine(message); }
One Final Thing You also want to consider what happens if the user types something weird in. This example doesn't include any error handling around user entry.