Java’da Utility Sınıflarını Doğru Tasarlamak: Prensipler ve İpuçları

Emre Dinç
8 min readJan 26, 2025

--

Java’da, Util ya da daha yaygın bir terimle Utility sınıfları, genellikle ortak işlevsellik sağlayan ve tekrarlanan kodları merkezi bir yerde toplayarak yazılımın daha modüler, anlaşılır ve bakımı kolay hale gelmesini sağlayan sınıflardır. Bu sınıflar, genellikle belirli bir amaca hizmet eden statik metotlardan oluşur. Java’daki Util sınıflarının kullanım amacı, farklı yerlerde kullanılacak ortak işlevleri bir araya toplamak ve kod tekrarını engellemektir.

Bu yazıda utility sınıflarını doğru tasarlamak için temel prensipleri ve en iyi şekilde uygulamayı ele almaya çalışacağım.

Utility Sınıfı Nedir?

Utility sınıfları, genellikle aşağıdaki işlevleri sağlar:

  • Statik Metotlar: Çoğu Util sınıfı, sınıfın bir nesnesi oluşturulmadan erişilebilen statik metotlar içerir. Örneğin, bir sayı dönüşümü (convert) veya tarih formatlama (date formatting) gibi sık kullanılan fonksiyonlar burada toplanabilir.
  • Tek Sorumluluk Prensibi: Util sınıfı genellikle bir işlevi yerine getirir ve bu işlevi çok sayıda farklı sınıf veya paketle paylaşabilirsiniz.
  • İçerisinde Nesne Olmayan Metodlar: Çoğunlukla yalnızca işlevsel metotlar içerir ve nesne yönelimli (object-oriented) prensiplere aykırı olarak, sınıf düzeyinde işlevsellik sunar.
  • Tekrar Eden Kodları Merkezileştirme: Kod tekrarını önleyerek yeniden kullanılabilir işlevler sunar.
  • Bağımsız İşlevsellik: Bir sınıfın durumu (state) veya örneğiyle ilişkilendirilmeden çalışan metotlar.

Utility Sınıf Örneği:

public class MathUtil {
// İki sayıyı toplama fonksiyonu
public static int sum(int a, int b) {
return a + b;
}

// Faktöriyel hesaplama fonksiyonu
public static int factorial(int n) {
if (n == 0) return 1;
return n * factorial(n - 1);
}
}

Yukarıdaki örnekte MathUtil sınıfı, matematiksel işlemleri (toplama ve faktöriyel hesaplama) gerçekleştiren statik metotlar içeriyor. Bu sınıfı başka bir sınıf içinde şöyle kullanabilirsiniz:

public class Main {
public static void main(String[] args) {
System.out.println(MathUtil.sum(5, 3)); // Çıktı: 8
System.out.println(MathUtil.factorial(5)); // Çıktı: 120
}
}

Utility Sınıflarını Tasarlarken Dikkat Edilmesi Gereken Prensipler

1. Sınıfı final Yapın

Utility sınıflarının kalıtım yoluyla değiştirilmesini önlemek için sınıfı final olarak işaretleyin.

public final class MathUtils {
// Burada metotların olduğunu hayal edin.
}

Utility sınıfları final olarak tanımlamak zorunda mıyız ? Diye sorduğunuzu duyar gibiyim. Cevap; hayır.

Genel mantık:

Eğer bir utility sınıfının statik bir yardımcı yapı olarak kalmasını istiyorsanız, final kullanın. Ancak, esnek bir yapı gerekiyorsa sınıfı açık bırakabilirsiniz.

Bir utility sınıfını final olarak tanımlamak zorunlu değilsiniz, ancak bu bir iyi uygulamadır. İşlevine ve tasarım amacına göre final kullanıp kullanmamak sizin tercihinizdir. Bunun sebebini daha iyi anlamak için final tanımlamanın faydalarını ve gerekli olmadığı durumları ele alalım.

