For those new to SRP, it stands for Single responsibility principle. Here is a snippet from Wikipedia on the meaning.
In object-oriented programming, the single responsibility principle states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
The term was introduced by Robert C. Martin in an article by the same name as part of his Principles of Object Oriented Design,[1] made popular by his book Agile Software Development, Principles, Patterns, and Practices[2]. Martin described it as being based on the principle of cohesion, as described by Tom DeMarco in his book Structured Analysis and Systems Specification[3].
Bear with me, this post is going to be code heavy….
So lets start out with a method you might come across. I am aware this code is probably inefficient. I wrote this quickly just to show a point and to respond to a question I received on twitter about PostSharp and SRP.
For this Example I am just using a simple console application with PostSharp and PostSharp-Aspects added to the project. At the top of the method I am adding the Exception handler aspect.
[ExceptionHandler] public List<WebTV_Promo_Ship> ParseCsvShipment(string path) { if (File.Exists(path)) { var parsedCsv = new List<WebTV_Promo_Ship>(); using (var reader = new CsvReader(new StreamReader(path))) { while (reader.Read()) { if (reader.GetField(0).Length > 2 && !reader.CurrentRecord.Contains("SKU#") && !reader.CurrentRecord.Contains("Ship Ready")) { parsedCsv.Add( new WebTV_Promo_Ship { SKU = reader.GetField(0), Status = reader.GetField(1), Ship_Date = Convert.ToDateTime(reader.GetField(2)), Customer_No = Convert.ToInt32(reader.GetField(3)), Customer_Name = reader.GetField(4), Customer_Addr = reader.GetField(5), Description = reader.GetField(6), Customer_Phone = reader.GetField(7), Tracking_No = reader.GetField(8) }); } } } return parsedCsv; } return null; }
Lets look at this method post build using Telerik JustDecompiler, the code looks like this.
public List<WebTV_Promo_Ship> ParseCsvShipment(string path) { List<WebTV_Promo_Ship> webTVPromoShips; List<WebTV_Promo_Ship> returnValue; bool flag; try { bool flag1 = !File.Exists(path); if (flag1) { webTVPromoShips = null; } else { List<WebTV_Promo_Ship> parsedCsv = new List<WebTV_Promo_Ship>(); CsvReader reader = new CsvReader(new StreamReader(path)); try { while (true) { flag1 = reader.Read(); if (!flag1) { break; } if (reader.GetField(0).Length <= 2 || Enumerable.Contains<string>(reader.CurrentRecord, "SKU#")) { flag = true; } else { flag = Enumerable.Contains<string>(reader.CurrentRecord, "Ship Ready"); } flag1 = flag; if (flag1) { continue; } WebTV_Promo_Ship webTVPromoShip = new WebTV_Promo_Ship(); webTVPromoShip.SKU = reader.GetField(0); webTVPromoShip.Status = reader.GetField(1); webTVPromoShip.Ship_Date = new DateTime?(Convert.ToDateTime(reader.GetField(2))); webTVPromoShip.Customer_No = Convert.ToInt32(reader.GetField(3)); webTVPromoShip.Customer_Name = reader.GetField(4); webTVPromoShip.Customer_Addr = reader.GetField(5); webTVPromoShip.Description = reader.GetField(6); webTVPromoShip.Customer_Phone = reader.GetField(7); webTVPromoShip.Tracking_No = reader.GetField(8); parsedCsv.Add(webTVPromoShip); } } finally { flag1 = reader == null; if (!flag1) { reader.Dispose(); } } webTVPromoShips = parsedCsv; } returnValue = webTVPromoShips; } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(this, null); MethodBase methodBase = FileController.m2; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a0.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; returnValue = (List<WebTV_Promo_Ship>)methodExecutionArg.ReturnValue; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } return returnValue; }
Looking at the compiled code a using statement is converted into a try finally block. So what happens is anything that throws an error in this section of code moves to the finally block and closes out the method. The exception is never raised and no one is aware of the issue.
There are many ways of fixing this issue, we are going to refactor and separate the code into multiple smaller methods.
Here is what the code looks like now:
[ExceptionHandler] public List<WebTV_Promo_Ship> ParseCsvShipment(string path) { if (File.Exists(path)) { var parsedCsv = ReadCsvShipment(path); return parsedCsv; } return null; } [ExceptionHandler] private static List<WebTV_Promo_Ship> ReadCsvShipment(string path) { var parsedCsv = new List<WebTV_Promo_Ship>(); ReadCsvStream(path, parsedCsv); return parsedCsv; } [ExceptionHandler] private static void ReadCsvStream(string path, List<WebTV_Promo_Ship> parsedCsv) { using (var reader = new CsvReader(new StreamReader(path))) { ReadCsvFile(parsedCsv, reader); } } [ExceptionHandler] private static void ReadCsvFile(List<WebTV_Promo_Ship> parsedCsv, CsvReader reader) { while (reader.Read()) { AddParsedCsvToList(parsedCsv, reader); } } [ExceptionHandler] private static void AddParsedCsvToList(List<WebTV_Promo_Ship> parsedCsv, CsvReader reader) { if (reader.GetField(0).Length > 2 && !reader.CurrentRecord.Contains("SKU#") && !reader.CurrentRecord.Contains("Ship Ready")) { parsedCsv.Add( new WebTV_Promo_Ship { SKU = reader.GetField(0), Status = reader.GetField(1), Ship_Date = Convert.ToDateTime(reader.GetField(2)), Customer_No = Convert.ToInt32(reader.GetField(3)), Customer_Name = reader.GetField(4), Customer_Addr = reader.GetField(5), Description = reader.GetField(6), Customer_Phone = reader.GetField(7), Tracking_No = reader.GetField(8) }); } }
When compiled the code now looks like this for all the methods:
public List<WebTV_Promo_Ship> ParseCsvShipment(string path) { List<WebTV_Promo_Ship> webTVPromoShips; List<WebTV_Promo_Ship> returnValue; try { bool flag = !File.Exists(path); if (flag) { webTVPromoShips = null; } else { List<WebTV_Promo_Ship> parsedCsv = FileController.ReadCsvShipment(path); webTVPromoShips = parsedCsv; } returnValue = webTVPromoShips; } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(this, null); MethodBase methodBase = FileController.m8; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a0.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; returnValue = (List<WebTV_Promo_Ship>)methodExecutionArg.ReturnValue; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } return returnValue; } private static List<WebTV_Promo_Ship> ReadCsvShipment(string path) { List<WebTV_Promo_Ship> returnValue; try { List<WebTV_Promo_Ship> parsedCsv = new List<WebTV_Promo_Ship>(); FileController.ReadCsvStream(path, parsedCsv); List<WebTV_Promo_Ship> webTVPromoShips = parsedCsv; returnValue = webTVPromoShips; } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(null, null); MethodBase methodBase = FileController.m9; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a1.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; returnValue = (List<WebTV_Promo_Ship>)methodExecutionArg.ReturnValue; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } return returnValue; } private static void ReadCsvStream(string path, List<WebTV_Promo_Ship> parsedCsv) { try { CsvReader reader = new CsvReader(new StreamReader(path)); try { FileController.ReadCsvFile(parsedCsv, reader); } finally { bool flag = reader == null; if (!flag) { reader.Dispose(); } } } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(null, null); MethodBase methodBase = FileController.m10; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a2.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } } private static void ReadCsvFile(List<WebTV_Promo_Ship> parsedCsv, CsvReader reader) { try { while (true) { bool flag = reader.Read(); if (!flag) { break; } FileController.AddParsedCsvToList(parsedCsv, reader); } } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(null, null); MethodBase methodBase = FileController.m11; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a3.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } } private static void AddParsedCsvToList(List<WebTV_Promo_Ship> parsedCsv, CsvReader reader) { bool flag; try { if (reader.GetField(0).Length <= 2 || Enumerable.Contains<string>(reader.CurrentRecord, "SKU#")) { flag = true; } else { flag = Enumerable.Contains<string>(reader.CurrentRecord, "Ship Ready"); } bool flag1 = flag; if (!flag1) { WebTV_Promo_Ship webTVPromoShip = new WebTV_Promo_Ship(); webTVPromoShip.SKU = reader.GetField(0); webTVPromoShip.Status = reader.GetField(1); webTVPromoShip.Ship_Date = new DateTime?(Convert.ToDateTime(reader.GetField(2))); webTVPromoShip.Customer_No = Convert.ToInt32(reader.GetField(3)); webTVPromoShip.Customer_Name = reader.GetField(4); webTVPromoShip.Customer_Addr = reader.GetField(5); webTVPromoShip.Description = reader.GetField(6); webTVPromoShip.Customer_Phone = reader.GetField(7); webTVPromoShip.Tracking_No = reader.GetField(8); parsedCsv.Add(webTVPromoShip); } } catch (Exception exception1) { Exception exception = exception1; MethodExecutionArgs methodExecutionArg = new MethodExecutionArgs(null, null); MethodBase methodBase = FileController.m12; methodExecutionArg.Method = methodBase; methodExecutionArg.Exception = exception; FileController.a4.OnException(methodExecutionArg); switch (methodExecutionArg.FlowBehavior) { case 1: { methodExecutionArg.Exception = null; } case 3: { methodExecutionArg.Exception = null; } case 4: { throw methodExecutionArg.Exception; } default: { throw; } } } }
As you can see the try catch blocks now encompass the code completely. Now if an error is thrown it should now properly propagate up to be logged.
To play devils advocate someone could say this may or may not have anything to do with PostSharp. That the same thing could be accomplished with proper SRP and try catch blocks. Which this is very true; however if you read by post Intro to Aspect Oriented Programming with PostSharp I give many examples/references of how Aspects improve coding. To the point though as I learn more about PostSharp, the more I improve in SRP and DRY because I am looking at the compiled code.
$author Thank you so much for a wonderful blog. It was such a great article. Have a great day!