Anasayfa » 8. Prototype Pattern

8. Prototype Pattern

by Levent KARAGÖL
13 dakikalık okuma süresi

Bu makalemizde, var olan bir nesnenin kopyasını çıkartarak yeni bir nesne oluşturma süreci ile ilgilenen Prototype tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Prototype tasarımın kalıbının ne olduğunu, hangi durumlarda hangi sorunlara çözüm sunduğunu konuşacak, sonrasında gelen bir talebe istinaden herhangi bir tasarım kalıbı kullanmadan bir çözüm üretecek, ardından bu çözümün problemlerini ve hangi yazılım tasarım prensiplerine aykırı olduğunu tartışacak, son olarak da Prototype tasarım kalıbı ile aynı talebe uygun bir çözüm üretmeye çalışacağız.

Not: Makale genelindeki örneklerin kaynak kodlarına Github üzerinden ulaşabilirsiniz.


Prototype Tasarım Kalıbı Nedir?

Klasik tasarım kalıplarındaki nesne oluşturma süreci ile ilgilenen kalıpların (Creational Design Patterns) sonuncusu olan Prototype tasarım kalıbı, özellikle iç içe geçmiş (nested) nesnelerin kopyalanarak yeni nesneler oluşturulması süreci ile ilgilenir.

JavaScript geliştiriciler olarak her ne kadar “JSON.parse(JSON.stringify(object))” şeklinde Serialize > Deserialize kopyalama yöntemimiz elimizin altında hazır bulunsa da, özellikle kompleks nesneler için bu işlem oldukça maliyetlidir ve mümkün mertebe kaçınılması gerekir.

Özellikle yazılıma yeni başlayan kişilerin referans tipinde olan iç içe geçmiş nesneleri kopyalama sırasında tekrar kullanarak yüzeysel kopyalama (Shallow Copy) yapması ve asıl nesne değiştiğinde kopyalanan nesnenin de değişeceğini fark etmemesi oldukça sık karşılaşılan bir durumdur.

Özellikle içe içe geçmiş (nested) nesnelerin kopyalanması sırasında derin klonlama (deep cloning) gereklidir ki, makalemizin konusu olan Prototype tasarım kalıbı tam olarak bu ihtiyaca çözüm üretir.


Talebimizi Alalım

Bir proje yönetim uygulamasını geliştiren ekipteyiz ve yazdığımız uygulamada projeler, her bir projenin altında görevler ve her bir görevin altında da kullanılan kaynaklar yer alıyor. Bizden de var olan bir projenin kopyasını çıkartacak bir fonksiyon yazmamız isteniyor.


İlk Çözüm Önerimizi Sunalım

Kaynaklar, görevler ve projeler için birer sınıf oluşturup, bunları iç içe birbirine bağlamak ve sonrasında bunların kopyasını çıkartacak bir metot hazırlamak mantıklı bir yol gibi gözüküyor. Öncelikle TypeScript serisindeki ilgili makalede önerilen “tsconfig.json” dosyasını oluşturarak işe başlayalım.

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true,
    "removeComments": true,
    "noUnusedParameters": true,
    "declaration": true,
    "lib": [
      "es2020",
      "esnext.asynciterable",
      "dom"
    ]
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}


Kaynak bilgilerini barındıracak sınıfımız ile kodlamaya başlayalım. Kaynak adı ve adedi değerlerini içerecek sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class Resource {
    constructor(public resourceName: string, public quantity: number) {
    }
}


Ardından görev bilgilerini ve görev için kullanılacak kaynakları barındıracak sınıfımız ile devam edelim. Görev adı, başlangıç ve bitir tarihi değerlerini ve göreve kaynak eklemekten sorumlu bir de metodu içerecek sınıfımızı aşağıdaki şekilde projemize ekleyelim.

import {Resource} from "./resource";

export class Task {
    public resources: Resource[] = [];

    constructor(public taskName: string, public startDate: Date, public endDate: Date) {

    }

    addResource(resource: Resource) {
        this.resources.push(resource);
    }
}


Sıra geldi proje bilgilerini ve projede yer alacak görevleri barındıracak sınıfımıza. Proje adı ve projeye görev eklemekten sorumlu bir de metodu içerecek sınıfımızı aşağıdaki şekilde projemize ekleyelim.

import {Task} from "./task";

export class Project {
    public tasks: Task[] = [];

    constructor(public projectName: string) {
    }

    addTask(task: Task) {
        this.tasks.push(task);
    }
}


Geldik asıl işi yapacak sınıfa. Kendine geçilen projenin ve tüm alt nesnelerin derin klonlamasını (deep cloning) yapacak sınıfımızı aşağıdaki şekilde projemize ekleyelim.

import {Project} from "./project";
import {Task} from "./task";
import {Resource} from "./resource";

