Giriş: Teoriden Pratiğe SoC Yolculuğu

Yazılım geliştirmenin temel taşlarından biri olan Sorumlulukların Ayrılması (Separation of Concerns - SoC) prensibi, karmaşıklığı yönetmek, sürdürülebilir ve esnek sistemler inşa etmek için vazgeçilmez bir yaklaşımdır. Önceki tartışmalarda SoC'nin ne olduğunu, neden önemli olduğunu ve genel faydalarını ele aldık. Ancak bu güçlü prensibin gerçek değerini anlamanın en iyi yolu, onu eylem halinde görmektir. Teorik kavramlar önemlidir, ancak pratikte nasıl uygulandıklarını görmek, soyut fikirleri somut becerilere dönüştürür.

Bu makalenin amacı, SoC prensibinin en yaygın ve temel uygulama biçimlerine odaklanarak, bu prensibi gerçek dünya senaryolarında nasıl hayata geçirebileceğimizi detaylı örneklerle göstermektir. Özellikle iki ana eksene odaklanacağız:

Temel UI / İş Mantığı / Veri Ayrımı: En temel düzeyde sorumlulukları ayırmanın mantığını ve nasıl yapılabileceğini inceleyeceğiz. Bu, daha karmaşık mimarilerin temelini oluşturur.

Katmanlı Mimari (Layered Architecture): UI/Mantık/Veri ayrımını daha resmi ve yapılandırılmış bir hale getiren, en yaygın kullanılan mimari desenlerden birini derinlemesine inceleyeceğiz. Farklı katmanların rollerini, aralarındaki etkileşimi ve bu yapının sağladığı avantajları somut kod örnekleri (kavramsal veya pseudo-kod düzeyinde) üzerinden göstereceğiz.

Bu yolculukta, basit bir konsol uygulamasından başlayarak daha kapsamlı bir web uygulaması senaryosuna doğru ilerleyeceğiz. Her adımda, sorumlulukların nasıl tanımlandığını, nasıl ayrıldığını ve bu ayrımın kodun kalitesini (okunabilirlik, test edilebilirlik, bakım kolaylığı) nasıl artırdığını gözlemleyeceğiz. Arayüzlerin (Interfaces), Veri Aktarım Nesnelerinin (DTOs) ve Bağımlılık Enjeksiyonu (Dependency Injection) gibi tamamlayıcı tekniklerin SoC'yi uygulamada nasıl kilit roller oynadığına değineceğiz.

Amacımız, SoC'nin sadece büyük, karmaşık sistemler için geçerli olmadığını, en basit uygulamalarda bile düşünülmesi gereken temel bir zihniyet olduğunu göstermektir. Bu makalenin sonunda, farklı sorumlulukları nasıl tanıyacağınız, bunları etkili bir şekilde nasıl ayıracağınız ve Katmanlı Mimari gibi desenleri projelerinizde nasıl uygulayacağınız konusunda daha net bir anlayışa ve pratik bir bakış açısına sahip olacaksınız. SoC'yi koda dökmek, daha temiz, daha modüler ve gelecekteki değişikliklere daha dayanıklı yazılımlar oluşturmanın kapısını aralayacaktır.

Bölüm 1: Temel Ayrım - Kullanıcı Arayüzü (UI), İş Mantığı (Logic) ve Veri (Data)

SoC'nin en temel ve sezgisel uygulamalarından biri, bir uygulamanın işlevselliğini üç ana sorumluluk alanına ayırmaktır:

Kullanıcı Arayüzü (User Interface - UI) / Sunum (Presentation): Kullanıcı ile sistem arasındaki etkileşimden sorumlu olan kısımdır. Bu katman şunları yapar:

Kullanıcıya bilgiyi gösterir (ekranlar, formlar, raporlar, mesajlar).

Kullanıcıdan girdi alır (buton tıklamaları, form doldurma, komut satırı argümanları).

Girdiyi işlenmesi için İş Mantığı katmanına iletir.

İş Mantığı katmanından aldığı sonuçları kullanıcıya uygun bir formatta sunar.

