May 23, 2017

C# anonim tiplerinin kullanılabilirliği

Eğer yazılım geliştirmeyle ilgili temel dersler aldıysanız programlama dillerinin verileri değişken ve sabitler üzerinde belirli tip tanımlarıyla (tamsayı, kesirli sayı, karakter, dizi, kompleks v.s.) tuttuğunu duymuşsunuzdur. Bu nedenle bilgisayar donanımının mantığına daha yakın duran düşük seviyeli programlama dilleri sizden tip tanımlarını sizden direkt istemekteyken, JavaScript, PHP ve benzeri yüksek seviyeli diller tanımlar söz konusu olduğunda daha esnek ve dinamik davranmaktadırlar.

Yine de bu konu hakkında yalnızca iki kutuptan söz etmek mümkün değil, çünkü ihtiyaçların çeşitliliği içerisinde orta bir yaklaşımı benimseyenler de mevcut. Bugün C#, Swift ve benzeri diller her iki dünyaya da hakim olabilmek adına kağıt üzerinde düşük seviye ensturmanlar sunmasına rağmen runtimeları sayesinde dinamikliği yönetebilir durumdalar.

C# 3.0 sürümü ile birlikte geliştiricilere "Anonymous Types" isimli bir kavram sunuldu. Bu sayede herhangi bir tip belirtmeden oluşturduğunuz obje CLR üzerinde runtime esnasında oluşturulan bir nesne tipine ait görünüyor. Ve bu objeleri dynamic keyword'ü kullandığımız noktalardan karşılayabiliyoruz.

Örneğin,

using System;

void PrintFirstName(dynamic anonObject) {
  Console.WriteLine(anonObject.firstname);
}

PrintFirstName(new { firstname = "Eser", lastname = "Özvataf", age = 34 });

Buradaki en büyük eksik (veya "trade off" denilebilir) ise bir dynamic nesnenin içeriğinin statik olarak "bilinmemesi" ve bu nedenle C# ekosisteminin güçlü olduğu IDE desteğinin sağlanamaması. Bu nesneleri kullandığımızda runtime'da ufak da olsa bir performans kaybı yaşanacağından da bahsetmemiz gerekiyor.

Type sistemine alışkın birçok geliştirici bu noktada "tanımla yeni bir tip, bunda bir sorun yok ki" dese de ben Domain Driven Design'ı benimseyip nesnelerimi katmanlara böldüğüm her sefer kendimi yönetilemez veya yönetimine zaman harcamanın çok verimli olmadığı bir yapı içerisinde buluyorum.

Örnek bir senaryo üzerinden gidelim;

İsteği karşılayan API endpoint, önce application katmanını çağırıyor, application katmanı ise domain katmanına ulaşıyor. Kural ise sabit; her katman kendine ait objeleri ancak ve ancak kendisine "direkt" erişen katmanla paylaşacak. Bu doğrultuda aşağıdaki gibi üç farklı metod oluşturuyoruz:

namespace MyProject.UserService.Api {
  ActionResult GetUser(UserQueryInput userQuery) { ... }
}

namespace MyProject.UserService.Application {
  UserDto GetUser(UserQueryDto userQuery) { ... }
}

namespace MyProject.UserService.Domain {
  UserEntity GetUser(UserQuery userQuery) { ... }
}

Fark edebileceğiniz birbirlerini çağıran bu üç metodun aldığı parametrelerin tipleri birbirinden farklı. Çünkü Api'ın aldığı UserQueryInput bu katmana özel, dönecek verinin sunumu (representasyonu) ile ilgili tercihler içerebilecekken, Application katmanındaki UserQueryDto da aynı şekilde uygulamanın özellikleriyle ilgili farklı öğeler barındırabilir. UserQuery ise yalın ve diğer uygulamaya özel durumlara karşı agnostik bir biçimde yalnızca veri alanlarını barındırıyor.

class UserQueryInput {
   string Username;
   bool ShowDetails; // kullanıcı bulunursa detay gösterelim mi?
}

class UserQueryDto {
   string Username;
   Guid InquirerUserId; // veriyi kim soruyor? sorgulamadan sonra log'a yazılacak
}

