Bu yazıda; Encapsulation, Abstraction, Inheritance ve Polymorphism gibi Nesne Yönelimli Programlama (OOP) kavramları temel alınarak geliştirilmiş bir araç kiralama sistemi örneğini ele alacağız.
Uygulama, gerçek hayat senaryolarını temel alan sınıf yapılarıyla tasarlanmış; veri yönetimi ise bellek (cache) üzerinde değil, harici .json dosyaları üzerinden sağlanmıştır.

Proje Mimari Yapısı

Uygulamayı geliştirirken, sorumlulukların net bir şekilde ayrıldığı ve OOP prensiplerini destekleyen katmanlı bir yapı hedefledik. Bu doğrultuda proje, aşağıdaki klasör yapısı üzerine inşa edilmiştir:

Akışta projenin kodları paylaşılmıştır. Yukarıdaki mimariyi sisteminizde oluşturarak projeyi çalıştırabiliriz. Python dilinde sürümden kaynaklı syntax hataları yaşanabilmektedir. Bu proje 3.10.0 sürümü ile geliştirilmiştir.


models/ Domain (iş) Nesneleri

models klasörü, uygulamanın temel iş nesnelerini barındırır. Nesne yönelimli programlama mimarisinin oluşturulduğu katman diyebiliriz.

  • vehicle.py
    Araçlara ait ortak özellik ve davranışların tanımlandığı temel sınıftır.
    Bu sınıf üzerinden abstraction sağlanmış ve alt sınıflar için ortak bir yapı oluşturulmuştur. abstraction bu noktada encapsulation yapısını zorunlu hale getirmiş oluyor. Dolayısıyla doğrudan bu class üzerinden nesne yaratamayız.
  • car.py, suv.py, truck.py
    Vehicle sınıfından türetilen bu alt sınıflar, farklı araç tiplerini temsil eder. Bu yapı sayesinde inheritance ve polymorphism kavramları pratikte uygulanmıştır. Yeni bir araç tipi eklenmek istediğinde vehicle sınıfından kalıtım alınarak yaratılabilir.
  • customer.py
    Sistemde araç kiralayan müşterilere ait bilgileri ve davranışları temsil eder.

Bu katmanda yer alan sınıflar yalnızca veri ve davranıştan sorumludur; veri okuma/yazma veya iş akışı yönetimi gibi sorumluluklar bu katmana dahil edilmemiştir. Böylece encapsulation korunmuştur.


managers/ İş Kuralları ve Kontrol Katmanı

managers klasörü, uygulamanın iş mantığını yöneten sınıfları içerir.

  • data_manager.py
    JSON dosyaları üzerinden veri okuma ve yazma işlemlerini merkezi bir noktadan yönetir.
    Bu sayede veri erişim mantığı tek bir yerde toplanmış ve tekrar eden kodların önüne geçilmiştir.
  • rental_manager.py
    Araç kiralama, teslim alma ve kiralama kayıtlarının yönetildiği iş kurallarını içerir.
    Model sınıflarını kullanarak sistemin akışını kontrol eder.

Bu ayrım sayesinde Single Responsibility Principle (SRP) uygulanmış olur.

SRP: Bir sınıf tek bir işi yapmalıdır. Bu prensip sayesinde kod daha okunabilir, bakımı kolaylaşır, değişiklikler diğer alanları etkilemez, test etmek kolaydır.


data/ Veri Kaynağı

Uygulamada kullanılan tüm veriler, bellek üzerinde tutulmak yerine data klasörü altındaki JSON dosyaları üzerinden yönetilmektedir.

  • vehicles.json → Araç bilgileri
  • customers.json → Müşteri bilgileri
  • rentals.json → Kiralama kayıtları

Bu yaklaşım, uygulamanın stateless çalışmasını sağlar ve veri katmanını uygulama mantığından ayırır.


app.py Uygulamanın Giriş Noktası