Odak noktası: Kullanıcı deneyimi, görünüm ve etkileşim. Teknoloji örnekleri: HTML/CSS/JavaScript, React/Angular/Vue, Windows Forms/WPF, iOS/Android UI kitleri, Konsol Girdi/Çıktı.

İş Mantığı (Business Logic) / Etki Alanı (Domain): Uygulamanın "beyni"dir. Sistemin ne yapması gerektiğini tanımlayan kuralları, hesaplamaları ve iş akışlarını içerir. Bu katman şunları yapar:

UI'dan gelen istekleri alır ve doğrular (validation).

Uygulamanın temel kurallarını uygular (örneğin, bir siparişin toplam tutarını hesaplamak, bir kullanıcının belirli bir eylemi yapma yetkisi olup olmadığını kontrol etmek).

Gerekli verileri almak veya kaydetmek için Veri katmanıyla etkileşime girer.

Sonuçları veya işlenmiş verileri UI katmanına geri gönderir.

Odak noktası: Uygulamanın temel amacı, kuralları ve süreçleri. İdeal olarak UI ve Veri teknolojilerinden bağımsız olmalıdır.

Veri (Data) / Kalıcılık (Persistence): Uygulamanın ihtiyaç duyduğu verilerin saklanması, alınması ve yönetilmesinden sorumludur. Bu katman şunları yapar:

İş Mantığı katmanından gelen veri isteklerini (oku, yaz, güncelle, sil - CRUD) yerine getirir.

Veritabanı, dosya sistemi, harici API'ler veya diğer depolama mekanizmaları ile etkileşime girer.

Veriyi İş Mantığı katmanının anlayabileceği bir formata dönüştürür (ve tersi).

Odak noktası: Verinin depolanması, erişimi ve tutarlılığı. Teknoloji örnekleri: SQL Veritabanları (MySQL, PostgreSQL), NoSQL Veritabanları (MongoDB, Redis), ORM araçları (Hibernate, Entity Framework), Dosya G/Ç, Harici API istemcileri.

Neden Bu Ayrım Önemli?

Bu üç temel sorumluluğu ayırmak, SoC'nin temel faydalarını küçük ölçekte bile görmemizi sağlar:

Değişiklik İzolasyonu: Kullanıcı arayüzünde yapılacak bir değişiklik (örneğin, bir butonu yeniden konumlandırmak veya renkleri değiştirmek) iş mantığını veya veritabanı erişim kodunu etkilememelidir. Benzer şekilde, bir iş kuralındaki değişiklik (örneğin, KDV oranının güncellenmesi) UI veya veri saklama şeklini değiştirmemelidir. Veritabanı teknolojisini değiştirmek (örneğin, MySQL'den MongoDB'ye geçmek) ideal olarak sadece Veri katmanını etkilemeli, iş kurallarını veya kullanıcı arayüzünü etkilememelidir.

Odaklanma: Geliştiriciler, belirli bir sorumluluk alanına odaklanabilirler. UI geliştiricisi arayüzle, iş mantığı geliştiricisi kurallarla, veritabanı uzmanı ise veri erişimiyle ilgilenebilir.

Test Edilebilirlik: İş mantığı, UI veya gerçek bir veritabanına bağımlı olmadan test edilebilir. UI, sahte (mock) iş mantığı servisleri ile test edilebilir. Veri erişim kodu, sahte verilerle veya bellek içi (in-memory) veritabanları ile test edilebilir.

Yeniden Kullanılabilirlik: İş mantığı, farklı kullanıcı arayüzleri (web, mobil, masaüstü) tarafından potansiyel olarak yeniden kullanılabilir. Veri erişim mantığı da benzer şekilde farklı uygulamalar veya servisler tarafından kullanılabilir.

Şimdi bu temel ayrımı daha somut örneklerle inceleyelim.

Bölüm 2: Örnek 1 - Basit Bir Konsol Uygulamasında SoC Kavramı

