S.O.L.I.D. Software Development, One Step at a Time (Cont.) Liskov Substitution Principle The Liskov Substitution Principle says that an object inheriting from a base class, interface, or other abstraction must be semantically substitutable for the original abstraction. Even if the original abstraction is poorly named, the intent of that abstraction should not be changed by the specific implementations. This requires a solid understanding of the context in which the interface was meant to be used. To illustrate what a semantic violation may look like in code, consider a square and a rectangle, as shown in Figure 8. If you are concerned with calculating the area of a resulting rectangle, you will need a height, a width and an area method that returns the resulting calculation.  Figure 8: Semantic violations are not always easy to see.public class Rectangle { public virtual int Height { get; set; } public virtual int Width { get; set; }
public int Area() { return Height * Width; } }
In geometry, you know that all squares are rectangles. You also know that not all rectangles are squares. Since a square “is a” rectangle, though, it seems intuitive that you could create a rectangle base class and have square inherit from that. But what happens when you try to change the height or width of a square? The height and width must be the same or you no longer have a square. If you try to inherit from rectangle to create a square, you end up changing the semantics of height and width to account for this. public class Square : Rectangle { public override int Height { get { return base.Height; } set { base.Height = value; base.Width = value; } } public override int Width { get { return base.Width; } set { base.Width = value; base.Height = value; } } }
What happens when you use a rectangle base class and assert the area of that rectangle? If you expect the rectangle’s area to be 20, you can set the rectangle’s height to 5 and width to 4. This will give you the result you expect. Rectangle rectangle = new Rectangle(); rectangle.Height = 4; rectangle.Width = 5; AssertTheArea(rectangle);
private void AssertTheArea(Rectangle rectangle) { int expectedArea = 20; int actualArea = rectangle.Area(); Debug.Assert(expectedArea == actualArea); }
What if you decide to pass a square into the AssertTheArea method, though? The method expects to find an area of 20. Let’s try to set the square’s height to 5. You know that this will also set the square’s width to 5. When you pass that square into the method, what happens? Rectangle square = new Square(); square.Height = 5; AssertTheArea(square);
private void AssertTheArea(Rectangle rectangle) { int expectedArea = 20; int actualArea = rectangle.Area(); Debug.Assert(expectedArea == actualArea); }
You get the wrong result because 5 x 5 is 25, not 20. That is too high, so now try a height of 4 instead. You know that 4 x 4 is 16. Unfortunately, that’s too low. So the question is, “how can you get 20 out of multiplying two integers?” The answer is: you can’t. The square-rectangle issue illustrates a violation of the Liskov Substitution Principle. You clearly have the wrong abstraction to represent both a square and a rectangle for this scenario. This is evidenced by the square overriding the height and width properties of the rectangle, and changing the expected behavior of a rectangle. What, then, would a correct abstraction be? In this case, you may want to use a simple Shape abstraction and only provide an Area method, as shown in Listing 5. Each specific implementation-square and rectangle-would then provide their own data and implementation for area allowing you to create additional shapes such as circles, triangles, and others that you don’t yet need. By limiting the abstraction to only what is common among all of the shapes, and ensuring that no shape has a different meaning for “area” you can help prevent LSP violations. A Quick-and-Dirty Database Reader After fuelling up with another energy drink and shaking off the sleep you so desperately want, you dive into the code for the database reader. Given the short time frame, you decide to take a shortcut and not introduce a new abstraction or a new method to the API. Rather, you decide to hard code the behavior of reading from the database into the application, facilitated by the use of a special file format reader. You know it’s not the brightest moment in your career, but you just want to get it out the door and go home for the night. What should have been an 800-meter race has now become a 50-meter dash in your mind. Listing 6 shows the result. You deliver the working code on time, and manage to make it home before falling asleep while driving. Overall, you consider it to be a successful day. The following week, you hear word that the network operations personnel liked the ability to read from a database. In fact, they liked it so much that they told another department about this feature. What they didn’t know, though, was that the code you delivered was written for one very specific database and didn’t actually read the connection string from a file. You knew that the security guys would have your head if you stored the real connection information in a plain text file, so you hard coded it into the file reader service. You created the “server=” content of the file as a placeholder to let you know that you should use the database connection reader. So, when the network operations personnel gave your code to the other department, everyone started wondering why the other department was now reading log files from the network operations center. All eyes are now looking squarely at you. Revisiting the Database Reader All of the eyes looking squarely at you were from your friends in the company, fortunately. After explaining the stress and sleeplessness that undermined your ability to code that day, they all laughed and asked when you would have a new version ready for them. Remembering that sprinting out of the gate during a marathon race is likely to cause the same problems again, you inform them that you’ll need a day or two to get the situation sorted out correctly. There’s no immediate need or CTO putting on the pressure at this point, so everyone agrees to the general timeline and waits patiently while you work. After a quick discussion with some coworkers, you realize that you had changed the semantics of the file format reader interface and introduced behavior that was incompatible. After a little more discussion, you end up with the design represented by Figure 9, and the change turns out to be fairly simple.  Figure 9: Restructuring database reading to correct the LSP violation.| " | By introducing a separate database reader service, you can remove the type-checking code from the file reader service.
| " |
By introducing a separate database reader service, you can remove the type-checking code from the file reader service. You can set up the database reader to read the required connection string from the company standard storage for sensitive data. That decision makes the people in network operations, security, and the other department that wants to use the code, happy. Next, you update the UI to include a “Send From Database” button as shown in Figure 10. This button calls into the same email sender object that you’ve been using as the public API. However, the email sender now has a ReadFromDatabase method along with a ReadFromFile method. This keeps the public API centralized while still providing the functionality that the various departments need.  Figure 10: The UI updated with the “Send From Database” functionality.public class EmailSender { public void ReadFile() { /* ... */ }
public void SendEmail() { /* ... */ }
public void ReadDatabase() { /* ... */ } }
With this newly structured system in place, you deliver the solution to both of the waiting departments. Your friends are happy to hear that you’ve been getting more sleep and that the application they’ve been waiting for is “finally done” -a day earlier than promised. Still More Use for Your Application Shortly after delivering the updated version of the application with the database reading capabilities, another department gets wind of it and they want to use the API. After a quick conversation with them to find out if your application is what they really need, you deliver the working bits. A day later, one of the developers from that department stops by your cube with a confused look on his face. After some quick chat, you realize that he’s confused by the email sender object. It seems that he doesn’t understand why there’s a “read from database” and “read from file” method on an object that is supposed to send email. |