Lesson 5
Backward Compatibility in Practice with C#
Backward Compatibility: Practice

Welcome back! Today, we'll master what we learned about backward compatibility in practice. Prepare to apply all the knowledge to practical tasks, but first, let's look at two examples and analyze them.

Task 1: Enhancing a Complex Data Processing Function with Method Overloading

Let's say that initially, we have a complex data processing class designed to operate on a list of Dictionary<string, object>, applying a transformation that converts all string values within the Dictionary to uppercase. Here's the initial version:

C#
1using System; 2using System.Collections.Generic; 3 4class DataProcessor 5{ 6 public void ProcessData(List<Dictionary<string, object>> items) 7 { 8 List<Dictionary<string, object>> processedItems = new List<Dictionary<string, object>>(); 9 foreach (var item in items) 10 { 11 Dictionary<string, object> processedItem = new Dictionary<string, object>(); 12 foreach (var entry in item) 13 { 14 if (entry.Value is string) 15 { 16 processedItem[entry.Key] = ((string)entry.Value).ToUpper(); 17 } 18 else 19 { 20 processedItem[entry.Key] = entry.Value; 21 } 22 } 23 processedItems.Add(processedItem); 24 } 25 for (int i = 0; i < Math.Min(3, processedItems.Count); i++) 26 { 27 Console.WriteLine("Processed Item: " + processedItems[i]); 28 } 29 } 30}

We intend to expand this functionality, adding capabilities to filter the items based on a condition and to allow for custom transformations. The aim is to retain backward compatibility while introducing these enhancements. Here's the updated approach using method overloading:

C#
1using System; 2using System.Collections.Generic; 3 4class DataProcessor 5{ 6 public void ProcessData(List<Dictionary<string, object>> items) 7 { 8 ProcessData(items, item => true, null); 9 } 10 11 public void ProcessData(List<Dictionary<string, object>> items, Func<Dictionary<string, object>, Dictionary<string, object>> transform) 12 { 13 ProcessData(items, item => true, transform); 14 } 15 16 public void ProcessData(List<Dictionary<string, object>> items, Predicate<Dictionary<string, object>> condition, Func<Dictionary<string, object>, Dictionary<string, object>> transform) 17 { 18 List<Dictionary<string, object>> processedItems = new List<Dictionary<string, object>>(); 19 foreach (var item in items) 20 { 21 if (condition(item)) // Apply condition to filter items 22 { 23 Dictionary<string, object> processedItem; 24 if (transform != null) 25 { 26 processedItem = transform(item); // Apply custom transformation if provided 27 } 28 else 29 { 30 // Default transformation: Convert string values to uppercase 31 processedItem = new Dictionary<string, object>(); 32 foreach (var entry in item) 33 { 34 if (entry.Value is string) 35 { 36 processedItem[entry.Key] = ((string)entry.Value).ToUpper(); 37 } 38 else 39 { 40 processedItem[entry.Key] = entry.Value; 41 } 42 } 43 } 44 processedItems.Add(processedItem); 45 } 46 } 47 for (int i = 0; i < Math.Min(3, processedItems.Count); i++) 48 { 49 Console.WriteLine("Processed Item: " + processedItems[i]); 50 } 51 } 52} 53 54// Usage examples: 55class Program 56{ 57 static void Main(string[] args) 58 { 59 List<Dictionary<string, object>> data = new List<Dictionary<string, object>>(); 60 Dictionary<string, object> item1 = new Dictionary<string, object> 61 { 62 { "name", "apple" }, 63 { "quantity", 10 } 64 }; 65 data.Add(item1); 66 Dictionary<string, object> item2 = new Dictionary<string, object> 67 { 68 { "name", "orange" }, 69 { "quantity", 5 } 70 }; 71 data.Add(item2); 72 73 DataProcessor processor = new DataProcessor(); 74 75 // Default behavior - convert string values to uppercase 76 processor.ProcessData(data); 77 78 // Custom filter - select items with a quantity greater than 5 79 processor.ProcessData(data, item => (int)item["quantity"] > 5, null); 80 81 // Custom transformation - convert names to uppercase and multiply the quantity by 2 82 processor.ProcessData(data, item => 83 { 84 Dictionary<string, object> transformed = new Dictionary<string, object>(); 85 foreach (var entry in item) 86 { 87 if (entry.Key.Equals("name")) 88 { 89 transformed[entry.Key] = ((string)entry.Value).ToUpper(); 90 } 91 else 92 { 93 transformed[entry.Key] = (int)entry.Value * 2; 94 } 95 } 96 return transformed; 97 }); 98 } 99}

In this evolved version, we've introduced method overloading with additional parameters: Predicate<Dictionary<string, object>> condition to filter the input list based on a given condition, and Func<Dictionary<string, object>, Dictionary<string, object>> transform for custom transformations of the filtered items. The default behavior processes all items, converting string values to uppercase, which ensures that the original functionality's behavior is maintained for existing code paths. This robust enhancement strategy facilitates adding new features to a function with significant complexity while preserving backward compatibility, showcasing an advanced application of evolving software capabilities responsively and responsibly.

Task 2: Using the Adapter Design Pattern for Backward Compatibility

Imagine now that we are building a music player, and recently, the market demands have grown. Now, users expect support not just for MP3 and WAV but also for FLAC files within our music player system. This development poses a unique challenge: How do we extend our music player's capabilities to embrace this new format without altering its established interface or the adapter we've already implemented for WAV support?

Let's say that we currently have a MusicPlayer class that can only play MP3 files:

C#
1using System; 2 3class MusicPlayer 4{ 5 public void Play(string file) 6 { 7 if (file.EndsWith(".mp3")) 8 { 9 Console.WriteLine("Playing " + file + " as mp3."); 10 } 11 else 12 { 13 Console.WriteLine("File format not supported."); 14 } 15 } 16}

Let's approach this challenge by introducing a composite adapter, a design that encapsulates multiple adapters or strategies to extend functionality in a modular and maintainable manner.

C#
1using System; 2using System.Collections.Generic; 3 4class MusicPlayerAdapter 5{ 6 private readonly MusicPlayer player; 7 private readonly Dictionary<string, Action> formatAdapters; 8 private string file; 9 10 public MusicPlayerAdapter(MusicPlayer player) 11 { 12 this.player = player; 13 this.formatAdapters = new Dictionary<string, Action> 14 { 15 { ".wav", ConvertAndPlayWav }, 16 { ".flac", ConvertAndPlayFlac } 17 }; 18 } 19 20 public void Play(string file) 21 { 22 this.file = file; 23 string fileExtension = System.IO.Path.GetExtension(file).ToLower(); 24 if (formatAdapters.TryGetValue(fileExtension, out var adapterFunc)) 25 { 26 adapterFunc(); 27 } 28 else 29 { 30 player.Play(file); 31 } 32 } 33 34 private void ConvertAndPlayWav() 35 { 36 // Simulate conversion 37 string convertedFile = file.Replace(".wav", ".mp3"); 38 Console.WriteLine("Converting " + file + " to " + convertedFile + " and playing as mp3..."); 39 player.Play(convertedFile); 40 } 41 42 private void ConvertAndPlayFlac() 43 { 44 // Simulate conversion 45 string convertedFile = file.Replace(".flac", ".mp3"); 46 Console.WriteLine("Converting " + file + " to " + convertedFile + " and playing as mp3..."); 47 player.Play(convertedFile); 48 } 49} 50 51// Upgraded music player with enhanced functionality through the composite adapter 52class Program 53{ 54 static void Main(string[] args) 55 { 56 MusicPlayer legacyPlayer = new MusicPlayer(); 57 MusicPlayerAdapter enhancedPlayer = new MusicPlayerAdapter(legacyPlayer); 58 enhancedPlayer.Play("song.mp3"); // Supported directly 59 enhancedPlayer.Play("song.wav"); // Supported through adaptation 60 enhancedPlayer.Play("song.flac"); // Newly supported through additional adaptation 61 } 62}

This sophisticated adaptation strategy ensures that we can extend the MusicPlayer to include support for additional file formats without disturbing its original code or the initial adapter pattern's implementation. The MusicPlayerAdapter thus acts as a unified interface to the legacy MusicPlayer, capable of handling various formats by determining the appropriate conversion strategy based on the file type.

Lesson Summary

Great job! You've delved into backward compatibility while learning how to utilize method overloading and the Adapter Design Pattern. Get ready for some hands-on practice to consolidate these concepts! Remember, practice makes perfect. Happy Coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.