SOLID Prensipleri

YUNUS YALÇINKAYA
4 min readAug 10, 2023

--

SOLID, yazılım geliştiricilerin sağlam, test edilebilir, genişletilebilir ve sürdürülebilir, nesne yönelimli yazılım sistemleri tasarlamasına yardımcı olan prensiplerdir. 5 prensipten oluşur ve prensiplerin her biri yazılım sistemlerini geliştirirken ortaya çıkabilecek belirli bir sorunu çözmektedir.

Bu ilkelere uymak başarıyı garanti etmez, ancak bunlardan kaçınmak çoğu durumda işlevsellik, maliyet veya her ikisi açısından en azından yetersiz sonuçlara yol açacaktır.

SOLID, 5 tane prensibin baş harflerinden oluşan bir kısaltmadır. Bu prensipler:

- Single Responsibility Principle
- Open Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle

Single Responsibility Principle

Bir sınıfa veya metoda tek bir görev verme. Örneğin PaymentService adında bir servisimiz varsa bu sınıf sadece payment ile ilgili metodları içermeli. Veya sendEmail adında bir metodumuz varsa bu metod sadece mail gönderme işlemini yapmalı.

Aşağıdaki koda bakıldığında hem sınıf, hem de metodlar bu prensibe uygun değildir. Bir sınıf içerisindeki metodlar birbiri ile aynı amaca hizmet etmelidir. Metodlar ise tek bir amaca hizmet etmelidir.

public class Base {

public void sendNotification(String option){
if("email".equalsIgnoreCase(option)){
// email sending operations
System.out.println("email sent");
} else if ("sms".equalsIgnoreCase(option)) {
// sms sending operations
System.out.println("sms sent");
}
}

public void pay(String option){
if("creditCard".equalsIgnoreCase(option)){
// payment operations
System.out.println("paid");
}
else if("cash".equalsIgnoreCase(option)){
// payment operations
System.out.println("paid");
}
}
}

Bu kodu Single Responsibility prensibine uygun hale getirmek için 2 ayrı sınıfta yazalım. Bir tanesini ödeme işlemleri için “Payment” sınıfı, diğeri ise bildirim gönderme işlemleri ile ilgili “Notification” sınıfı olarak oluşturalım. Ayrıca metodları da tek bir iş yapacak şekilde bölelim. Doğru kullanım aşağıda verilmiştir.

public class Payment {

public void payWithCreditCard(){
// payment operations
System.out.println("paid");
}
public void payWithCash(){
// payment operations
System.out.println("paid");
}
}
public class Notification {

public void sendEmail() {
// email sending operations
System.out.println("email sent");
}

public void sendSms() {
// sms sending operations
System.out.println("sms sent");
}
}

Open Closed Principle

Kod gelişime açık, değişime kapalı olmalıdır. Yani koda yeni özellikler eklendiğinde, mevcut kod değişmemelidir. Generic tipler ve polimorfizm kullanımı bu prensibi destekler.

Daha iyi anlamak için basit bir örnek verelim. Öncelikle yanlış kullanımı görmek için aşağıdaki koda bakalım.

public class CalculateArea {

public void calculate(GeometricalShape geometricalShape){

if(geometricalShape instanceof Circle){
System.out.println( 3 * Math.pow(((Circle) geometricalShape).getRadius(),2));
}
else if(geometricalShape instanceof Square){
System.out.println(Math.pow(((Square) geometricalShape).getEdgeLength(), 2));
}
}
}

@Getter
@Setter
@AllArgsConstructor
class Circle implements GeometricalShape{
private int radius;
}

@Getter
@Setter
@AllArgsConstructor
class Square implements GeometricalShape{
private int edgeLength;
}

Yukarıdaki koda bakıldığında her geometrik şekil eklenildiğinde, calculate() metodu değişmek zorundadır. Ayrıca bu şekiller arttıkça, calculate metodunda birçok if bloğu olacak ve bu da kodun okunurluğunu azaltacak ve test edilebilirliğini düşürecektir. Bu kodu aşağıdaki gibi oluşturduğumuzda, ne kadar geometrik şekil eklersek ekleyelim mevcut kod değişmeyecektir.