SoC'nin en karmaşık mimarilere özgü olmadığını göstermek için çok basit bir örnekle başlayalım: Kullanıcıdan iki sayı alıp toplamını ekrana yazdıran bir konsol uygulaması.

Senaryo: İki tam sayı iste, topla ve sonucu göster.

Yaklaşım 1: SoC Olmadan (Her Şey Bir Arada)

import java.util.InputMismatchException;
import java.util.Scanner;

public class HesapMakinesiKarışık {

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int sayi1 = 0;
    int sayi2 = 0;
    boolean girdiGecerli = false;

    // --- Kullanıcı Arayüzü ve Girdi Alma ---
    System.out.println("Basit Toplama Hesap Makinesi");

    while (!girdiGecerli) {
        try {
            System.out.print("Birinci sayıyı girin: ");
            sayi1 = scanner.nextInt(); // UI + Veri Okuma

            System.out.print("İkinci sayıyı girin: ");
            sayi2 = scanner.nextInt(); // UI + Veri Okuma

            girdiGecerli = true; // Basit Doğrulama (Logic)

        } catch (InputMismatchException e) {
            System.out.println("Hatalı girdi! Lütfen tam sayı girin.");
            scanner.next(); // Hatalı girdiyi temizle (UI Hata Yönetimi)
        }
    }

    // --- İş Mantığı ---
    int toplam = sayi1 + sayi2; // Hesaplama (Logic)

    // --- Sonucu Gösterme (UI) ---
    System.out.println("Sayıların toplamı: " + toplam);

    scanner.close(); // Kaynak Yönetimi (Potansiyel olarak Veri/Altyapı)
}

}

Bu kod çalışır, ancak farklı sorumluluklar (main metodu içinde) iç içe geçmiştir:

Kullanıcıya mesaj gösterme ve girdi alma (UI).

Girdi doğrulama (basit de olsa Logic).

Asıl hesaplama işlemi (Logic).

Sonucu gösterme (UI).

Hata yönetimi (UI/Logic karışımı).

Yaklaşım 2: SoC Uygulanmış (Kavramsal Ayrım)

Şimdi aynı işlevi, sorumlulukları ayrı metotlara veya basit sınıflara ayırarak yapalım. Bu tam bir katmanlı mimari olmasa da, SoC zihniyetini yansıtır.

import java.util.InputMismatchException;
import java.util.Scanner;

// Sorumluluk: Kullanıcı ile etkileşim (UI)
class KonsolUI {
private Scanner scanner = new Scanner(System.in);

public void baslikGoster() {
    System.out.println("Basit Toplama Hesap Makinesi");
}

public int sayiIste(String mesaj) {
    while (true) { // Girdi doğrulama döngüsü UI katmanında
        try {
            System.out.print(mesaj);
            return scanner.nextInt();
        } catch (InputMismatchException e) {
            System.out.println("Hatalı girdi! Lütfen tam sayı girin.");
            scanner.next(); // Hatalı girdiyi temizle
        }
    }
}

public void sonucGoster(int sonuc) {
    System.out.println("Sayıların toplamı: " + sonuc);
}

public void kapat() {
     scanner.close();
}

}

// Sorumluluk: Hesaplama işlemleri (İş Mantığı)
class HesaplamaServisi {
public int topla(int a, int b) {
// Burada daha karmaşık iş kuralları veya doğrulamalar olabilir
// Örneğin: if (a < 0 || b < 0) throw new IllegalArgumentException("Negatif sayılar desteklenmiyor");
return a + b;
}
}

// Ana uygulama - Katmanları koordine eder
public class HesapMakinesiAyrık {

public static void main(String[] args) {
    KonsolUI ui = new KonsolUI();
    HesaplamaServisi hesaplamaServisi = new HesaplamaServisi();

    ui.baslikGoster();

    // UI katmanı girdi almaktan sorumlu
    int sayi1 = ui.sayiIste("Birinci sayıyı girin: ");
    int sayi2 = ui.sayiIste("İkinci sayıyı girin: ");

    // İş Mantığı katmanı hesaplamadan sorumlu
    int toplam = hesaplamaServisi.topla(sayi1, sayi2);

    // UI katmanı sonucu göstermekten sorumlu
    ui.sonucGoster(toplam);

    ui.kapat();
}

}

