In dit artikel zult u over de SOLID-principes leren. U zult deel uitkomen op elk principe samen met Java-codevoorbeelden.
SOLID-principes zijn een set van vijf ontwerptheorieën die gebruikt worden in objectgeoriënteerde programmeren. Door deze beginselen te volgen, kunt u robuuste software ontwikkelen. Het zal uw code efficiënter, leesbaarder en onderhoudbaarder maken.
SOLID is een acroniem dat staat voor:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Single Responsibility Principle
De single responsibility principle stelt dat elke klasse een enkele, gerichte verantwoordelijkheid moet hebben, een enkele reden om te veranderen.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
In het bovenstaande voorbeeld heeft de Employee
klasse enkele klassen-specifieke gedragingen zoals getDesignation
& updateSalary
.
Bovendien heeft de klasse ook een andere methode genaamd sendMail
die de verantwoordelijkheid van de Employee
klasse vervalt.
Dit gedrag is niet specifiek aan deze klasse en het schenden van het enkele verantwoordelijkheidsprincipe. Om dit te overwinnen, kunt u de methode sendMail
verplaatsen naar een aparte klasse.
Hier is hoe:
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
Open/Closed Principe
Volgens het open/closed principe moeten componenten open zijn voor uitbreiding, maar gesloten voor wijziging. Om dit principe te begrijpen, nemen we een voorbeeld van een klasse die de oppervlakte van een vorm berekent.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// bereken de oppervlakte van Vierkant
} else if(shape instanceof Circle){
// bereken de oppervlakte van Cirkel
}
return areaOfShape;
}
Het probleem met het bovenstaande voorbeeld is dat als er een nieuwe instantie is van het type Vorm
waarvoor je in de toekomst de oppervlakte moet berekenen, je de bovenstaande klasse moet wijzigen door nog een voorwaardelijk else-if
blok toe te voegen. Uiteindelijk moet je dit doen voor elk nieuw object van het type Vorm
.
Om dit te voorkomen, kun je een interface maken en elke Vorm
deze interface laten implementeren. Dan kan elke klasse zijn eigen implementatie bieden voor het berekenen van de oppervlakte. Hierdoor kan je programma in de toekomst gemakkelijk worden uitgebreid.
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-substitutieprincipe
Het Liskov-substitutieprincipe stelt dat je een superklasseobject moet kunnen vervangen door een subklasseobject zonder de juistheid van het programma te beïnvloeden.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { //een of andere implementatie }
}
class Ostrich extends Bird {
@Override
public void fly() { /// dummy-implementatie }
}
In het bovenstaande voorbeeld breiden de klasse Eagle
en de klasse Ostrich
beide de klasse Bird
uit en overschrijven de methode fly()
. De klasse Ostrich
moet echter een dummy-implementatie bieden omdat het niet kan vliegen, en gedraagt zich daarom niet op dezelfde manier als we het object van de klasse Bird
vervangen.
Dit schendt het Liskov-substitutieprincipe. Om dit op te lossen, kunnen we een aparte klasse maken voor vogels die kunnen vliegen en de klasse Eagle
ervan laten uitbreiden, terwijl andere vogels een andere klasse kunnen uitbreiden die geen fly
-gedrag bevat.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // enkele implementatie }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // enkele implementatie }
}
Interface Segregation Principle
Volgens het interface segregation principe moet je kleine, gerichte interfaces bouwen die de client niet dwingen om gedrag te implementeren dat ze niet nodig hebben.
Een eenvoudig voorbeeld zou zijn om een interface te hebben die zowel het gebied als het volume van een vorm berekent.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // bereken het gebied }
double calculateVolume(){ // dummy-implementatie }
}
Het probleem hiermee is dat als een vorm van het type Square
dit implementeert, het gedwongen wordt om de methode calculateVolume()
te implementeren, terwijl dit niet nodig is.
Aan de andere kant kan een Kubus
beide implementeren. Om dit op te lossen kunnen we de interface opdelen en twee aparte interfaces hebben: één voor het berekenen van het oppervlak en een andere voor het berekenen van het volume. Dit stelt individuele vormen in staat om te beslissen wat ze willen implementeren.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // bereken het oppervlak }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // bereken het oppervlak }
@Override
public double calculateVolume() {// bereken het volume }
}
Beginselen van de Dependency Inversion
In het beginsel van de dependency inversion zouden modules op hoog niveau niet afhankelijk moeten zijn van modules op laag niveau. Met andere woorden, je moet abstractie volgen en zorgen voor losse koppeling
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 het gegeven voorbeeld is de klasse Employee
direct afhankelijk van de klasse EmailNotification
, wat een module op laag niveau is. Dit schendt het beginsel van de dependency inversion.
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(){
// implementeer notificatie via e-mail
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
In het bovenstaande voorbeeld hebben we gezorgd voor losse koppeling. Employee
is niet afhankelijk van een concrete implementatie, maar alleen van de abstractie (notificatie-interface).
Als we de notificatiemodus moeten wijzigen, kunnen we een nieuwe implementatie maken en deze doorgeven aan Employee
.
Conclusie
Tot slot hebben we de essentie van de SOLID-principes behandeld aan de hand van eenvoudige voorbeelden in dit artikel.
Deze principes vormen de bouwstenen voor het ontwikkelen van applicaties die zeer uitbreidbaar en herbruikbaar zijn.
Ga met mij verbonden op LinkedIn.
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/