Önek ve Sonekler Arayüz Adlarına Ait Değildir
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?
- Arayüzün adını
Translator
yapın - Orijinal sınıfı
DatabaseTranslator
olarak yeniden adlandırın veTranslator
- Ve yeni sınıflar oluşturursunuz
GettextTranslator
ve belkiNeonTranslator
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.
Yorum göndermek için lütfen giriş yapın