Utility Sınıfını final Yapmanın Amacı ve Faydaları

  1. Kalıtımı Engeller
  • Utility sınıflarının kalıtım yoluyla genişletilmesi genellikle mantıklı değildir çünkü bu sınıflar yalnızca statik metotlar içerir. Bir sınıfı final yaparak, başka bir sınıfın bu sınıfı extend etmesini önlemiş olursunuz.

2. Amacı Belirginleştirir

  • final anahtar kelimesi, bu sınıfın yalnızca yardımcı işlevler sunan bir yapı olduğunu vurgular. Sınıfı genişleterek farklı bir amaca uyarlama ihtimalini azaltır.

3. Tasarım Yanlışlarını Önler

  • Eğer başka bir geliştirici utility sınıfınızı genişletir ve statik olmayan metotlar eklerse, bu sınıfın utility doğası bozulur. final ile bunu önlemiş olursunuz.

Utility Sınıfını final Yapmayacağımız Durumlar

Bazı durumlarda, utility sınıfını final yapmanız gerekmeyebilir:

  1. Genişletilebilir Utility Metotları Sağlama
  • Eğer bir sınıfın statik olmayan bazı özel işlevlerle genişletilmesini istiyorsanız, final olmamalıdır. Örneğin, Apache Commons gibi kütüphanelerde bulunan bazı sınıflar, genişletilerek özelleştirilmesi için tasarlanmıştır.

2. Birden Fazla Amacı Olan Alt Sınıflar

  • Utility sınıfını farklı projelerde özelleştirmek gerekiyorsa (örneğin, belirli bir uygulama için belirli metotlar eklemek), final olarak tanımlamamak daha uygun olabilir. Ancak bu durum nadiren karşılaşılır.

Örnek:

public class MathUtils {
protected MathUtils() {
// Nesne oluşturmayı engeller
}

public static int add(int a, int b) {
return a + b;
}
}

// Genişletilebilir
public class CustomMathUtils extends MathUtils {
public static int multiply(int a, int b) {
return a * b;
}
}

2. Nesne Oluşturulmasını Engelleyin

Util sınıflar genelde sadece statik metotlar içerir ve bu metotlar herhangi bir nesne durumuna (örneğin alanlara) bağlı değildir. Bu nedenle bir utility sınıfından nesne oluşturulması gerekmez. Bu sayede gereksiz bellek kullanımı ve olası performans kayıplarının önüne geçmiş oluruz.

Bunun için:

  • Private bir constructor tanımlayın.
  • Constructor’a açıklama ekleyerek amacını belirtin.
public final class DateUtils {
private DateUtils() {
throw new UnsupportedOperationException("Bu sınıftan nesne oluşturulamaz.");
}
}

3. Statik Metotlar Kullanın

Utility sınıflarında tüm metotlar static olmalıdır. Bu, sınıf örneği olmadan metotlara erişimi sağlar.

public static int add(int a, int b) {
return a + b;
}

4. Tek Bir Amaca Hizmet Edin

Utility sınıfları belirli bir işlev grubunu gerçekleştirmek için tasarlanmalıdır. Çok fazla farklı sorumluluk eklemek, sınıfın karmaşıklaşmasına yol açar.

  • İyi bir örnek:
    FileUtils → Dosya işlemleri için
    StringUtils → String işlemleri için
  • Kötü bir örnek:
    HelperUtils → Belirsiz, her işlevi içeren bir sınıf.

5. Third Party Bağımlılıklarını Azaltın

Utility sınıflarında mümkün olduğunca az bağımlılık kullanın. Aksi takdirde sınıf, bağımlılıklara göre karmaşıklaşabilir.

6. Yardımcı Sınıfları Genişletmek Yerine Yeni Sınıflar Oluşturun

Bir utility sınıfı çok büyürse, işlevleri mantıksal bir şekilde bölerek yeni sınıflar oluşturun.

