Java Kodunuzun Nesne-Merkezli Olmadığının 10 İşareti – VIII: Karmaşık metotlar.

Geliştirdiğimiz Java kodunun nesne-merkezli olmadığının 10 işaretinin neler olduğunu bu yazıda listelemiş ve ilk altı maddeyi daha önce ele almıştık. Şimdi de yedinci maddeyi inceleyelim:

Karmaşık metotlar. (Complex methods.)

Aslında bu madde ile bir önceki madde (Çok sayıda metoda sahip olan sınıflar.) yapı olarak benzer özelliklere sahipler. Bu iki madde, nesne-merkezli yazılımlar için yazılım kalitesinin en temel ölçütlerindendir. Bir sınıfın sahip olduğu metot sayısı ile metotların karmaşıklık düzeyi, nesne-merkezli kod parçasının kalitesini hızlı bir şekilde ortaya koyar.

Bir sınıftaki metotların karmaşıklık düzeyini belirleyen temelde 3 etmen vardır: Metodun satır sayısı, metoda geçilen parametre sayısı ve metodun içinde verilen kararların sayısı.

Metotların uzunluğu, en basit karmaşıklık ölçütü olarak görülebilir. Ne kadar kısa metot, o kadar kaliteli kod. Ya da tersinden bakarsak ne kadar uzun metot, o kadar karmaşık kod. Uzun metot, çok iş demektir. Muhtemelen de farklı metotlarda yapılabilecek pek çok işi bir arada yapmak demektir. Halbuki her metot sadece ve sadece bir iş yapmalıdır.

“Clean Code” adlı kitabında R. Martin, sınıflar içın söylediği iki cümleyi metotlar için de tekrarlıyor:

“The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.”

Yani

“Fonksiyonların ilk kuralı, onların küçük olması gerektiğidir. Fonksiyonların ikinci kuralı ise onların bundan da küçük olması gerektiğidir.”

Metotlar, iş gören yapılardır. Metotların gördüğü işler farklı seviyelerde olabilir. Bazen metotlar bir süreci başlatırlar, bazen başlatılan sürecin içindeki adımlardan birini yerine getirirler, bazen de o bir adımın içinde çok basit bir şey yaparlar.  Nesne oluşturmak ya da bir süreci yönetmek ya da ya da vergiyi hesaplamak gibi çok basit bir iş yapmak şeklinde vb. Her halukarda bir metotta tek bir iş yapılmalı. Örneğin aşağıdaki metodlarla bu durumlu açıklayalım:

public void login(String tckn, String password) throws NoSuchCustomerException, CustomerAlreadyLoggedException, WrongCustomerCredentialsException, MaxNumberOfFailedLoggingAttemptExceededException, CustomerLockedException;

login() metodunun, örneğin bir web uygulamasında web katmanından çağrılan CustomerService isimli bir servis nesnesi metodu olduğunu düşünelim. Belli ki bu metot, kullanıcı bilgileriyle, o kullanıcıyı uygulamaya kabul eden sürecin başladığı yerdir. Ve bu metotdun fırlattığı sıra dışı durumlara bakınca, bu metotta öyle bir müşteri olup olmadığına karar verilmesinden, müşterinin sistemle kilitli durumda olduğunun anlaşılmasına kadar pek çok iş yapılıyor.

	@Override
	public void login(String tckn, String password) throws NoSuchCustomerException, CustomerLockedException,
			CustomerAlreadyLoggedException, WrongCustomerCredentialsException,
			MaxNumberOfFailedLoggingAttemptExceededException {
		Customer customer = customerDao.retrieveCustomer(tckn);

		// If passwords match, customer hasn't already been locked nor logged in
		// Customer loggs in and it is now currentCustomer
		if (customer.getPassword().equals(password) & !customer.isLocked() & !customer.isLoggedIn()) {
			// customer.logsin is a property in atm.properties. If it is "yes"
			// database is updated when
			// a customer logs in. Updated part in db is CUSTOMERS.LOGGEDIN
			customer.setLoggedIn(true);
			if (customerDao.updateCustomer(customer))
			currentCustomer = customer;
			loginAttemptCount = 0;
		} else if (customer.isLoggedIn()) {
			throw new CustomerAlreadyLoggedException("Customer is already logged in. Please first log out.");
		} else if (customer.isLocked()) {
			throw new CustomerLockedException("Customer is locked. Please consult your admin.");
		} else if (!customer.getPassword().equals(password)) {
			loginAttemptCount++;
			if (loginAttemptCount == Integer.parseInt(ATMProperties.getProperty("customer.maxFailedLoginAttempt"))) {
				customer.setLocked(true);
				customerDao.updateCustomer(customer);
				throw new MaxNumberOfFailedLoggingAttemptExceededException("Max number of login attempt reached: "
						+ loginAttemptCount);
			}
			throw new WrongCustomerCredentialsException("TCKN/password is wrong.");
		}
	}