class UserQuery {
   string Username;
}

Yukarıda da "alt seviye" ve "üst seviye" dillerden bahsetmiş, C#'ın alt seviyede konumlanıp üst seviye özellikler sağladığından bahsetmiştim. Tekrar özetlemek gerekirse alt seviye bilgisayar mimarisine, üst seviye ise insan algısına yakınsar. Bu noktada benim algım için yukarıdaki üç sınıf da prensipte "aynı veri tipi"ni içermekte. Fakat ben bunları ayrı ayrı tipler olarak tanımlamalıyım.

Ayrıca bu tipleri tanımlarken Domain Driven Design için koyduğumuz "her katman ancak ilişkide bulunduğu katmandan referans alabilir" kuralını hatırlarsak, bu veri tipleri arasında Inheritance da kullanmıyor olmamız gerekecek.

Bu birbirinden tamamiyle farklı ve bağımsız sınıfların birbirlerine dönüşümleri için "mapper" ismi verdiğimiz, kaynak nesneden okuyup hedef nesneye yazan ve çoğunlukla "reflection" kullandığından dolayı performans maliyetli sınıflar/araçlar kullanmaktayız.

namespace MyProject.UserService.Api {
  ActionResult GetUser(UserQueryInput userQuery) {
    UserQueryDto applicationUserQuery = AutoMapper.Map<UserQueryDto>(userQuery);
    var result = this.Application.GetUser(applicationUserQuery);

    return this.Json(result);
  }
}

namespace MyProject.UserService.Application {
  UserDto GetUser(UserQueryDto userQuery) {
    UserQuery domainUserQuery = AutoMapper.Map<UserQuery>(userQuery);
    var result = this.Domain.GetUser(domainUserQuery);

    return AutoMapper.Map<UserDto>(result);
  }
}

namespace MyProject.UserService.Domain {
  UserEntity GetUser(UserQuery userQuery) {
    return this.Repository.Get(x => x.Username == userQuery.Username);
  }
}

Burada anonim tipleri kullansaydık kod okunabilirliği açısından güzel olmaz mıydı? Deneyelim...

namespace MyProject.UserService.Api {
  ActionResult GetUser(dynamic userQuery) {
    var result = this.Application.GetUser(userQuery);

    return this.Json(result);
  }
}

namespace MyProject.UserService.Application {
  dynamic GetUser(dynamic userQuery) {
    var result = this.Domain.GetUser(domainUserQuery);

    return result;
  }
}

namespace MyProject.UserService.Domain {
  dynamic GetUser(dynamic userQuery) {
    return this.Repository.Get(x => x.Username == userQuery.Username);
  }
}

Apayrı bir noktaya geldik, sanki artık yazdığımız kod C# değilmiş gibi. Bunun en büyük nedenlerinden biri type-safety'i kaybetmiş olmamız. Bir adım daha giderek dynamic yerine Dynamic<IUserQuery> yazacak ve mükemmel bir duck typing örneğine imza atacakmışız gibi hissediyorsunuz değil mi?

Maalesef henüz C#'da ne IDE desteği ve static checking için böyle bir generic tip kullanımı var, ne de dinamik tiplerden reflection olmaksızın casting imkanı var. Durum böyle olunca da anonim tiplerin dil içerisinde birinci dereceden vatandaş olduğunu düşünmüyorum.

Fakat aynı zamanda teknik engellerin olmadığının da C#'ın son sürümlerinde eklenen ValueTuple desteği nedeniyle farkındayım. C# açısından anonim tipler ile ValueTuple'ların arasında büyük farklar bulunmuyor, ama ValueTuple'larda değerleri isimlendirebiliyoruz.

Özetle, yukarıda saydığım nedenlerden ötürü C#'da anonim tiplerin ve dinamik runtime'ın halen kullanılabilirliğini düşük buluyorum. Umarım ki ilerleyen sürümlerde kullanım açısından daha dil ile bütünleşik bir noktaya gelirler.

  • LinkedIn
  • Tumblr
  • Reddit
  • Pocket
Comments powered by Disqus