app.py, uygulamanın çalıştırıldığı ana dosyadır. Manager sınıfları üzerinden sistemin akışı burada başlatılır ve kontrol edilir.

Models Katmanı


Bu noktadan itibaren uygulamanın model katmanını oluşturmaya başlıyoruz.
models klasörü, sistemin temelini oluşturan iş nesnelerini barındırdığı için projenin en kritik parçalarından biridir.

İlk adımda; araç, müşteri ve araç tiplerine ait sınıfları tanımlayarak nesne yönelimli yapının temelini atacağız. Bu süreçte abstraction, encapsulation ve inheritance gibi OOP kavramlarını pratik örnekler üzerinden ele alacağız. Hiyerarşik yapıysa aşağıdaki görselde yer verilmiştir.

vehicle.py dosyasını kodlayalım:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, brand, model, year, daily_price):
        self.brand = brand
        self.model = model
        self.year = year

        self._daily_price = daily_price
        self._is_available = True

        self._validate_price()

    def _validate_price(self):
        if self._daily_price <= 0:
            raise ValueError("Günlük fiyat pozitif olmalıdır.")

    def get_daily_price(self):
        return self._daily_price

    def set_daily_price(self, new_price):
        if new_price <= 0:
            raise ValueError("Günlük fiyat pozitif olmalıdır.")
        self._daily_price = new_price

    def mark_as_rented(self):
        self._is_available = False

    def mark_as_returned(self):
        self._is_available = True

    def is_available(self):
        return self._is_available

    @abstractmethod
    def calculate_price(self, days: int):
        pass

    def to_dict(self):
        return {
            "type": self.__class__.__name__,
            "brand": self.brand,
            "model": self.model,
            "year": self.year,
            "daily_price": self._daily_price,
            "is_available": self._is_available
        }

Vehicle sınıfı, sistemdeki tüm araç tipleri için ortak bir temel (abstraction) oluşturmak amacıyla tasarlanmıştır. Bu sınıf, Python’un ABC yapısı kullanılarak soyut sınıf olarak tanımlanmış ve alt sınıfların belirli davranışları zorunlu olarak implemente etmesi sağlanmıştır.

Sınıf içerisinde araçlara ait marka, model ve yıl gibi ortak özellikler tanımlanırken; günlük ücret ve müsaitlik durumu protected (_) değişkenler ile encapsulation prensibine uygun şekilde saklanmıştır. Bu sayede ilgili alanlara yalnızca sınıfın kendisi ve alt sınıflar üzerinden erişim sağlanmaktadır.

Fiyat bilgisinin her zaman geçerli olmasını sağlamak adına doğrulama (validation) mekanizması kurulmuş, getter ve setter metodları ile kontrollü erişim uygulanmıştır. Ayrıca aracın kiralanma ve teslim edilme durumları için state management fonksiyonları eklenmiştir. getter ve setter metotları kullanımı anlayabilmek adına vehicle.py ve customer.py dosyasında farklı mantıkta kullanılmıştır. Python dilinde overloading mantığı yoktur. Bunun için @property kavramı kullanılmaktadır (customer.py dosyası incelenebilir).

calculate_price metodu ise soyut (abstract) olarak tanımlanarak, her araç tipinin kendi fiyatlandırma mantığını uygulaması zorunlu hale getirilmiştir. Bu yapı büyük projelerde sıkça kullanıldığı için mantığını iyi anlamak önemlidir. Son olarak to_dict metodu ile araç nesnelerinin JSON formatına dönüştürülmesi sağlanarak veri katmanıyla uyumlu bir yapı oluşturulmuştur.

car.py dosyasını kodlayalım:

from .vehicle import Vehicle

class Car(Vehicle):
    def calculate_price(self, days: int):
        return self._daily_price * days

suv.py dosyasını kodlayalım:

from .vehicle import Vehicle

class SUV(Vehicle):
    def calculate_price(self, days: int):
        return self._daily_price * days * 1.3