Bu şekilde, bir süreci başlatan metotlara ihtiyaç pek tabi ki vardır ama bu durum devasa metotlar yazmamızı gerektirmez. Çok açık ki bu metot çok iş yapıyor. Önce “customerDao” üzerinden Customer nesnesini getiriyor. Daha sonra nesnenin üzerindeki bilgilere bakarak o nesnenin temsil ettiği müşterinin sisteme girip giremeyeceğine karar veriyor. Bunun için de hem password kontrolü yapıyor ve yanlış password girildiğinde kilitlenme sayısına gelip gelmediğini anlıyor, hem de müşterinin zaten sistemde olup olmadığını ve daha önceden kilitlenip kilitlenmediğini kontrol ediyor. Bu metot belki şu şekilde daha sağlıklı yazılabilirdi:

	public void login(String tckn, String password) throws NoSuchCustomerException, CustomerLockedException,
			CustomerAlreadyLoggedException, WrongCustomerCredentialsException,
			MaxNumberOfFailedLoggingAttemptExceededException {
		Customer customer = customerDao.retrieveCustomer(tckn);
		loginCustomer(customer, password);
	}

Yukarıdaki login metodunun ilk haliyle sonraki, iyileştirilmiş halini kiyasladığımızda, ikinci halin çok daha kısa ve anlaşılır olduğunu, çünkü sadece tek bir iş yaptığını görebiliriz. login() metodu, login için gerekli sürecin adımlarını sırayla çağırarak, müşterinin sisteme dahil olmasını sağlamakta, varsa oluşan sıra dışı durumları bir üst ortama aktarmaktadır. Orijinal halinde bu metotta olan adımlar ise farklı beş metoda dağılmıştır:

	private void checkIfCustomerAlreadyLoggedIn(Customer customer) throws CustomerAlreadyLoggedException {
		if (customer.isLoggedIn()) {
			throw new CustomerAlreadyLoggedException("Customer is already logged in. Please first log out.");
		}
	}

	private void checkIfCustomerLocked(Customer customer) throws CustomerLockedException {
		if (customer.isLocked()) {
			throw new CustomerLockedException("Customer is locked. Please consult your admin.");
		}
	}

	private void checkCustomerPassword(Customer customer, String password) throws CustomerLockedException,
			MaxNumberOfFailedLoggingAttemptExceededException, WrongCustomerCredentialsException {
		if (!customer.getPassword().equals(password)) {
			loginAttemptCount++;
			checkLoginAttempCount(customer);
			throw new WrongCustomerCredentialsException("Wrong password!");
		}
	}
	
	private void checkLoginAttempCount(Customer customer) throws MaxNumberOfFailedLoggingAttemptExceededException{
		if (loginAttemptCount == Integer.parseInt(ATMProperties.getProperty("customer.maxFailedLoginAttempt"))) {
			lockCustomer(customer);
		}
	}
	
	private void lockCustomer(Customer customer) throws MaxNumberOfFailedLoggingAttemptExceededException{
		customer.setLocked(true);
		throw new MaxNumberOfFailedLoggingAttemptExceededException("Max number of login attempt reached: "
				+ loginAttemptCount);
	}