Bu ikinci yaklaşımda ne değişti?

KonsolUI sınıfı sadece kullanıcı etkileşimi, girdi alma ve çıktı gösterme ile ilgilenir. Hesaplamanın nasıl yapıldığını bilmez.

HesaplamaServisi sınıfı sadece toplama işlemiyle ilgilenir. Sayıların nereden geldiğini (konsol, dosya, web isteği?) veya sonucun nereye gideceğini bilmez.

HesapMakinesiAyrık sınıfının main metodu, bu iki ayrı sorumluluğu koordine eder.

Faydaları (Bu Küçük Ölçekte Bile):

Okunabilirlik: Her sınıfın/metodun amacı daha nettir.

Test Edilebilirlik: HesaplamaServisi.topla metodu, konsola veya kullanıcı girdisine ihtiyaç duymadan doğrudan test edilebilir (assertEquals(5, servis.topla(2, 3));). KonsolUI sınıfı da (biraz daha zor olsa da, mocklama veya girdi/çıktı yönlendirme ile) test edilebilir.

Değiştirilebilirlik: Eğer kullanıcı arayüzünü konsoldan bir grafik arayüze (GUI) değiştirmek istersek, sadece KonsolUI yerine yeni bir GrafikUI sınıfı yazıp main metodunda onu kullanmamız yeterli olurdu. HesaplamaServisi hiç değişmezdi. Eğer toplama kuralı değişseydi (örneğin, taşma kontrolü eklemek), sadece HesaplamaServisi değişirdi.

Bu basit örnek bile, sorumlulukları ayırmanın kodun yapısını nasıl iyileştirdiğini göstermektedir. Şimdi daha yapılandırılmış bir yaklaşıma, Katmanlı Mimariye geçelim.

Bölüm 3: Örnek 2 - Katmanlı Mimari ile Web Uygulaması

Katmanlı Mimari, SoC'yi uygulamak için en yaygın kullanılan ve yapılandırılmış desenlerden biridir. Genellikle Sunum (Presentation), İş Mantığı (Business Logic - BLL) ve Veri Erişimi (Data Access - DAL) katmanlarından oluşur.

Senaryo: Kullanıcıların görevler (tasks) ekleyebileceği, listeleyebileceği ve tamamlandı olarak işaretleyebileceği basit bir "Yapılacaklar Listesi" (To-Do List) web uygulaması.

Mimarinin Katmanları:

Sunum Katmanı (Presentation Layer):

Sorumluluk: HTTP isteklerini işlemek, kullanıcı arayüzünü (HTML veya JSON API) oluşturmak, kullanıcı girdisini almak, İş Mantığı Katmanı'nı çağırmak, sonuçları kullanıcıya sunmak.

Teknolojiler: Web Framework (Spring Boot, ASP.NET Core, Django, Express.js), HTML/CSS/JS, Template Engine (Thymeleaf, Jinja2), API (RESTful).

Bileşenler: Controller'lar (MVC), API Endpoint'leri, View'lar/Template'ler.

İş Mantığı Katmanı (Business Logic Layer - BLL):

Sorumluluk: Uygulamanın iş kurallarını ve iş akışlarını uygulamak. Girdi doğrulaması yapmak, Veri Erişim Katmanı'nı kullanarak verileri yönetmek.

Teknolojiler: Genellikle temel programlama dili (Java, C#, Python).

Bileşenler: Servis Sınıfları (TaskService), Domain Nesneleri/Entities (Task), Yardımcı sınıflar.

Veri Erişim Katmanı (Data Access Layer - DAL):

Sorumluluk: Veritabanı veya diğer kalıcı depolama mekanizmaları ile etkileşime girmek. CRUD (Create, Read, Update, Delete) operasyonlarını gerçekleştirmek.

Teknolojiler: Veritabanı (SQL, NoSQL), ORM (Hibernate, Entity Framework, SQLAlchemy), JDBC/ADO.NET.

Bileşenler: Repository Sınıfları (TaskRepository), Veritabanı bağlantı yönetimi.

Bağımlılık Yönü: Sunum -> BLL -> DAL. Alt katmanlar üst katmanları bilmez.

Kod Örnekleri (Kavramsal/Pseudo-kod):

  1. Veri Erişim Katmanı (DAL)

Görev Varlığı (Entity - Domain Katmanında da olabilir):

public class Task {
private long id;
private String description;
private boolean completed;
private LocalDate dueDate;
// Getters and Setters...
}

Repository Arayüzü (DAL Arayüzü - BLL bu arayüze bağımlı olacak):

public interface ITaskRepository {
Task findById(long id);
List findAll();
List findByCompletionStatus(boolean completed);
Task save(Task task); // Hem ekleme hem güncelleme için
void deleteById(long id);
}

Repository Uygulaması (DAL Uygulaması - Veritabanı Teknolojisine Bağlı):

// Örnek: JPA/Hibernate kullanan bir implementasyon
@Repository // Spring'de DAL bileşenini işaretler
public class JpaTaskRepository implements ITaskRepository {
@PersistenceContext
private EntityManager entityManager; // Veya Spring Data JPA Repository kullanılabilir

@Override
public Task findById(long id) {
    return entityManager.find(Task.class, id);
}

@Override
public List findAll() {
    return entityManager.createQuery("SELECT t FROM Task t", Task.class).getResultList();
}

@Override
public List findByCompletionStatus(boolean completed) {
     return entityManager.createQuery("SELECT t FROM Task t WHERE t.completed = :status", Task.class)
                   .setParameter("status", completed)
                   .getResultList();
}

@Override
@Transactional // İşlem yönetimi genellikle Servis katmanında olsa da, basitlik için burada olabilir
public Task save(Task task) {
    if (task.getId() == 0) { // Veya null kontrolü
        entityManager.persist(task); // Yeni görev ekle
        return task;
    } else {
        return entityManager.merge(task); // Mevcut görevi güncelle
    }
}

@Override
@Transactional
public void deleteById(long id) {
    Task task = findById(id);
    if (task != null) {
        entityManager.remove(task);
    }
}

}

SoC Notu: JpaTaskRepository, veritabanı ile nasıl konuşulacağının detaylarını bilir (JPA kullanır). İş mantığı (TaskService) bu detayları bilmez, sadece ITaskRepository arayüzünü bilir.

  1. İş Mantığı Katmanı (BLL)

Servis Arayüzü (Opsiyonel ama iyi pratik):

public interface ITaskService {
Task getTaskById(long id);
List getAllTasks();
Task addNewTask(String description, LocalDate dueDate);
Task markTaskAsCompleted(long id);
void deleteTask(long id);
}

Servis Uygulaması (BLL):

@Service // Spring'de BLL bileşenini işaretler
public class TaskService implements ITaskService {

private final ITaskRepository taskRepository; // DAL'a arayüz üzerinden bağımlılık

// Dependency Injection ile Repository enjekte edilir
@Autowired
public TaskService(ITaskRepository taskRepository) {
    this.taskRepository = taskRepository;
}

@Override
public Task getTaskById(long id) {
    Task task = taskRepository.findById(id);
    if (task == null) {
        // Hata yönetimi - Örnek: Kendi Exception türümüzü fırlatabiliriz
        throw new TaskNotFoundException("Görev bulunamadı: ID = " + id);
    }
    return task;
}

@Override
public List getAllTasks() {
    return taskRepository.findAll();
}

@Override
@Transactional // İşlemler genellikle servis metotlarında yönetilir
public Task addNewTask(String description, LocalDate dueDate) {
    // İş Kuralı Örneği: Açıklama boş olamaz
    if (description == null || description.trim().isEmpty()) {
        throw new IllegalArgumentException("Görev açıklaması boş olamaz.");
    }
    // İş Kuralı Örneği: Geçmiş tarihli görev eklenemez (Basit kural)
    if (dueDate != null && dueDate.isBefore(LocalDate.now())) {
         throw new IllegalArgumentException("Geçmiş tarihli görev eklenemez.");
    }

    Task newTask = new Task();
    newTask.setDescription(description);
    newTask.setCompleted(false);
    newTask.setDueDate(dueDate);

    return taskRepository.save(newTask);
}

@Override
@Transactional
public Task markTaskAsCompleted(long id) {
    Task task = getTaskById(id); // Önce görevi bul (veya findById kullan)
    if (task.isCompleted()) {
         System.out.println("Görev zaten tamamlanmış: ID = " + id);
         return task; // Veya hata fırlat
    }
    task.setCompleted(true);
    return taskRepository.save(task); // Güncellemek için save kullanılır
}

@Override
@Transactional
public void deleteTask(long id) {
    // Önce var olup olmadığını kontrol etmek iyi bir pratik olabilir
    getTaskById(id); // Yoksa TaskNotFoundException fırlatır
    taskRepository.deleteById(id);
}

}

SoC Notu: TaskService iş kurallarını (açıklama boş olamaz, geçmiş tarihli görev olmaz) ve iş akışını (önce bul, sonra tamamlandı yap) içerir. Verilerin nasıl saklandığını (SQL mi, NoSQL mi?) veya kullanıcıya nasıl gösterileceğini (HTML mi, JSON mı?) bilmez. Sadece ITaskRepository arayüzüne bağımlıdır.

  1. Sunum Katmanı (Presentation Layer - Örnek: Spring Boot REST Controller)

Veri Aktarım Nesnesi (Data Transfer Object - DTO): Katmanlar arasında veri taşımak için kullanılır. Domain nesnelerini (Entity) doğrudan dışarıya açmaktan kaçınmak için iyi bir pratiktir.

public class TaskDTO { // Sunum katmanına özel veri yapısı
private long id;
private String description;
private boolean completed;
private String formattedDueDate; // UI için formatlanmış tarih
// Getters and Setters...

// Entity'den DTO'ya dönüşüm metodu (veya Mapper kütüphanesi kullanılır)
public static TaskDTO fromEntity(Task task) {
    TaskDTO dto = new TaskDTO();
    dto.setId(task.getId());
    dto.setDescription(task.getDescription());
    dto.setCompleted(task.isCompleted());
    if (task.getDueDate() != null) {
         // Örnek formatlama
         dto.setFormattedDueDate(task.getDueDate().format(DateTimeFormatter.ISO_DATE));
    }
    return dto;
}

}

public class CreateTaskRequest { // Yeni görev oluşturma isteği için DTO
private String description;
private String dueDate; // String olarak alıp serviste parse edilebilir
// Getters and Setters...
}

REST Controller (Sunum):

@RestController
@RequestMapping("/api/tasks") // API yolunu tanımlar
public class TaskController {

private final ITaskService taskService; // BLL'e arayüz üzerinden bağımlılık

@Autowired
public TaskController(ITaskService taskService) {
    this.taskService = taskService;
}

@GetMapping("/{id}") // GET /api/tasks/{id}
public ResponseEntity getTask(@PathVariable long id) {
    try {
        Task task = taskService.getTaskById(id);
        return ResponseEntity.ok(TaskDTO.fromEntity(task)); // Entity'yi DTO'ya çevir
    } catch (TaskNotFoundException e) {
        return ResponseEntity.notFound().build();
    }
}

@GetMapping // GET /api/tasks
public List getAllTasks() {
    List tasks = taskService.getAllTasks();
    // Tüm listeyi DTO'ya çevir (Stream API ile örnek)
    return tasks.stream()
                .map(TaskDTO::fromEntity)
                .collect(Collectors.toList());
}

@PostMapping // POST /api/tasks
public ResponseEntity createTask(@RequestBody CreateTaskRequest request) {
    try {
        LocalDate dueDate = null;
        if (request.getDueDate() != null && !request.getDueDate().isEmpty()) {
            dueDate = LocalDate.parse(request.getDueDate()); // Basit parse etme
        }
        Task newTask = taskService.addNewTask(request.getDescription(), dueDate);
        // Oluşturulan görevin detaylarını ve konumunu dönmek iyi pratiktir (201 Created)
        URI location = URI.create("/api/tasks/" + newTask.getId());
        return ResponseEntity.created(location).body(TaskDTO.fromEntity(newTask));
    } catch (IllegalArgumentException e) {
        // İş mantığı hatası (örn. geçersiz girdi)
        return ResponseEntity.badRequest().body(null); // Hata mesajı da eklenebilir
    }
}

@PutMapping("/{id}/complete") // PUT /api/tasks/{id}/complete
public ResponseEntity completeTask(@PathVariable long id) {
     try {
        Task updatedTask = taskService.markTaskAsCompleted(id);
        return ResponseEntity.ok(TaskDTO.fromEntity(updatedTask));
    } catch (TaskNotFoundException e) {
        return ResponseEntity.notFound().build();
    }
}

@DeleteMapping("/{id}") // DELETE /api/tasks/{id}
public ResponseEntity deleteTask(@PathVariable long id) {
     try {
        taskService.deleteTask(id);
        return ResponseEntity.noContent().build(); // Başarılı silme - 204 No Content
    } catch (TaskNotFoundException e) {
        return ResponseEntity.notFound().build();
    }
}

}

SoC Notu: TaskController HTTP isteklerini ve yanıtlarını yönetir. İstek verilerini alır, ITaskService'i çağırır ve dönen sonuçları (DTO'lara çevirerek) HTTP yanıtına dönüştürür. İş kurallarının ne olduğunu veya verilerin nasıl saklandığını bilmez. Sadece ITaskService arayüzüne bağımlıdır. DTO kullanımı, iç Domain nesnelerinin (Task) yapısının dış dünyaya sızmasını engeller ve UI'a özel veri formatlamasına olanak tanır.