KAÇINILMASI GEREKEN HATALAR

1. Stateful Utility Sınıfları

Utility sınıflarının state tutması tasarım açısından yanlıştır.

Utility sınıfları bir işlem gerçekleştirdiğinde, bu işlemin sonucunu kaydetmez veya sonraki işlemler için bir durum (state) saklamaz. Eğer bir utility sınıfı state tutarsa, bu sınıfın amacından sapmasına ve kullanımının karmaşıklaşmasına yol açar.

// Hatalı kullanım
public class StatefulUtils {

// Bu sınıf bir durum (state) tutuyor
private static int counter = 0;

// Counter değerini artıran bir metot
public static int increment() {
return ++counter;
}
}

Yukarıdaki kod örneği neden hatalı ?

  • Beklenmeyen Davranışlar: Bu sınıfın increment() metodunu çağıran farklı parçalar, aynı counter değerini etkiler. Bu, beklenmeyen sonuçlara yol açabilir.

Örneğin, iki farklı iş parçacığı aynı anda bu sınıfı kullanırsa, sonuçları kontrol etmek zorlaşır.

  • Thread-Safety Sorunları: Birden fazla iş parçacığı bu state’i (counter) değiştirmeye çalışırsa, beklenmedik davranışlar ve hatalar ortaya çıkar (örneğin race condition problemleri).
  • Test Edilebilirlik: Stateful bir sınıfı test etmek zordur çünkü önceki işlemler sınıfın davranışını etkiler. Bu, bağımsız test yazmayı engeller.

Doğru Yaklaşım: Stateless Utility Sınıfları

Utility sınıfları her çağrıda aynı giriş değerlerine aynı sonucu vermelidir. Bu sayede test edilebilirlik ve kullanım kolaylığı sağlanır.

public final class NumberUtils {
private NumberUtils() {
throw new UnsupportedOperationException("Bu sınıftan nesne oluşturulamaz.");
}

// Durum tutmadan işlem yapar
public static boolean isEven(int number) {
return number % 2 == 0;
}

public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
  • Bu sınıfta herhangi bir state (örneğin bir counter, list, ya da map) bulunmaz.
  • Her çağrı, sadece giriş değerine göre çalışır ve hiçbir şeyi saklamaz.

2. Çok Fazla Sorumluluk Yüklemek

Bir utility sınıfı yalnızca belirli bir konuyla ilgili işlevleri içermelidir. Farklı konuları bir araya getirmek kodun bakımını zorlaştırır.

3. İsimlendirme

Bir utility sınıfının ismini belirlerken, sınıfın sahip olduğu sorumluluğa en uygun ismi seçmeliyiz. Aksi halde diğer geliştiriciler için sınıfın kullanımında kafa karışıklığı yaratabilir.

Util Class Kullanmanın Avantajları

  1. Kod Tekrarını Önler: Ortak fonksiyonları merkezi bir yerde toplayarak, kod tekrarını önlersiniz. Bu, yazılımın bakımını kolaylaştırır.
  2. Modülerlik: Fonksiyonel işlevleri sınıflar arasında paylaşarak, yazılımın daha modüler ve daha az bağımlı olmasını sağlar.
  3. Bakım Kolaylığı: Util sınıflarında yapılan bir değişiklik, bu sınıfı kullanan tüm kodlarda otomatik olarak güncellenir. Bu, bakım süreçlerini daha verimli hale getirir.

Kullanımın Dezavantajları

