SOLID Principles : Guidelines for software development :: LSP
The SOLID principles provide five guidelines that, when followed, can dramatically enhance the maintainability of software.
Liskov Substitution Principle Intro |
The LSP specifies that functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
The Liskov Substitution Principle (LSP) can be worded in various ways.
The original wording was described by Barbara Liskov as, “If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T”.
Robert Cecil Martin’s simpler version is, “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it”.
For languages such as C#, this can be changed to “Code that uses a base class must be able to substitute a subclass without knowing it”.
- The LSP applies to inheritance hierarchies. It specifies that you should design your classes so that client dependencies can be substituted with subclasses without the client knowing about the change.
- All subclasses must, therefore, operate the same manner as their base classes.
- The specific functionality of the subclass may be different but must conform to the expected behaviour of the base class. To be a true behavioural subtype, the subclass must not only implement the base class’s methods and properties but also conform to its implied behaviour.
This requires compliance with several rules.
- The first rule is that there should be contravariance between parameters of the base class’s methods and the matching parameters in subclasses.
This means that the parameters in subclasses must either be the same types as those in the base class or must be less restrictive. Similarly, there must be covariance between method return values in the base class and its subclasses.
This specifies that the subclass’ return types must be the same as, or more restrictive than, the base class’ return types. - The next rule concerns preconditions and postconditions. A precondition of a class is a rule that must be in place before an action can be taken.
For example, before calling a method that reads from a database you may need to satisfy the precondition that the database connection is open.
Postconditions describe the state of objects after a process is completed.For example, it may be assumed that the database connection is closed after executing a SQL statement. The LSP states that the preconditions of a base class must not be strengthened by a subclass and that postconditions cannot be weakened in subclasses.
- Next the LSP considers invariants. An invariant describes a condition of a process that is true before the process begins and remains true afterwards.
For example, a class may include a method that reads text from a file. If the method handles the opening and closing of the file, an invariant may be that the file is not open
before the call or afterwards. To comply with the LSP, the invariants of a base class must not be changed by a subclass. - The next rule is the history constraint. By their nature, subclasses include all of the methods and properties of their superclasses. They may also add further members.
The history constraint says that new or modified members should not modify the state of an object in a manner that would not be permitted by the base class. For example, if the base class represents an object with a fixed size, the subclass should not permit this size to be modified. - The final LSP rule specifies that a subclass should not throw exceptions that are not thrown by the base class unless they are subtypes of exceptions that may be thrown by the base class.
The above rules cannot be controlled by the compiler or limited by object-oriented programming languages.
Instead, you must carefully consider the design of class hierarchies and of types that may be subclassed in the future. Failing to do so risks the creation of subclasses that break rules and create bugs in types that are dependent upon them.
One common indication of non-compliance with the LSP is when a client class checks the type of its dependencies. This may be by reading a property of an object that artificially describes its type or by using reflection to obtain the type. Often a switch statement will be used to perform a different action according to the type of the dependency. This additional complexity also violates the Open / Closed Principle (OCP), as the client class will need to be modified as further subclasses are introduced.
Download Examples |
Code Available Before LSP
ProjectFile.java
package com.javaskool.before;
public class ProjectFile {
public String FilePath;// { accessor; mutator; }
public byte[] FileData;// { accessor; mutator; }
public void LoadFileData()
{
// Retrieve FileData from disk
}
public void SaveFileData()
{
// Write FileData to disk
}
public boolean isReadOnly()
{
return true;
}
}
ReadOnlyFile.java
package com.javaskool.before;
public class ReadOnlyFile extends ProjectFile {
public void SaveFileData()
{
//throw new Exception();
}
}
Project.java
package com.javaskool.before;
import java.util.Collection;
public class Project {
public Collection<ProjectFile> ProjectFiles;// { accessor; mutator; }
public void LoadAllFiles()
{
for(ProjectFile file : ProjectFiles)
{
file.LoadFileData();
}
}
public void SaveAllFiles()
{
for(ProjectFile file:ProjectFiles)
{
if (file.isReadOnly())
file.SaveFileData();
}
}
}
Code Available After LSP
ProjectFile.java
package com.javaskool.after;
public class ProjectFile {
public String FilePath;// { accessor; mutator; }
public byte[] FileData;// { accessor; mutator; }
public void LoadFileData()
{
// Retrieve FileData from disk
}
}
WriteableFile.java
package com.javaskool.after;
public class WriteableFile {
public void SaveFileData()
{
// Write FileData to disk
}
}
Project.java
package com.javaskool.after;
import java.util.Collection;
public class Project {
public Collectiol<ProjectFile> AllFiles;// { accessor; mutator; }
public Collection<WriteableFile> WriteableFiles;// { accessor; mutator; }
public void LoadAllFiles()
{
for(ProjectFile file: AllFiles)
{
file.LoadFileData();
}
}
public void SaveAllWriteableFiles()
{
for (WriteableFile file : WriteableFiles)
{
file.SaveFileData();
}
}
}
Recent Comments