Arayüz Adlarına Önek ve Sonekler Ait Değildir

3 yıl önce Yazan David Grudl  

Arayüzlerde I öneki veya Interface soneki, aynı şekilde soyut sınıflarda Abstract kullanmak bir antipatterndir. Temiz kodda yeri yoktur. Arayüz adlarını ayırt etmek aslında OOP prensiplerini bulanıklaştırır, koda gürültü katar ve geliştirme sırasında karmaşıklığa neden olur. Nedenleri şunlardır:

Tip = sınıf + arayüz + alt sınıflar

OOP dünyasında, sınıflar ve arayüzler tip olarak kabul edilir. Bir özelliği veya parametreyi bildirirken bir tip kullanırsam, geliştirici açısından güvendiği tipin bir sınıf mı yoksa arayüz mü olduğu arasında bir fark yoktur. Bu harika bir şeydir, bu sayede arayüzler aslında bu kadar kullanışlıdır. Bu, varlıklarına anlam katar. (Cidden: bu prensip geçerli olmasaydı arayüzler ne işe yarardı? Bir düşünün.)

Şu koda bakın:

class FormRenderer
{
	public function __construct(
		private Form $form,
		private Translator $translator,
	) {
	}
}

Yapıcı şöyle der: “Bir forma ve bir çevirmene ihtiyacım var.” Ve GettextTranslator mı yoksa DatabaseTranslator mı alacağı umurunda değildir. Ve aynı zamanda, bir kullanıcı olarak, Translator'ın bir arayüz mü, soyut bir sınıf mı yoksa somut bir sınıf mı olduğu umurumda değildir.

Gerçekten umurumda değil mi? Aslında hayır, itiraf etmeliyim ki oldukça meraklıyım, bu yüzden yabancı bir kütüphaneyi incelerken, tipin arkasında neyin saklı olduğuna bir göz atarım ve fareyi üzerine getiririm:

Aha, şimdi biliyorum. Ve bu kadar. Bunun bir sınıf mı yoksa arayüz mü olduğunu bilmek, örneğini oluşturmak isteseydim önemli olurdu, ancak durum bu değil, şimdi sadece değişkenin tipinden bahsediyorum. Ve burada bu detaylardan soyutlanmak istiyorum. Ve kesinlikle onları koduma dahil etmek istemiyorum! Tipin arkasında neyin saklı olduğu, tanımının bir parçasıdır, tipin kendisinin değil.

Ve şimdi başka bir koda bakın:

class FormRenderer
{
	public function __construct(
		private AbstractForm $form,
		private TranslatorInterface $translator,
	) {
	}
}

Bu yapıcı tanımı kelimenin tam anlamıyla şöyle der: Soyut bir forma ve bir çevirmen arayüzüne ihtiyacım var.” Ama bu saçmalık. Oluşturması gereken somut bir forma ihtiyacı var. Soyut bir forma değil. Ve çevirmen görevini yerine getiren bir nesneye ihtiyacı var. Bir arayüze ihtiyacı yok.

Interface ve Abstract kelimelerinin göz ardı edilmesi gerektiğini biliyorsunuz. Yapıcının önceki örnektekiyle aynı şeyi istediğini biliyorsunuz. Ama… ciddi misiniz? Gerçekten göz ardı edilmesi gereken kelimeleri kullanmayı adlandırma kurallarınıza dahil etmenin iyi bir fikir olduğunu mu düşünüyorsunuz?

Sonuçta, OOP prensipleri hakkında yanlış bir izlenim yaratıyor. Yeni başlayan biri kafası karışmış olmalı: “Eğer Translator tipi 1) Translator sınıfının bir nesnesi 2) Translator arayüzünü uygulayan bir nesne veya 3) onlardan miras alan bir nesne anlamına geliyorsa, o zaman TranslatorInterface ne anlama geliyor?” Buna mantıklı bir cevap vermek mümkün değil.

Translator da bir arayüz olabilmesine rağmen TranslatorInterface yazdığımızda, totoloji yapıyoruz. interface TranslatorInterface bildirdiğimizde de aynı şey geçerli. Ve böyle devam eder. Sonunda bir programcı şakası ortaya çıkar:

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Constructor
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

İstisnai Uygulama

TranslatorInterface gibi bir şey gördüğümde, muhtemelen Translator implements TranslatorInterface adında bir uygulamanın da var olması muhtemeldir. Bu beni düşünmeye itiyor: Translator'ı Translator olarak adlandırılma ayrıcalığına sahip kılan şey nedir? Diğer her uygulama, örneğin GettextTranslator veya DatabaseTranslator gibi açıklayıcı bir isme ihtiyaç duyar, ancak bu, sıfatsız Translator olarak adlandırıldığı için öncelikli konumunun gösterdiği gibi bir şekilde "varsayılan"dır.

