LSP - Liskov Substitution Principle

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

This means that all of the derived classes should retain the existing behavior of the base class as expected by the client while extending the functionality as fulfilled by the parent class. This also means that the client code that consumes a specific class or interface should be able to use a derived class or a different implementation of the interface without affecting or having to change its internal behavior, which ultimately minimizes the impact on the consumer code as a result of a change added by the derived class.

Let's demonstrate this principle with an example. We create the interface for settings when we have different types of settings, such as UserSettings, MachineSettings, and ReadOnlySettings. Settings would be required to be loaded and saved. We will first show the example violating the LSP and then do the correction and adhere to the LSP. Let's have our simple setting interface:

    public interface ISettings 
{
void Load();
void Save();
}

Let's have our sample classes implementing the ISettings interface:

    public class UserSettings : ISettings 
{
public void Load()
{
//Loads the user settings
}

public void Save()
{
//Saves the user settings
}
}
public class MachineSettings : ISettings
{
public void Load()
{
//Loads the machine settings
}

public void Save()
{
//Saves the machine settings
}
}
/// <summary>
/// Says this class holds readonly or constant settings /
configuration parameters to the software
/// </summary>
public class ReadOnlySettings : ISettings
{
public void Load()
{
//Loads some readonly/constant settings
}

public void Save()
{
throw new NotImplementedException();
}
}

Now let's look at a sample client code making use of these settings classes and realize how it breaks the principle:

    [Fact] 
public void Test_Client_Violating()
{
List<ISettings> allSettings = new List<ISettings>();

ISettings setting = new LSP.Bad.UserSettings();
allSettings.Add(setting);

setting = new LSP.Bad.MachineSettings();
allSettings.Add(setting);

setting = new LSP.Bad.ReadOnlySettings();
allSettings.Add(setting);

//Load all types of settings
allSettings.ForEach(s => s.Load());

//Do some changes to settings objects..

//Following line fails because client (actually)
does not know it has to catch
allSettings.ForEach(s => s.Save());
}

The last line in this code fails because the client code does not expect to catch (and possibly ignore) the exception. Otherwise, it has to put a specific case to work to detect whether the class is ReadOnlySettings; then, it does not call its Save() method, which is clearly a very bad practice.

Now let's look at how to solve this problem and adhere to the Liskov Substitution Principle:

    public interface IReadableSettings 
{
void Load();
}
public interface IWriteableSettings
{
void Save();
}
public interface ReadOnlySetting : IReadableSettings
{
public void Load()
{
//Loads the machine settings
}
}

Here, you can see that we have divided the interfaces according to their correct purpose or need and then implemented the ReadOnlySettings class using only the required interface, that is, IReadableSettings.

What we did here is basically segregate the interfaces.

Let's now take a look at the sample client code using the various types of settings classes:

    [Fact] 
public void Test_Client_NonViolating()
{
var allLoadableSettings = new List<IReadableSettings>();
var allSaveableSettings = new List<IWriteableSettings>();

var userSettings = new LSP.Good.UserSettings();
allLoadableSettings.Add(userSettings);
allSaveableSettings.Add(userSettings);

var machineSettings = new LSP.Good.MachineSettings();
allLoadableSettings.Add(machineSettings);
allSaveableSettings.Add(machineSettings);

var readOnlySettings = new LSP.Good.ReadOnlySettings();
allLoadableSettings.Add(readOnlySettings);
//allSaveableSettings.Add(readOnlySettings); Cannot
compile this line;
readOnlySettings is not save-able/writable settings

//Load all types of settings
allLoadableSettings.ForEach(s => s.Load());

//Do some changes to settings objects..

allSaveableSettings.ForEach(s => s.Save()); //Now this
line clearly does not fail :)
}

From the preceding code, it's evident that loading and saving setting interfaces are segregated cleanly according to their responsibilities and the code works as expected by the client.

From this example, you can understand how a neatly designed code follows the fundamental OOD principles and how closely related these SOLID principles are in the sense that usually, when trying to adhere to one SOLID principle, you automatically follow one or more of the other principles. Once a mature programmer starts following SOLID principles, he/she does not need to remember them--over a period of time, it becomes part of their nature or a good coding habit.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.107.229