Welcome back! Today, we'll master what we learned about backward compatibility in practice. Get prepared to apply all the knowledge to practice tasks, but first, let's look at two examples and analyze them.
Let's say that initially, we have a complex data processing function designed to operate on an array of objects, applying a transformation that converts all string values within the objects to uppercase. Here's the initial version:
TypeScript1type Item = { [key: string]: any }; 2 3function processData(items: Item[]): void { 4 const processedItems = items.map(item => { 5 const processedItem: Item = {}; 6 for (const key in item) { 7 processedItem[key] = typeof item[key] === 'string' ? item[key].toUpperCase() : item[key]; 8 } 9 return processedItem; 10 }); 11 12 processedItems.slice(0, 3).forEach(item => console.log(`Processed Item: ${JSON.stringify(item)}`)); 13} 14 15// Default behavior - convert string values to uppercase 16processData([{ name: "apple", quantity: 10 }, { name: "orange", quantity: 5 }]);
We intend to expand this function, adding capabilities to filter the items based on a condition and allowing for custom transformations. The aim is to retain backward compatibility while introducing these enhancements. Here's the updated approach:
TypeScript1type ConditionFunction = (item: Item) => boolean; 2type TransformFunction = (item: Item) => Item; 3 4function processData( 5 items: Item[], 6 condition: ConditionFunction = () => true, 7 transform: TransformFunction | null = null 8): void { 9 const processedItems = items.reduce<Item[]>((acc, item) => { 10 if (condition(item)) { 11 let processedItem: Item; 12 if (transform) { 13 processedItem = transform(item); 14 } else { 15 processedItem = {}; 16 for (const key in item) { 17 processedItem[key] = typeof item[key] === 'string' ? item[key].toUpperCase() : item[key]; 18 } 19 } 20 acc.push(processedItem); 21 } 22 return acc; 23 }, []); 24 25 processedItems.slice(0, 3).forEach(item => console.log(`Processed Item: ${JSON.stringify(item)}`)); 26} 27 28// Default behavior - convert string values to uppercase 29processData([{ name: "apple", quantity: 10 }, { name: "orange", quantity: 5 }]); 30 31// Custom filter - select items with a quantity greater than 5 32processData( 33 [{ name: "apple", quantity: 10 }, { name: "orange", quantity: 5 }], 34 item => item.quantity > 5 35); 36 37// Custom transformation - convert names to uppercase and multiply the quantity by 2 38const customTransform: TransformFunction = item => { 39 const transformedItem: Item = {}; 40 for (const key in item) { 41 transformedItem[key] = key === "name" ? item[key].toUpperCase() : item[key] * 2; 42 } 43 return transformedItem; 44}; 45processData( 46 [{ name: "apple", quantity: 10 }, { name: "orange", quantity: 5 }], 47 () => true, 48 customTransform 49);
In the evolved version, we've introduced two optional parameters: condition
, a function to filter the input array based on a given condition, and transform
, a function for custom transformations of the filtered items. The default behavior processes all items, converting string values to uppercase, which ensures that the original function'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 responsibly.
Imagine now that we are building a music player, and recently, 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:
TypeScript1class MusicPlayer { 2 play(file: string): void { 3 if (file.endsWith(".mp3")) { 4 console.log(`Playing ${file} as mp3.`); 5 } else { 6 console.log("File format not supported."); 7 } 8 } 9} 10 11// Example usage 12const player = new MusicPlayer(); 13player.play("song.mp3"); 14player.play("song.wav");
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.
TypeScript1class MusicPlayerAdapter { 2 private player: MusicPlayer; 3 private formatAdapters: { [extension: string]: (file: string) => void }; 4 5 constructor(player: MusicPlayer) { 6 this.player = player; 7 this.formatAdapters = { 8 ".wav": this convertAndPlayWav.bind(this), 9 ".flac": this convertAndPlayFlac.bind(this), 10 }; 11 } 12 13 play(file: string): void { 14 const fileExtension = file.slice(file.lastIndexOf(".")).toLowerCase(); 15 const adapterFunc = this.formatAdapters[fileExtension]; 16 17 if (adapterFunc) { 18 adapterFunc(file); 19 } else { 20 this.player.play(file); 21 } 22 } 23 24 private convertAndPlayWav(file: string): void { 25 // Simulate conversion 26 const convertedFile = file.replace(".wav", ".mp3"); 27 console.log(`Converting ${file} to ${convertedFile} and playing as mp3...`); 28 this.player.play(convertedFile); 29 } 30 31 private convertAndPlayFlac(file: string): void { 32 // Simulate conversion 33 const convertedFile = file.replace(".flac", ".mp3"); 34 console.log(`Converting ${file} to ${convertedFile} and playing as mp3...`); 35 this.player.play(convertedFile); 36 } 37} 38 39// Upgraded music player with enhanced functionality through the composite adapter 40const legacyPlayer = new MusicPlayer(); 41const enhancedPlayer = new MusicPlayerAdapter(legacyPlayer); 42enhancedPlayer.play("song.mp3"); // Supported directly 43enhancedPlayer.play("song.wav"); // Supported through adaptation 44enhancedPlayer.play("song.flac"); // Newly supported through additional adaptation
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.
Great job! You've delved into backward compatibility while learning how to utilize default parameters and the Adapter Design Pattern. Get ready for some hands-on practice to consolidate these concepts! Remember, practice makes perfect. Happy Coding!