Hatta insanları kararsız bırakır ve Translator için mi yoksa TranslatorInterface için mi tip ipucu yazacaklarını bilemezler. İstemci kodunda her ikisi de karışır, eminim buna zaten birçok kez rastlamışsınızdır (Nette'de örneğin Nette\Http\Request vs IRequest ile ilgili olarak).

İstisnai uygulamadan kurtulup genel Translator adını arayüz için bırakmak daha iyi olmaz mıydı? Yani, belirli bir ada sahip somut uygulamalar + genel bir ada sahip genel bir arayüz. Bu mantıklı.

Açıklayıcı adın yükü o zaman tamamen uygulamaların üzerindedir. TranslatorInterface'i Translator olarak yeniden adlandırırsak, eski Translator sınıfımızın yeni bir isme ihtiyacı olur. İnsanlar bu sorunu DefaultTranslator olarak adlandırarak çözme eğilimindedir, ben de suçluyum. Ama yine, onu Default yapan ne kadar istisnai? Tembel olmayın ve ne yaptığını ve diğer olası uygulamalardan neden farklı olduğunu iyice düşünün.

Peki ya birden fazla uygulama hayal edemiyorsam? Ya aklıma sadece bir geçerli yol geliyorsa? O zaman basitçe bir arayüz oluşturmayın. En azından şimdilik.

İşte, başka bir uygulama ortaya çıktı

Ve işte başlıyoruz! İkinci bir uygulamaya ihtiyacımız var. Bu sık sık olur. Çevirileri, örneğin veritabanına kaydetmek gibi tek bir kanıtlanmış yöntemden başka bir şekilde saklama ihtiyacı hiç doğmamıştı, ancak şimdi yeni bir gereksinim ortaya çıktı ve uygulamada birden fazla çevirmen olması gerekiyor.

Bu aynı zamanda orijinal tek çevirmenin özgüllüğünün ne olduğunu açıkça fark ettiğiniz andır. Bu bir veritabanı çevirmeniydi, varsayılan değil.

Bununla ne yapmalı?

  1. Translator adından bir arayüz yapın
  2. Orijinal sınıfı DatabaseTranslator olarak yeniden adlandırın ve Translator'ı uygulayacaktır
  3. Ve yeni GettextTranslator ve belki NeonTranslator sınıflarını oluşturun

Tüm bu değişiklikler çok rahat ve kolay bir şekilde yapılır, özellikle uygulama dependency injection prensiplerine uygun olarak oluşturulmuşsa. Kodda hiçbir şeyi değiştirmeye gerek yoktur, sadece DI konteyner yapılandırmasında TranslatorDatabaseTranslator olarak değiştiririz. Bu harika!

Ancak, önekleme/soneklemekte ısrar etseydik durum tamamen farklı olurdu. Uygulama genelinde kodda tipleri Translator'dan TranslatorInterface'e yeniden adlandırmak zorunda kalırdık. Böyle bir yeniden adlandırma, yalnızca kurala uymak için tamamen amaçlı olurdu, ancak az önce gösterdiğimiz gibi OOP'nin anlamına aykırı olurdu. Arayüz değişmedi, kullanıcı kodu değişmedi, ancak kural yeniden adlandırmayı mı gerektiriyor? O zaman bu hatalı bir kuraldır.

Ayrıca, zamanla bir arayüzden daha iyi bir soyut sınıfın olacağı ortaya çıksaydı, tekrar yeniden adlandırırdık. Böyle bir müdahale hiç de önemsiz olmayabilir, örneğin kod birden fazla pakete dağıtılmışsa veya üçüncü taraflarca kullanılıyorsa.

Ama herkes böyle yapıyor

Herkes değil. PHP dünyasında arayüzlerin ve soyut sınıfların adlarını ayırt etmenin Zend Framework ve ardından Symfony, yani büyük oyuncular tarafından popüler hale getirildiği doğrudur. Bu yaklaşım PSR tarafından da benimsendi, ki bu paradoksal olarak yalnızca arayüzler yayınlar ve yine de her birinin adında arayüz kelimesini belirtir.

Öte yandan, diğer önemli bir framework olan Laravel, arayüzleri ve soyut sınıfları hiçbir şekilde ayırt etmez. Örneğin popüler veritabanı katmanı Doctrine de bunu yapmaz. Ve PHP'deki standart kütüphane de bunu yapmaz (arayüzlerimiz Throwable veya Iterator, soyut sınıfımız FilterIterator, vb. vardır).

PHP dışındaki dünyaya bakacak olursak, örneğin C# arayüzler için I önekini kullanır, tersine Java veya TypeScript'te adlar ayırt edilmez.

Yani herkes yapmıyor, ancak yapsalar bile bu doğru olduğu anlamına gelmez. Başkalarının ne yaptığını düşünmeden benimsemek mantıklı değildir, çünkü hataları da benimseyebilirsiniz. Diğerlerinin muhtemelen kendilerinin de kurtulmak isteyeceği, ancak çok zor olan hataları.

Kodda neyin arayüz olduğunu anlayamıyorum

Birçok programcı, önek/soneklerin kendileri için yararlı olduğunu, çünkü sayesinde kodda neyin arayüz olduğunu hemen anladıklarını iddia edecektir. Böyle bir ayrımın eksikliğini hissedeceklerini düşünüyorlar. Öyleyse bir deneyelim, bu örneklerde neyin sınıf neyin arayüz olduğunu anlayabilir misiniz?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X her zaman bir sınıftır, Y bir arayüzdür, önekler/sonekler olmadan bile nettir. Elbette IDE de bunu bilir ve ilgili bağlamda size her zaman doğru şekilde ipucu verecektir.

Peki ya burada:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

Bu durumlarda anlayamazsınız. En başta söylediğimiz gibi, burada geliştirici açısından neyin sınıf neyin arayüz olduğu arasında bir fark olmamalıdır. Bu da tam olarak arayüzlere ve soyut sınıflara anlam katar.

Eğer burada sınıfı arayüzden ayırt edebilseydiniz, temel OOP prensibini inkar etmiş olurdunuz. Ve arayüzler anlamını yitirirdi.

Buna alışkınım

Alışkanlıkları değiştirmek acı verir 🙂 Kaç kez sadece düşüncesi bile. Ama haksızlık etmeyelim, birçok insan değişikliklerden hoşlanır ve onları dört gözle bekler, ancak çoğu için alışkanlık demir bir gömlektir.

Ancak geçmişe bakmak yeterlidir, bazı alışkanlıkların zamanla nasıl yok olduğunu görmek için. Muhtemelen en ünlüsü, seksenlerden beri kullanılan ve Microsoft tarafından popülerleştirilen Macar gösterimidir. Gösterim, her değişkenin adının veri tipini simgeleyen bir kısaltmayla başlamasına dayanıyordu. PHP'de şöyle görünürdü: echo $this->strName veya $this->intCount++. Macar gösteriminden doksanlarda vazgeçilmeye başlandı ve bugün Microsoft kılavuzlarında geliştiricileri doğrudan bundan caydırıyor.

Bir zamanlar vazgeçilmezdi ve bugün kimse onu özlemiyor.

Ama neden bu kadar eskiye gidelim? Belki hatırlarsınız, PHP'de genel olmayan sınıf üyelerini alt çizgiyle ayırmak adettendi (Zend Framework'ten bir örnek). Bu, public/protected/private görünürlük değiştiricilerine sahip PHP 5'in çoktan var olduğu bir zamandı. Ancak programcılar bunu alışkanlıktan yapıyorlardı. Alt çizgiler olmadan kodda yönlerini kaybedeceklerine inanıyorlardı. “Kodda genel olanları özel olanlardan nasıl ayırt ederdim, ha?”

