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

2 yıl önce Kimden David Grudl  

Arayüzler için I prefix or Interface ve soyut sınıflar için Abstract son ekini kullanmak bir antipatterndir. Saf koda ait değildir. Arayüz adlarını ayırt etmek aslında OOP ilkelerini bulanıklaştırır, koda gürültü ekler ve geliştirme sırasında karmaşıklıklara neden olur. İşte nedenleri.

Tür = Sınıf + Arayüz + Torunlar

OOP dünyasında hem sınıflar hem de arayüzler tip olarak kabul edilir. Bir özelliği veya parametreyi bildirirken bir tür kullanırsam, geliştiricinin bakış açısından, güvendikleri türün bir sınıf veya bir arayüz olması arasında bir fark yoktur. Aslında arayüzleri bu kadar kullanışlı kılan şey de budur. Varlıklarına anlam kazandıran da budur. (Cidden: bu prensip geçerli olmasaydı arayüzler ne işe yarardı? Bunu düşünmeye çalışın).

Şu koda bir göz atın:

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

Kurucu şöyle der: "Bir forma ve bir çevirmene ihtiyacım var. " Ve bir GettextTranslator nesnesi mi yoksa bir DatabaseTranslator nesnesi mi aldığı umurunda değil. Aynı zamanda, bir kullanıcı olarak ben de Translator 'un bir arayüz mü, soyut bir sınıf mı yoksa somut bir sınıf mı olduğunu umursamıyorum.

Gerçekten umurumda değil mi? Aslında, hayır, oldukça meraklı olduğumu itiraf ediyorum, bu yüzden başka birinin kütüphanesini keşfederken, yazının arkasında ne olduğuna bir göz atıyorum ve üzerine geliyorum:

Oh, anlıyorum. Bu da hikayenin sonu. Bunun bir sınıf mı yoksa bir arayüz mü olduğunu bilmek, bir örneğini oluşturmak isteseydim önemli olurdu, ancak durum bu değil, şu anda sadece değişkenin türünden bahsediyorum. İşte bu noktada uygulama detaylarından uzak durmak istiyorum. Ve kesinlikle kodumun içine gömülmelerini istemiyorum! Bir türün arkasında ne olduğu tanımının bir parçasıdır, türün kendisi değil.

Şimdi diğer 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: "Çevirmenin bir soyut formuna ve bir arayüzüne ihtiyacım var. " Ama bu aptalca. İşlemek için somut bir forma ihtiyacı vardır. Soyut bir forma değil. Ve çevirmen olarak hareket eden bir nesneye ihtiyacı vardır. Bir arayüze ihtiyacı yoktur.

Interface ve Abstract kelimelerinin göz ardı edilmesi gerektiğini biliyorsunuz. Kurucunun önceki örnektekiyle aynı şeyi istediğini de. Ama… gerçekten mi? İsimlendirme kurallarına göz ardı edilecek kelimelerin kullanımını dahil etmek gerçekten iyi bir fikir gibi görünüyor mu?

Sonuçta, OOP ilkeleri hakkında yanlış bir fikir yaratır. Yeni başlayan birinin 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) bunlardan miras alan bir nesne anlamına geliyorsa, o zaman TranslatorInterface ile kastedilen nedir?” Bunun makul bir cevabı yoktur.

TranslatorInterface kullandığımızda, Translator bir arayüz olsa bile, bir totoloji yapmış oluruz. Aynı şey interface TranslatorInterface bildirdiğimizde de olur. Yavaş yavaş, bir programlama şakası ortaya çıkacaktır:

interface TranslatorInterface
{
}

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

Olağanüstü Uygulama

TranslatorInterface gibi bir şey gördüğümde, muhtemelen Translator implements TranslatorInterface adında bir uygulama vardır. Bu beni meraklandırıyor: Translator 'u Translator olarak adlandırılma ayrıcalığına sahip olacak kadar özel kılan nedir? Diğer tüm uygulamaların GettextTranslator veya DatabaseTranslator gibi açıklayıcı bir isme ihtiyacı vardır, ancak bu, etiket olmadan Translator olarak adlandırılmasının tercih edilen durumunun önerdiği gibi bir tür “varsayılan” dır.