truck.py dosyasını kodlayalım:

from .vehicle import Vehicle

class Truck(Vehicle):
    # calculate_price fonksiyonu abstract bir fonksiyon olduğu için yazmak zorundayız.
    def calculate_price(self, days: int):
        return (self._daily_price * days)+500

Car, SUV ve Truck sınıfları, Vehicle soyut sınıfından türetilmiş olup sistemdeki farklı araç tiplerini temsil eder. Bu sınıfların temel amacı, ortak özellikleri tekrar etmeden kalıtım (inheritance) yoluyla kullanmak ve araç tipine özgü davranışları tanımlamaktır.

Her üç sınıfta da calculate_price metodu implemente edilmiştir. Bu metodun kullanılma nedeni, her araç tipinin fiyatlandırma mantığının farklı olabilmesidir. Böylece aynı arayüz korunurken, araç tipine göre farklı hesaplama kuralları uygulanabilmektedir. Bu yaklaşım, polymorphism kavramının pratik bir karşılığıdır.

Alt sınıflar yalnızca kendi sorumluluk alanlarına odaklanır; temel alanlar, durum yönetimi ve doğrulama işlemleri Vehicle sınıfı üzerinden devralınır. Bu sayede kod tekrarından kaçınılır, sistem daha okunabilir ve genişletilebilir hale gelir.

Yeni bir araç tipi eklenmek istendiğinde, mevcut yapıyı bozmadan yalnızca Vehicle sınıfından türeyen yeni bir sınıf oluşturmak yeterlidir. Bu mantık uygulamanın yönetilebilir ve sürdürülebilir olmasını sağlar.

customer.py dosyasını kodlayalım:

# uuid kütüphanesi benzersiz numaralar üretmek için kullanılır.
import uuid

class Customer:
    def __init__(self, name: str, email: str, phone: str):
        self._customer_id = str(uuid.uuid4())   # otomatik benzersiz ID
        self.name = name
        self.email = email
        self.phone = phone

    # ---- Encapsulation ------
    @property
    def customer_id(self):
        return self._customer_id

    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if len(value) < 2:
            raise ValueError("İsim en az 2 karakter olmalıdır.")
        self._name = value

    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        if "@" not in value:
            raise ValueError("Geçersiz e-mail formatı.")
        self._email = value

    @property
    def phone(self):
        return self._phone
    
    @phone.setter
    def phone(self, value):
        if len(value) < 10:
            raise ValueError("Telefon numarası en az 10 haneli olmalıdır.")
        self._phone = value

    def to_dict(self):
        return {
            "customer_id": self.customer_id,
            "name": self.name,
            "email": self.email,
            "phone": self.phone
        }

    def __str__(self):
        return f"{self.name} ({self.email})"

Customer sınıfı, araç kiralama sisteminde müşteri bilgilerini temsil eden modeldir. Her müşteri için sistem içerisinde benzersiz bir kimlik oluşturmak amacıyla uuid kütüphanesi kullanılmış ve müşteri ID’si otomatik olarak üretilmiştir. Bu yaklaşım, veri bütünlüğünü korumak açısından kritik öneme sahiptir.

Sınıf içerisinde yer alan name, email ve phone alanları encapsulation prensibine uygun olarak @property yapısı ile yönetilmektedir. Bu sayede alanlara doğrudan erişim yerine, kontrollü getter ve setter metodları üzerinden erişim sağlanmıştır.

Property setter’lar içerisinde yapılan doğrulamalar sayesinde; isim uzunluğu, e-posta formatı ve telefon numarası gibi alanlar daha nesne oluşturulurken kontrol altına alınır. Böylece hatalı verilerin sistem içerisine girmesi engellenir ve iş kuralları model katmanında merkezi olarak yönetilmiş olur.

to_dict metodu, müşteri nesnesinin JSON formatına dönüştürülmesini sağlarken; __str__ metodu ise müşteri bilgisinin okunabilir bir metin olarak temsil edilmesine imkan tanır.

