In diesem Artikel lernst du die SOLID-Prinzipien kennen. Du erhältst eine Verständnis jedes Prinzips zusammen mit Java-Code-Beispielen.
SOLID-Prinzipien sind eine Reihe von fünf Designprinzipien, die in objektorientiertem Programmieren verwendet werden. Der Anschluss an diese Prinzipien wird dir helfen, robuster Software zu entwickeln. Diese Prinzipien machen deinen Code effizienter, lesbarer und maintainable.
SOLID ist ein Akronym, das für folgendes steht:
- Single Responsibility Principle (Einzelfunktionalitätsprinzip)
- Open/Closed Principle (Offen/Geschlossen-Prinzip)
- Liskov Substitution Principle (Liskov-Substitution-Prinzip)
- Interface Segregation Principle (Schnittstellensegregationprinzip)
- Dependency Inversion Principle (Abhängigkeitsinversionprinzip)
Einzelfunktionalitätsprinzip
Das Einzelfunktionalitätsprinzip besagt, dass jede Klasse nur eine einzige, fokussierte Verantwortung haben muss, eine einzige Grund, sich zu ändern.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
In dem oben genannten Beispiel hat die Employee
-Klasse einige spezifische Verhaltensweisen der Employee
-Klasse, wie getDesignation
& updateSalary
.
Zusätzlich hat sie auch eine andere Methode namens sendMail
, die von der Verantwortung der Employee
-Klasse abweicht.
Dieses Verhalten ist nicht spezifisch für diese Klasse und verletzt somit das Einzelfunktionalitätsprinzip. Um dies zu überwinden, kannst du die Methode sendMail
in eine separate Klasse verschieben.
So kann es aussehen:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
Offen/Geschlossen Prinzip
Nach dem Offen/Geschlossen Prinzip müssen Komponenten für Erweiterungen offen sein, aber geschlossen für Änderungen. Um dieses Prinzip zu verstehen, nehmen wir ein Beispiel einer Klasse, die den Flächeninhalt eines Geometrischen Objekts berechnet.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// Berechne den Flächeninhalt eines Quadrats
} else if(shape instanceof Circle){
// Berechne den Flächeninhalt eines Kreises
}
return areaOfShape;
}
Das Problem mit dem oben genannten Beispiel besteht darin, dass falls in Zukunft ein neuer Fall von Typ Shape
auftritt, für den Sie den Flächeninhalt berechnen müssen, Sie die oben genannte Klasse ändern müssen, indem Sie einen weiteren else-if
-Block hinzufügen. Sie werden dies für jeden neuen Shape
-Objekt tun müssen.
Um dies zu vermeiden, können Sie ein Interface erstellen und jeder Shape
-Klasse dieses Interface implementieren. Danach kann jeder Klasse ihre eigene Implementierung bereitstellen, um den Flächeninhalt zu berechnen. Dies wird Ihrem Programm zukünftig eine leichte Erweiterbarkeit ermöglichen.
interface IAreaCalculator(){
double area();
}
class Square implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Square");
return 0.0;
}
}
class Circle implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Circle");
return 0.0;
}
}
Liskov-SubstitutionPrinzip
Das Liskov-SubstitutionPrinzip besagt, dass Sie ein übergeordnetes Objekt mit einem UntergeordnetenObjekt ersetzen können, ohne die Korrektheit des Programms zu beeinträchtigen.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { // einige Implementierung }
}
class Ostrich extends Bird {
@Override
public void fly() { // leeres Implementation }
}
In dem obigen Beispiel verlassen die Klassen Eagle
und Ostrich
die Basisklasse Bird
und überschreiben die Methode fly()
. Allerdings ist die Klasse Ostrich
gezwungen, eine leere Implementierung bereitzustellen, da sie nicht fliegen kann, und verhält sich daher nicht gleich, wenn wir ein Objekt der Klasse Bird
durch ein Objekt der Klasse Ostrich
ersetzen.
Dies verletzt die Liskov-Substitutionseigenschaft. Um dies zu beheben, können wir eine separate Klasse für fliegende Vögel erstellen und die Klasse Eagle
daraus ableiten, während andere Vögel eine andere Klasse erben, die keine fly
-Verhalten enthält.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // some implementation }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // some implementation }
}
Interface Segregation Principle
gemäß dem Interface Segregation Principle sollten Sie kleine, konzentrierte Schnittstellen aufbauen, die das Client nicht zwingen, Implementierungen für nicht benötigtes Verhalten durchzuführen.
Ein direkter Beispiel wäre, eine Schnittstelle zu haben, die sowohl den Flächen- als auch den Volumenberechnung für ein Gegenstandsmodell übernimmt.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // calculate the area }
double calculateVolume(){ // dummy implementation }
}
Das Problem bei diesem ist, dass ein Square
-Gegenstand, wenn es diese Schnittstelle implementiert, dazu gezwungen ist, die Methode calculateVolume()
zu implementieren, die es nicht braucht.
Andererseits kann ein Cube
sowohl das eine als auch das andere implementieren. Um dies zu überwinden, können wir die Schnittstelle aufteilen und zwei getrennte Schnittstellen verwenden: eine für die Berechnung des Flächeninhalts und eine weitere für die Berechnung des Volumens. Dies ermöglicht es jedem einzelnen geometrischen Objekt, selbst zu entscheiden, was es implementieren soll.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // Berechne die Fläche }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // Berechne die Fläche }
@Override
public double calculateVolume() {// Berechne das Volumen }
}
Abhängigkeitsinversionprinzip
According to the dependency inversion principle, high-level modules should not depend on low-level modules. In other words, you must follow abstraction and ensure loose coupling
public interface Notification {
void notify();
}
public class EmailNotification implements Notification {
public void notify() {
System.out.println("Sending notification via email");
}
}
public class Employee {
private EmailNotification emailNotification;
public Employee(EmailNotification emailNotification) {
this.emailNotification = emailNotification;
}
public void notifyUser() {
emailNotification.notify();
}
}
In the given example, the Employee
class depends directly on the EmailNotification
class, which is a low-level module. This violates the dependency inversion principle.
public interface Notification{
public void notify();
}
public class Employee{
private Notification notification;
public Employee(Notification notification){
this.notification = notification;
}
public void notifyUser(){
notification.notify();
}
}
public class EmailNotification implements Notification{
public void notify(){
//implement notification via email
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
In the above example, we have ensured loose coupling. Employee
is not dependent on any concrete implementation, rather, it depends only on the abstraction (notification interface).
If we need to change the notification mode, we can create a new implementation and pass it to the Employee
.
Conclusion
In conclusion, we’ve covered the essence of SOLID principles through straightforward examples in this article.
These principles form the building blocks for developing applications that are highly extensible and reusable.
Bitte verbinden Sie sich mit mir auf LinkedIn.
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/