The Liskov Substitution Principle (LSP) stands as one of the essential SOLID principles in object-oriented programming and design. Originally presented by Barbara Liskov in 1987, this principle is meticulously crafted to offer direction on how inheritance and polymorphism are implemented in object-oriented programming. In languages such as C# and various others following object-oriented paradigms, the Liskov Substitution Principle serves as a crucial directive for the creation and utilization of classes and inheritance hierarchies.
The Liskov Substitution Principle can be defined as follows:
"Subtypes must be interchangeable with their base types without affecting the correctness of the program."
It signifies that when there is a base class and a derived class, instances of the derived class must be capable of substituting instances of the base class without causing any unforeseen issues or breaching the program's invariants. In C#, this principle is frequently observed in inheritance and polymorphism, where derived classes are expected to conform to the same agreement (interface) as their base classes.
Here are some essential points to grasp about the Liskov Substitution Principle in C#:
- The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
- This principle ensures that a derived class should extend the base class without changing its behavior.
- Violating the Liskov Substitution Principle can lead to unexpected behavior in the program.
- By adhering to this principle, it promotes code reusability and makes the codebase more flexible and maintainable.
- In C#, implementing interfaces and using abstract classes are common ways to follow the Liskov Substitution Principle effectively.
Inheritance Hierarchy: Within C#, the Liskov Substitution Principle (LSP) focuses mainly on the connection between a parent class and its child classes. Child classes are expected to expand and enhance the functionality of the parent class while upholding the identical interface (methods and properties).
Method Signatures: Methods within derived classes are required to have identical method signatures to those in the base class. This signifies that the derived class has the ability to override or extend the functionality of the base class methods, while ensuring consistency in the method's name, parameters, and return types.
A derived class is expected to fulfill or adjust the postconditions (expected behavior) of the base class methods. It is essential that the derived class does not compromise or breach these conditions. Essentially, a derived class has the flexibility to enhance its methods to be more permissive but should not make them more restrictive.
Preconditions: An inherited class is responsible for reinforcing or upholding the preconditions (required conditions) of the base class methods. It must not weaken these preconditions. This guarantees that any code depending on the base class interface will continue to function accurately with instances of the derived class.
If the base class method raises exceptions under specific circumstances, the derived class has the option to raise the same exceptions or more precise exceptions. It must avoid throwing broader or unexpected exceptions.
How to use Liskov Substitution Principl? in C#:
If you intend to apply the Liskov Substitution Principle (LSP) effectively in C#, adhere to these guidelines and best practices:
D?sign a Common Bas? Class or Int?rfac?:
Begin by formulating a foundational class or interface that outlines a set of functions and attributes that are common to all subclasses. This universal base class or interface serves as the blueprint that all derived subclasses are required to follow.
Ov?rrid? or Impl?m?nt M?thods Appropriat?ly:
In derived classes, override or implement the methods and properties specified in the base class or interface as required. Validate that the functionality of these methods aligns with the requirements established by the base class or interface.
Maintain th? Sam? M?thod Signatur?s:
In derived classes, the method signatures (method name, parameters, and return type) must match those in the base class or interface. This ensures that objects of derived classes are interchangeable with objects of the base class.
Follow Postconditions and Pr?conditions:
Ensure that the postconditions (expected behavior) of overridden methods in derived classes are at least as stringent as those of the base class. Put differently, derived classes must meet or loosen the same conditions.
Uphold or enhance the preconditions (necessary conditions) of the base class methods in derived classes. Derived classes must not weaken these preconditions.
Avoid Using th? "n?w" K?yword Unn?c?ssarily:
In C#, the "new" keyword is employed to conceal or mask a method or property in a derived class. Exercise caution when utilizing this term, as it has the potential to cause confusion and unexpected outcomes if not applied cautiously. Generally, it is preferable to override methods rather than shadow them.
T?st Polymorphism:
Test the code for polymorphism by generating instances of both the parent class and subclasses and employing them interchangeably. Validate that the functionality of the subclasses remains in accordance with the agreement of the parent class.
Docum?nt Your D?sign:
Clearly outline the base class or interface and offer instructions on how derived classes should implement their methods. This detailed documentation is crucial for fostering a shared comprehension among developers.
R?gularly R?vi?w and R?factor:
Regularly inspect the codebase to ensure that derived classes continue to comply with the base class contract. If modifications or updates are necessary, refactor the code to uphold consistency.
Exampl?:
Let's consider an example to showcase the application of the Liskov Substitution Principle in C#:
using Syst?m;
class Shap?
{
public virtual doubl? Ar?a()
{
r?turn 0;
}
}
class Circl? : Shap?
{
public doubl? Radius { g?t; s?t; }
public ov?rrid? doubl? Ar?a()
{
r?turn Math.PI * Radius * Radius;
}
}
class R?ctangl? : Shap?
{
public doubl? Width { g?t; s?t; }
public doubl? H?ight { g?t; s?t; }
public ov?rrid? doubl? Ar?a()
{
r?turn Width * H?ight;
}
}
class Program
{
static void Main(string[] args)
{
Shap? circl? = n?w Circl? { Radius = 5 };
Shap? r?ctangl? = n?w R?ctangl? { Width = 4, H?ight = 6 };
Consol?.Writ?Lin?("Ar?a of th? circl?: " + circl?.Ar?a());
Consol?.Writ?Lin?("Ar?a of th? r?ctangl?: " + r?ctangl?.Ar?a());
}
}
Output:
Ar?a of th? circl?: 78.5398163397448
Ar?a of th? r?ctangl?: 24
Explanation:
- In this example, w? d?fin? a bas? class Shap? with a virtual m?thod Ar?a , which calculat?s th? ar?a of a g?om?tric shap?. By d?fault, it r?turns 0, but d?riv?d class?s can ov?rrid? this m?thod to provid? specific impl?m?ntations.
- W? cr?at? a d?riv?d class Circl? that inh?rits from Shap?. It introduc?s an additional prop?rty Radius and ov?rrid?s th? Ar?a m?thod to calculat? th? ar?a of a circl? using th? formula πr² .
- Similarly, w? cr?at? anoth?r d?riv?d class R?ctangl? that inh?rits from Shap?. It introduc?s prop?rti?s Width and height and ov?rrid?s th? Ar?a m?thod to calculat? th? ar?a of a r?ctangl? using th? formula width * height.
- In th? Main m?thod, w? d?monstrat? th? Liskov Substitution Principl?:
- W? cr?at? an obj?ct of th? Circl? class and s?t its Radius prop?rty to 5.
- W? cr?at? an obj?ct of th? R?ctangl? class and s?t its Width to 4 and height to 6.
- After that, w? calls th? Ar?a m?thod on both obj?cts, which works int?rchang?ably b?caus? of polymorphism and adh?r?nc? to th? Liskov Substitution Principl?.
- Finally, w? print th? calculat?d ar?as of th? circl? and th? r?ctangl? using Consol?.Writ?Lin?.
H?r?'s th? ?xplanation in a mor? structur?d form:
- Th? Shap? class s?rv?s as a bas? class for various g?om?tric shap?s. It d?fin?s a virtual Ar?a m?thod that r?turns 0 by d?fault.
- Th? Circl? class is a specific shap? (a circl?) that d?riv?s from Shap?. It ov?rrid?s th? Ar?a m?thod to calculat? th? ar?a bas?d on th? radius provid?d.
- Th? R?ctangl? class is another specific shap? (a r?ctangl?) that also d?riv?s from Shap?. It ov?rrid?s th? Ar?a m?thod to calculat? th? ar?a bas?d on th? width and height.
- In th? Main m?thod, w? cr?at? instanc?s of Circl? and R?ctangl? and s?t th?ir sp?cific prop?rti?s (Radius and Width/height).
- After that, w? call th? Ar?a m?thod on th?s? obj?cts, and due to polymorphism and adh?r?nc? to th? Liskov Substitution Principl?, th? appropriat? ov?rridd?n m?thod is ?x?cut?d for ?ach obj?ct, corr?ctly calculating and r?turning th? ar?a.
- Th? calculat?d ar?as ar? print?d to th? consol?, d?monstrating th? Liskov Substitution Principl? by substituting d?riv?d obj?cts (Circl? and R?ctangl?) for th? bas? class (Shap?) without alt?ring th? corr?ctn?ss of th? program.
Compl?xity Analysis:
Tim? Compl?xity:
- Cr?ating th? obj?cts circl? and r?ctangl? involv?s minimal tim? compl?xity and is usually consid?r?d O(1) , as it does not d?p?nd on th? siz? of data structure or any loops.
- Calling th? Ar?a m?thod on th?s? obj?cts also has minimal compl?xity. In this sp?cific cont?xt, it d?p?nds on th? impl?m?ntation of th? Ar?a m?thod for ?ach class, but it g?n?rally involv?s basic arithm?tic op?rations. So, it has O(1) time complexity for both th? Circl? and R?ctangl? class?s.
- Printing th? results with Consol?.Writ?Lin? is also typically O(1), as it doesn't scal? with input siz?.
- Ov?rall, th? tim? compl?xity of this cod? is O(1) or constant tim?. Th? ?x?cution tim? do?sn't d?p?nd on th? siz? of any data structur?s or th? input siz?.
Spac? Compl?xity:
- Cr?ating obj?cts circl? and r?ctangl? consum?s m?mory to stor? th?ir prop?rti?s (Radius, Width, and height) and r?f?r?nc?s. Th? spac? r?quir?d is proportional to th? numb?r of obj?ct prop?rti?s, but it doesn't d?p?nd on th? input siz?. So, it has O(1) space complexity.
- Calling th? Ar?a m?thod doesn't hav? any significant impact on spac? compl?xity sinc? it primarily involv?s calculations without additional m?mory allocations.
- Th? Consol?.Writ?Lin? stat?m?nts don't consum? ?xtra spac? that scal?s with th? input. Th? spac? n??d?d for printing is usually small and constant, making it O(1) .
- In summary, the spac? compl?xity of this cod? is also O(1) or constant spac?. It doesn't d?p?nd on th? siz? of input data or any data structur?s.
Exampl?:
Let's consider a scenario where we calculate the area of a circle and a rectangle in C# without implementing the Liskov Substitution Principle.
using Syst?m;
class Shap?
{
public doubl? Ar?a()
{
r?turn 0;
}
}
class Circl? : Shap?
{
public doubl? Radius { g?t; s?t; }
public n?w doubl? Ar?a()
{
r?turn Math.PI * Radius * Radius;
}
}
class R?ctangl? : Shap?
{
public doubl? Width { g?t; s?t; }
public doubl? H?ight { g?t; s?t; }
public n?w doubl? Ar?a()
{
r?turn Width * H?ight;
}
}
class Program
{
static void Main(string[] args)
{
Shap? circl? = n?w Circl? { Radius = 5 };
Shap? r?ctangl? = n?w R?ctangl? { Width = 4, H?ight = 6 };
Consol?.Writ?Lin?("Ar?a of th? circl?: " + circl?.Ar?a());
Consol?.Writ?Lin?("Ar?a of th? r?ctangl?: " + r?ctangl?.Ar?a());
}
}
Output:
Ar?a of th? circl?: 0
Ar?a of th? r?ctangl?: 0
Explanation:
- In this example, w? hav? a bas? class, Shap?, that d?fin?s a m?thod Ar?a r?turning 0. Th? Ar?a m?thod is not d?clar?d as virtual or abstract, so it cannot b? ov?rridd?n by d?riv?d class?s.
- After that, two d?riv?d class?s, Circl? and R?ctangl? , inh?rit from Shap?. How?v?r, th?y introduc? th?ir own Ar?a m?thods using th? n?w k?yword. It ?ff?ctiv?ly hid?s th? Ar?a m?thod of th? bas? class rather than ov?rriding it. It means that Circl? and R?ctangl? have their own Ar?a m?thods s?parat? from th? bas? class.
- In th? Main m?thod , w? cr?at? instanc?s of Circl? and R?ctangl? and assign th?m to Shap? r?f?r?nc?s (circl? and r?ctangl?). It is allow?d b?caus? d?riv?d class?s can b? assign?d to bas? class r?f?r?nc?s.
- Wh?n w? call th? Ar?a m?thod on circl? and r?ctangl?, th? b?havior is d?t?rmin?d by th? typ? of th? r?f?r?nc? variabl?, not th? actual typ? of th? obj?ct. In this case, th? n?w k?yword caus?s th? bas? class's m?thod to b? invok?d, not th? ov?rridd?n m?thods in Circl? and R?ctangl?. It r?sults in th? incorr?ct behavior b?caus? th? bas? class Ar?a m?thod always r?turns 0.
- W? print th? ar?as of th? circl? and th? r?ctangl? to th? consol?, but th? valu?s will b? 0 for both, as th? Ar?a m?thod of th? bas? class is invok?d.
- In summary, this cod? d?monstrat?s how using th? n?w k?yword in th? d?riv?d class?s (Circl? and R?ctangl?) caus?s th? bas? class m?thod to b? hidd?n and not ov?rridd?n. It l?ads to un?xp?ct?d and incorr?ct behavior wh?n obj?cts of d?riv?d class?s ar? us?d through bas? class r?f?r?nc?s. It's an ?xampl? of not adh?ring to th? Liskov Substitution Principl?, which r?comm?nds that d?riv?d class?s should ?xt?nd and not hid? th? b?havior of th? bas? class.
Compl?xity Analysis:
Tim? compl?xity:
The time complexity of this code is O(1) or constant time. The execution duration remains consistent regardless of the dimensions of any data structures or the input size.
Spac? compl?xity:
The space complexity of this code remains O(1) or constant space, similar to the Liskov Substitution Principle-adhering code.
Th? time and spacial complexities of th? uncompliant cod? with th? Liskov Substitution Principl? ar? both O(1), indicating that th?y ar? constant and not affected by th? input siz? or data structur?s. This is du? to th? foundational structur? and actions of th? program, including instantiation of objects, method invocations, and console printing, which do not vary with input size and involve fundamental arithmetic and memory handling.
R?al tim? Applications:
Here are some real-world applications or situations where the Liskov Substitution Principle (LSP) is implemented in software design.
G?om?tric Shap?s:
In the realm of graphic design and computer-aided design (CAD) software, a multitude of geometric forms such as circles, rectangles, triangles, and polygons can be altered and displayed. These forms adhere to a shared geometric shape interface or base class.
The LSP enables software to manage various shapes consistently, simplifying tasks such as resizing, rotating, and rendering.
Banking Syst?ms:
Banking software consists of different categories of accounts, like savings, checking, and credit card accounts, which are subclasses derived from a shared base account class.
The LSP principle ensures that operations like transactions, managing accounts, and calculating interest can be consistently handled, simplifying the process of introducing new types of accounts later on.
V?hicl? Manag?m?nt:
Transportation and logistics software often handle various types of vehicles, such as automobiles, trucks, bicycles, and motorcycles.
This vehicle category follows a standard vehicle interface or parent class, enabling software to handle elements such as monitoring, fuel usage, and upkeep consistently.
Robotics:
In the field of robotics, various categories of robots exist, including drones, human-like robots, and industrial robots.
These robots conform to a standardized robot control interface, allowing activities such as motion planning, path tracking, and sensor data processing to be consistently implemented across various robot variations.