Managers Katmanı


Managers katmanı, uygulamanın iş kurallarını ve operasyonel akışını yöneten yapıyı temsil eder. Model katmanında tanımlanan nesneler, bu katman aracılığıyla kullanılarak sistemin nasıl çalışacağı belirlenir. Bu katman iki temel kırılımdan oluşur:

  • DataManager: Uygulamada kullanılan verilerin harici JSON dosyaları üzerinden okunması ve yazılması süreçlerinden sorumludur.
  • RentalManager: Araç kiralama, teslim alma ve kiralama süreçlerine ait iş akışlarını yöneten katmandır.

DataManager.py dosyasını kodlayalım:

import json
import os

class DataManager:
    def __init__(self, base_path="data"):
        self.base_path = base_path 

        self.vehicles_file = os.path.join(base_path, "vehicles.json")
        self.customers_file = os.path.join(base_path, "customers.json")
        self.rentals_file = os.path.join(base_path, "rentals.json")

        self._ensure_files()

    def _ensure_files(self):
        os.makedirs(self.base_path, exist_ok=True)

        for file in [self.vehicles_file, self.customers_file, self.rentals_file]:
            if not os.path.exists(file):
                with open(file, "w", encoding="utf-8") as f:
                    json.dump([], f)

    def _read_json(self, file_path):
        with open(file_path, "r", encoding="utf-8") as f:
            return json.load(f)

    def _write_json(self, file_path, data):
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=4)

    def load_vehicles(self):
        return self._read_json(self.vehicles_file)

    def save_vehicles(self, vehicles):
        self._write_json(self.vehicles_file, vehicles)

    def load_customers(self):
        return self._read_json(self.customers_file)

    def save_customers(self, customers):
        self._write_json(self.customers_file, customers)

    def load_rentals(self):
        return self._read_json(self.rentals_file)

    def save_rentals(self, rentals):
        self._write_json(self.rentals_file, rentals)

DataManager, uygulamada kullanılan tüm verilerin harici JSON dosyaları üzerinden okunması ve yazılmasından sorumludur. Bu sınıf sayesinde veri erişim mantığı tek bir noktada toplanmış ve model ile iş kuralları katmanı dosya sistemi detaylarından bağımsız hale getirilmiştir.

Sınıf içerisinde; araçlar, müşteriler ve kiralama kayıtları için ayrı JSON dosyaları tanımlanmış, uygulama başlatılırken bu dosyaların varlığı kontrol edilerek gerekirse otomatik olarak oluşturulması sağlanmıştır. Dosya yolları oluşturulurken os.path.join kullanılarak işletim sistemine bağımlı yol problemlerinin önüne geçilmiştir.

Veri okuma ve yazma işlemleri ortak yardımcı fonksiyonlar üzerinden yönetilerek kod tekrarından kaçınılmış, yükleme ve kaydetme işlemleri ise daha okunabilir metodlar aracılığıyla dış dünyaya sunulmuştur. Uygulamamız çalıştığında __init__ fonksiyonu içerisinde yer alan _ensure_file() fonksiyonu çalışacağı için vehicles.json, customer.json ve vehicles.json dosyaları otomatik oluşturulacaktır.

RentalManager.py dosyasını kodlayalım:

RentalManager yapısı projenin beyni konumundadır. app.py üzerinden gelen isteklere gerekli çözümler üretmesi beklenir. Dolayısıyla bu noktada app.py dosyasında oluşturulan her yetenek/işlem seçeneği için RentalManager yapısında bir fonksiyon tanımlıyor. Bu noktada uygulamanın yeteneklerine değinmekte fayda var:

1) Araç ekle

2) Araçları listele

3) Müşteri ekle

4) Müşterileri listele

5) Araç kirala

6) Araç teslim al

7) Kiralama kayıtlarını listele

