Data Transfer Object

Mustafa Akçakaya
ClevelTeam
Published in
4 min readNov 14, 2020
Poltergeist ya da Gipsy Wagon :)

Nasıl ki bir uygulamayı yaptığı iş bakımından bir bütün (holistik?) olarak ele aldığımızda rahatlıkla onu oluşturan parçalardan daha fazlası olduğunu görebiliyorsak mimari olarak ele aldığımızda da aynı şekilde onu oluşturan parçaların da kendine has dinamikleri olduğunu görebiliriz. İster monolitik olsun ister mikro servis üzerine kurulu olsun, uygulama gibi her parça kendi içinde parçalara, katmanlara ayrılıyor. İşleyiş ise bu katmanların yaptıkları işler ve kendi aralarında haberleşmelerinin toplamı olmuş oluyor. Haberleşme ile kastettiğim ise her türlü veri aktarımı, verinin uygulama ve alt yapı içerisindeki yolculuğu… Veri, katmanlar arasında yol alırken değişiyor, yenileniyor ya da yok oluyor. Olası kaotik durumları engellemek için bu devinimi kontrol altında tutmamız gerekiyor. Bunun için kullanılan yollardan biri Data Transfer Object yani kısaca DTO. Verinin iş birimleri ya da katmanlar arasındaki yolculuğunu kontrol altında tutmak adına onu kapsülleştirip taşınabilir bir obje haline getirmek olarak tanımlayabiliriz. Bizim ilkel dediğimiz tipler (string, integer, array…) zaten taşınamıyor mu? Elbette taşınabiliyor fakat bir iş için çok sayıda, aynı ya da farklı tiplere sahip değişkenlere ya da nesnelere ihtiyacımız olduğunda bunları kontrol altında zorlaşıyor. Ev taşıdığınızı düşünün, bunu kontrollü yapmak istersek (ki isteyeceğinize inanıyorum) nasıl yapabiliriz? Çok detaya girmeden ortak özelliklere sahip eşyaları kolilere koyduğumuzu ve kolilerin üzerini işaretlediğimizi söyleyebilirim. Böylece hem taşıma hem de yeni evimizde yerleştirme işi kolaylaşmış olacak. Buna benzer şekilde uygulama içerisinde de veri aktarımını bu şekilde gerçekleştirmek için başvurucağımız yollardan biri işte DTO olmuş olacak.

Önce sorunu görelim:

public function store(UserStoreRequest $request)
{
$user = $this->userService->createUser(
$request->name,
$request->surname,
$request->email,
$request->address,
$request->province,
$request->county,
$request->zip,
$request->phone,
$request->gsm,
$request->password,
);

event(UserCreated($user));

return new UserResource($user);
}

Güzel, create yöntemi içerisinde tek tek değişkenleri görebiliyorum ama iyi bir yol gibi gözükmüyor. Çok sayıda değişken, çok sayıda parametre, parametre sırası… ki bunun çok daha fazlasının olacağı durumlar da olacaktır. Bunu daha basit ve portatif hale getirmeye çalışalım:

public function store(UserStoreRequest $request): UserResource
{
$userData = $request->validated(); // array

$user = $this->userService->createUser($userData);

event(UserCreated($user));

return new UserResource($user);
}

Şimdi daha iyi gibi fakat bu sefer de array $userData değişkenine yabancı kaldım. İçinde ne var ne yok bilmiyorum. Bu aşamada bir sıkıntı olmayabilir belki ama create yöntemi içerine girdiğimizde elimizde ne olacağını biliyor muyuz?

public function create(array $userData): User
{
// Burada $userData kullanarak bir çok işlem yaptığımızı
// düşünelim. Örneğin kullanıcının emailine ihtiyacım varsa
// $userData['email'] şeklinde alabilirim fakat bu aşamada
// $userData'nın bir email elemanına sahip olduğunu nasıl
// bilebilirim?
// ...

return $this->userRepository->create($userData);
}

Veri üzerinde manipülasyon yapmaya çalıştığımızda ise durum daha da karışık bir hale gelebilir. Bu sefer aynı örneği DTO ile yapmalım. Çok basit bir DTO ile başlayalım:

class UserData
{
public string $name;
public string $surname;
public string $email;
public ?string $gsm = null;
public ?string $password = null;
public array $selected = [];

public function __construct(array $data)
{
foreach ($data as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}
}

public function toArray(): array
{
$reflect = new ReflectionClass($this);

$result = [];

foreach ($reflect->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
$result[$property->getName()] = $this->{$property->getName()};
}

return $result;
}
}

Yaptığı çok fazla bir iş yok. Özellikleri var ve yapıcı yöntem içerisinde verilen diziden kullanılabilir özellikleri alıyor. Bir de özellikleri array olarak veren toArray yöntemine sahip (İdeal bir DTO değil, birçok kontrol ve özellik eksik, sadece DTO’yu anlatmak için kullandığım basit bir örnek olarak düşünün). Aynı örneği bu sefer DTO ile deneyelim:

public function store(UserStoreRequest $request): UserResource
{
$userData = new UserData($request->validated());

$user = $this->userService->createUser($userData);

event(UserCreated($user));

return new UserResource($user);
}

Şimdi create yöntemine bakalım:

public function create(UserData $userData): User
{
// Burada artık UserData içinde ne olduğunu biliyorum.
// Özellikle PhpStorm gibi bir IDE
// bana auto-complete özelliği ile yardımcı olacak.

// Örneğin:
if ($userData->password) {
$userData->password = bcrypt($userData->password);
}

return $this->userRepository->create($userData->toArray());
}

Eskisinden çok daha iyi. Şimdi kontrolün bende olduğunu hissediyorum.

Bu elbette DTO’yu her yerde kullanabileceğim anlamına gelmiyor. Evet, bazı durumları kurtardığı doğru fakat basitlik ilkesini de fazla çiğnememek gerekiyor. Fonksiyon argümanları benim hala dostum. Birkaç argüman için gereksiz yere DTO kullanmak çoğu zaman Poltergeist olarak bilinen anti-pattern ile adlandırılır. Yükü almak yerine size daha fazla yük çıkarabilir.

DTO basit bir objedir, bunu unutmamız lazım. Sizin başka bir işlem yapacağınız, başka katmanları içine taşıyacağınız bir obje hiçbir zaman olmamalıdır. Görevi sadece değerleri taşımak ve bunları sunmak (serialization) olmalıdır. Kendilerine has accessor/mutotar ve getter/setter özelliklerine sahip olabilirler fakat bunlar da basit düzeyde ve kesinlikle bağımsız olmalıdır. Örneğin UserData::$selected[] şeklinde bir elemanımız olsun ve veritabanından gelen json string bir ifade ile dolacak olsun. Bu işlemi UserData dışında değil de içeride yaparak kodun daha portatif ve organize olmasını sağlayabiliriz. Örneğin UserData::prepareSelected($selected) şeklinde bir yöntem ekleyebiliriz:

public static function prepareSelected($selected): array
{
if (is_array($selected)) {
return $selected;
}

$selected = json_decode($selected, true);

if (!is_array($selected)) {
return [];
}

return $selected;
}

Böylece objemizi oluştururken şu şekilde yapabiliriz:

$userData->selected = UserData::prepareSelected(‘[1, 2, 3, 4]’);

Bu konuda konuşulacak, tartışılacak çok şey vardır. Sadece kısa bir giriş yapmak istedim. Son olarak Laravel içinde DTO kullanmak için şu paketi tavsiye ediyorum:

https://github.com/spatie/data-transfer-object

--

--