Yukarıdaki beş metodun her birisinin sadece ve sadece bir iş yaptığına dikkat edin.

Bence idealde metotlarımız bu kadar kısa, üçer-beşer satırdan ibaret olmalı. Eskiden fonksiyonların bir ekrandan daha uzun olmaması gerektiği söylenirdi. Benim 15’lik makinamda, Eclipse’de 11 puntoluk Consolas ile tam 80 satır, aşağıya doğru kaydırmaya gerek kalmadan tek bir ekranda görünüyor. 80 satır bir metot için çok!

R. Martin Clean Code’da, bir metodun olabildiğince 20 satırı geçmemesi gerektiğini söyler. “Refactoring in Large Software Projects” isimli kitapda da  M. Lippert ve S. Roock, mimari kötü kokulardan bahsederken bir metodun ortalama olarak 30 satırı geçmemesi gerektiğinden bahseder.

Sınıfların karmaşıklık ölçütlerinden bahsederken metot sayısını ele almış ve karşı örnekleri de göstererek, bu durumun problemlerinden bahsetmiştik. Benzer durum metotların karmaşıklığını satır sayısıyla ölçerken de geçerli. Bu konuda Steven C. MacConnell’in Code Complete kitabındaki tartışmaya bakablirsiniz.

Bir metodun aldığı argüman sayısı da o metodun karmaşıklığı hakkında bize bilgi verir. İdealde bir metoda geçilen parametre sayısının sıfır olmasını bekleriz. Bu durumda o metodu kodlamak, anlamak, test etmek vs. çok daha kolay olur. Kendisine hiç parametre geçilmediği halde içinde tonla iş yapan metot yazmak elbette mümkündür ama sonuçta satır sayısı gibi bu ölçüt de mutlak olmayıp, bize kötü kokular veren cinstendir. Gerçekte bir motodun aldığı argüman sayısı, o metodun API tasarım kalitesini belirler. Parametre sayısı constructor ve factory metotlarıyla façade rolü olan sınıfların metotlarında artabilir ama bir sistemdeki metotlara geçilen parametrelerin ortalama sayısının olabildiğince 1 civarında olması tercih edilir. PMD Code Size ölçütünün ölçümlerinden biri olan ExcessiveParameterList’de uyarı seviyesini 10 olarak belirlemiştir. Yani 10’dan fazla parametre alan metotların gözden geçirilmesini tavsiye etmektedir.

Bir diğer metot karmaşıklığı ölçütü de “Cyclomatic Complexity” denen ve 1976 yılında MacCabe tarafında geliştirilmiş olan ölçüttür. Dilimize karar karmaşıklığı olarak da çevrilebilecek bu ölçüt, bir metotta alınan kararların sayısı üzerine bina edilmiştir. Dolayısıyla bir metotaki if-else, switch-case, while, for gibi karar mekanizmaları ne kadar az kullanılırsa o kadar sağlıklı metotlara sahip oluruz. PMD Code Size ölçütünün ölçümlerinden biri olan CyclomaticComplexity’de uyarı seviyesini 10 olarak belirlemiştir. Yani 10’dan fazla karar mekanizmasına sahip olan metotların gözden geçirilmesini tavsiye etmektedir. Bu konuyu ileride daha geniş ele almak üzere burada bırakalım.

Metotlar, kodumuzun ana yapılarındandır. Bu yüzden metotların basitliği ve anlaşırlığı, sistemimizin basitliği ve anlaşırlığını belirler. Unutmayalım, “küçük güzeldir” (small is beautiful).

Toplam görüntülenme sayısı: 972