export class ProjectCloner {

    static cloneProject(project: Project): Project {

        let copyProject = new Project(project.projectName);

        for (let task of project.tasks) {

            let copyTask = new Task(task.taskName, task.startDate, task.endDate);

            for (let resource of task.resources) {
                copyTask.addResource(new Resource(resource.resourceName, resource.quantity));
            }

            copyProject.addTask(copyTask);
        }

        return project;
    }
}

Sınıfımız iç içe döngüler içerse de aslında yaptığı iş oldukça basit. Yüzeysel kopyalamadan (Shallow Copy) kaçınmak için en içten en dışa doğru, sadece referans tipi olmayan değişkenlerin değerlerini kullanarak yeni nesneler oluşturup birbirine bağlayarak oluşan yeni proje nesnesini geriye dönüyor.

Son olarak “app.ts” dosyamızda yeni bir proje oluşturup, bu sınıfı aşağıdaki şekilde kullanalım.

import {Project} from "./project";
import {Task} from "./task";
import {Resource} from "./resource";
import {ProjectCloner} from "./project-cloner";

// İlk projenin tanımlanması

let birthdateProject = new Project("Doğum Günü Etkinliği");

let placePreparationTask = new Task("Mekanın Hazırlanması", new Date(2023, 5, 7), new Date(2023, 5, 8));

placePreparationTask.addResource(new Resource("Masa", 100));
placePreparationTask.addResource(new Resource("Sandalye", 400));
placePreparationTask.addResource(new Resource("Müzik Sistemi", 1));

birthdateProject.addTask(placePreparationTask);

let mealPreparationTask = new Task("Yemeklerin Hazırlanması", new Date(2023, 5, 7), new Date(2023, 5, 8));

mealPreparationTask.addResource(new Resource("Ana Yemek", 400));
mealPreparationTask.addResource(new Resource("Doğum Günü Pastası", 1));

birthdateProject.addTask(mealPreparationTask);

console.log("Asıl Proje Nesnesi");
console.log("------------------------------------------------------------");
console.dir(birthdateProject, {depth: null});


// Projenin kopyalanması

let copyProject = ProjectCloner.cloneProject(birthdateProject);

console.log();
console.log("Kopya Proje Nesnesi");
console.log("------------------------------------------------------------");
console.dir(copyProject, {depth: null});

Projemizi oluşturup tek bir satırda projenin kopyasını oluşturduk, görev tamamlandı!

Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.


Peki Bu Çözümün Nesi Yanlış?

Elbette her bir nesne için Factory sınıfları olmadan doğrudan “app.ts” içerisinde nesneleri oluşturmamız, şimdiye kadar işlediğimiz tüm tasarım kalıplarına aykırı ama şimdilik bunu görmezden gelelim, çünkü makalemiz nesne klonlama ile alakalı.

Nesne klonlama ile alakalı tüm işlemleri “ProjectCloner” sınıfı içerisindeki “cloneProject” metodunda topladık. Buradaki “cloneProject” metodumuz, tüm nesnelerin hangi parametreleri alması gerektiğini, bu parametrelerin değer tipi mi yoksa referans tipi mi olduğunu, hangi nesnenin hangi alt nesneleri içermesi gerektiğini ve nesnelerin hangi sırayla oluşturulması gerektiğini bilmesi gerekiyor. Dolayısıyla nesnelerden hangisinde değişiklik yaparsak yapalım, “ProjectCloner” sınıfı da buna uygun şekilde güncellenmek zorunda.

Bu da bizim Single Responsibility, Separation of Concerns ve Law of Demeter tasarım prensiplerine uygun olmayan şekilde kodlama yaptığımıza işaret ediyor.


Nasıl Bir Çözüm Sunulabilirdi?

Tekrar hatırlatmak istiyorum, makalemizin konusu nesne klonlama ile alakalı olduğu için, normalde olması gereken Factory sınıflarını, konudan uzaklaşmamak ve örneği iyice karışık hale getirmemek için es geçiyoruz.

İdealde her nesne ile alakalı iş mantıkları, ilgili nesne içerisinde kalmalı. Bunun için her bir nesnenin kopyalanması işlemi, ilgili nesne içerisinde yer alan “clone” metodu ile yapılabilir.

Öncelikle kaynaklar ile alakalı sınıfımızı aşağıdaki şekilde değiştirelim. Sınıfımıza “clone” isminde bir metot ekliyoruz ve nesnenin yeni bir kopyasını dönmesini sağlıyoruz.

export class Resource {
    constructor(public resourceName: string, public quantity: number) {
    }

    clone(): Resource {
        return new Resource(this.resourceName, this. Quantity);
    }
}


Ardından görevler ile alakalı sınıfımızı aşağıdaki şekilde değiştirelim. Sınıfımıza benzer şekilde “clone” isminde bir metot ekliyoruz ve nesnenin yeni bir kopyasını dönmesini sağlıyoruz. Burada görev içerisindeki her bir kaynak nesnesinin kendi “clone” metodunu çağırdığımıza dikkat edelim. “Task” sınıfı, “Resource” sınıfında yer alan değerlerin hangisinin referans, hangisinin değer tipi olduğu ile ilgilenmiyor. Dolayısıyla ileride “Resource” sınıfında yapılacak değişiklikler yüzünden “Task” sınıfımızı değiştirmemiz gerekmeyecektir.

import {Resource} from "./resource";

export class Task {
    public resources: Resource[] = [];

    constructor(public taskName: string, public startDate: Date, public endDate: Date) {
    }

    addResource(resource: Resource) {
        this.resources.push(resource);
    }

    clone(): Task {
        let newTask = new Task(this.taskName, this.startDate, this.endDate);

        this.resources.forEach(resource => newTask.addResource(resource.clone()));

        return newTask;
    }
}


Sıra geldi projeler ile alakalı sınıfımıza. Bunu sınıfa da aynı şekilde “clone” isminde bir metot ekliyoruz ve nesnenin yeni bir kopyasını dönmesini sağlıyoruz. Burada da proje içerisindeki her bir görev nesnesinin kendi “clone” metodunu çağırdığımıza dikkat edelim. Aynı şekilde “Project” sınıfı, “Task” sınıfında yer alan değerlerin hangisinin referans, hangisinin değer tipi olduğuyla veya görev nesnesinin hangi alt nesneleri içerdiğiyle ilgilenmiyor. Dolayısıyla ileride “Task” veya “Resource” sınıfında yapılacak değişiklikler yüzünden “Project” sınıfımızı değiştirmemiz gerekmeyecektir.

import {Task} from "./task";

export class Project {
    public tasks: Task[] = [];

    constructor(public projectName: string) {
    }

    addTask(task: Task) {
        this.tasks.push(task);
    }

    clone(): Project {
        let newProject = new Project(this.projectName);

        this.tasks.forEach(task => newProject.addTask(task.clone()));

        return newProject;
    }
}


Var olan bir proje nesnesini kopyalamak için ilgili nesnenin “clone” metodunu çağırmamız yeterli olduğuna göre, artık “ProjectCloner” gibi bir sınıfa ihtiyacımız kalmamış gibi gözüküyor. Bu sınıfı projemizden kaldırıp “app.ts” dosyamızı aşağıdaki şekilde güncelleyebiliriz.

import {Project} from "./project";
import {Task} from "./task";
import {Resource} from "./resource";

// İlk projenin tanımlanması

let birthdateProject = new Project("Doğum Günü Etkinliği");

let placePreparationTask = new Task("Mekanın Hazırlanması", new Date(2023, 5, 7), new Date(2023, 5, 8));

placePreparationTask.addResource(new Resource("Masa", 100));
placePreparationTask.addResource(new Resource("Sandalye", 400));
placePreparationTask.addResource(new Resource("Müzik Sistemi", 1));

birthdateProject.addTask(placePreparationTask);

let mealPreparationTask = new Task("Yemeklerin Hazırlanması", new Date(2023, 5, 7), new Date(2023, 5, 8));

mealPreparationTask.addResource(new Resource("Ana Yemek", 400));
mealPreparationTask.addResource(new Resource("Doğum Günü Pastası", 1));

birthdateProject.addTask(mealPreparationTask);

console.log("Asıl Proje Nesnesi");
console.log("------------------------------------------------------------");
console.dir(birthdateProject, {depth: null});


// Projenin kopyalanması

let copyProject = birthdateProject.clone();

console.log();
console.log("Kopya Proje Nesnesi");
console.log("------------------------------------------------------------");
console.dir(copyProject, {depth: null});


Prototype tasarım kalıbı sayesinde her nesne kendi klonunu üretmekten sorumlu oldu ve her bir nesne sadece kendi altındaki nesnenin klonlama metodunu çağırarak görevi yerine getirdi. Üç basit nesne yerine daha fazla sayıda ve kompleks nesnelerimiz olsaydı, aradaki fark çok daha belirgin şekilde karşımıza çıkacaktı.

Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.




Böylece hem Prototype tasarım kalıbı konusunun, hem de klasik tasarım kalıplarındaki nesne oluşturma süreci ile ilgilenen kalıpların (Creational Design Patterns) sonuna gelmiş olduk. Bir sonraki makalemizde, sistemdeki bileşenlerin nasıl bir araya geleceği ve birlikte nasıl çalışacağı konusuyla ilgilenen kalıplardan (Structural Design Patterns) ilki olan Adapter tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

İLGİNİZİ ÇEKEBİLİR

Bir Yorum Yaz