İnsanların bile kafası karışıyor ve Translator için mi yoksa TranslatorInterface için mi ipucu yazacaklarını bilemiyorlar. Sonra her ikisi de istemci kodunda karışıyor, eminim bununla birçok kez karşılaşmışsınızdır (örneğin Nette'de Nette\Http\Request ile IRequest arasındaki bağlantıda).

Özel uygulamadan kurtulmak ve arayüz için Translator genel adını korumak daha iyi olmaz mı? Yani, özel bir isme sahip özel uygulamalara + genel bir isme sahip genel bir arayüze sahip olmak. Bu mantıklı.

Bu durumda açıklayıcı bir ismin yükü tamamen uygulamaların üzerindedir. Eğer TranslatorInterface sınıfını Translator olarak yeniden adlandırırsak, eski Translator sınıfımızın yeni bir isme ihtiyacı olacaktır. İnsanlar bu sorunu DefaultTranslator diyerek çözme eğilimindedir, ben bile bundan suçluyum. Ama yine de, Default olarak adlandırılmasını bu kadar özel kılan nedir? Tembel olmayın ve ne yaptığı ve diğer olası uygulamalardan neden farklı olduğu hakkında iyice düşünün.

Peki ya birkaç olası uygulama hayal edemiyorsam? Ya sadece tek bir geçerli yol düşünebiliyorsam? O zaman arayüzü oluşturmayın. En azından şimdilik.

Eh, Başka Bir Uygulama Daha Var

Ve işte burada! İkinci bir uygulamaya ihtiyacımız var. Bu her zaman olur. Çevirileri birden fazla kanıtlanmış şekilde, örneğin bir veritabanında saklamaya hiç ihtiyaç duyulmadı, ancak şimdi yeni bir gereksinim var ve uygulamada daha fazla çevirmene ihtiyacımız var.

Bu aynı zamanda orijinal tek çevirmenin özelliklerinin ne olduğunu açıkça anladığınız zamandır. Bu bir veritabanı çevirmeniydi, varsayılan değildi.

Ne olmuş ona?

  1. Arayüzün adını Translator yapın
  2. Orijinal sınıfı DatabaseTranslator olarak yeniden adlandırın ve Translator
  3. Ve yeni sınıflar oluşturursunuz GettextTranslator ve belki NeonTranslator

Tüm bu değişiklikler, özellikle uygulama bağımlılık enjeksiyonu ilkelerine göre oluşturulmuşsa, çok kullanışlı ve kolaydır. Kodda hiçbir şeyi değiştirmeye gerek yok, sadece DI konteyner yapılandırmasında Translator adresini DatabaseTranslator olarak değiştirin. İşte bu harika!

Ancak, önekleme/sonekleme konusunda ısrarcı olursak taban tabana farklı bir durum ortaya çıkacaktır. Uygulama genelinde koddaki türleri Translator 'dan TranslatorInterface 'a yeniden adlandırmamız gerekirdi. Böyle bir yeniden adlandırma tamamen gelenek uğruna olacaktır, ancak az önce gösterdiğimiz gibi OOP'nin ruhuna aykırı olacaktır. Arayüz değişmedi, kullanıcı kodu değişmedi ama kurallar yeniden adlandırmayı mı gerektiriyor? O zaman bu kusurlu bir kuraldır.

Dahası, zaman içinde soyut bir sınıfın arayüzden daha iyi olacağı ortaya çıkarsa, yeniden adlandırırız. Böyle bir işlem, örneğin kod birden fazla pakete yayıldığında veya üçüncü taraflarca kullanıldığında hiç de önemsiz olmayabilir.

Ama Herkes Yapıyor

Herkes bilmiyor. PHP dünyasında Zend Framework ve ardından Symfony gibi büyük oyuncuların arayüz ve soyut sınıf isimleri ayrımını popüler hale getirdiği doğrudur. Bu yaklaşım, ironik bir şekilde yalnızca arayüzleri yayınlayan, ancak her birinin adında arayüz kelimesini içeren PSR tarafından benimsenmiştir.

Öte yandan, bir diğer büyük framework olan Laravel, arayüzleri ve soyut sınıfları ayırt etmemektedir. Örneğin popüler Doctrine veritabanı katmanı bile bunu yapmaz. Ve PHP'deki standart kütüphane de bunu yapmaz (bu yüzden Throwable veya Iterator arayüzlerine, FilterIterator soyut sınıfına vb. sahibiz).

PHP dışındaki dünyaya bakacak olursak, C# arayüzler için I önekini kullanırken, Java veya TypeScript'te isimler farklı değildir.

Yani bunu herkes yapmıyor, ama yapsalar bile bu iyi bir şey olduğu anlamına gelmiyor. Başkalarının yaptıklarını akılsızca benimsemek akıllıca değildir, çünkü siz de hataları benimsiyor olabilirsiniz. Diğerlerinin kurtulmayı tercih edeceği hatalar, bugünlerde çok büyük bir lokma.

Kodda Arayüzün Ne Olduğunu Bilmiyorum

Birçok programcı, öneklerin/soneklerin kendileri için yararlı olduğunu, çünkü kodda hangi arayüzlerin olduğunu hemen bilmelerini sağladığını iddia edecektir. Böyle bir ayrımı kaçıracaklarını düşünüyorlar. Bakalım, bu örneklerde neyin sınıf neyin arayüz olduğunu söyleyebilecek misiniz?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X is always a class, Y bir arayüzdür, ön ekler / son ekler olmadan bile belirsizdir. Elbette, IDE de bunu biliyor ve belirli bir bağlamda size her zaman doğru ipucunu verecektir.

Peki ya burası?

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try { ... } catch (A $x) { ... }

Bu durumlarda, bunu bilmeyeceksiniz. En başta da söylediğimiz gibi, bir geliştiricinin bakış açısından neyin sınıf neyin arayüz olduğu arasında bir fark olmamalıdır. Arayüzlere ve soyut sınıflara anlamını veren de budur.

Eğer burada bir sınıf ile bir arayüz arasında ayrım yapabilseydiniz, OOP'nin temel prensibini inkar etmiş olurdunuz. Ve arayüzler anlamsız hale gelirdi.

Buna Alışkınım

Alışkanlıkları değiştirmek sadece acı verir 🙂 Çoğu zaman fikri bile acıtır. Değişikliklere ilgi duyan ve onları dört gözle bekleyen insanları suçlamayalım, Çek atasözünde olduğu gibi çoğu için alışkanlık demir bir gömlektir.

Ancak geçmişe bir bakın, bazı alışkanlıkların zaman içinde nasıl yok olup gittiğini göreceksiniz. Belki de en ünlüsü seksenli yıllardan beri kullanılan ve Microsoft tarafından popülerleştirilen Macar notasyonudur. Bu gösterim, her değişkenin adının veri türünü simgeleyen bir kısaltma ile başlamasından oluşuyordu. PHP'de echo $this->strName veya $this->intCount++ gibi görünürdü. Macar notasyonu doksanlı yıllarda terk edilmeye başlandı ve bugün Microsoft'un yönergeleri geliştiricileri bunu kullanmaktan doğrudan caydırıyor.

Eskiden önemli bir özellikti ve bugün kimse bunu kaçırmıyor.

Ama neden bu kadar uzun zaman öncesine gidelim? PHP'de, sınıfların herkese açık olmayan üyelerini alt çizgi ile ayırt etmenin geleneksel olduğunu hatırlayabilirsiniz (Zend Framework'ten örnek). Bu, public/protected/private görünürlük değiştiricilerine sahip PHP 5'in olduğu zamanlardı. Ancak programcılar bunu alışkanlıklarından dolayı yapıyorlardı. Alt çizgi olmadan kodu anlamayı bırakacaklarına ikna olmuşlardı. “Koddaki public ve private özellikleri nasıl ayırt edebilirim ki?”

Bugün kimse alt çizgi kullanmıyor. Ve kimse onları özlemiyor. Zaman, korkuların yanlış olduğunu gayet iyi kanıtladı.

Yine de bu, “Kodda bir arayüzü bir sınıftan nasıl ayırt edebilirim ki?” itirazıyla tamamen aynıdır.

On yıl önce önek/postek kullanmayı bıraktım. Asla geri dönmem, harika bir karardı. Geri dönmek isteyen başka bir programcı da tanımıyorum. Bir arkadaşımın dediği gibi, “Deneyin ve bir ay içinde daha önce farklı yaptığınızı anlamayacaksınız.”

Tutarlılığı Korumak İstiyorum

Bir programcının şöyle dediğini hayal edebiliyorum: “Önekleri ve sonekleri kullanmak gerçekten saçma, anlıyorum, sadece kodumu zaten bu şekilde oluşturdum ve değiştirmek çok zor. Ve eğer bunlar olmadan yeni kodu doğru bir şekilde yazmaya başlarsam, tutarsızlıkla sonuçlanacağım, ki bu belki de kötü gelenekten bile daha kötü.”

Aslında, PHP sistem kütüphanesini kullandığınız için kodunuz zaten tutarsızdır:

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

Ve elinizi vicdanınıza koyun, bunun bir önemi var mı? Bunun daha tutarlı olacağını hiç düşündünüz mü?

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

Yoksa bu mu?

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

Hiç sanmıyorum. Tutarlılık göründüğü kadar büyük bir rol oynamıyor. Aksine, göz daha az görsel gürültüyü tercih eder, beyin ise tasarımın netliğini tercih eder. Bu nedenle geleneği ayarlamak ve yeni arayüzleri ön ekler ve son ekler olmadan doğru bir şekilde yazmaya başlamak mantıklıdır.

Büyük projelerden bile kasıtlı olarak kaldırılabilirler. Buna bir örnek, geçmişte arayüz adlarında I öneklerini kullanan ve geriye dönük tam uyumluluğu koruyarak birkaç yıl önce aşamalı olarak kaldırmaya başladığı Nette Framework'tür.