Single Responsibility Principle

Bir kod ile karşı karşıya kaldığınızda burada ne yapılıyor acaba diye hiç düşündünüz mü? İşte o kod kuvvetle muhtemel SRP’ye (single responsibility principle) uygun yazılmamıştır. Çünkü SRP’nin asıl amacı okunabilirliği ve anlaşılabilirliği ve dolayısıyla güncellenebilirliği yüksek kod parçaları geliştirmektir. Bir satır, bir method, yada bir sınıf hiç farketmez… Hepsi tek bir iş yapmalı ve kendi işini yapmalıdır. Bu sebeple SRP’yi cümle, method, sınıf seviyelerine uygulamak mümkündür.
Aslında SRP’yi bir felsefe olarak düşünmemiz gerekir. Dolayısı ile sadece kod yazarken değil bir veritabanı şeması tasarlarken, bir tablo oluştururken, bir mikro servis geliştirirken yine bu felsefeyi göz önünde bulundurmamız gerekir.
Cümle Seviyesi
var name = "Emre";
var age = 29;
var result = name.equals("Ahmet") ? age < 18 ? false : true : name.equals("Emre") ? age < 24 ? true : false : false;
Yukarıda gördüğümüz üçlü operatörler (ternary operator) ile oluşturulmuş ifade ilk bakışta anlaması zor bir ifade. Tek bir cümlede hem isim kırılımı hem yaş kırılımı kontrol ediliyor hemde result ifadesi dönülüyor. Tabiki biraz göz gezdirdiğimizde çözebiliyoruz fakat neden daha basit olmasın?
Ayrıca koşul ifadesine baktığımızda isim kırılımı için herhangi bir kısıtlama gözükmüyor. Dolayısı ile yeni gelecek isimlendirme ile buradaki koşul ifadesi giderek büyüyebilecek bir durumda. Burada isim kırılımını switch-case ile yapmak kodun daha anlaşılır olmasını sağlayacaktır.
switch (name) {
case "Ahmet":
result = age >= 18;
break;
case "Emre":
result = age < 24;
break;
default:
result = false;
break;
}
Peki üçlü operatörü kullanmak madem kodun okunabilirliğini azaltıyor o zaman neden var? Aslında üçlü operatör kodun okunabilirliğini azaltmaz hatta artırabilir. Önemli olan doğru yerde kullanmak. SRP’nin söylediği herkes tek bir iş yapsın. Dolayısı ile bir satırda istediğimiz şey tek bir işlemin yapılması. Tek bir koşulun olduğu durumda üçlü operatörü kullanmak daha mantıklı olacaktır.
Cümle seviyesinde örnek vermek aslında biraz zor. SRP genellikle method ve sınıf seviyelerinde daha çok kullanılıyor. Fakat felsefeyi anlayarak sadece bu seviyelerde kalmamak ve kodun her alanında bu felsefeyi uygulamak gerektiğini düşündüğüm için bu seviyede de örnek vermek istedim.
Metot Seviyesi
Metod seviyesinde SRP’yi incelerken bir sınıfa ait temel metodlarını (constructer, getter, setter, toString vb.) göz ardı edeceğiz. Bunların dışında kalan metodları da 2 ana başlık altında inceleyebiliriz. Bunlardan ilki daha alt sınıflara bölünemeyecek kadar küçük olan ve tek bir iş yapan atomik metodlar. İkincisi ise bu atomik metodları kullanarak bir süreci yöneten metodlar. Dolayısı ile bir metodla karşılaştığımızda amacımız bu metodun daha küçük atomik metodlara bölünüp bölünemeyeceğini kontrol etmek olmalı.
Bir metod çok fazla parametre alıyorsa (3 ve üzeri), satır sayısı çoksa (10–20 satır üzeri) bu metodun gözden geçirilmesi gerekir. Çünkü bir metodun çok fazla parametre alması veya çok fazla kod içermesi kuvvetle muhtemel birden fazla iş yaptığını gösterir ve daha küçük metodlara ayrılabilirler.
public static void processAndSaveUserData(List<String> userData, String filePath, boolean shouldSendEmail, String email) {
// Verileri işleme
for (int i = 0; i < userData.size(); i++) {
userData.set(i, userData.get(i).toUpperCase());
}
// Verileri dosyaya kaydetme
FileWriter writer = null;
try {
writer = new FileWriter(filePath);
for (String data : userData) {
writer.write(data + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// E-posta gönderme
if (shouldSendEmail) {
System.out.println("Sending email to " + email);
}
}
Evet yukarıda bir metod görüyoruz. Gerek metodun aldığı parametrelerden gerek kod fazlalığından, metodun tekrar gözden geçirilmesi gerektiği sonucunu çıkarabiliriz. Kodu detaylıca incelediğimiz metodumuz önce kendisine verilen userData listesini işliyor daha sonra bir dosyaya yazıyor ve son olarak e-posta gönderimi yapıyor. Gördüğümüz gibi 1 metod 3 farklı iş yapıyor. Burada yapılan her bir işi ayrı bir metoda taşımak mümkün.
private static List<String> processUserData(List<String> userData) {
for (int i = 0; i < userData.size(); i++) {
userData.set(i, userData.get(i).toUpperCase());
}
return userData;
}
private static void saveUserData(List<String> userData, String filePath) {
FileWriter writer = null;
try {
writer = new FileWriter(filePath);
for (String data : userData) {
writer.write(data + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void sendMail(String email) {
System.out.println("Sending email to " + email);
}
Bu 3 farklı metod artık tek bir iş yapıyor ve tekrar bölünemeyecek kadar küçüldüler. İşte bu metodlar artık bizim atomik metodlarımız. Şimdide süreci yönetecek metodumuzu yazalım.
public static void handleUserData() {
var userData = processUserData(List.of("user1", "user2"));
saveUserData(userData, "path/file.txt");
sendMail("iletisim@emrecogalan.com");
}
Bu metodumuz da görüldüğü üzere atomik metodları kullanarak ilgili süreci yönetiyor. İlk bakışta birden fazla iş yapıyor gibi gözüksede aslında yaptığı tek iş ilgili süreci yönetmek. Bu süreçte yapılması gereken tüm işlemler alt metodlara yüklenmiş.
Sınıf Seviyesi
Sınıf seviyesinde SRP’yi uygularken yine aynı şeyleri yapıyoruz. Çok fazla sayıda metod içeren sınıfları (şişman sınıf) düzenlememiz gerekiyor. Aynı işi yapan veya aynı domain üzerinde iş yapan metodların aynı sınıfta yer almasını sağlarken diğer metodları farklı sınıflara ayırıyoruz. Yukarıda verdiğim örneğe devam edecek olursak “handleUserData, saveUserData, processUserData” metodları user ile ilgili işlemler olduğundan UserManager gibi bir sınıf altında toplayabiliriz. Fakat mail gönderme işlemini yapan metodu EmailService gibi bir sınıfa alarak ayırmamız gerekir.
class EmailService {
static void sendMail(String email) {
System.out.println("Sending email to " + email);
}
}
class UserManager {
public static void handleUserData() {
var userData = processUserData(List.of("user1", "user2"));
saveUserData(userData, "path/file.txt");
EmailService.sendMail("iletisim@emrecogalan.com");
}
private static List<String> processUserData(List<String> userData) {
for (int i = 0; i < userData.size(); i++) {
userData.set(i, userData.get(i).toUpperCase());
}
return userData;
}
private static void saveUserData(List<String> userData, String filePath) {
FileWriter writer = null;
try {
writer = new FileWriter(filePath);
for (String data : userData) {
writer.write(data + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Sonuç
Sonuç olarak SRP bir bakış açısıdır, bir zorunluluk değildir. Kodun okunabilirliğini, anlaşılabilirliğini ve dolayısıyla güncellenebilirliğini artırmayı hedefler. Bunun için de yapılması gereken odaklanan noktada tek bir işin yapılıyor olmasıdır. Bu bir cümle, metod, sınıf, katman, paket, mikroservis olabilir. Hatta bir veritabanı tablosu veya şeması olabilir. Bakış açısı hep aynı olmalıdır.
Tüm Yazılım Prensipleri
Serideki tüm yazılar burada.

