Temiz ve Anlaşılır Kod Yazma Sanatı

Temiz ve Anlaşılır Kod Yazma Sanatına Giriş başlıklı daha önceki yazımızın ilk bölümünde temiz kod geliştirme yaklaşımının ne olduğu, neleri amaçladığı ve neden önem verilmesi gereken bir konsept olduğu vurgulanmaktadır. Ayrıca isimlendirme, fonksiyonlar ve yorumlar gibi alanlarda uygulamaları ele alınmıştır. Bu konular temel olsa da yazılım bu konulardan başlayıp daha yapısal, detaylı, sistemsel süreçleri ve bunlar arasındaki ilişkileri de içerisinde barındırmaktadır. Dolayısıyla bir yazılım geliştirme sürecinin her aşamasında temiz kod yaklaşımları geliştirip uygulamak önem arz etmektedir. Bu yazımızda ise Clean Code: A Handbook of Agile Software Craftsmanship [1] kitabı rehberliğinde biçimlendirme, nesneler ve veri yapıları, hata yönetimi, üçüncü taraf kod kullanımları, birim testler, sistemler gibi alanlarda temiz kodun nasıl uygulanabileceği konularına değineceğiz.
Biçimlendirme
Temiz kod geliştirmeden bahsederken, kod biçimlendirme başlığı belki de akla son sıralarda gelebilecek bir kavram olabilir. Ancak biçimlendirme, yazılan bir kod değerlendirmeye alındığında göze çarpacak ilk unsurdur. Dolayısıyla bu ilk intiba olumlu olduğunda kodu inceleyen kişiler biçimlendirmeye gösterilen özene bakarak, projenin diğer detaylarına da nasıl yaklaşıldığına dair bir ön fikir elde edebilir.
Genel bir algı olarak “kodu çalışır duruma getirmek” geliştiricinin ilk işi olduğu düşünülebilir. Ancak daha önemli konulardan biri de kodun, hem istenilen gereksinimleri karşılaması yani doğru çalışması hem de yazılan kodu okuyan kişiler tarafından kolayca anlaşılabilmesidir. Bugün oluşturulan işlevselliğin bir sonraki sürümde değişebileceği yüksek bir olasılıktır; ancak kodun okunabilirliği, yapılacak tüm değişiklikler üzerinde derin bir etki yaratır. Kodlama tarzı, orijinal kodu tanınmayacak hale getirmiş olsa bile, uzun vadede kodun bakımını ve genişletilebilirliğini etkilemeye devam edecektir.
Kod biçimlendirmesine dikkat edilmeli ve bu kapsamda bir takım kolay uygulanabilir kurallar belirlenip bunlara uygun şekilde geliştirme süreci yürütülmelidir. Biçimlendirme yönetimi yapılırken, bu sürecin kolaylaştırılması adına ilgili kuralları otomatik bir şekilde yeni yazılan koda uygulayabilecek araçlar kullanılabilir.
Dikey Biçimlendirme
Projede dosya boyutlarının ne kadar büyük olması gerektiğine karar vermek zor olabilir. Bu soruya verilecek yanıt, projelerin karakteristiklerine bağlı olarak farklılık gösterebilir.
Java’da dosya boyutu genellikle sınıf boyutu ile yakından ilişkilidir. Bir sınıf ne kadar karmaşıksa, kaynak dosyası da o kadar büyük olabilir. Ancak sınıf karmaşıklığına neden olan tek etken dosya boyutu değildir. Kodun temiz yazılmamış olması da sınıf karmaşıklığa neden olmaktadır.
Farklı projeler incelendiğinde, dosya boyutlarının ne kadar değişken olduğunu görmek mümkündür. Projelerin bazılarında; ortalama dosya boyutu yaklaşık 65 satırdır ve dosyaların büyük bir kısmı 40 ile 100+ satır arasında değişmektedir. Farklı projeler farklı ihtiyaçlar doğurur, ancak genel bir kural olarak dosya boyutlarının 200 satır civarında olması, üst sınırın ise 500 satırı geçmemesi önerilir. Dosya boyutu, bir projenin başarısı için kritik bir faktör olabilir. Dosyalarınız ne kadar küçük ve yönetilebilir olursa, karmaşıklık o kadar azalır ve kodunuz o kadar sürdürülebilir olur.
Tabi ki bu sayılar bir kesinlik ifade etmemekle birlikte uygulanması zorunlu değildir. Küçük boyutlu dosyaları okuyup anlamakla geçecek süre, büyüklere nazaran kısa olacağı için genel bir tercihi işaret etmektedir.
Gazete Metaforu
Gazete metaforu, kaynak dosyalarının gazete benzeri bir yapıda olmasını ifade eder. Gazete yukarıdan aşağı okunduğunda ilk başta yazının başlığı görülür ve bu başlık sayesinde yazının içeriğine dair ipuçları alınır. Böylelikle ilgili yazının okunup okunmayacağına karar verilir. Yazının detayları, sonraki safhalarda aşağı doğru okundukça ortaya çıkar. Kaynak kod dosyalarında da benzer bir yaklaşım izlenmelidir. Dosya basit ve açıklayıcı bir ada sahip olmalı, iyi soyutlanmış fonksiyonlar ve konseptler dosya başında bulunmalıdır. Aşağı doğru ilerledikçe detaylı işlemler ve az soyutlanmış yapılar yerleştirilmelidir.
Kavramlar Arası Dikey Açıklık
Kod okuması soldan sağa ve yukarıdan aşağı doğru yapılır. Her satır bir ifadeyi her satır grubu ise bir düşünceyi temsil eder. Boş satırlar, kod içinde farklı konseptleri birbirinden ayırmak için kullanılır. Örneğin, paket tanımı, paketleri projeye dahil etme işlemleri ve fonksiyonlar arasında boş satırların olması kodun görsel düzeni üzerinde derin bir etki yapar. Boş bir satır gördüğünüzde, gözünüz otomatik olarak onu takip eden ilk satıra odaklanır, bu da yeni ve ayrı bir konseptin başladığı anlamına gelir. Kodun okunabilirliği açısından bu dikey açıklıkların önemi aşağıdaki örnek üzerinden takip edilebilir:
1
2
3
4
5
6
7
8
9
10
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",Pattern.MULTILINE + Pattern.DOTALL);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1)); }
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>"); html.append(childHtml()).append("</b>"); return html.toString();}
}
Bu şekilde yazılmış bir kodun okunabilirliği ciddi derecede azalttığı görülebilir.
Kavramlar arası dikey açıklık sağlanarak düzeltildiğinde ise aşağıdaki gibi daha okunur bir kod elde edilir:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}
Dikey Yoğunluk
Dikey yoğunluk birbiri ile yakın ilişkili kavramların bir arada olma durumunu ifade eder. Birbirine bağımlı kod satırları arasında yorum satırlarının bulunması ve boşluklar bırakılması okunurluğu düşürebilmektedir.
Örneğin aşağıdaki kod parçaları incelendiğinde yorum satırı bulunmayanların daha okunur ve takip edilebilir olduğu görülmektedir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReporterConfig {
/**
* The class name of the reporter listener
*/
private String m_className;
/**
* The properties of the reporter listener
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
1
2
3
4
5
6
7
8
9
10
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
Yakın ilişkili kavramlar dikey olarak birbirine yakın tutulmalıdır. Çok iyi bir neden olmadığı sürece yakın kavramlar farklı kaynak dosyalarında yer almamalıdır. Okuyucular kaynak dosyaları ve sınıflar arasında dolaşmak zorunda kalmamalıdır.
Değişken Tanımlamaları
Değişkenler mümkün olduğunca kullanıldığı yerlere yakın tanımlanmalıdır. Özellikle kısa fonksiyonlar söz konusu olduğunda, yerel değişkenlerin fonksiyonun en üstünde yer alması önerilmektedir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void readPreferences() {
InputStream is= null;
try {
is= new FileInputStream(getPreferencesFile());
setPreferences(new Properties(getPreferences()));
getPreferences().load(is);
} catch (IOException e) {
try {
if (is != null)
is.close();
} catch (IOException e1) {
}
}
}
Döngü içerisinde kullanılacak ve sonrasında bir anlam ifade etmeyecek değişkenler yine döngü içerisinde tanımlanmalıdır. Örneğin aşağıdaki kod parçasında “i” değişkeni takip numaralarını oluşturmak için kullanılan ve sadece döngü içerisinde anlamı olan geçici bir değişkendir.
1
2
3
4
String[] takipNumaralari = new String[kargoSayisi];
for (int i = 0; i < kargoSayisi; i++) {
takipNumaralari[i] = "TN" + (i + 1);
}
Sınıf Değişkeni Tanımlama
Sınıf değişkenlerinin nereye konulması gerektiği konusunda birçok tartışma bulunmaktadır. C++’da genel olarak sınıf değişkenlerinin tamamı sınıfın alt kısmına, Java’da ise sınıfın en üstüne konulmaktadır. Burada önemli olan nokta sınıf değişkenlerinin nerede bulunduğunun herkes tarafından bilinmesidir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Ogrenci {
private static int ogrenciSayisi = 0;
private static double toplamNotOrtalamalari = 0;
private String ad;
private int numara;
private double notOrtalamasi;
public Ogrenci(String ad, int numara, double notOrtalamasi) {
this.ad = ad;
this.numara = numara;
this.notOrtalamasi = notOrtalamasi;
ogrenciSayisi++;
toplamNotOrtalamalari += notOrtalamasi;
}
public static int getOgrenciSayisi() {
return ogrenciSayisi;
}
public static double getOrtalamaNotOrtalamasi() {
if (ogrenciSayisi > 0) {
return toplamNotOrtalamalari / ogrenciSayisi;
} else {
return 0;
}
}
public String getAd() {
return ad;
}
public int getNumara() {
return numara;
}
public double getNotOrtalamasi() {
return notOrtalamasi;
}
}
Bağımlı Fonksiyonlar
Bir fonksiyon başka bir fonksiyonu çağırıyorsa, bu iki fonksiyonun kod içerisinde dikey olarak yakın olması büyük önem taşımaktadır. Uygulanması mümkünse, çağıran fonksiyonun çağrılan fonksiyonun üstünde yer alması gerekir. Bu yaklaşım, kodun doğal bir akışa sahip olmasını sağlar. Eğer bu kurala sadık kalınırsa, kodu okuyanlar fonksiyon tanımlarının, kullanıldıkları yerden hemen sonra geleceğine güvenebilirler. Böylece çağrılan işlevleri bulmak kolaylaşır ve tüm modülün okunabilirliği büyük ölçüde artar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ReportGenerator {
public String generateReport(List<Data> dataList) {
StringBuilder report = new StringBuilder();
report.append(getHeader());
report.append(getDataLines(dataList));
report.append(getFooter());
return report.toString();
}
private String getHeader() {
return "=== Report Header ===\n";
}
private String getDataLines(List<Data> dataList) {
StringBuilder dataLines = new StringBuilder();
for (Data data : dataList) {
dataLines.append(data.getName())
.append(": ")
.append(data.getValue())
.append("\n");
}
return dataLines.toString();
}
private String getFooter() {
return "=== Report Footer ===\n";
}
}
Yatay Açıklık ve Yoğunluk
Yatay boşluk birbiri ile ilişkili kavramları birleştirmek ve daha zayıf olanları ayırmak için kullanılır.
1
2
3
4
5
6
7
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
Dikkat edilirse atama operatörünün sağ ve sol ve kısmına boşluk bırakılmıştır. Bu boşluklar anlamsal olarak ifadelerin birbirinden ayrılmalarını sağlamaktadır. Diğer bir taraftan fonksiyon ismi ile argümanları birbirine sıkıca bağlıdır ve bu anlamsal durumu vurgulamak için arada boşluk bırakılmamıştır.
Yatay Hizalama
Yatay olarak hizalamak okunurluğu düşürebilmektedir. Örneğin aşağıdaki kod parçasında değişken tanımları yatay olarak hizalanmıştır. Bu durum değişken adlarının türlerine bakmadan okunmasına neden olabilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContext context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;
public FitNesseExpediter(Socket s,
FitNesseContext context) throws Exception
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTimeLimit = 10000;
}
}
Bunun yerine aşağıdaki kod çok daha anlaşılır olacaktır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContext context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;
public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTimeLimit = 10000;
}
}
Girintileme
Kaynak dosya, kendi içinde bir hiyerarşi içermektedir. Bir bütün dosya içerisinde bireysel sınıflar, sınıflar içinde metotlar, metotlar içinde bloklar bulunmaktadır. Bu hiyerarşide her düzey bir kapsamdır ve kapsamların hiyerarşisini okunur hale getirmek için girintileme yapılmasına ihtiyaç duyulmaktadır.
Sınıf içerisindeki metotlar sınıfın bir seviye sağına, metot içindeki tanımlar metodun bir seviye sağına ve metot içerisindeki bloklar da bulundukları blokların bir seviye sağında yer almalıdır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
Takım Kuralları
Her yazılımcının kendi biçimlendirme tercihleri olabilir; ancak, yazılımcı bir takımın parçasıysa, o takımın belirlediği biçimlendirme kurallarına uyması gerekir. Takımdaki üyeler basit biçimlendirme kuralları üzerinde anlaşmalı ve her bir üye bu kuralı takip etmelidir. Bu durum tutarlılık açısından önemlidir. Belirlenen bu kurallar, “IDE Code Formatter” içerisinde tanımlanarak tüm üyelerin bu kurala uyması sağlanabilir.
İyi bir proje tutarlı ve tek bir tarzda olmalıdır. Okuyucu bir dosyada gördüğü format tarzını diğer dosyalarda da görmelidir.
Nesneler ve Veri Yapıları
Veri Soyutlama
Bir sınıfın içeriğini saklamak, o sınıfın içerisine bir fonksiyon katmanı koymak değildir. Bu yaklaşımın yerine kullanıcıların verinin özünü manipüle etmelerine izin veren soyut arayüzler tercih edilmelidir. Böylece kullanıcılar verinin arkasındaki karmaşık mekanizmaları anlamadan, veriyle etkileşimde bulunabilmeleri için gerekli araçlara bu soyut arayüzler ile erişebilirler ve verinin nasıl işlendiğini bilmeden veri üzerinde işlem yapabilirler. Soyut arayüzler ile veriye erişim, yazılımın genişletilmesini ve bakımını kolaylaştırır. Çünkü değişiklikler sınıfın iç yapısında gizli kalır ve kullanıcılara yansımaz. Bu sayede, kod daha modüler ve anlaşılır hale gelerek daha fazla hata yapılması önlenmiş olur.
1
2
3
4
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
1
2
3
public interface Vehicle {
double getPercentFuelRemaining();
}
Yukarıdaki iki yapı ele alındığında ilk yapıda erişilebilen verilerle araçta ne kadar yakıt kaldığı bilgisi bir işlemden geçirilerek hesaplanabilir. Ancak burada ikinci yapı tercih edilmelidir. Çünkü verinin detaylarını paylaşmamak gerekir. Aksine verileri soyut ifadelerle ifade etmek tercih edilmelidir. Eğer kullanıcı için ne kadar yakıt kaldığı bilgisi önemli ise verinin geri kalan ayrıntıları paylaşılmamalıdır. Bu sadece arayüzleri veya getter/setter metotları kullanarak gerçekleşmez. Gerçek niyet, nesnenin içerdiği veriyi en iyi şekilde temsil edebilecek şekle sokulması olmalıdır. En kötü seçenek ise getter/setter metotları eklemektir.
Veri/Nesne Simetri Karşıtlığı
Nesneler verilerini soyutlamalar arkasında saklarlar ve bu verileri işleyecek fonksiyonları açarlar. Veri yapıları ise nesnelerini dışa açarlar ve anlamlı hiçbir fonksiyonları yoktur. Bu ikisi birbirine tamamen zıttır.
Prosedürel Şekil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
Yukarıdaki örnekte, Geometry sınıfı üç tane şekil (shape) sınıfı üzerinde işlemler yapmaktadır. Şekil sınıfları hiçbir davranış sergilemeyen basit veri yapılarıdır. Tüm davranış Geometry sınıfı içerisindedir.
Geometry sınıfına perimeter() isimli bir fonksiyon eklendiği düşünüldüğünde; şekil sınıfları değişmemiş ve bağlı olan diğer sınıflar da etkilenmemiş olacaktır. Diğer bir taraftan yeni bir şekil eklenirse Geometry sınıfındaki tüm fonksiyonların değişmesi gerekecektir.
Nesne yönelimli çözüm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
Geometry isimli bir sınıfa ihtiyaç bulunmamaktadır çünkü area() çok biçimli bir metottur. Yani eğer yeni bir şekil eklenirse, mevcut fonksiyonların hiçbiri etkilenmeyecektir. Ancak yeni bir fonksiyon eklenirse, tüm şekiller değişmek zorunda kalacaktır.
Prosedürel kod (veri yapılarını kullanan kod), mevcut veri yapılarını değiştirmeden yeni fonksiyonlar eklemeyi kolaylaştırır. Nesne yönelimli kod ise mevcut fonksiyonları değiştirmeden yeni sınıflar eklemeyi kolaylaştırır.
Prosedür kodu, tüm fonksiyonların değişmesi gerektiğinden yeni veri yapılarının eklenmesini zorlaştırır. Nesne yönelimli kod ise tüm sınıfların değişmesi gerektiğinden yeni fonksiyonlar eklemeyi zorlaştırır.
Yani nesne yönelimli için zor olan şeyler prosedürler için kolayken, prosedürler için zor olan şeyler nesne yönelimli için kolaydır.
Demeter Yasası (Law of Demeter)
Modül, değiştirdiği bir nesnenin içini bilmemelidir. Nesneler, verilerini saklar ve işlemlerini açarlar. Bu kapsamda, bir nesne iç yapısını erişimciler aracılığıyla açmamalıdır.
C isimli bir sınıf ve bunun f isimli bir fonksiyonu olsun. Bu fonksiyon sadece şunların metotlarını çağırmalıdır:
- C ’ye ait metotlar
- f tarafından oluşturulmuş bir nesnenin metotları
- f fonksiyonuna argüman olarak geçilen bir nesneye ait metotlar
- C içerisinde örnek değişken olarak tutulan bir nesneye ait metotlar
Tren Enkazı
Zincirleme şekilde birbirini çağıran fonksiyonlar bulunduğunda tren enkazları benzetmesi yapılır. Bu terim, kodun okunabilirliğini ve bakımını zorlaştıran, birbirine bağlı çok sayıda işlemi ifade eder. Genellikle bir nesnenin bir dizi metodu veya özelliği bir arada çağırıldığında kullanılır. Bu durum, kodun karmaşık ve zor anlaşılır hale gelmesine neden olur.
1
String result = object1.method1().object2().method2().object3().method3();
Yukarıdaki örnek incelendiğinde hangi metodun ne yaptığı tam olarak belli değil ve okunurluğu da azdır. Akışı anlamak için metot zincirinin takip edilmesi gerekmektedir.
Okunabilirliği arttırmak için kod, aşağıdaki gibi parçalara ayrılabilir. Bu şekilde her adım daha anlaşılır ve okunur olacaktır.
1
2
3
4
5
6
7
Type1 intermediateResult1 = object1.method1();
Type2 intermediateResult2 = intermediateResult1.object2().method2();
Type3 intermediateResult3 = intermediateResult2.object3().method3();
String result = intermediateResult3.getFinalResult();
Veri Aktarım Nesneleri (Data Transfer Objects)
Veri aktarım nesneleri(DTO) genel değişkenleri olan ve başka bir işlevi olmayan sınıflardır. DTO’lar, özellikle veri tabanlarıyla iletişim kurarken veya domain nesnelerini dış dünyaya güvenli şekilde açarken kullanılabilecek yapılardır.
1
2
3
4
5
public class User {
public String name;
public String email;
}
DTO’ların daha yaygın olan formu ise “bean” formudur. Bu form, özel değişkenlere sahip olan ve bu değişkenlerin getter ve setter yapıları tarafından manipüle edildiği bir veri yapısı formudur. Bu yapı genellikle veri erişimini ve manipülasyonunu daha düzenli hale getirirken, aynı zamanda veri güvenliğini de artırır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User {
public String name;
public String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Hata Yönetimi
Hata yönetimi yazılım geliştirme sürecinin bir parçasıdır. Geliştiriciler programın hata ile karşılaşma senaryolarını ele alıp neler yapılması gerektiğini belirlemek ve bu gibi durumlarda programın çalışmasını sürdürmekle yükümlülerdir. Aynı zamanda bu süreci de doğru yönetmek ve hata yönetimini temiz kod prensiplerine uygun işletmek gereklidir. Hata yönetim sürecini ele alırken kodun anlaşılırlığını azaltmamak ve karmaşıklığı arttırmamak önemlidir.
Hata Kodu Dönmek Yerine İstisna (Exception) Kullanmak
Eski yazılım dillerinde, ‘istisna’ benzeri konseptler olmadığı için yazılımcılar hata durumlarında; hata kodları dönmek ya da loglama yapmak gibi birtakım yaklaşımlar geliştirmişlerdir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// Check the state of the device
if (handle != DeviceHandle.INVALID) {
// Save the device status to the record field
retrieveDeviceRecord(handle);
// If not suspended, shut down
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: " + DEV1.toString());
}
}
...
}
Yukarıdaki kodda loglama işleminin bir örneği bulunmaktadır. Cihazın durumu kontrol edilmekte ve bu duruma göre çeşitli işlemler uygulanıp kayıt bilgisi tutulmaktadır. Örnekte hem kod karmaşık bir yapıda hem de konseptler birbirinden ayrılmamış durumdadır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
...
}
...
}
Örnek temiz koda uygun şekilde düzenlendiğinde, hata atması beklenen metot çağrısının ardından bu hatayı yakalayacak catch kısmı gelmektedir ve arada herhangi bir işlem bulunmamaktadır. Ayrıca cihazı kapatmak için oluşturulmuş yapı ve hata yakalama konseptleri birbirinden ayrılmış durumdadır. İki yapı da kafa karışıklığına sebebiyet vermeyecek şekilde ele alınıp, incelenebilecek haldedir.
İlk Önce Try-Catch-Finally Bloklarını Yazmak
“Try-Catch-Finally” ifadesi, kodun beklenmedik bir durumda fırlattığı istisnaların yönetilebileceği bir blok elde edilmesini sağlar. “Try-catch-finally” bloğunun “try” kısmında kod işletildiğinde, bu kodun herhangi bir noktasında iptal edilebileceği ve ardından “catch” bloklarında devam edebileceği belirtilmiş olur. “finally” bloğu ise “try-catch” bloğunun son kısmında yer alır ve istisna oluşsun ya da oluşmasın her durumda çalıştırılır. “finally” bloğu, genellikle kodunun temizlenmesi, kaynakların serbest bırakılması veya diğer sonlandırma işlemlerinin gerçekleştirilmesi gibi işlemler için kullanılır.
1
2
3
4
5
6
7
8
9
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
stream.close();
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}
Koda “Try-Catch-Finally” ile başlamak iyi bir uygulama olarak önerilir çünkü “Try” bloğunda oluşabilecek bir hatanın “Catch” bloklarında yakalanacağı düşünüldüğünde, bir hata senaryosunda kodun nasıl çalışabileceğini bilmek bu kodun yönetilebilirliği açısından büyük bir fayda sağlayacaktır.
Kontrolsüz (Unchecked) İstisnalar Kullanmak
Kontrollü istisna (checked exception) kavramı, derleme zamanında kontrol edilip yönetilen hata durumlarını ifade eder. Java’da metot imzasının sonuna “Throws” anahtar kelimesiyle beraber listelenip yönetilebilirler. Ancak bir metot, imzasında kontrollü istisna barındırıyorsa bu metodu çağıran tüm metotların da imzasında ilgili istisna bulunmalıdır. Dolayısıyla alt seviyede bulunan bir metot içerisindeki bu şekilde bir değişim, onu çağıran tüm üst seviye metotları etkilemiş olacaktır. Bu durum SOLID prensiplerinden biri olan Open/Closed prensibinin sağlanmasını engeller. Ayrıca üst seviye bir metot çağırdığı alt seviye metoda dair bir detayı öğrenmiş olur bu da nesne yönelimli programlama konseptlerinden biri olan kapsülleme(encapsulation) ilkesini bozmuş olur.

Kontrollü istisnalar kullanıldıkları durumlarda bahsedilen kademeler dikkat edilerek uygulanırken, öte yandan kontrolsüz istisna türleri kademeli bir aktarım gerektirmeden metot özelinde ele alınıp yönetilebilir.
İstisnalarla Bağlam Sağlamak
Fırlatılan her istisna, kaynağa ve hatanın yerine ait yeterli bilgi sağlamalıdır. Java’da herhangi bir istisnadan bir hata izi bulunabilir ya da bazen bu izler hiçbir şey söylemez. Bu nedenle fırlatılan istisnalarda bilgilendirici hata mesajları verilmelidir. Başarısız olan işlemden, hatanın tipinden bahsedilmelidir. Eğer uygulamada loglama yapılıyorsa, “catch” bloğunda bu hatayı loglayabilecek yeterli bilgi geçilmelidir.
Çağıranın İhtiyaçlarına Göre İstisna Sınıfları Tanımlamak
Hatalar sınıflandırılırken hatanın kaynağı veya tipine göre farklı yaklaşımlar kullanılabilir. Hata ağ kaynaklı mı, programlama hatası mı, bir bağlantı mı kurulamadı ve benzeri gibi. Ancak bir istisna sınıfı tanımlarken dikkat edilmesi gereken yegâne durum bu hatanın nasıl yakalandığıdır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}
Örnekte çeşitli istisna durumları için tekrar eden süreçler var. Bunlar bir sarmalayıcı sınıf(wrapper class) görevinde olan LocalPort sınıfında basitçe aşağıdaki gibi ele alınabilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
}
Böylelikle asıl uygulamanın işlediği yerdeki karmaşıklık azaltılıp hatalar başka bir sınıfta ele alınmış olur. İlk baştaki kod da bu şekilde daha temiz bir hâl alır.
1
2
3
4
5
6
7
8
9
10
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
Normal Bir Akış Tanımlamak
Aşağıdaki masrafları hesaplayan kod parçası incelendiğinde;
1
2
3
4
5
6
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
Eğer öğünler masraflandırılmışsa ‘toplama’ eklenmekte, değilse o gün için belirlenen yemek tutarını almaktadır. Bu özel durum ile ilgilenilmediği durumda, kod çok daha temiz görünürdü:
1
2
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
“ExpenseReportDAO“ sınıfı her zaman bir “MealExpense” nesnesi dönecek şekilde değiştirildiğinde kod daha basit hale getirilmiş olur. Eğer hiç masraf yoksa, günlük “MealExpense” nesnesi dönecektir:
1
2
3
4
5
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// return the per diem default
}
}
Özel durumları ele alacak sınıfları oluşturarak kullanım sunan yapıya “Special Case Pattern (Özel Durum Deseni)” denir. Bu desen kullanıldığında, sınıfın çağırıldığı kod parçalarında istisnai durumlarla uğraşılmak zorunda kalınmayacaktır. Bu davranış özel durum nesnesi ile kapsüllenmiş olur.
Null Değerler Dönmek
Null döndüren bir fonksiyon veya metot, çağrıldığı yerde null kontrolü gerektirir. Bu durum, kodun okunabilirliğini ve bakımını zorlaştırır. Ayrıca, null kontrolünün unutulduğu durumlarda “NullPointerException” gibi hatalara yol açabilir. Bu nedenle, fonksiyonların ve metotların null döndürmemesi, daha temiz ve hata ihtimali daha düşük bir kod yazılmasını sağlar. Alternatif olarak, boş bir koleksiyon veya özel durumları belirten bir nesne döndürmek, null döndürme sorununu çözebilir.
Örneğin, liste döndüren bir fonksiyonun hiçbir sonuç bulamadığı durumlarda boş bir liste döndürmesi, null kontrolü gerektirmez ve daha temiz bir kod yazılmasını sağlar. Aynı şekilde, bir nesne döndüren bir fonksiyonun hiçbir sonuç bulamadığı durumlarda özel bir nesne (örneğin optional sınıfları) döndürmesi, null kontrolü gerektirmeyen ve daha temiz bir kod yazılmasını sağlar. Bu yaklaşımlar, temiz kod prensiplerine uygun bir şekilde kod yazmayı kolaylaştırır.
1
2
3
4
5
6
7
8
9
10
11
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
Yukaridaki bu kod göze normal gelebilir, ancak oldukça kötüdür. Null dönüldüğünde aslında gene iş çıkarıyor ve topu fonksiyonu çağıranlara atıyor. Uygulamanın kontrolden çıkması için, tek bir eksik null kontrolü yeterli gibi görünüyor. İlk if’ten sonraki satırda null kontrolü yok. “peristentStore” nesnesi null olmuş olsaydı, çalışma anında çok minimal bir “NullPointerException” alırdık.
Null Parametre Geçmek
Fonksiyonlara null parametreler göndererek kullanmak beklenmeyen hatalara ve programın çökmesine neden olabilir. Null referansları, bir değişkenin herhangi bir nesneyi göstermediği ve bu nedenle bir değer içermediği anlamına gelir. Bu tür referanslar, özellikle Java gibi dillerde “NullPointerException” gibi hatalara yol açabilir ve bu da uygulamanın güvenilirliğini ve kullanıcı deneyimini olumsuz etkiler. Bu tarz durumlarda neler yapılabilir?
“Optional” sınıflar veya “Null Object Pattern” gibi alternatifler, null referanslarının kullanımını azaltabilir. “Optional” sınıflar, bir değerin var olabileceğini veya olmayabileceğini açıkça ifade eder ve böylece null kontrolü yapma ihtiyacını ortadan kaldırır. “Null Object Pattern” ise, bir nesnenin yokluğunu temsil etmek için kullanılan ve gerçek bir nesnenin davranışını taklit eden, ancak hiçbir işlem yapmayan bir nesne yaratır. Bu yaklaşımlar, yazılımın daha okunabilir ve bakımının daha kolay olmasını sağlar. Ayrıca, null kontrolü gerektiren kod miktarını azaltarak, hataların önlenmesine ve yazılım kalitesinin artırılmasına katkıda bulunur.
Sınırlar
Bir sistem veya uygulama geliştirilirken yazılım nadiren geliştiricinin yüzde yüz kontrolü altındadır. Çoğu zaman üçüncü taraf paketler, açık kaynak uygulamalar yazılımda kullanılır. Bazen de şirket içi özel bileşenler(components) veya alt sistemler, üzerinde çalışılan uygulamaya dahil edilir. Dolayısıyla bu tarz dışardan dahil edilen yapıların da kod geliştirirken temiz kod prensipleriyle ele alınıp kullanılması gerekmektedir.
Üçüncü Taraf Kod Kullanımı
Genel kullanıma açık olan paketler veya çerçeveler (frameworks) geniş bir alanda kullanım sunmayı ve böylece daha çok platforma ve kullanıcıya hizmet vermeyi hedeflerler. Öte yandan kullanıcılar ise aldıkları hizmetin kendi ihtiyaçlarına, spesifik gereksinimlerine çare olmasını beklerler. Bu durum hizmet veren taraf ve hizmet alan taraf arasında bir beklenti uyuşmazlığına sebep olurken aynı zamanda geliştirilen kodun sınırlarını belirlemede de soruna yol açabilir.
Günümüzdeki bir çok yazılımda üçüncü taraf kütüphanelerden gelen yapıların kullanıldığı görülmektedir. Üçüncü taraf kütüphanelerden kod kullanmak bize bazı avantajlar sunmaktadır. Örneğin, hali hazırda yazılmış olan Map gibi veri yapıları kullanılarak verilere erişim kolaylaştırılabilir ve kod yazım süreci daha hızlandırılabilir. Map sınıfı örneği baz alındığında bizlere sağladığı bazı metotlar şunlardır:
- clear() void – Map
- containsKey(Object key) boolean – Map
- containsValue(Object value) boolean – Map • entrySet() Set – Map
- equals(Object o) boolean – Map
- get(Object key) Object – Map
- getClass() Class<? extends Object> – Object • hashCode() int – Map
- isEmpty() boolean – Map
- keySet() Set – Map
- notify() void – Object
- notifyAll() void – Object
- put(Object key, Object value) Object – Map • putAll(Map t) void – Map
- remove(Object key) Object – Map
- size() int – Map
- toString() String – Object
- values() Collection – Map
- wait() void – Object
- wait(long timeout) void – Object
- wait(long timeout, int nanos) void – Object
Yukarıdaki yapıların bir çok avantajı olduğu gibi dezavantajları da vardır. Örneğin bir uygulamada kitapların tutulması gereken bir Map olsun. Bunun için ilk olarak Map tanımı yapılması gerekir.
1
Map kitaplar = new HashMap();
Uygulamanın bir bölümünde kitaplar arasından belirli bir ISBN’e sahip olan kitabın getirilmeye çalışıldığı ve mevcut kitaplar üzerinde de herhangi bir manipülasyon işlemine izin verilmek istenmediği düşünülebilir.
1
Kitap kitap = (Kitap)kitaplar.get(isbn)
Yukarıdaki örnekte yapılan Map tanımından dolayı kitapların getirilmeye çalışıldığı her durumda, gelen nesnenin Kitap sınıfına dönüştürülmesi gerekmektedir. Bu dönüşüm her seferinde yapılması gerektiği için kodun okunabilirliğini oldukça düşürecektir.
Okunabilirliği arttırmak için yapılabilecek ilk çözümlerden birisi Jenerik(Generics) kullanmaktır. Yukarıda bulunan örnek için jenerik kullanarak sağlanan çözüm aşağıdaki gibidir.
1
2
3
Map<Kitap> kitaplar = new HashMap<Kitap>();
Kitap kitap = kitaplar.get(isbn)
Bu çözüm okunabilirliği arttırsada Map üzerinde yapılabilecek bir manipülasyon ihtimali halen mevcuttur. Map sınıfını kullanarak bu durumdan kaçınılabilecek en temiz yollarından birisi Map yapısını bir sınıf ile kapsayarak sadece ihtiyaç olan özelliklerinin dışarıya kullanıma açılmasını sağlamak olacaktır.
1
2
3
4
5
6
7
8
public class Kitaplar {
private Map kitaplar = new HashMap();
public Kitap getByISBN(String isbn) {
return (Kitap) kitaplar.get(isbn);
}
}
Örneğin bu durumu ele alan daha temiz yöntem yukarıdaki gibidir. Kitaplar sınıfını kullanan hiçbir kullanıcı içerisinde Jenerik bir yapı mı kullanıldı veya tip dönüşümü mü var önemsemez çünkü dışarıya açılan metodun dönüş tipi Kitap olduğu için her halükarda Kitap tipinde bir nesne döneceği garanti edilmiş olur, dolayısıyla zaten hiçbir zaman böyle bir ihtiyaç da hissetmezler. Bu sayede, jenerik yapı veya tip dönüşümü tercihi de kodun içerisine gömülmüş olur.Bu şekildeki kullanımda, arayüz yapısı olan Map dışarıya kapalı halde olur. Böylelikle Kitaplar sınıfı ve onu kullananlar arasındaki sınırın ne olduğu görülebilir. Kullanıcılar, Kitaplar sınıfından bir Kitap nesnesi alıyor, ancak bu nesnenin nasıl tutulduğuna dair bir fikirleri yok ayrıca herhangi bir manipülasyon yapma ihtimalleri de bulunmuyor.Yukarıdaki örnek baz alınarak her Map sınıfının bu biçimde kapsüllenmesi Map yapısının her kullanımı için önerilmemektedir. Aksine bu gibi yapıların kod içerisinde fazla dolaştırılmaması gerekir. Örnekteki Map benzeri sınırdaki arayüz yapıları, sınıflara yakın ya da sınıfların içinde tutulmalıdır. Dikkat edilmesi gereken en önemli noktalardan biri de bu yapıları genel API’lerden döndürmekten veya bu API’lere argüman olarak kabul etmekten kaçınmak gerektiğidir.
Sınırları Keşfetmek ve Öğrenmek
Üçüncü taraf kod, daha kısa sürede daha fazla işlevsellik sunulmasına yardımcı olur. Bu kodların nasıl kullanılacağı net değilse ilk olarak dokümanın okunup sisteme nasıl dahil edileceği düşünülebilir. Sonrasında üçüncü taraf kodu kullanmak için kod yazılabilir ve istenilen özellikleri sağlayıp sağlamadığı test edilebilir. Karşılaşılan hatalar incelenerek, sorunların uygulama kodundan mı yoksa üçüncü taraf kodundan mı kaynaklandığının araştırılması gerekebilir.
Üçüncü taraf kodu anlaması, entegre etmesi zordur ve nasıl çalıştığını anlamak için sürekli yeni şeyler denemek yerine test yazılması değerlendirilebilir. Üçüncü parti yapıları test etmek o yapıyı kullananların görevi olmamakla birlikte belki de bu yapıların öğrenilmesi için yapılacak en doğru işlemlerden biridir. Jim Newkirk bu tür testleri öğrenme testleri olarak adlandırmaktadır. Projeye dahil edilen yeni özellikleri keşfetmeden veya denemeden önce üçüncü parti koda dair anlayışın geliştirilmesi adına çeşitli testler yazılabilir. Bu testler dışarıdan dahil edilen API’nin ne kadar anlaşıldığını test eden ve API’den ne istendiğine odaklanılan testlerdir. Ayrıca üçüncü taraf kodun yeni bir sürümü çıktığında davranışsal fark olup olmadığını görmek için de öğrenme testleri kullanılabilir.
Üçüncü taraf kodun ihtiyaçlar ile uyumlu kalacağının da bir garantisi yoktur. Yeni sürümler ile yeni yetenekler eklenip hatalar düzeltilebilir. Her yeni sürüm yanında risk ile birlikte gelir. Uygulama kodunda testler bulunuyorsa üçüncü taraf kodun yeni sürümü ile uyuşmama durumu bu testler sayesinde hemen öğrenilebilecektir. Geçişi kolaylaştıran bu sınır testleri olmadan üçüncü taraf kodun, eski sürümünde olması gerekenden daha uzun süre kalması, yeni sürüm geçişinin ertelenmesi gibi eğilimler baş göstermesine neden olabilir.
Öğrenme Testlerinin Faydaları
Öğrenme testlerini yazarken geçen zaman büyük bir maliyet gibi düşünülsede durum böyle değildir. Dışarıdan dahil edilecek bir API’nin projede kullanılabilmesi için zaten bir öğrenim ve keşfetme süreci oluşacaktır. Öğrenme testleri de bu süreci deneyimleyerek öğrenmenin, API’yi özümsemenin kolay bir yoludur. Ayrıca ileride yeni sürümlerle gelecek güncellemelerin herhangi bir davranışsal yapıyı değiştirip değiştirmediğini anlamanın en kolay yolu da yine öğrenim testlerini çalıştırmaktır. Böylelikle eğer bir değişim varsa testler hata verecek ve hem testlerin hem de kodda kullanılan ilgili işlevlerin hemen düzeltilmesi sağlanacaktır.
Henüz Mevcut Olmayan Kodu Kullanma
Yazılım geliştirme süreçlerinde bazen henüz tasarımı kesinleşmemiş ve son hali oluşturulmamış ek bileşenleri kullanma durumuyla karşılaşılabilir. Böyle bir durumda geliştirme sürecini yavaşlatmamak adına ilgili ek bileşen varmış gibi davranılarak geliştirme çalışmalarına devam edilebilir. Arayüz son halini almadığı için tam olarak nasıl bir iletişim olacağı kesin olmasa da istenilen işlevsellik bilindiğinden mevcut kodlar ile ek sistem arasına net bir sınır belirlenerek kodlama yapılabilir çünkü sınırın diğer tarafında ne olduğu bilinmese de nerede başladığı ve nerede bittiği belirlidir. Bunun için ek sistemin işleyişine benzer sahte sistemler kullanılabilinir, böylelikle tasarım süreci kolaylaşır ve ekibin kodlar üzerinde daha iyi kontrol sağlamasına, işlevselliğe ve okunurluğa odaklanmasına olanak tanır.

Yukarıdaki örnekte, bir yazılım ekibinin işlevleri genel olarak belirli olan “Transmitter” isimli bir alt sistem ile çalışması gerekmektedir ancak bu sistem henüz yazılmamıştır dolayısıyla doğrudan kullanımı mümkün değildir. Bu durumdan etkilenmemek için sahte bir API olarak kendi ihtiyaçları doğrultusunda “Transmitter” arayüzünü oluşturmuş ve sistemlerine dahil etmişlerdir. Böylelikle gerçek bir API üzerinden beklenilen işlevsellik temsil edilebilmiştir. “CommunicationController” sınıfını gerçek “Transmitter API” yapısından ayırmış ve “Transmitter API” yapısının gerçekte kullanılacak hali sisteme alındığında bu kontrol sınıfının etkilenmemesi sağlanmıştır. “Transmitter Adapter” sınıfı ise API tarafından sağlanacak arayüzle aradaki uyumu sağlamak için eklenmiştir. “Fake Transmitter” sınıfı “Communication Controller” yapısını test etmek amacı ile kullanılır. Bu yapı ile ekip kendi geliştireceği kısımlara odaklanabilmiş, modüler bir yaklaşım ortaya koyarak, okunur ve temiz kod oluşturabilmiştir.
Temiz Sınırlar
Sınırlarda değişimler olur. İyi yazılımlar, büyük yatırımlar ve yeniden çalışmalar olmadan değişikliklere uyum sağlarlar. Üçüncü taraf kodlar kullanıldığında, sistemi korumak için özel bir dikkat göstermeli ve gelecekte yapılacak değişikliklerin maliyetli olmayacağından emin olunmalıdır.
Sınırlardaki kod, beklentileri tanımlayan kesin ayrımlara ve testlere ihtiyaç duyar. Üçüncü taraf yapıları sarmak için arayüzler ve sınıflar kullanılabilir. Bu şekilde kodun okunabilirliği artar. Sınırlar boyunca tutarlı kullanımı teşvik eder ve üçüncü tarafa ait kod değiştiğinde daha az bakım gerektirir.
Birim Testler
Testler yazılım doğrulamanın temel unsurlarından biri olarak yazılım geliştirme sürecinin çok önemli bir parçasıdır. Bu yüzden yazılan testlerin temiz kod standartlarına uygun olması gerekmektedir.
“Agile” ve “Test Driven Development (TDD)” hareketi birçok yazılımcıyı otomatik olarak test yazmaya teşvik etti. Birim testler öncesinde programların çalışıp çalışmadığından emin olmak için yazılan testler iken sonrasında eklenen yeni geliştirmelerin ve değişikliklerin de mevcut yazılımı bozup bozmadığını anlamak için kullanılmaya başlandı.
Herkes “TDD” yaklaşımının üretim kodundan önce birim testleri yazmamızı istediğini bilir. Ancak bu kural buz dağının sadece görünen kısmıdır:
- Geçmeyen bir birim testi yazılmadan, uygulama kodu yazılmamalıdır.
- Aynı anda birden fazla geçmeyen birim testi yazılmamalıdır. Derleme hatası da geçmeyen test demektir.
- O anda ki geçmeyen testi geçirecek üretim kodundan başka üretim kodu yazılmamalıdır.
Bu üç yasa programcıları otuz saniye uzunluğunda bir döngüye sokar. Testler ve üretim kodu birlikte yazılmışlardır. Sadece testler uygulama kodundan birkaç saniye öncedir.
Bu şekilde çalışılırsa, her gün düzinelerce, her ay yüzlerce ve her yıl binlerce test yazılmış olur. Ve testler hemen hemen tüm uygulama kodunu kapsar. Uygulama kodunun boyutuna rakip olabilecek test kodu, yıldırıcı bir yönetim problemi oluşturabilir.
Testleri Temiz Tutma
Uygulama kodları yazılırken ne kadar dikkat edilmesi gerekiyorsa aynı dikkat test kodları yazılırken de uygulanmalıdır. Bunun en büyük nedenlerinden birisi uygulama kodları değiştikçe o kodlar için yazılmış testlerinde güncellenmesi gerektiğidir. Testlerin karmaşık ve oldukça kirli yazıldığı bir durumda testleri değiştirmekte çok fazla zaman alacaktır. Test kodu ne kadar karışık olursa, yeni testleri dahil etmek için harcanacak zaman, yeni uygulama kodunu yazmak için harcanacak zamandan daha fazla olacaktır.
Karmaşık ve okunabilirliği düşük yani kirli testlerin yazılması takım içinde yeni eklenen kodlara test yazılmasını zorlaştıracak ve yazılımcıları test yazmaktan uzaklaştıracaktır. Bu yüzden gerçek ortamda çalışan kodlara gösterilen özen, test kodlarına da aynı şekilde gösterilmelidir.
Testler olmadan uygulama kodunu esnek tutan durum kaybedilir. Uygulama kodunu esnek, sürdürülebilir ve yeniden kullanılabilir olmasını sağlayan birim testlerdir. Bunun nedeni ise basittir. Testler varsa kodda değişiklik yapılmasından korkulmaz. Testler olmadan her değişiklik olası bir hatadır. Mimari ne kadar esnek olursa olsun, tasarım ne kadar güzel şekilde bölümlenmiş olursa olsun testler olmadan fark edilmeyen hataların ortaya çıkması korkusuyla değişiklik yapma konusunda isteksiz olunacaktır. Test kapsamı ne kadar yüksek olursa değişiklikten o kadar az korkulacaktır.
Sonuç olarak test kodu, uygulama kodu kadar değerlidir. Düşünce, tasarım ve özen gerektirir. Uygulama kodu kadar temiz tutulmalıdır.
Çifte Standart
Test kodlarının, gerçek ortam kodları kadar temiz yazılması gerektiğinden bahsedilmişti. Burada test kodlarının test ortamında ve gerçek ortam kodlarının da gerçek ortamda çalıştırıldığı unutulmamalıdır. Test için yazdığımız kodlar, gerçek ortam kodlarından farklı bir mühendislik standartlarına sahip olabilir. Yine de yazılacak testler basit, etkileyici ve anlaşılabilir olmalıdır ancak gerçek ortam kodu kadar verimli olmak zorunda değildir. Sonuçta bunlar, gerçek ortamda değil test ortamında çalışır ve bu iki ortamın çok farklı ihtiyaçları vardır. Örneğin:
1
2
3
4
5
6
7
8
9
10
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
Testi okurken, gözünüzün kontrol edilen durumun adı ile kontrol edilen durumun anlamı arasında gidip gelmesi gerekmektedir. “heaterState” ve “coolerState” ifadesinin durumu için sürekli “assertFalse” ve “assertTrue” gibi durumlarını kontrol eden test metodunun kontrol edilmesi gerekir ve bu testin okunmasını zorlaştırır.
1
2
3
4
5
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
Yukarıdaki örnekte durumları açık olanlar büyük harf ile durumları kapalı olanlar küçük harfler ile ifade edilmiştir ve durumlar her zaman şu sıra ile yazıldığı düşünülmüştür: {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}.
Bu durum temiz kod prensiplerine ters olan zihinsel haritalandırma kuralına ters düşmektedir ama testlerin tümüne toplu bir halde bakıldığında test sınıfının anlaşılmasını kolaylaştıracak ve okuyan kişiye zaman kazandıracaktır. Örneğin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
tooHot();
assertEquals("hBChl", hw.getState());
}
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
tooCold();
assertEquals("HBchl", hw.getState());
}
@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
wayTooHot();
assertEquals("hBCHl", hw.getState());
}
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
Testlerin tamamına topluca bakıldığında kodun neyi test etmek istediği basit bir şekilde anlaşılabilmektedir. Bu yüzden bu kural için test kodlarında çifte standart uygulanabilir.
Test Başına Tek Bir Önerme(Assert)
JUnit testlerinde her bir test metodun tek bir önerme ifadesi içermesi gerektiği ifade edilir. Bu kural biraz zorlayıcı gözükse de tek önermeye sahip testler daha hızlı ve kolay bir şekilde anlaşılmayı sağlar. Ayrıca test yazarken bazı kalıp yapılar kullanmak anlaşılırlığı arttırabilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void testGetPageHierarchyAsXml() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContain(
"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
);
}
Burada klasik Given-When-Then yapısını görülebilir. Dikkat edildiğinde farklı önermeleri test etmek için kod tekrarı oluştuğu görülür. Temiz kod yaklaşımlarında kod tekrarı kaçınılması gereken bir durumdur. Bundan kaçınmak için “Given” ve “When” kısımları @Before anotasyonuna sahip bir metotta ele alınıp, “Then” kısmı her bir @Test metotunda yazılabilir. Veya “Template Method Pattern” gibi farklı çözümlere gidilebilir. Ancak bu çözümler ufak bir problem için çok fazla mekanizma gerektirir. Bunun sonucu olarak temiz kod birden fazla önermeyi aynı senaryo içerisinde kullanmakta sorun görmez. Sonuç olarak kod tekrarından kaçınmak ve bunu da en kısa ve eforsuz şekilde yapmak için bir test metodunda birden fazla önerme kontrol edilebilir.
Test Başına Tek Bir Konsept
Her test fonksiyonunda tek bir konsept test edilmelidir.
Aşağıdaki örnek incelendiğinde bu testin üç farklı konsepti içerdiği, bu nedenle üç farklı teste ayrılabileceği görülmektedir. Hepsini aynı fonksiyonda birleştirmek, her bölümün neden orada olduğunu ve o bölüm tarafından nelerin test edildiğini anlamayı zorlaştırır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void testAddMonths() {
SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
SerialDate d2 = SerialDate.addMonths(1, d1);
assertEquals(30, d2.getDayOfMonth());
assertEquals(6, d2.getMonth());
assertEquals(2004, d2.getYYYY());
SerialDate d3 = SerialDate.addMonths(2, d1);
assertEquals(31, d3.getDayOfMonth());
assertEquals(7, d3.getMonth());
assertEquals(2004, d3.getYYYY());
SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
assertEquals(30, d4.getDayOfMonth());
assertEquals(7, d4.getMonth());
assertEquals(2004, d4.getYYYY());
}
Ayırmak istenilen üç test fonksiyonu aşağıdakiler gibi olabilir:
31 günü olan aylardan birinin son günü olsun (örneğin 31 Mayıs);
- 30 günü olan bir ay eklendiğinde (Haziran gibi), tarih ayın 31’i değil 30’u olmalıdır.
- İki ay eklenildiğinde, ikinci ayın 31 günü varsa , tarih ayın 31’i olmalıdır.
30 günü olan aylardan birinin son günü olsun;
- Bu ayın üzerine 31 günü olan bir ay eklenildiğinde tarih ayın 31’i değil 30’u olmalıdır.
Yani soruna neden olan şey birden fazla “assert” ifadesi değildir. Aksine, test edilen birden fazla konseptin olmasıdır. Bu nedenle en iyi kural, konsept başına “assert” sayısını en aza indirgemek ve test fonksiyonu başına sadece bir konsept test etmektir.
F.I.R.S.T Kuralı
Temiz testler, F.I.R.S.T kısaltmasını oluşturan şu beş kuralı takip eder:
-
Fast (Hızlı): Testler hızlı olmalıdır. Testlerin hızlı olması, sık sık çalıştırılabilmelerini sağlar. Yavaş çalışan testler, problemleri erken tespit etmeyi ve gidermeyi zorlaştırır.
-
Independent (Bağımsız): Testler birbirinden bağımsız olmalıdır. Bir test, bir sonraki testin koşullarını belirleme konusunda etkili olmamalıdır. Her test, bağımsız olarak ve istenilen sırayla çalıştırılabilir olmalıdır. Bağımlı testler, hata tespitini karmaşıklaştırabilir.
-
Repeatable (Tekrarlanabilir): Testler herhangi bir ortamda çalışabilir olmalıdır. Birim testleri, üretim ortamında, QA ortamında veya kişisel bilgisayarınızda çalıştırılabilir olmalıdır. Testlerin herhangi bir ortamda tekrarlanamaması, başarısızlıkları mazeretlendirmeye neden olabilir.
-
Self-Validating (Kendini Doğrulayan): Testler başarılı veya başarısız olmalıdır. Test sonuçlarını anlamak için log dosyalarına bakma veya manuel olarak metin dosyalarını karşılaştırma ihtiyacı olmamalıdır.
-
Timely (Zamanında): Testler zamanında yazılmalıdır. Birim testleri, üretim kodu yazıldıktan hemen önce oluşturulmalıdır. Testleri kod yazdıktan sonra yazmak, üretim kodunun test edilmesini zorlaştırabilir ve tasarımı etkileyebilir.
Sınıflar
Sınıf Organizasyonu
Java’da bir sınıf, aşağıdaki sırayla listelenmelidir:
- Genel statik sabitler
- Özel statik değişkenler
- Özel sınıf değişkenleri
- Genel fonksiyonlar
Genel fonksiyonlar tarafından çağrılan özel yardımcı programlar, fonksiyonun hemen arkasında bulunmalıdır. Bu şekilde sıralanmış bir kodun okunabilirliği yüksek olacaktır. Daha önceki yazılarda bahsedilen gazete metaforuna benzer bir yaklaşım burada da bulunmaktadır.
Kapsülleme (Encapsulation)
Genellikle değişkenler ve yardımcı fonksiyonlar özel tutulmaya çalışılır. Fakat bu konuda her zaman çok fazla ısrarcı olunmamalıdır. Bazı durumlarda bazı değişkenlere ya da yardımcı fonksiyonlara test sınıfları içerisinden ulaşılabilmesi gerekebilir. Bu tarz durumlarda bu değişken ya da yardımcı fonksiyonlar aşağıdaki gibi “protected” olarak tanımlanabilir.
1
2
3
4
5
6
7
public class MyClass {
protected int number;
protected void utilityMethod() {
// Implementation...
}
}
Bu duruma karar verirken kullanılabilecek kural şu şekildedir: Aynı paketteki bir testin bir işlevi çağırması veya bir değişkene erişmesi gerekiyorsa, bu “protected” ya da paketin kapsamında olacak şekilde kullanılmalıdır.
Bu kural uygulanırken her zaman dikkat edilmesi gereken en önemli konu, yazılan kodda gizliliği muhafaza edebilmektir. Bu pratik uygulanırken kapsüllemenin kaybedilmesi son tercih olmalıdır.
Sınıflar Küçük Olmalıdır
Sınıfların küçük olması, kodun daha anlaşılır, bakımı daha kolay ve değişikliklere daha esnek olmasını sağlar. Bu, fonksiyonlar için geçerli olan “küçük ve belirgin” ilkesini yansıtır. Küçük sınıflar, tek bir sorumluluğu yerine getirir ve genellikle tek bir konsepti temsil eder.
Örneğin, bu sınıf, çok sayıda metodu bünyesinde barındırmakta ve gerektiğinden çok fazla sorumluluk almaktadır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SuperDashboard extends JFrame implements MetaDataUser {
public String getCustomizerLanguagePath();
public void setSystemConfigPath(String systemConfigPath);
public String getSystemConfigDocument();
public void setSystemConfigDocument(String systemConfigDocument);
public boolean getGuruState();
public boolean getNoviceState();
...//and other 50 public methods
}
Bir sınıfın sorumlulukları arttıkça, okunabilirliği düşmektedir. Bu sınıfın daha az sorumluluğa sahip hali şu şekilde olabilir:
1
2
3
4
5
6
7
8
9
10
11
12
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent();
public void setLastFocused(Component lastFocused);
public int getMajorVersionNumber();
public int getMinorVersionNumber();
public int getBuildNumber();
}
Bir sınıfın adı, onun hangi sorumlulukları yerine getirdiğini açıkça ifade etmelidir. Sınıfın adı ne kadar açık ve belirginse, sınıfın adı o kadar iyidir. Eğer bir sınıfın adını net bir şekilde belirlenemiyorsa, bu durum o sınıfın muhtemelen çok büyük olduğunu veya çok fazla sorumluluğu üstlendiğini gösterebilir.
Örneğin, “Processor”, “Manager” veya “Super” gibi belirsiz terimler içeren sınıf adları, genellikle çok sayıda sorumluluğun bir araya getirildiğini gösterir.
Ayrıca sınıfın kısa bir açıklaması “eğer”, “ya da”, “veya”, “fakat” kelimeleri kullanılmadan maksimum 25 kelimeyle yazılabilmelidir. Bu kelimelerin kullanılması sorumlulukların fazlalaşması demektir.
Tek Sorumluluk İlkesi
Tek Sorumluluk İlkesi(SRP), bir sınıf veya modülün değişmek için tek bir nedeni olması gerektiğini belirtir. Sınıfların tek bir sorumluluğu ve değişmek için tek bir nedeni olmalıdır.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent();
public void setLastFocused(Component lastFocused);
public int getMajorVersionNumber();
public int getMinorVersionNumber();
public int getBuildNumber();
}
Bu örnek sınıf incelendiğinde değiştirilmesi gereken iki temel sebep bulunmaktadır. İlki, sınıfın içinde versiyon kontrolünün gerçekleştiriliyor olmasıdır. İkincisi ise, “Java Swing” bileşenlerinin yönetilmesidir.
Sorumlulukları (değişim nedenlerini) belirlemeye çalışmak çoğu zaman kodu daha iyi anlayıp soyutlamaya yardımcı olur. Sürüm bilgileriyle ilgilenen üç “SuperDashboard” metodu “Version” adlı ayrı bir sınıfa çıkartılabilir. “Version” sınıfı, diğer uygulamalarda yeniden kullanım potansiyeli yüksek olan bir yapıdır.
1
2
3
4
5
public class Version {
public int getMajorVersionNumber();
public int getMinorVersionNumber();
public int getBuildNumber();
}
SRP ilkesi, nesne yönelimli programlama tasarımının temel kavramlarından biridir. Bu ilke, anlaşılması ve uygulanması kolay olmasına karşın sıkça yanlış kullanılır. Yaygın yapılan hata, kod ihtiyaçları karşıladıktan sonra üzerinde daha fazla çalışma yapılmamasıdır. Bu sebeple, SRP ilkesine uyulmayan kodlar sıkça karşımıza çıkar. Kodunuz çalışır duruma geldikten sonra, yeniden düzenleme yapılmalı ve birden fazla görev üstlenen sınıflar varsa, bu sınıfların her biri yalnızca tek bir görev üstlenecek şekilde ayrılmalıdır. Böylece kodunuz daha temiz ve yönetilebilir hale gelir.
Yazılan kod teslim edilmeden önce şu soru sorulmalıdır: Her biri iyi tanımlanmış ve iyi bir şekilde etiketlenmiş bileşenler içeren çok sayıda küçük gözlere bölünmüş bir alet çantası mı yoksa her şeyin içine atıldığı, birkaç gözü bulunan bir alet çantası mı daha kullanışlıdır?
Özetle, sistemler birkaç büyük sınıftan değil, birçok küçük sınıftan oluşmalıdır. Her küçük sınıf tek bir sorumluluğu kapsamalı ve değişmek için tek bir nedeni olmalıdır.
Uyum (Cohesion)
Bir sınıfın içindeki değişkenlerin sayısı ne kadar az olursa, o sınıfın metotları bu değişkenleri ne kadar çok kullanırsa, o sınıfın metotlarıyla değişkenleri arasındaki uyum o kadar artar. Her bir değişkenin tüm metotlarda kullanıldığı bir sınıf, en yüksek uyum seviyesine sahip olarak kabul edilir. Gerçekte bu seviyede bir uyumu sağlamak her zaman mümkün olmasa da, sınıfların uyumlu bir şekilde tasarlanması, metotların ve değişkenlerin mantıklı ve tutarlı bir bütünlük içinde olması amaçlanmalıdır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
Yüksek seviyede uyumlu sınıfın kodlaması yukarıda görülebilir çünkü “size” metodu hariç tüm metotlar iki değişkeni de kullanır.
Metotların küçük ve az sayıda parametre ile tasarlanması önemlidir; ancak bu durum, aynı sınıf içinde fazla sayıda değişkenin tanımlanmasına yol açabilir. Böyle bir durumda, sınıfın işlevselliği iki veya daha fazla alt sınıfa bölünerek daha yönetilebilir ve anlaşılır hale getirilmelidir.
Değişim İçin Organize Olmak
Çoğu sistemde değişiklik kaçınılmazdır. Her bir değişiklik, sistemin geri kalan kısmının beklenildiği gibi işlememesi riskini beraberinde getirir. Temiz ve düzenli bir sistemde, bu değişim riskini azaltmak amacıyla sınıflar düzenli olarak gözden geçirilip yeniden düzenlenir.
Örneğin, aşağıdaki “Sql” sınıfı, SQL ifadeleri oluşturmak için kullanılır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Sql {
public Sql(String table, Column[] columns);
public String create();
public String insert(Object[] fields);
public String selectAll();
public String findByKey(String keyColumn, String keyValue);
public String select(Column column, String pattern);
public String select(Criteria criteria);
public String preparedInsert();
private String columnList(Column[] columns);
private String valuesList(Object[] fields, final Column[] columns);
private String selectWithCriteria(String criteria);
private String placeholderList(Column[] columns);
}
Şu an için “update” işlevini desteklemeyen “Sql” sınıfı, “update” özelliğinin eklenmesi gerektiğinde değişikliğe uğramak zorunda kalacaktır. Sınıfta yapılacak herhangi bir değişiklik, diğer kod parçalarını etkileyebilecek potansiyele sahiptir, bu yüzden sınıfın yeniden test edilmesi gerekecektir. Bu durum, sınıfın “Tek Sorumluluk” prensibini ihlal ettiğini gösterir.
SRP(Tek Sorumluluk Prensibi) ihlali, sınıfta yalnızca “select” işlemleri için kullanılan “private” metotlar gibi, özelleşmiş işlevlerin varlığından kolayca anlaşılabilir.
Sınıfın “public” metotları ve özel işlevler için kullanılan metotlar gibi, “valuesList” gibi özel metotlar, sınıfın türevleri veya daha erişilebilir yerlere taşınarak bu ihlal giderilebilir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Yeniden yapılandırılmış Sql sınıfı
public abstract class Sql {
// Temel sınıf, her alt sınıfın uygulaması gereken bir metot
public Sql(String table, Column[] columns) {
// SQL sorguları için tablo ve sütun bilgisini alır
}
// Alt sınıfların uygulayacağı metot
abstract public String generate();
// SQL sorgusunu üretmek için
}
// SQL sorgusu oluşturmak için alt sınıf
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns) {
super(table, columns);
}
@Override
public String generate() {
// CREATE SQL sorgusu oluşturma işlemi
}
}
// SQL sorgusu ekleme işlemi için alt sınıf
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields) {
super(table, columns);
this.fields = fields; // Veri alanlarını saklar
}
@Override
public String generate() {
// INSERT SQL sorgusu oluşturma işlemi
}
private Object[] fields; // Veri alanlarını saklayan örnek değişken
}
// SQL sorgusu seçme işlemi için alt sınıf
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns) {
super(table, columns);
}
@Override
public String generate() {
// SELECT SQL sorgusu oluşturma işlemi
}
}
Bu yapı, her bir SQL sorgu türü için ayrı alt sınıflar kullanarak sınıflar arasındaki bağımlılığı azaltır ve SRP’ye uygun bir tasarım sunar. Her alt sınıf, kendi görevine odaklanır ve değişikliklerin diğer sınıfları etkilemesini önler. Bu sayede, sistemi genişletmek için sadece yeni sınıflar eklenilmesi gerekir ve mevcut kodun değiştirilmesi gerekmez.
Değişimden İzole Olmak
İhtiyaçlar değiştikçe kodların da güncellenmesi gerekmektedir. Yazılım geliştirirken, hangi bölümlerin değişebileceğini ve hangi bölümlerin sabit kalacağını belirlemek, değişikliklerden etkilenmemek için önemlidir. Bir sınıfın somut detaylarına bağlı diğer sınıflar, bu detaylardaki değişikliklerden etkilenebilirler. Bu etkiyi azaltmak için, arayüzler (interfaces) ve soyut (abstract) sınıflar kullanılarak, sınıflar arası bağımlılıkların izolasyonu sağlanabilir ve böylece kodun esnekliği artırılabilir.
Bir “Portfolio” sınıfı oluşturulduğunda ve portföyün değerini elde etmek için harici bir “TokyoStockExchange” API’sine bağlı olunduğu durumda, test senaryoları böyle bir aramanın değişkenliğinden etkilenir. Her beş dakikada bir farklı yanıt alındığından test yazmak imkansızlaşır. “Portfolio” sınıfını doğrudan “TokyoStockExchange” sınıfına bağlı olacak şekilde tasarlamak yerine, tek bir metoda sahip “StockExchange” adında bir arayüz oluşturulur:
1
2
3
public interface StockExchange {
Money currentPrice(String symbol);
}
Bu arayüzü uygulayan TokyoStockExchange sınıfı tasarlanır. Ayrıca Portföy constructor’ı argüman olarak StockExchange referansını almalıdır:
1
2
3
4
5
6
public class Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
}
Bu sayede test ederden doğrudan TokyoStockExchange kullanmak yerine StockExchange arayüzünü uygulayan bir uygulama kullanılabilir:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;
@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio(exchange);
}
@Test
public void GivenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500, portfolio.value());
}
}
Bir sistem bu şekilde test edilebilecek kadar ayrıştırılırsa, aynı zamanda daha esnek olacak ve daha fazla yeniden kullanımı teşvik edecektir. Bağımlılığın olmaması, sistemin öğelerinin birbirinden ve değişimden daha iyi izole edildiği anlamına gelir. Bu izolasyon sistemin her bir öğesinin anlaşılmasını kolaylaştırır.
Sistemler
Yazılım sistemlerinde uygun düzeyde soyutlama ve modülerlik, sistemin başarılı bir şekilde çalışması için önemlidir. Temiz kod prensipleri bu soyutlama ve modülerliği daha alt soyutlama seviyelerinde gerçekleştirilmesine yardımcı olur.
Oluşturma Aşamalarının Kullanımdan Ayrılması
Oluşturma (construction), kullanımdan çok farklı bir süreçtir. Uygulama nesneleri oluşturulurken ve birbiri ile ilişkilendirilirken, nesnelerin oluşturulma süreci çalışma zamanı mantığından (runtime logic) ayrılmalıdır.
Oluşturma süreci her yazılımda dikkat edilmesi gereken önemli bir süreçtir. Bu süreçte yazılım mimarisi oluşturulurken “Separation of Concerns” (Kaygıların Ayrılması) olarak bilinen tasarım tekniği uygulanabilir.
Başlatma işlemine yönelik kod özeldir ve çalışma zamanı mantığıyla karıştırılır. Örneğin:
1
2
3
4
5
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Good enough default for most cases?
return service;
}
Yukarıdaki kodda görüldüğü üzere “Lazy Initialization/Evaluation” deyiminin uygulanışına bir örnektir ve çeşitli avantajları vardır. Nesne kullanılmadığı sürece nesnenin oluşturulması bir yük oluşturmaz ve sonuç olarak başlangıç süresi kısalabilir. Ayrıca hiçbir zaman null değer döndürülmemesi de sağlanmış olur.
“MyServiceImpl” sınıfına ve bu sınıfın gerektirdiği her şeye bağımlılık oluşur. Çalışma zamanında bu türden bir nesne hiç kullanılmamış olsa bile, bu bağımlılıkları çözmeden derleme yapılamaz.
Bu şekilde tanımlamalarda test etme işlemi de oldukça zor olabilir. Eğer “MyServiceImpl” çok fazla işleve sahip bir nesne ise metodları birim testlerinde çalıştırılmadan önce taklit (mock) edildiğinden emin olunmalıdır. “getService” metoduyla birlikte başlangıç süreci mantığı ile çalışma süreci mantığı karıştırıldığı için tüm çalıştırma senaryoları test edilmelidir (null olma senaryosu ve null olmama durumunda doğru servise erişilmesi gibi). Bu iki sorumluluk, metodun birden fazla iş yaptığı anlamına gelmektedir ve küçük bir şekilde de olsa tek sorumluluk ilkesini ihlal etmektedir.
Main Metodunun Ayrılması
Yapım sürecini kullanımdan ayırmanın bir yolu, sürecin tüm yönlerini “main” veya “main” tarafından çağrılan modüllere taşımak ve sistemin geri kalanını, tüm nesnelerin uygun şekilde inşa edildiğini ve bağlandığını varsayarak tasarlamaktır.
“Main” fonksiyonu, sistem için gerekli nesneleri oluşturur ve ardından bunları kullanan uygulamaya aktarır. “Main” ile uygulama arasındaki bariyeri geçen bağımlılık oklarının yönüne bakıldığında bütün oklar tek bir yönü gösterir ve “main” fonksiyonundan uzaklaşırlar. Buradaki amaç uygulamanın “main” ve yapım süreci hakkında hiçbir bilgiye sahip olmadığından emin olmaktır.

Fabrikalar (Factories)
Fabrikalar, nesne oluşturma sürecini yönetmek ve bu süreci uygulamanın diğer kısımlarından soyutlamak için kullanılan bir tasarım desenidir.
Nesnelerin oluşturulma şeklini merkezileştirerek bağımlılıkların yönetimini kolaylaştırmaktadır. Bu, nesnelerin nasıl ve ne zaman oluşturulacağına dair bilginin tek bir yerde toplanmasını sağlar ve böylece kodun daha temiz ve anlaşılır olmasına katkıda bulunur. Ayrıca, fabrika sınıflarının kullanımı ve test edilebilirliği artırır. Çünkü nesne oluşturma sürecini soyutlayarak, test sırasında gerçek nesneler yerine taklit (“mock” veya “stub”) nesnelerin kullanılmasını kolaylaştırır.
Örneğin, bir sipariş işleme sisteminde, uygulamanın bir “Order” eklemesi için “LineItem” örneklerini oluşturması gerekir. Bu durumda, “LineItem” nesnelerinin ne zaman oluşturulacağına ilişkin kontrolü uygulamaya vermek için “ABSTRACT FACTORY” modeli kullanılabilir, ancak bu yapının ayrıntıları uygulama kodundan ayrı tutulmalıdır.

Bu detaylar, “main” tarafında olan “LineItemFactoryImplementation” sınıfında tutulur. Uygulama, “LineItem” örneklerinin ne zaman oluşturulacağı üzerinde tam kontrole sahiptir ve hatta uygulamaya özel yapıcı argümanları da sağlayabilir.
Bağımlılık Enjeksiyonu ( Dependency Injection)
Kontrolü tersine çevirme (Inversion of Control — IoC) yönteminin bağımlılıkların yönetimi (dependency management) için bir uygulaması olan Bağımlılık Enjeksiyonu (Dependency Injection), nesnelerin oluşumunu kullanımdan ayırmada güçlü bir mekanizmadır.
Bağımlılık Enjeksiyonu, sınıfların ve modüllerin birbirlerine olan bağımlılıklarını azaltarak, kodun daha düzenli ve anlaşılır olmasını sağlar. Bağımlılıklar, sınıfların dışında oluşturulur ve sınıflara enjekte edilir, bu da sınıfların daha az sorumluluk taşımasına ve daha odaklı olmasına olanak tanır. Bu sayede kodun okunabilirliği artar ve kod üzerinde çalışan diğer geliştiriciler için anlaması daha kolay bir yapı oluşur.
Kontrolün tersine çevrilmesi, bir nesnenin ikincil sorumluluklarını, bu sorumluluklara adanmış bir diğer nesneye aktarır ve böylece tek sorumluluk ilkesini destekler. Bağımlılık yönetimini gerçekleştirmenin amacı olarak bir nesne, bağımlılıklarının başlatılmasının sorumluluğunu almamalıdır, bunun yerine ilgili süreci başka bir mekanizmaya devretmelidir ki böylelikle kontrolün tersine çevrilmesi sağlanmış olsun.
Ölçeklendirme
Sistemler zamanla büyüyüp genişlerler ki bu normal bir süreçtir. Bir sistemin gelecekte büyüyebileceği tahmin edilse bile geleceğe göre işlemler yapmak masraflı olacaktır. Örneğin bir yerleşim yeri ileride daha fazla gelişip bir otoyola ihtiyaç duyabilir ancak bu ihtimal yerleşim yeri kurulurken oraya otoban inşa etme maliyetini göze almaya değmez. Dolayısıyla bir sistem geliştirilirken bugünün ihtiyaçları gözetilmeli, ardından sistem gelecekte genişleyebilecek şekilde dizayn edilmelidir. Yinelemeli ve artırımlı çeviklik, test odaklı geliştirme, yeniden düzenleme ve temiz kod prensiplerinin amacı bunu sağlamaktır.
Kesişen Kaygılar (Cross-Cutting Concerns)
Kesişen kaygılar, yazılım geliştirme sürecinde belirli bir işlevselliği uygulamak için birden fazla bölümü etkileyen kaygılardır. Örneğin, günlük tutma, kimlik doğrulama, yetkilendirme gibi işlevler, uygulamanın farklı parçaları üzerinde yaygın bir şekilde dağılmış olabilir. Bu kaygılar, işlevselliği uygulayan kodun farklı yerlerde tekrarlanmasına veya farklı modüller arasında yayılmasına neden olabilir. Çapraz-Kesişen Kaygıların yönetilmesi, kodun temiz ve düzenli kalmasına yardımcı olur. Bu, genellikle “Aspect-Oriented Programming (AOP)” gibi teknikler ile gerçekleştirilir.
AOP, geliştiricilerin daha temiz, daha düzenli kod yazmalarına yardımcı olan bir kodlama yaklaşımıdır. AOP, kodu modüllere veya yönlere (Aspect) ayırarak, ortak görevleri (Örneğin günlük kaydı veya hata işleme gibi) ana program mantığından ayırır. Bu, kodu daha düzenli ve gelecekte değiştirilmesi daha kolay hale getirir.
Sistem Mimarisini Test Edin
Uygulamanızın etki alanı mantığını “Plain Old Java Object (POJO)” sınıflarını kullanıp kod seviyesindeki tüm mimari kaygılardan ayrıştırarak yazmak, yazılan mimariyi gerçekten test etmenizi mümkün kılar. Gerektikçe yeni teknolojileri benimseyerek mimarinizi basitten karmaşığa doğru geliştirebilirsiniz. Her zaman “Big Design Up Front” olarak bilinen her şeyden önce büyük bir tasarım yapma fikri uygulanmayabilir. Çünkü daha önce yapılan değişikleri ve oluşturulan mimariyi geride bırakma fikri psikolojik olarak değişime uyum sağlamayı zorlaştırmaktadır.
Genel kapsam, hedefler ve zamanlamayla ilgili beklentiler olabilir ancak değişen koşullara yanıt verilebilmelidir. Bazı API’ler kaygıların ayrılmasında aşırıya kaçar ve gerekli olmayan karmaşıklığa neden olabilir. İyi bir API, genellikle görünmez olmalıdır, böylece ekip çoğunlukla kullanıcı taleplerine odaklanabilir. Özetle, ideal bir sistem mimarisi, her biri POJO nesnelerle uygulanan modülerleştirilmiş alanlardan oluşur. Farklı alanlar, minimal düzeyde istila edici “Aspect” veya benzeri araçlarla birbirine entegre edilir. Bu mimari, tıpkı kod gibi test odaklı olabilir.
Karar Verme Sürecini Optimize Edin
Modülerlik ve “Separation of Concerns (Kaygıların Ayrılması)”, merkezi olmayan yönetimi ve karar almayı mümkün kılar. Yeterince büyük bir sistemde kimse tüm kararları veremez. Kararları mümkün olan son ana kadar ertelemek de iyi bir yaklaşımdır. Bu, tembellik ya da sorumsuzluk değil; mümkün olan en iyi bilgilerle bilinçli seçimler yapılmasını hedeflemektir. Erken verilen karar, ideal olmayan bilgiyle verilen bir karardır. Çok erken karar verilirse, çok daha az müşteri geri bildirimine, projeye ilişkin zihinsel yansımaya ve uygulama seçimleriyle ilgili deneyime sahip olunur.
Sistemler Alan Özgü Dillere İhtiyaç Duyar
Yazılım geliştirmede, kodun, yazılımın bağlı olduğu iş kurallarına hakim bir alan uzmanının yazabileceği, yapılandırılmış bir düzyazı biçimi gibi okunacak şekilde yazılmasına izin veren, standart dillerden ayrı, küçük komut dosyası dilleri veya API’ler olan Alana Özgü Diller (DSL’ler) oluşturulmasına son zamanlarda ilgi artmıştır.
İyi hazırlanmış bir DSL, iş kuralları ile onu uygulayan kod arasındaki “iletişim boşluğunu” en aza indirir. İş kurallarının mantığını, bu kurallara hakim bir alan uzmanının kullandığı terminolojiye uygun yazmak, kuralların uygulamaya yanlış bir şekilde çevrilme riskini düşürür.
Sonuç
Yazılımcılar okunabilir, dönüştürülebilir ve anlaşılabilir şekilde temiz kod yazma hedefine ulaşmak için kod kalitesine ve sürekli iyileştirmeye önem vermelidir. Clean Code kitabındaki bölümlerden elde ettiğimiz bilgilerle, temiz kodun doğru bir şekilde uygulanma yöntemlerini örneklerle birlikte açıkladık. Bu doğrultuda, biçimlendirme, nesneler ve veri yapıları, hata yönetimi, üçüncü taraf kod kullanımları, birim testler ve sistemler gibi önemli konulara odaklandık. Bu konuları örneklerle destekleyerek, temiz kod yazma konusunda kapsamlı bir blog yazısı sunmayı amaçladık.
Kaynakça
Resim 1. Web sayfası, https://www.oracle.com/technical-resources/articles/middleware/soa-luttikhuizen-fault-handling-1.html, Fault Handling and Prevention - Part 1 by Guido Schmutz and Ronald van Luttikhuizen, An Introduction to Fault Handling in a Service-Oriented Environment ,November 2012
[1] Clean Code: A Handbook of Agile Software Craftsmanship, Martin, R.C., 2009, Pearson Education, Bölüm 5, Bölüm 6, Bölüm 7, Bölüm 8, Bölüm 9, Bölüm 10 & Bölüm 11