Java Kodunuzun Nesne-Merkezli Olmadığının 10 İşareti – III: Yüksek İçerik Bağımlılığı
Geliştirdiğimiz Java kodunun nesne-merkezli olmadığının 10 işaretinin neler olduğunu bu yazıda listelemiş ve bu yazıda da ilk madde olarak sınıflardaki aşırı statik metot kullanımını ele almıştık. Şimdi de ikinci maddeyi ele alalım:
Yüksek içerik bağımlılığı (high content coupling).
Malum bağımlılık (coupling), bir yazılım elemanının, diğer elemanlar hakkındaki bilgisinin derecesidir. Arzu edilen, mümkün olan en az derece bağımlılığa sahip yapılar kurgulamaktır. İster framework kurgulayın, isterse basit bir iş yapan metot kurgulayın, hem karmaşıklığı yönetmek hem de daha rahat değişebilen yapılar elde edebilmek için daima bağımlılığı az yazılım elemanları ile yazılım geliştirtirin.
Yazılım mühendisliği ya da “iyi kod nasıl yazılır” cinsinden kitaplar bağımlılık konusunu etraflıca ele alırlar ve farklı bağımlılık şekillerini incelerler. Örneğin wikipedia buna güzel bir görsel ile yer ayırmıştır. Bu anlamda içerik bağımlılığı, en kötü bağımlılıktır. Çünkü bir yazılım elemanı, bir başka yazılım elemanının iç yapısına bağımlıdır. Sınıf seviyesinde ele alındığında içerik bağımlılığı, bir sınıfın, bir başka sınıfın iç yapısına bağımlı olması demektir. (Bundan sonra bağlamımız sınıf/arayüz gibi tip seviyesi olacaktır.) Bu durumda bir sınıfı anlamak için bağımlı olduğu sınıf(lar)ı anlamak gereklidir. Benzer şekilde böyle bir sınıfı değiştirmek için bağımlı olduğu sınıf(lar)ı da değiştirmek gerekebilecektir. Bunlar ise karmaşıklığı arttıran ve değişimi zorlaştıran dolayısıyla da maliyeti arttıran, istenilmeyen durumlardır. Bu yüzden bu tür bağımlılığa “pathological coupling” yani “hastalıklı bağımlılık” da denir.
Bağımlılığı sıfır olan bir sınıftan bahsedemeyiz. Sadece bir sınıfımız varsa, evet bağımlılığı sıfırdır. Yani no coupling! Birbirine bağımlılığı olmayan, bölük pörçük oluşturulmuş sınıflar da bir sistem oluşturmayacağından, bir sistemin sınıfları kendi aralarında bir şekilde bir bağımlılığa sahip olmak zorundadırlar. Bu noktada bağımlılığın keyfiyeti önemlidir. İçerik bağımlılığı, bir sınıf, bir başka sınıfın iç yapılarına ulaşmasıdır dedik. Yani bir sınıfın üye değişkenleri (sınıf ve nesne değişkenleri) dışarıdan erişilebiliyorsa, içerik bağımlılığı oluşur. Bu anlamda bir başka sınıfın üye değişkenlerine erişmek ya da onları değiştirmek, içerik bağımlılığıdır. Bir sınıfın üye değişkenleri dış erişime karşı korunmuyorsa, diğer sınıflar sıklıkla bu değişkenlere ulaşıyorlar demektir. Bu durumda o sınıf hizmet yerine veri sağlıyor demektir. Yani diğer sınıflar, söz konusu sınıf üzerinde metot çağrısı yapmak yerine veri alış verişi yapıyorlar demektir. Örneğin aşağıdaki Date sınıfı bu türden bir sınıftır:
public class Date { private int day; private String month; private int year; public Date(int day, String month, int year) { this.day = day; this.month = month; this.year = year; } public int getDay() { return day; } public void setDay(int day) { this.day = day; } public String getMonth() { return month; } public void setMonth(String month) { this.month = month; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public String toString() { return "Date: " + day + " - " + month + " - " + year; } }
Pek çok programcı, Date sınıfının nesne-merkezli olduğunu iddia edecektir. Çünkü sarmalama (encapsualtion) kurallarına uyulmuş, yani alanlar private yapılmış ve doğrudan erişim engellenmiştir. Aslında yukarıdaki sınıf, tarih ile ilgili sadece veri taşımakta bundan dolayı da diğer sınıflarla arasında oluşacak olan bağımlılığin seviyesi içerik olarak kalacaktır. Yani Date sınıfı hizmet değil, veri sağlayacaktır. Bundan dolayı örneğin bir Date nesnesine sahip olup da ertesi günü elde etmek isteyen bir başka nesne, ya da Date nesnesini farklı formatlarda basmak isteyen bir diğer nesne, Date nesnesinden get metotlarıyla bilgi alacak ve bu fonksiyonlari kendileri yerine getireceklerdir. Yani örneğin “ertesi gün” hizmeti Client1 nesnesi tarafından şöyle geliştirilmiş olabilir:
/** * Client1 is the class that calculates next day of a given Date. * * @author akin * */ public class Client1 { public Date getNextDay(Date date) { int day = date.getDay(); String month = date.getMonth(); int year = date.getYear(); // First check wrong days and months if (day > 31) { System.out.println("Wrong date entered: " + date); System.exit(1); } if (year < 0) { System.out.println("Wrong date entered: " + date); System.exit(1); } if (!isMonthCorrect(month)) { System.out.println("Wrong date entered: " + date); System.exit(1); } // February is a special case so handle it separately if (month.equalsIgnoreCase("February") || month.equalsIgnoreCase("Subat")) { if (day == 28) { boolean isLeapYear = isLeapYear(year); if (isLeapYear) day = 29; else { day = 1; month = "March"; } } else if (day == 29) { boolean isLeapYear = isLeapYear(year); if (isLeapYear) { day = 1; month = "March"; } else { System.out.println("Wrong date entered: " + date); System.exit(1); } } else if (day == 30 || day == 31) { System.out.println("Wrong date entered: " + date); System.exit(1); } else { day++; } date.setDay(day); date.setMonth(month); return date; } // For other months if (day < 30) day++; else { if (day == 30) { day = 1; if (month.equalsIgnoreCase("April") || month.equalsIgnoreCase("Nisan")) month = "May"; else if (month.equalsIgnoreCase("June") || month.equalsIgnoreCase("Haziran")) month = "July"; else if (month.equalsIgnoreCase("September") || month.equalsIgnoreCase("Eylul")) month = "October"; else if (month.equalsIgnoreCase("November") || month.equalsIgnoreCase("Kasim")) month = "December"; } else if (day == 31) { day = 1; if (month.equalsIgnoreCase("January") || month.equalsIgnoreCase("Ocak")) month = "February"; else if (month.equalsIgnoreCase("March") || month.equalsIgnoreCase("Mart")) month = "April"; else if (month.equalsIgnoreCase("May") || month.equalsIgnoreCase("Mayis")) month = "June"; else if (month.equalsIgnoreCase("July") || month.equalsIgnoreCase("Temmuz")) month = "August"; else if (month.equalsIgnoreCase("August") || month.equalsIgnoreCase("Agustos")) month = "September"; else if (month.equalsIgnoreCase("October") || month.equalsIgnoreCase("Ekim")) month = "November"; else if (month.equalsIgnoreCase("December") || month.equalsIgnoreCase("Aralik")) { month = "January"; year++; } } } date.setDay(day); date.setMonth(month); date.setYear(year); return date; } private boolean isLeapYear(int year) { boolean isLeapYear = false; // divisible by 4 isLeapYear = (year % 4 == 0); // divisible by 4 and not 100 isLeapYear = isLeapYear && (year % 100 != 0); // divisible by 4 and not 100 unless divisible by 400 isLeapYear = isLeapYear || (year % 400 == 0); return isLeapYear; } private boolean isMonthCorrect(String month) { boolean isCorrectMonth = false; if (month.equalsIgnoreCase("January") || month.equalsIgnoreCase("February") || month.equalsIgnoreCase("March") || month.equalsIgnoreCase("April") || month.equalsIgnoreCase("May") || month.equalsIgnoreCase("June") || month.equalsIgnoreCase("July") || month.equalsIgnoreCase("August") || month.equalsIgnoreCase("September") || month.equalsIgnoreCase("October") || month.equalsIgnoreCase("November") || month.equalsIgnoreCase("December") || month.equalsIgnoreCase("Ocak") || month.equalsIgnoreCase("Subat") || month.equalsIgnoreCase("Mart") || month.equalsIgnoreCase("Nisan") || month.equalsIgnoreCase("Mayis") || month.equalsIgnoreCase("Haziran") || month.equalsIgnoreCase("Temmuz") || month.equalsIgnoreCase("Agustos") || month.equalsIgnoreCase("Eylul") || month.equalsIgnoreCase("Ekim") || month.equalsIgnoreCase("Kasim") || month.equalsIgnoreCase("Aralik")) isCorrectMonth = true; return isCorrectMonth; } }
Verilen bir Date nesnesini farklı formatlarda basan Client2 sınıfına bir bakalım:
/** * Client2 is the class that prints a given Date in different formats. * * @author akin * */ public class Client2 { public void printDate1(Date date) { int day = date.getDay(); String month = date.getMonth(); int year = date.getYear(); System.out.println("Date (dd-mm-yyyy): " + day + " - " + month + " - " + year); } public void printDate2(Date date) { int day = date.getDay(); String month = date.getMonth(); int year = date.getYear(); System.out.println("Date (mm-dd-yyyy): " + month + " - " + day + " - " + year); } public void printDate3(Date date) { int day = date.getDay(); String month = date.getMonth(); int year = date.getYear(); System.out.println("Date (dd/mm/yyyy): " + day + " / " + month + " / " + year); } public void printDate4(Date date) { int day = date.getDay(); String month = date.getMonth(); int year = date.getYear(); System.out.println("Date (mm/dd/yyyy): " + month + " / " + day + " / " + year); } }
Eğer Client1 ve Client2 gibi bir sınıf bir hizmeti yerine getirmek, bir şeye karar vermek vb. için bir başka sınıfın iç yapılarına ulaşıyor ya da ondan veri alıyorsa, bu sınıfların aralarındaki bağımlılık yüksektir. Bu durumda Client1 ve Client2 sınıflarını anlamak içın Date sınıfını anlamaya, Date sınıfını değiştirdiğinizde de muhtemelen bu iki sınıfı değiştirmeye ihtiyacınız olacak demektir. Bu şekilde bağımlılığı yüksek olan sınıfların birliktelikleri de (cohesion) düşüktür ya da bir başka deyişle, Date sınıfı tarih kavramının hepsini halletmek yerine sadece bir kısmını halletmekte, bundan dolayı da diğer kısımları farklı yerlerde halledilmektedir. Yani tarih ile ilgili veri ve hizmet bir yerde toplanmayıp, pek çok yere dağılmıştır. Bir de birbirinden habersiz programcıların, bu türden Date üzerine hizmet veren metotları farklı yerlerde farklı şekillerde yazdıklarını düşünün. İşte, iyi programcılarla iyi bir yazılım geliştirilmemesinin en temel sebebi budur. İyi futbolculardan iyi bir takım olmamasına karşın, vasat ama iyi organize olmuş oyunculardan oluşmuş takımın daha iyi performans göstermesi gibidir bu durum. İlki program yazan programcılardır, ikincisi ise yazılım geliştiren bir takımdır. Yukarıdaki Date sınıfının içerik bağımlılığından kurtarmanın yolu, bu sınıfın veri taşıyıp dağıtmasının önüne geçmektir. Date ya da bir sınıf veri taşımak için kurgulanmamalıdır, hizmet vermek için kurgulanmalıdır. Yani, Client1 ve Client2‘deki servisler aslında Date sınıfında olmalıdır. Diğer sınıflar da Date‘den veri yerine hizmet almalıdırlar. Böyle bir yapıdaki Date sınıfının arayüzü pekala şöyle bir DateI arayüzü ile temsil edilebilir:
/** * DateI represents date related services to be implemented in subclasses. * @author akin * */ public interface DateI { public abstract String toString(); public abstract String print(); public abstract String printShort(); public abstract String printLong(); public abstract String print(Locale locale); public abstract DateI getNextDay(); public abstract String getWeekDay(); public abstract String print(Locale locale, DateFormat format); }
Bağımlılığı sadece arayüz seviyesinde tuttuğumuzda, en sağlıklı bağımlılığa ulaşmış oluruz. Eğer Date sınıfı, DateI arayüzünü implement ederse ya da yerine getirirse, üzerinde sadece hizmet veren metotlar olacak ve veri taşımak yerine hizmet verecektir. DateI‘nin metotlarından bazılarının String döndürdüğüne dikkat edin. Buradaki String taşınan veriden ziyade, hizmet sonucu üretilen nesneyi temsil etmektedir ve kaçılmazdır. Bu anlamda sınıflar arasında gidip gelen veriyi ne kadar az tutarsanız, içerik bağımlılığınız o derece az oalcak ve sisteminizi hem anlamak hem de değiştirmek yani bakımını yapmak o kadar kolay olacaktır. Bu yüzden DateI arayüzünde get/set metotlarının olmadığına dikkat edin. “Getter ve setter metotları şeytandır” diyen Allen Holub‘ı da burada anmakta fayda vardır.
Bu durum GoF tarafından “program to an interface, not an implementation” yani “arayüze programlama yapın, gerçekleştirmeye değil” şeklinde ifade edilmiştir. Dolayısıyla açık/kapalı prensibi (open/closed principle) olarak bildiğimiz, “genişlemeye açık, değişikliğe kapalı” (open for extension, closed for modification) şeklindeki kurala da uyarsak, DateI‘nin sunduğu arayüz hiç bir zaman değişmeyecek demektir. Bu durumda Date sınıfını kullanan yani odnan hizmet olan hiç bir sınıfın, Date‘deki bir değişiklikten dolayı değişmesi gerekmeyecektir.
Bu yüzden, üç-beş private alanı bir araya getirip, sağ tıkla set/get metotlarını üreterek yapılan şey programlama olabilir ama nesne-merkezli programlama değildir. Bu şekilde program yazılabilir ama o program hiç bir zaman software/yazılım seviyesine çıkmayacaktır.
Amacımız, az bağımlılıklı ve yüksek birliktelikli ya da odaklı (lowly-coupled, highly-cohesive) yazılımlar geliştirmektir. Hemen her türlü yazılım kalitesi temelde bu iki kavram üzerine bina edilir. Bunları göz önüne almadan hangi dili ya da hangi frameworkü kullandığınızın hiç bir önemi yoktur. Ama ben bu türden kodlara o kadar çok ve sık rastlıyorum ki.
Bağımlılığı az, birlikteliği yüksek insan ve programcı olmak ümidiyle…
Toplam görüntülenme sayısı: 1664