public class CalculateArea {

public void calculate(GeometricalShape geometricalShape){

geometricalShape.calculateArea();
}
}

@Getter
@Setter
@AllArgsConstructor
class Circle implements GeometricalShape {

private int radius;

public void calculateArea(){
System.out.println( 3 * Math.pow(radius,2));
}

}

@Getter
@Setter
@AllArgsConstructor
class Square implements GeometricalShape {

private int edgeLength;

public void calculateArea(){
System.out.println( 3 * Math.pow(edgeLength,2));
}
}

Liskov Substitution Principle

Türetilen bir sınıf, türeyen bir sınıfın bütün özelliklerini kullanabilmelidir. Yani türeyen sınıfın yerine kullanılabilmelidir. Örneğin, bir sınıfın implement edildiği interface’deki bir metodu kullanmaması gerekiyorsa bu prensibe aykırı davranılmış olur. Örneğin aşağıdaki interface’e bakıldığında, bu interface’i implement edecek sınıf, run() ve fly() metodlarının ikisini de override etmek zorundadır. Başka bir deyişle, implement edecek sınıf bu iki özelliği de karşılamalıdır. Yani uçamayan veya koşamayan bir hayvan sınıfı, bu interface’i implemente etmemelidir.

public interface Animal {

void run();
void fly();
}

Interface Segregation Principle

Bir interface’e gereğinden fazla yetenek eklemek yerine, daha çok özelleştirilmiş birden fazla interface kullanılmalıdır. Yanlış kullanıma örnek vermek için aşağıdaki kodları inceleyelim.

public interface Service {
void pay();
void sendNotification();
}
public class NotificationService implements Service{
@Override
public void pay() {

}

@Override
public void sendNotification() {
System.out.println("sent notification");
}
}
public class PaymentService implements Service{

@Override
public void pay() {
System.out.println("paid");
}

@Override
public void sendNotification() {

}
}

Yukarıdaki kodlara bakıldığında hem Interface Segregation hem de Liskov Substitution prensiplerine aykırıdır. Burada ayrı işler için kullanılan servisler için ayrı interface’ler kullanılmalıdır. Doğru kullanım aşağıdaki kod bloklarında verilmiştir.

public interface PaymentService {
void pay();
}
public interface NotificationService {
void sendNotification();
}
public class PaymentController implements PaymentService{
@Override
public void pay() {
System.out.println("paid");
}
}
public class NotificationController implements NotificationService{
@Override
public void sendNotification() {
System.out.println("sent notification");
}
}

Dependency Inversion Principle

Üst seviye sınıflar alt seviye sınıflara bağımlı olmamalıdır. Böylelikle alt sınıflarda yapılan değişikliker üst sınıfları etkilemez. Bu da araya bir soyutlama katmanı ekleyerek sağlanır. Yani oluşturulan sınıflar bir arayüzden implemente edilmelidir.

Aşağıdaki örneğe bakıldığında, API katmanındaki bir controller sınıfı ile business katmanındaki bir servis sınıfının birbirine doğrudan bağlı olduğu gözükmektedir. Bu yanlış bir kullanımdır.

@RestController
@RequestMapping("/api/payment")
@AllArgsConstructor
public class PaymentController {
PaymentService service;

@GetMapping
public void getPayment(){
service.create();
}
}
@Service
@NoArgsConstructor
@AllArgsConstructor
public class PaymentService {

public void create(){
System.out.println("created payment");
}
}

Doğru şekilde kullanım için bu iki sınıf arasına bir soyut katman eklenmelidir. Bu işlem de interface kullanılarak yapılabilir. Doğru kullanım aşağıda verilmiştir.

public interface PaymentService {
void create();
}
@Service
@NoArgsConstructor
@AllArgsConstructor
public class PaymentManager implements PaymentService{

public void create(){
System.out.println("created payment");
}
}
@RestController
@RequestMapping("/api/payment")
@AllArgsConstructor
public class PaymentController {
PaymentService service;

@GetMapping
public void getPayment(){
service.create();
}
}

Kaynaklar

Madasu, Vamsi & Venna, Trinadh & Eltaeib, Tarik. (2015). SOLID Principles in Software Architecture and Introduction to RESM Concept in OOP. Journal of Engineering Science and Technology. 2. 3159–40.

--

--