Java Genel Kurallar, Pratikler, Dizayn ve Pattern'ler
Java programlama dilinde dikkat etmeniz gereken kuralları, tecrübelerle oluşturulmuş pratikleri, efektif ve clean bir kod için izlemeniz gereken dizayn ve pattern'leri kod örnekleriyle birlikte maddeler halinde sunuyoruz.
Program, değişikliklere karşı ön görülebilir ve küçük bir kısmının etkileneceği şekilde tasarlanmalıdır.
Bunun en basit örneği scope'un dar tutulması (ihtiyacı olmayan kodun görmemesi sağlanmalı).
Lokal olan global olandan daha tercih edilesi çünkü ihtiyacı olmayan görmezse nereye bakacağımızı biliriz.
API : Application Program Interface
Bir class'ın public olan tüm değişken ve methodlarıdır.
İdealde bir class'ın tek bir amacı olmalıdır. Sahip olduğu tüm değişkenler ve methodlar buna yönelik olmalıdır.
Modüler program geliştirmek, projede çalışan bir çok kişinin daha bağımsız çalışabilmesine olanak tanıdığı gibi, seperation of concern ve unit testi yazılması ve erken safhalarda bug'ların çözülmesinde önemli rol oynar.
Encapsulation kuralı: Bir class'ın implementation detayları, o sınıfın kullanıcılarından gizli tutulmalıdır.
Single Responsibility kuralı class'larda uygulanırken, refactör aşamasında işi o iş için Most Qualified Class'a vermeliyiz. Bunu uygulamak o anki şartlara göre değerlendirilip karar verilmeli.
Bağımlılıkları mümkün olabildiği yerlerde dependency injection yöntemi ile vermeliyiz. Aşağıda constructor injection örneği mevcut. Burada kullanılan sınıf gelen Scanner objesinin kaynağını bilmek durumunda olmadığından daha esnek bir yapıya sahip. Hatta bu tarz değiştirilebilir parametreler yazılımın kullanımda konfigurasyon olarak yorumlanabilir, bunları yönetmek için kod dışında olacak bir config dosyası ile yönetimi yapılabilir. Bu kod içerisine ilgisi olmayan bir sınıfa hard-coded yazmaktan kesinlikle daha iyi bir yaklaşım.
Scanner scanner = new Scanner(System.in);
// Yerine
Scanner scanner;
public Modular (Scanner scanner) {
this.scanner = scanner;
}
Low Coupling Kuralı: Bir sınıfın bağımlılıklarını olabildiğince az tutmalıyız, bu sayede bağımlılıklarda olacak değişikliklerden minimum seviyede etkileniriz.
Bunu mediation yaparak sağlayabiliriz fakat bu bazı zamanlarda single responsibility rule'u ile çatışabilir ve bu durumda trade-off'ların farkında olmalıyız.
Aşağıda Java HashMap'inde mediation kullanıldığı fakat daha hızlı bir çözüm için doğrudan HashMap'in kullandığı class'a bağımlılık verildiği görülüyor.
Set<String> keys = map.keySet();
// Önce key'i buluyor daha sonra değer için data structure'a tekrar erişiyor
for (String s : keys) {
System.out.println(map.get(s));
}
// Doğrudan data structure'a erişip değeri alıyor
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getValue());
}
Open/Closed Kuralı: Bir sınıf yeni özellikler ekleneceğinde modifikasyona kapalı extension'a açık olmalıdır. Bunu en basit yöntem olarak interface'lere kod yazarak sağlayabiliriz. Polymorphism burada önemli rol oynuyor. Sürekli if check'leriye bakmak, her sınıfa coupling oluşturmak yerine aşağıdaki örnekteki gibi extension sağlayabiliriz.
public static void main(String[] args) {
A a = new A();
B b = new B();
List<C> list = Arrays.asList(a, b);
for (C c : list) {
if (c instanceof A i) {
i.doStuff();
}
}
// yerine
for (C c : list) {
c.doStuff();
}
}
class A implements C{
int x = 0;
@Override
public void doStuff() {
System.out.println(x);
}
}
class B implements C{
int y = 1;
@Override
public void doStuff() {
// nothing
}
public void doOtherStuff() {
System.out.println(y);
}
}
interface C {
void doStuff();
}
C'yi kullanarak A ve B'nin varlıklarını bilmeden yazmış olduk bu Transparency kuralı için önemli.
Aşağıda Comparable ve Comparator interface kullanımları örneklenmiştir.
public interface Comparable<T> {
int compareTo(T t);
}
public class ... implements Parent, Comparable<Parent> {
@Override
public int compareTo(Parent o) {
int a = getX();
int b = o.getX();
if (a == b) {
return 0;
} else {
return a - b;
}
}
...
}
public class ...2 implements Comparator<Parent> {
@Override
public int compare(Parent p1, Parent p2) {
return p1.getX() - p2.getX();
}
}
class ...1 implements Comparator<Parent> {
@Override
public int compare(Parent o1, Parent o2) {
return o1.myStr.compareTo(o2.myStr);
}
}
...
Collections.sort(arrayList, new ...2);
OR
Collections.sort(arrayList, Comparator.comparing(Parent::getX));
OR
Comparator<Parent> comparator = (p1, p2) -> p1.getX() - p2.getX();
arrayList.sort(comparator);
OR
arrayList.sort((p1, p2) -> p1.getX() - p2.getX());
Liskov Substitution Kuralı: Özetle bir parent class'dan child çıkıyorsak bu child'ın gerçekten parent tipinde olması gerekiyor. Benzetme yaparsak Oyuncak arabanın parentini araba yapmamalıyız. IS-A relationship'in sağlanması gerekiyor.
Peki birden fazla class'da ortak fonksiyonlar ve benzerlikler çokça mevcut bu durumda inheritance yapmamalı mıyız? Evet. Inheritance'ı IS-A relationship durumunda kullanmalıyız. Söz konusu durumda abstract class kullanılmalı. Abstract class'ları kategorize eden sınıflar olarak düşünebiliriz.
Don't repeat yourself (DRY)'de abstract class'lar büyük öneme sahip. Ayrıca bunun yanında interface üzerinden de override etmek üzere ya da default'ta kullanmak üzere kod alabiliriz. Bunun örneği aşağıda static ve default fonksiyon örneğinde mevcut.
interface Sample {
static int getIntStuff() {
return 0;
}
default void doStuff() {
System.out.println("I do");
}
}
abstract class Categorie {
protected int x;
protected Categorie() {
// protected olması ile sadece çocukları tarafından çağrılabilir konumda.
}
void doStuff() {
System.out.println("I do");
}
abstract void doOtherStuff();
}
Bir liste, bir collection üzerinden geçerken bunu iterator'lar ile yapmak hem sağlanan encapsulation ile nasıl olduğunu concern'den ayırmamızı sağlıyor, hem method isimleri gibi şeylerden gelecekte olacak değişimlerden kaçınmamızı sağlıyor hem de farklı yöntemlerle iteration yapabilmemize olanak sağlıyor. Aşağıda Iterator yazımı, itarable sınıf üzerinden kullanımı ve external, internal iteration örnekleri gösterilmiştir.
class SomeIterator implements Iterator<Integer> {
private Random rand = new Random();
int count = 0;
@Override
public boolean hasNext() {
return count < 15;
}
@Override
public Integer next() {
count++;
return rand.nextInt(10);
}
}
class SomeIterable implements Iterable<Integer>{
public Iterator<Integer> iterator() {
return new SomeIterator();
}
}
class X {
public static void main(String[] args) {
SomeIterable s = new SomeIterable();
Iterator<Integer> i = s.iterator();
// explicit örneği
int counter = 0;
while (counter++ < 3 && i.hasNext()) {
System.out.println(i.next());
}
// external iteration
for (Integer sI : s) {
System.out.println(sI);
if (counter++ > 10) break;
}
// internal iteration
myConsumer<Integer, Integer> mC = new myConsumerClass();
s.forEach(mC);
// returns max of randoms
System.out.println(mC.result());
s.forEach(integer -> System.out.println(integer));
}
}
interface myConsumer<T, R> extends Consumer<T> {
R result();
}
class myConsumerClass implements myConsumer<Integer, Integer> {
int result = 0;
@Override
public Integer result() {
return result;
}
@Override
public void accept(Integer integer) {
result = Math.max(result, integer);
}
}
Stream'ler ile alakalı bazı örnekler aşağıdadır ve filter'larda kullandığımız predicate gösterilmiştir. Stream kullanımı çoklu erişim ile ilgili bir problem olmadığında çok kolay bir şekilde paralel'e çevrilebilmektedir ve bu çok önemli bir özelliktir. (bkz. Paralel Stream)
public class Streams {
public static void main(String[] args) {
List<Character> list = Arrays.asList('A', 'B', 'Z');
// Array olsaydı Stream.of() kullanırdık
list.stream().forEach(System.out::println);
list.stream().filter(c -> !c.equals('A'))
.map(c -> Character.getNumericValue(c))
.forEach(c -> System.out.println(c));
int i = list.parallelStream()
.map(c -> Character.getNumericValue(c))
.reduce(0, (c1, c2) -> Math.max(c1, c2));
System.out.println(i);
}
}
interface Predicate<T> {
boolean test(T t);
}
Template Pattern kısa özeti kısmi implementation yapılarak değişken olacak olan yani override edilmesi beklenen method'ları abstract bırakıp onun haricindeki mekanizmaları bunlardan türeterek uyguladığımız patterndir.
public abstract class Template {
public boolean isXGreaterThan10() {
return getX() > 10;
}
protected abstract int getX();
}
Strategy Pattern kısaca bir yöntemin farklı farklı şekillerde implement edilmesini istediğimiz durumlarda bunları ortak interface ile bağlayıp kendi sınıflarında gerçeklememizdir. Open/Closed kuralı için önemlidir. Template pattern'a genellikle superior'dur. En basitinden inheritance gerektirmemesi esneklik sağlar. Ama strategy'nin her durumda kullanılmaması gerekir, inheritance ile çok kolay bir şekilde çözümlenen durumda ekstra komplekslik kazandırmasına gerek yoktur.
public class Strategy {
Operation myOperation;
Strategy (Operation operation) {
myOperation = operation;
}
void doStuff() {
myOperation.doStuff();
}
}
interface Operation {
void doStuff();
}
class X1 implements Operation {
@Override
public void doStuff() {
System.out.println("X1");
}
}
class X2 implements Operation {
@Override
public void doStuff() {
System.out.println("X2");
}
}
Command Pattern kısaca strategy pattern gibi uygulanır fakat yaratılış amacı alternatif çözümler sunmak değil farklı farklı çözümleri sunmakta kullanılır.
interface Operations {
void execute();
}
class DoAStuff implements Operations {
@Override
public void execute() {
System.out.println("A stuff");
}
}
class DoBStuff implements Operations {
@Override
public void execute() {
System.out.println("B stuff");
}
}
Bazen sabit olan class'lar yaratırız, her new dediğimiz sınıf aslında bir diğerinin aynısıdır. İşte bu durum gerçekleniyor ise object caching kullanmak en iyi çözümdür. Aşağıda Java tarafından yapılan cache'lemelere örnekler verilmiştir.
-128 ile 127 arasındaki intgerlar cache'den alınır, her false ya da boolean çağırdığımızda aslında cache'lenmiş objeyi çağırıyoruz:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
Cache'leme yaparken Singleton class'lardan yani sabit sayıda instance'ı olan class'lardan yararlanabiliriz. Bu iş için best practise olarak enum kullanımı aşağıdadır.
public enum Singleton {
LOW("low"), MID("medium"), HIGH("high");
private String name;
Singleton(String name) {
this.name = name;
}
}
enum OneInstanceSingleton {
INSTANCE;
void doStuff() {
}
}
Encapsulation'ı interface üzerinden implemention yaparak sağlıyoruz strategy'de olduğu gibi. Ama object creation'ı gerekiyorsa bunu nasıl sağlayabilirizin cevabı Factory'de gizli. Aşağıda hem cache'leyerek kullanım, hem pattern kullanımı hem de customize edilmilmiş factory gösterilmiştir.
public enum Factories implements XFactory {
X3(new X3Factory()), X4(new X4Factory());
XFactory factory;
Factories(XFactory xFactory) {
this.factory = xFactory;
}
@Override
public Xs create() {
return factory.create();
}
}
interface XFactory {
Xs create();
static Xs createInstance(int type) {
XFactory factory;
if (type == 1) {
factory = new X3Factory();
} else {
factory = new X4Factory();
}
return factory.create();
}
}
class X3Factory implements XFactory {
@Override
public X3 create() {
return new X3();
}
}
class X4Factory implements XFactory {
@Override
public X4 create() {
return new X4();
}
}
interface Xs {
}
class X3 implements Xs {
}
class X4 implements Xs {
}
class UseThem {
static XFactory[] factories = Factories.values();
public static void main(String[] args) {
XFactory factory = factories[0];
// true
System.out.println(factory.create() instanceof X3);
}
}
class CustomizedXFactory implements XFactory {
@Override
public Xs create() {
System.out.println("DO STUFF");
return null;
}
}
Adapter pattern kullanılma senaryosu üzerinden anlatacak olursak; diyelim ki Stack implementation'ı yapmak istiyoruz ve buna çok benzer başka bir sınıf var örneğin Vector. Burada Vector'u extend edip yaparsak inheritance for reuse hatasına düşmüş oluruz. Bunun yerine aşağıdaki gibi basit bir adapter yapısı kullanıyoruz. Benzer bir sınıfı kullanarak kendi yapımızı kuruyoruz.
public class Stack<E> implements StackApi<E>{
private Vector<E> v = new Vector<>();
public boolean empty() {
return v.size() == 0;
}
public void push(E item) {
v.add(item);
}
public E pop() {
return v.remove(v.size() - 1);
}
}
Başka bir kullanım senaryosu olarak da farklı class'lardan ortak sonuçlara varmak istediğimiz durumda bunları tek bir interface'de birleştirebileceğimiz ayrı ayrı ortaklanan sınıflara ait adapter class'ları yazabiliriz.
Decorator pattern yapı olarak adapter pattern gibi wrapping'e dayanıyor. Farkı mevcutta olan sınıf'a değişiklikler yapılmış halininde kullanılabilmesini sağlamak.
interface DecoratedInterface {
String getX();
void doStuff();
}
abstract class AbstractDecorator implements DecoratedInterface {
}
class BaseX implements DecoratedInterface {
@Override
public String getX() {
return "BaseX";
}
@Override
public void doStuff() {
System.out.println("Do stuff");
}
}
class DecoratorX extends AbstractDecorator {
@Override
public String getX() {
return "DecoratorX";
}
@Override
public void doStuff() {
throw new RuntimeException();
}
}
Composite pattern birden fazla sayıda element'ten child'dan oluşan yapılarda gözlemlenebilir. GUI component'lerinde sıklıkla gözlemleriz. Implement ettiği interface'i eğer kendi içerisinde aynı zamanda kullanıyorsa burada ağaç yapılı bir composite pattern görmek mümkün.
class MyIterator implements Iterator<MyItem> {
private Stack<Iterator<MyItem>> s = new Stack<>();
public MyIterator(MyItem i) {
Collection<MyItem> c = Collections.singleton(i);
s.push(c.iterator());
}
@Override
public boolean hasNext() {
return !s.isEmpty();
}
@Override
public MyItem next() {
MyItem item = s.peek().next();
if (!s.peek().hasNext()) {
s.pop();
}
Iterator<MyItem> iter = item.childIterator();
if (iter.hasNext()) {
s.push(iter);
}
return item;
}
}
interface MyItem extends Iterable<MyItem> {
Iterator<MyItem> childIterator();
default Iterator<MyItem> iterator() {
return new MyIterator(this);
}
}
class BasicItem implements MyItem {
@Override
public Iterator<MyItem> childIterator() {
return Collections.emptyIterator();
}
}
class ComponentItem implements MyItem {
private Map<MyItem, Integer> map = new HashMap<>();
void doStuff(MyItem item, int integer) {
map.put(item, integer);
}
@Override
public Iterator<MyItem> childIterator() {
return map.keySet().iterator();
}
}
Observer pattern event'ler sonucunda başka sınıflarda etki yaratabilmek için kullanılan bir yöntem. Event takibi yapılacak sınıfların listesi dinamik olarak tutulur ve bu sınıflardan event geldiğinde tetiklenme mekanizmasıyla çalışır.
public class Observer {
static EventSource some = new EventSource();
public static void main(String[] args) {
// tek update fonksiyonu olsa idi kullanılabilirdi
// some.addObserver(i -> System.out.println("anonymous observer"));
}
}
class EventSource {
private List<MyObserver> observerList = new ArrayList<>();
public void addObserver(MyObserver observer) {
observerList.add(observer);
}
public void removeObserver(MyObserver observer) {
observerList.remove(observer);
}
void doStuff() {
observerList.forEach(obs -> obs.update(0));
}
}
interface MyObserver {
// push technique
void update(int integer);
// pull technique
void update(MyItem item);
}
class EventDest implements MyObserver {
public EventDest(EventSource some) {
some.addObserver(this);
}
@Override
public void update(int integer) {
System.out.println("Event is triggered");
}
@Override
public void update(MyItem item) {
// get integer from item
}
}
Visitor pattern'i kullanıyoruz çünkü Java multiple dispatch sağlamadığından bir component'in cinsini runtime'da overload edemiyoruz. Bunun yerine aşağıdaki gibi open/closed'u koruyacak bir yöntem izlemeliyiz.
public interface Visitor {
void visit(BasicComponent component);
void visit(OtherComponent component);
}
class ComponentSource extends Component {
List<Component> list = new ArrayList<>();
@Override
void accept(Visitor v) {
for (Component c : list) {
c.accept(v);
}
}
}
abstract class Component {
abstract void accept(Visitor v);
}
class BasicComponent extends Component {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
class OtherComponent extends Component {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
class VisitorImpls implements Visitor {
@Override
public void visit(BasicComponent component) {
System.out.println("Basic");
}
@Override
public void visit(OtherComponent component) {
System.out.println("Other");
}
}
class X5 {
public static void main(String[] args) {
Visitor v = new VisitorImpls();
ComponentSource source = new ComponentSource();
source.list.add(new BasicComponent());
source.list.add(new OtherComponent());
source.accept(v);
}
}
Anlaması en kolay pattern'lardan birisi olan Facade Pattern basitçe client'ten detayları gizlemek ve low coupling sağlamak için araya koyduğumuz ne yapılacağını direk veren sınıf.
public class Facade {
A1 a1 = new A1();
A2 a2 = new A2();
void doA1Stuff() {
a1.doStuff();
}
void doA2Stuff() {
a2.doStuff();
}
}
class A1 {
void doStuff() {
System.out.println("A1 stuff");
}
}
class A2 {
void doStuff() {
System.out.println("A2 stuff");
}
}
Singleton'ı yukarıda enum ile yapabildiğimizi görmüştük şimdi multithreading environment'ta alternatif çözümlere bakalım. Bu çözümlerin hepsini Reflection API ile bozmak mümkün Enum hariç.
// eager initialization
class Singleton2 {
private static final Singleton2 instance = new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance() {
return instance;
}
}
// static block initialization
class Singleton3 {
private static Singleton3 instance;
static {
try {
instance = new Singleton3();
} catch (Exception e) {
throw new RuntimeException();
}
}
public static Singleton3 getInstance() {
return instance;
}
}
// lazy
class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
// synchronized
class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static synchronized Singleton5 getInstance() {
if (instance == null) {
instance = new Singleton5();
}
return instance;
}
}
// Double check ile synchronized'ın performansa olan etkisini azaltıyor
class Singleton6 {
private static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
// Kendi kendine senkronizasyon sağladığı için en iyi yöntem olduğunu söyleyebiliriz
class Singleton7 {
private Singleton7() {
}
private static class SingletonHelper {
private static final Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonHelper.instance;
}
}