Bugün kimse alt çizgi kullanmıyor. Ve kimse onları özlemiyor. Zaman, endişelerin yersiz olduğunu mükemmel bir şekilde kanıtladı.

Oysa bu, “Kodda arayüzü sınıftan nasıl ayırt ederdim, ha?” itirazıyla tamamen aynıdır.

On yıl önce önek/sonek kullanmayı bıraktım. Asla geri dönmezdim, harika bir karardı. Geri dönmek isteyen başka bir programcı da tanımıyorum. Bir arkadaşımın dediği gibi: “Dene ve bir ay sonra bunu bir zamanlar nasıl farklı yaptığını anlamayacaksın.”

Tutarlılığı korumak istiyorum

Bir programcının şöyle dediğini hayal edebiliyorum: “Önek ve sonek kullanmak gerçekten anlamsız, anlıyorum, ama kodumu zaten bu şekilde oluşturdum ve değişiklik çok zor. Ve yeni kodu onlarsız doğru yazmaya başlarsam, belki de kötü bir kuraldan daha kötü olan bir tutarsızlık yaratacağım.”

Aslında, kodunuz zaten tutarsız, çünkü hiçbir önek ve soneki olmayan PHP sistem kütüphanesini kullanıyorsunuz:

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

Ve elinizi kalbinize koyun, bu rahatsız ediyor mu? Hiç bunun daha tutarlı olacağını düşündünüz mü?

class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
	public function add(string|StringableInterface $item): void
	{
	}
}

Veya bu?

try {
	$command = $this->find($name);
} catch (ThrowableInterface $e) {
	return $e->getMessage();
}

Sanmıyorum. Tutarlılık göründüğü kadar önemli bir rol oynamıyor. Aksine, göz daha az görsel gürültüyü, beyin tasarımın temizliğini tercih eder. Yani, kuralı düzenlemek ve yeni arayüzleri önek ve sonek olmadan doğru yazmaya başlamak mantıklıdır.

Büyük projelerden bile kasıtlı olarak kaldırılabilirler. Örnek olarak, tarihsel olarak arayüz adlarında I öneklerini kullanan Nette Framework verilebilir, ki bundan birkaç yıl önce tam geriye dönük uyumluluğu koruyarak kademeli olarak kurtulmaya başladı.