Katmanlı Mimarinin Faydaları (Bu Örnekte):

Bakım Kolaylığı: Veritabanı değişirse (örn. JPA yerine JDBC veya başka bir ORM), sadece JpaTaskRepository değişir, TaskService veya TaskController etkilenmez. Yeni bir iş kuralı eklenirse, sadece TaskService güncellenir. API formatı değişirse (JSON yerine XML), sadece TaskController (ve belki DTO'lar) değişir.

Test Edilebilirlik:

TaskRepository mocklanarak TaskService'in iş mantığı veritabanından bağımsız test edilebilir.

TaskService mocklanarak TaskController'ın HTTP istek/yanıt yönetimi iş mantığından bağımsız test edilebilir.

Esneklik: Farklı bir sunum katmanı (örn. bir mobil uygulama backend'i veya bir komut satırı aracı) aynı TaskService'i kullanabilir.

Takım Çalışması: Farklı geliştiriciler veya takımlar farklı katmanlar üzerinde paralel çalışabilirler (arayüzler tanımlandıktan sonra).

Bölüm 4: SoC Sadece Arka Uçta Değil - Ön Uç (Frontend) Örnekleri

SoC prensibi sadece sunucu tarafı (backend) uygulamalar için geçerli değildir. Modern ön uç (frontend) geliştirmede de karmaşıklığı yönetmek için kritik öneme sahiptir. Bölüm 5.2'deki React/Redux örneği gibi desenler, UI (View), UI Durumu/Mantığı (State/Logic) ve API Etkileşimini (Data Fetching) birbirinden ayırır.

Bileşenler (View): Sadece UI'ı oluşturur ve kullanıcı olaylarını iletir.

Durum Yönetimi (State/Store - Logic/Data State): Uygulamanın UI durumunu merkezi olarak yönetir, durum değişiklik mantığını içerir.

API Servisleri (Data Fetching): Backend API'leri ile iletişimi soyutlayan fonksiyonlar veya sınıflar.

Bu ayrım, ön uç kodunu daha test edilebilir, bakımı kolay ve yeniden kullanılabilir hale getirir. UI görünümünü değiştirmek durum mantığını etkilemez, durum mantığını test etmek UI veya API çağrılarına bağımlı olmaz.

Bölüm 5: Önemli Tamamlayıcı Teknikler

SoC'yi etkili bir şekilde uygulamak genellikle şu tekniklerle desteklenir:

Arayüzler (Interfaces): Katmanlar veya modüller arasındaki kontratları tanımlar. Somut implementasyon detaylarını gizler ve düşük bağımlılık (low coupling) sağlar. BLL'in DAL'a veya Sunum'un BLL'e arayüzler üzerinden bağlanması temel bir pratiktir.

Bağımlılık Enjeksiyonu (Dependency Injection - DI): Bir sınıfın ihtiyaç duyduğu bağımlılıkların (örneğin, TaskService'in ITaskRepository'ye olan ihtiyacı) dışarıdan (genellikle bir DI konteyneri/framework'ü tarafından) sağlanmasıdır. Bu, sınıfları bağımlılıklarını kendilerinin yaratma sorumluluğundan kurtarır, düşük bağımlılığı teşvik eder ve test edilebilirliği büyük ölçüde artırır (testlerde sahte/mock bağımlılıklar enjekte edilebilir). Yukarıdaki örneklerde @Autowired anotasyonu (Spring) veya kurucu metot enjeksiyonu DI kullanımını gösterir.

Veri Aktarım Nesneleri (Data Transfer Objects - DTOs): Katmanlar arasında veri taşımak için kullanılan basit nesnelerdir. İç (domain/entity) modellerin detaylarının dış katmanlara sızmasını engeller, API kontratlarını stabil tutmaya yardımcı olur ve UI'a özel veri formatlamasına olanak tanır.

Sonuç: SoC - Daha İyi Koda Giden Yol Haritası

Bu makalede incelediğimiz örnekler – basit bir konsol uygulamasından katmanlı bir web uygulamasına kadar – Sorumlulukların Ayrılması (SoC) prensibinin pratikte nasıl uygulanabileceğini göstermektedir. Temel UI/Mantık/Veri ayrımından başlayarak Katmanlı Mimari gibi daha yapılandırılmış desenlere kadar, SoC'nin amacı hep aynıdır: karmaşıklığı yönetmek, kodu daha modüler, anlaşılır, test edilebilir ve bakımı kolay hale getirmek.

Gördüğümüz gibi, SoC sadece büyük projeler için değil, her ölçekteki yazılım için değerlidir. Sorumlulukları bilinçli bir şekilde ayırmak, başlangıçta küçük bir ek çaba gerektirse de, projenin yaşam döngüsü boyunca katlanarak geri döner. Değişiklik yapmanın kolaylaşması, hataların azalması, testlerin güvenilir olması ve yeni özelliklerin daha hızlı eklenebilmesi, SoC'nin sağladığı somut faydalardır.

Arayüzler, Bağımlılık Enjeksiyonu ve DTO'lar gibi teknikler, SoC'yi uygulamada bize yardımcı olan güçlü araçlardır. Ancak en önemlisi, SoC'nin bir zihniyet olmasıdır. Kodu yazarken sürekli olarak "Bu parçanın sorumluluğu nedir?" ve "Bu sorumluluk diğerlerinden yeterince ayrılmış mı?" sorularını sormak, bizi doğal olarak daha iyi tasarımlara yönlendirir.

SoC'yi benimseyerek, sadece daha temiz kod yazmakla kalmaz, aynı zamanda daha sağlam, esnek ve uzun ömürlü yazılım sistemleri inşa ederiz. Bu, hem geliştiriciler hem de son kullanıcılar için daha iyi bir deneyim anlamına gelir. Bu nedenle, SoC'yi yazılım geliştirme pratiğinizin temel bir parçası haline getirmek, daha başarılı projelere giden yolda atılacak en önemli adımlardan biridir.

Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Linkedin