  1. Test Zorlukları: Statik metotlar sınıflar arası bağımlılıkları artırabilir ve test yazmayı zorlaştırabilir. Bir Util sınıfındaki metodu test etmek için, genellikle sınıfın kendisi üzerinde de test yapılması gerekir.
  2. Aşırı Kullanım: Fazla Util sınıfı kullanmak, yazılımın nesne yönelimli prensiplerine aykırı olabilir. Çok fazla statik metot kullanımı, kodun daha az esnek ve genişletilebilir olmasına neden olabilir.

1. Test Zorlukları

Statik metotlar, genellikle test yazmayı zorlaştıran bir faktördür. Çünkü statik metotlar doğrudan sınıfın kendisinden çağrılır ve bu da bağımlılıkları soyutlamanızı engeller. Özellikle dışa bağımlı metotları test etmek daha karmaşık hale gelebilir.

Örnek Senaryo: Statik Metodun Test Edilmesi

Aşağıda bir FileUtil sınıfı ve bu sınıfın bir metodunun nasıl test edilemeyeceğiyle ilgili bir örnek gösterelim. Bu metot, dosya okuma işlemi yapmaktadır ve dışarıdaki bir kaynağa bağımlıdır.

public class FileUtil {
public static String readFile(String filePath) {
// Gerçek dosya okuma işlemi
// Bu metot dışa bağımlıdır ve kolayca test edilemez.
return "Dosya içeriği";
}
}

Bu sınıfın readFile metodunu test etmeye çalışalım. Bu metodu test ederken karşılaşacağımız sorunları aşağıda gösterelim.

Test Kodu

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FileUtilTest {
@Test
public void testReadFile() {
// FileUtil.readFile() dışa bağımlı olduğu için burada gerçek bir dosya erişimi yapıyor.
// Bu durumda gerçek dosya üzerinde test yapılması gerekecek ki bu da istenmeyen bir durumdur.
String content = FileUtil.readFile("path/to/file.txt");
assertEquals("Dosya içeriği", content);
}
}

Sorunlar:

  1. Dışa Bağımlılık: Test, dosyanın gerçekten mevcut olduğu bir ortamda çalışmalıdır. Dosya bulunmazsa test başarısız olur.
  2. Yan Etkiler: Gerçek dosya sistemine erişmek, testlerinizi istikrarsız hale getirebilir. Dosyanın içeriği değişirse test de başarısız olabilir.
  3. Test İzolasyonu: readFile metodu dışa bağımlı olduğundan, bu testi diğer işlevlerden izole etmek zordur. Testlerin bağımsız çalışması gerekirken, bu metodun dışa bağımlı olması, testler arasında yan etkiler yaratabilir.

Çözüm:

Bu tür durumlarda, mocking (taklit etme) teknikleri kullanılarak dışa bağımlılıklar izole edilebilir. Ancak, statik metotlar için bu tür testler genellikle daha karmaşıktır. Statik metotları mocklamak genellikle PowerMock veya Mockito gibi kütüphanelerle yapılabilir, fakat bu işlem biraz daha karmaşıktır.

2. Aşırı Kullanım

Util sınıflarının aşırı kullanımı, yazılımın modülerliğini ve esnekliğini azaltabilir. Bir sınıf çok fazla sorumluluk aldığında, tek bir sınıf içinde bir araya gelen metotlar, yazılımı yönetilmesi zor hale getirebilir. Ayrıca, nesne yönelimli (OOP) tasarım prensiplerine aykırı bir şekilde statik metotlar kullanmak, genişletilebilirliği kısıtlar.

Örnek Senaryo: Aşırı Kullanılan Util Sınıfı

Diyelim ki bir StringUtil sınıfı, çok fazla işlevi tek bir sınıf içinde barındırıyor. Bu, zamanla bakım zorlukları yaratabilir.

public class StringUtil {

// String'i ters çevirir
public static String reverse(String input) {
StringBuilder reversed = new StringBuilder(input);
return reversed.reverse().toString();
}

// String'i küçük harfe dönüştürür
public static String toLowerCase(String input) {
return input.toLowerCase();
}

// String'i büyük harfe dönüştürür
public static String toUpperCase(String input) {
return input.toUpperCase();
}

// String'deki boşlukları temizler
public static String removeSpaces(String input) {
return input.replace(" ", "");
}

// String'i tersten ve büyük harfe dönüştürür
public static String reverseAndToUpper(String input) {
String reversed = reverse(input);
return reversed.toUpperCase();
}

// String'deki tüm rakamları kaldırır
public static String removeNumbers(String input) {
return input.replaceAll("\\d", "");
}
}

Burada, StringUtil sınıfı sadece String ile ilgili işlemler yapıyor ama içerdiği metot sayısı arttıkça, sınıfın bakımı zorlaşır. Ayrıca, sınıfın tek sorumluluk prensibi ihlal ediliyor çünkü bir sınıf hem dönüştürme işlemleri yapıyor hem de string manipülasyonları sağlıyor.

Problem:

  • Tek Sorumluluk Prensibi İhlali: StringUtil sınıfı birçok farklı işlevi gerçekleştiriyor (tersten yazma, küçük/büyük harfe dönüştürme, boşluk temizleme, rakam kaldırma vb.). Bu durum, sınıfın çok fazla sorumluluk üstlenmesine neden olur. Bu da yazılımın bakımını zorlaştırır.
  • Kodun Karışıklığı: Çok sayıda statik metot içerdiği için, bu sınıfı kullanan diğer sınıflar birbirinden bağımsız metotları sürekli çağırır. Bu durum, sınıfın kullanımını karmaşıklaştırabilir.
  • Genel Esneklik Kaybı: Bu sınıf, değişikliklere karşı daha az esnektir. Örneğin, gelecekte StringUtil sınıfına yeni bir özellik eklemek veya mevcut özellikleri değiştirmek, sınıfın diğer fonksiyonlarını etkileyebilir. Ayrıca, bu sınıfın kullanımını genişletmek de zorlaşır çünkü her şey bir aradadır.

Çözüm:

  • Daha Küçük, Odaklanmış Sınıflar: Bir sınıfın tek bir sorumluluğu olmalı. Örneğin, string’i tersine çevirmek için StringReversalUtil gibi ayrı bir sınıf, küçük harfe dönüştürmek için StringCaseUtil gibi bir başka sınıf oluşturulabilir. Bu şekilde, sınıflar birbirlerinden bağımsız çalışır ve bakım kolaylaşır.
  • Daha Esnek Yapılar: Eğer StringUtil gibi bir sınıf, nesne yönelimli tasarıma daha uygun hale getirilirse, daha esnek hale gelir. Örneğin, her bir işlev için bir sınıf oluşturulabilir ve bu sınıflar tek bir ortak arayüzü (interface) implement edebilir.

Özet

Utility sınıfları, yazılımda tekrarlanan işlevlerin merkezi bir yerden yönetilmesi için çok faydalıdır. Ancak, yalnızca ihtiyaç duyulan işlevler için kullanılmalı ve fazla statik metot kullanımından kaçınılmalıdır. Java’nın zaman içinde sunduğu yeni özelliklerle, Util sınıflarının rolü evrilmiş ve fonksiyonel programlama gibi yeni paradigmalarla entegrasyonu sağlanmıştır.

Ayrıca, doğru tasarlandığında kodunuzu daha temiz, daha modüler ve daha bakımı kolay hale getirir. Buraya kadar bahsettiğim prensipler ve ipuçları, bu sınıfları tasarlarken karşılaşabileceğiniz birçok yaygın hatayı önlemenize yardımcı olacaktır.

NOT:

Bu yazımı da diğerlerinde olduğu gibi bir çok kaynaktan araştırma yaparak kendimce aktarmaya çalıştım. Eksikleri olabilir, bu sebeple farklı kaynaklardan da araştırma yapmanızı tavsiye ederim. Bilgi dağarcığımı artırmak ve taze tutmak amacım iken, bu yazıları hazırlamak benim için bir araç. Buraya kadar okuduğunuz için teşekkür ederim :)

--

--

Emre Dinç
Emre Dinç

Written by Emre Dinç

Java Backend Developer | @DincDev on Twitter

No responses yet