Her bir işlem için RentalManager.py dosyasında fonksiyon yaratacağımıza değindik. Burada işlemleri 3 aşamada inceleyebiliriz.

1.) Araç İşlemleri

RentalManager, uygulamadaki araç ve kiralama süreçlerini yöneten iş kuralı katmanıdır. Bu sınıf, veri okuma veya yazma işlemlerini doğrudan gerçekleştirmez; bu sorumluluğu dışarıdan enjekte edilen DataManager nesnesine bırakır. Böylece sorumlulukların ayrıştırılması sağlanmış olur.

Araç ekleme sürecinde, sistemde kayıtlı araçlar önce JSON dosyasından alınır ve kullanıcıdan gelen araç tipine göre ilgili sınıf (Car, SUV, Truck) üzerinden yeni bir nesne oluşturulur. Bu yaklaşım, farklı araç tiplerinin aynı arayüz üzerinden yönetilmesini sağlayarak polymorphism kavramını pratikte uygulanabilir hale getirir.

Oluşturulan araç nesnesi, JSON formatıyla uyumlu hale getirilmek için to_dict metodu aracılığıyla dönüştürülür ve mevcut araç listesine eklenir. Güncellenen liste, tekrar veri kaynağına yazılarak kalıcı hale getirilir.

list_vehicles metodu ise sistemde kayıtlı tüm araçları tek bir noktadan listeleyerek, araç verilerine erişimi sade ve kontrollü bir şekilde sunar.

2.) Müşteri İşlemleri

RentalManager içerisinde yer alan müşteri işlemleri, sistemdeki müşteri kayıtlarının yönetilmesini sağlar. Yeni bir müşteri eklenirken, mevcut müşteri listesi JSON dosyasından alınır ve dışarıdan gelen bilgiler kullanılarak Customer modeli üzerinden yeni bir nesne oluşturulur.

Oluşturulan müşteri nesnesi, to_dict metodu ile JSON formatına dönüştürülerek veri listesine eklenir ve güncel liste tekrar veri kaynağına yazılır. Bu sayede müşteri bilgileri kalıcı hale getirilmiş olur.

list_customers metodu ise sistemde kayıtlı tüm müşterilerin tek bir noktadan listelenmesini sağlayarak, müşteri verilerine erişimi sade ve kontrollü bir şekilde sunar.

3.) Kiralama İşlemleri

RentalManager içerisindeki kiralama işlemleri, sistemin en kritik iş akışını temsil eder. rent_vehicle metodu; müşteri, araç ve kiralama kayıtlarını birlikte ele alarak kontrollü bir kiralama süreci yürütür.

Kiralama adımında sırasıyla; müşteri bilgisi doğrulanır, seçilen aracın geçerliliği kontrol edilir ve aracın müsaitlik durumu incelenir. Ardından araç tipine göre ilgili sınıf üzerinden nesne oluşturularak fiyat hesaplama işlemi polymorphism sayesinde gerçekleştirilir. Bu yaklaşım, araç tipine özel fiyatlandırma mantıklarının tek bir arayüz üzerinden çalışmasını sağlar.

Kiralama tamamlandığında aracın durumu güncellenir ve işlemle ilgili bilgiler kiralama kayıtlarına eklenerek kalıcı hale getirilir. Hatalı senaryolarda ise ValueError kullanılarak süreç güvenli bir şekilde sonlandırılır.

return_vehicle metodu ise teslim sürecini yönetir. İlgili aracın durumu tekrar müsait olarak işaretlenir ve güncel bilgiler veri kaynağına yazılır. Bu sayede kiralama ve teslim işlemleri tutarlı ve izlenebilir bir yapı içerisinde yönetilmiş olur.

app.py Yapısı


Projenin tetiklendiği ve başlatıldığı arayüzdür. Bu proje bu noktaya kadar bir arayüze/frontende bağlanabilir. Fakat biz sadece .py formatında ilerlediğimiz için console ekranı üzerinde seçim yaptırıyoruz.

app.py, araç kiralama sisteminin çalıştırıldığı ana dosyadır. Bu dosya, kullanıcı ile sistem arasındaki etkileşimi yönetir ve tüm işlemleri RentalManager ve DataManager üzerinden koordine eder.

Uygulama, basit bir menü tabanlı arayüz sunarak; araç ekleme, listeleme, müşteri yönetimi, kiralama ve teslim alma gibi işlemleri kullanıcıdan alınan seçimlere göre gerçekleştirir. İş kuralları doğrudan bu dosya içerisinde yazılmak yerine, ilgili manager sınıflarına yönlendirilerek katmanlı mimari korunmuştur.

Kullanıcıdan alınan girdiler try-except bloğu ile kontrol altına alınarak, hatalı senaryolarda uygulamanın beklenmedik şekilde sonlanması engellenmiştir. Böylece sistem, hem okunabilir hem de sürdürülebilir bir akış yapısına kavuşmuştur.

Uygulamanın Sonuçları ve Değerlendirme


Bu uygulama kapsamında, temel araç kiralama süreçlerini yöneten console tabanlı bir uygulama geliştirilmiştir. Müşteri, araç ve kiralama işlemleri ayrıştırılmış yapılar üzerinden ele alınmış; sade ve anlaşılır bir akış ile sistemin işleyişi başarıyla modellenmiştir. Uygulama, daha gelişmiş arayüz ve sistem entegrasyonları için sağlam bir temel oluşturmaktadır.
app.py dosyasını çalıştıralım.

Seçim Ekranı, kullanıcı ile sistem arasındaki etkileşimin sağlandığı ana menüdür. Console üzerinden sunulan bu menü aracılığıyla araç, müşteri ve kiralama işlemleri numaralandırılmış seçenekler üzerinden yönetilmektedir. Kullanıcı, ilgili işlemi seçerek uygulama akışını yönlendirebilir.


Seçim: 1 senaryosu (Araç Ekleme)

Seçenek 1 ile ilerlendiğinde uygulama bizden araç tipi, marka, model, yıl ve günlük fiyat gibi bilgileri istiyorum. Bilgileri girdiğimizde vehicles.json dosyasını kontrol edebiliriz. Aktardığımız bilgiler buradan json formatında görüntülenmelidir.

Seçim: 2 senaryosu (Araç Listeleme)

Bu seçenek yürütüldüğünde sistemde kayıtlı tüm araçlar listelenerek müsait durumu yansıtılmaktadır.

Seçim: 3 senaryosu (Müşteri Ekleme)

Müşteriye ait isim soyisim, e-mail ve telefon bilgileri istenir. Süreç tamamlandığında customers.json dosyası kontrol edildiğinde bilgilerin benzersiz id ile kaydedildiği görüntülenmelidir.

Seçim: 4 senaryosu (Müşteri Listeleme)

Sistem tarafından belirlenen benzersiz ID ile kullanıcı bilgilerini listeleyebiliriz.

Seçim: 5 senaryosu (Araç Kiralama)

Bu aşama önemlidir. Müşteri ile araç ilişkisi kurularak araç müsaitlik durumu güncellenir. Yapılan kiralama süreçleri rental.json dosyasına eklenir.

Seçim: 6 senaryosu (Araç Teslim Al)

Araç teslim alınması durumunda tercih edilecek seçenektir. Seçenek yürütüldüğünde aracın durumu müsait olarak güncellenir.

Seçim: 7 senaryosu (Kiralama Kayıtlarını Listele)

Geçmiş kiralama kayıtları bu seçenek üzerinden listelenir.

Mevcut mimari ile console ekranında yapılacaklar özetle yukarıdaki gibidir. Proje geliştirilebilir veya bir arayüze entegre edilebilir.


Daha fazla bilgi ve destek almak için mail ya da formu kullanarak iletişime geçebilirsiniz.

,

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir