EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Nesne Yönelimli Programlamaya Giriş
Nesne yönelimli programlama (Object Oriented Programming) bir bilgisayar programlama yaklaşımıdır. Bu yaklaşım, günümüzde çok sayıda güncel programlama dili tarafından desteklenir. Nesne Yönelimli Programlama yaklaşımı 1960'lı yılların sonuna doğru ortaya çıkmıştır. O dönemde yazılım dünyasında karşılaşılan sorunlara çözüm olması amacıyla geliştirilmiştir. Yazılımların kapsamı, içeriği ve karmaşıklığı ile birlikte boyutları da sürekli artış gösteriyordu. Bu artış ile birlikte, yazılan kodu hızlı bir şekilde gelişime açık ve esnek tutmak için gereken bakım maliyeti, zaman ve çaba çok daha hızlı bir artış sergiliyordu. Nesne Yönelimli Programlama, bu soruna bir çözüm olarak gelişti. Çözümdeki başlıca özellik, yazılımda birimselliğin (modularity) benimsenmiş olmasıdır. Nesne Yönelimli Programlama ayrıca, kapsülleme (encapsulation), kalıtım (inheritance) ve çok biçimlilik (polymorphism) gibi yazılımın bakımını ve aynı yazılım projesi üzerinde ekip çalışmasını kolaylaştıran kavramları da yazılım dünyasına kazandırmıştır. Sağladığı bu avantajlardan dolayı, Nesne Yönelimli Programlama, günümüzde geniş çaplı yazılım projelerinde yaygın bir biçimde tercih edilip kullanılmaktadır. Nesne Yönelimli Programlama'da her bilgisayar programının, etkileşim içerisinde olan birimler veya nesneler kümesinden oluştuğu varsayılır. Bu nesnelerin her biri, kendi içerisinde veri işleyip diğer nesneler ile çift yönlü veri alışverişinde bulunabilir. Nesne Yönelimli Programlama öncesinde -nesne tabanlı prosedürel programlama zamanında- programlar, sadece bir komut dizisi veya birer fonksiyon kümesi olarak görülmekteydi. Nesne tabanlı programlama dilleri, nesne kullanımını desteklemelerine rağmen, kalıtım gibi nesne yönelimli programlama dillerine özgü özellikleri taşımazlar. Nesne yönelimli programlama dillerine örnek olarak ABAP/4, Simula, Smalltalk, C++, Object Pascal, Objective-C, Eiffel, Python, Java, C Sharp, Visual Basic.NET ve REALbasic'i sayabiliriz. Nesne tabanlı olup da nesne yönelimli olmayan programlama dili olarak da Visual Basic iyi bir örnek teşkil etmektedir. Visual Basic nesne ve sınıfları desteklese de, kalıtımdan yoksundur.
2 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Nesne Yönelimli Dillerin Tarihçesi
3 / 59
Yeni bir programlama dili öğrenilirken genellikle dilin söz dizimi, nasıl değişken tanımlanacağı, çalışma akışının nasıl kontrol edileceği vb. özelliklerine fazlasıyla vakit ayırılır. Bunun yanında kaliteli kod yazmak için dilin perde arkasında kullandığı metodoloji ve prensipleri de kavramaya ihtiyaç vardır. C#, tamamen nesne yönelimli bir dildir. Bu yüzden optimum dizayn edilmiş uygulamalar oluşturmak için, dilin nesne yönelimli özelliklerinin iyi kavranması şarttır. Eğer nesne yönelimli uygulama geliştirme modeline yeniysen, prosedürel diller üzerinde tecrüben olsa dahi, eski bilgiler burada sana uygun bir altyapı sağlamayacaktır. Nesne yönelimli programlama, problem çözümlerinin dizayn edilip programlanması hakkında yeni bir düşünce yoludur. Ayrıca eğer programlama dünyasında henüz yeniysen, Basic, Cobol, C vb. prosedürel diller yerine nesne yönelimli bir dil ile başlamanın öğrenme sürecini hızlandıracağı, yapılan araştırmalar sonucu ortaya çıkan bir gerçektir. Bu kişiler, OOP öğrenme sürecini güçleştirecek prosedürel dil alışkanlıklarından kurtulmak zorunda kalmazlar. Programlama dünyasına temiz bir yolda çıkıp hızlıca ilerlerler. Prosedürel dil geçmişi olup, nesne yönelimli programlamaya C# ile geçiş yapan bir kişi için verilebilecek en güzel tavsiye; öğrenme sürecinde açık fikirli olup kendisini bu yeni dünyaya adapte etmeye çalışması ve eski alışkanlıkları ile karşılaştırma yapmamasıdır. Nesne yönelimli bir dil ile programlama sürecinin sayısız faydası vardır. Daha efektif kod yazmak ve kolayca modifiye edilebilecek bir sisteme sahip olmak bu faydalardan sadece bir kaçıdır. Bu mimariyi anlamak ve kullanmak ilk başlarda kolay olmayabilir, hatta uzun süre alabilir; ancak bu sürenin sonunda, nesne yönelimli programlamanın verdiği sözleri yerine getirdiği (doğru uygulandığında) görülecektir. Programlamaya Prosedürel Yaklaşım Problemlerin çözülmesi için programlar yazılır, uygulamalar geliştirilir. Programlamaya prosedürel yaklaşımda bir problem küçük parçalara bölünür ve her bir problem ayrı ayrı çözülür. Daha sonra bütün problemi çözmek için bu küçük çözümler birleştirilir. Her bir küçük problem, bir görev olarak ele alınır. Bu görevi gerçekleştirmek için prosedür adı verilen kod blokları yazılır. Bir prosedür başka bir prosedürden erişilebilir. Prosedürler birleştirilip bir program oluşturulabilir. Daha sonra bütün programlar birbirlerine entegre edilip yazılım uygulaması ortaya çıkar. Tipik bir prosedürel program bir satır kod ile başlar. Ardından kontrol deyimleri, operatörler, iş mantığının uygulanması ile ilerlenir ve son satıra gelindiğinde program sonlanır. Fortran, Pascal ve C, prosedürel yaklaşımı esas alan programlama dillerinden bazılarıdır. Aşağıda, bir çalışanın mesleği tabanlı prim hesaplamasını yapan sözde kod (pseudocode) yer almaktadır. Begin character cAdi character cMeslegi numeric nPrim Procedure PrimHesapla begin if cMeslegi = “Mudur” begin nPrim = 1000 end if cMeslegi = “Analist” begin nPrim = 500 end display nPrim end accept cAdi accept cMeslegi Call PrimHesapla 4 / 59
End Burada veriler çalışanın adı, mesleği ve prim miktarlarıdır. PrimHesapla prosedürü, çalışanın primini hesaplayan iş mantığını içerir, program satır satır çalışır ve prim miktarı hesaplanır.
Prosedürel Programlama Yaklaşımında Sınırlamalar Prosedürel programlama yaklaşımında prosedürler, uygulama içerisinde bir defa yazılıp istenilen yerlerde çağrılırlar. Ancak bir uygulamaya ait prosedürler, başka bir uygulama tarafından kullanılamazlar. Çünkü kod parçaları birbirlerine bağlıdır ve belirli bir seriyi takip ederler. Örneğin PrimHesapla() prosedürü başka bir uygulamada kullanılmak istenirse, ikinci uygulamada yeniden yazılması gerekmektedir. Prosedürel yaklaşım kullanılarak geliştirilen büyük bir uygulama kolayca modifiye edilemez. Yazılan bütün kodlar birbirlerine sıkı sıkı bağlıdır ve bir yerde yapılan değişikliğin, uygulamanın bir çok yerine etkilerini ele almak gerekir. Oysa bir yazılım uygulamasının dinamik ihtiyaçlara cevap verebilmesi, değişiklikleri sessizce yansıtabilmesi gerekmektedir. Prosedürel yaklaşımda bu tarz değişiklikler yüklü miktarda yeniden kod yazmayı gerektirir. Bu durum ise uygulamanın bakım maliyetini (hem zaman, hem ekonomik olarak) arttırır. Programlamaya, prosedürel yaklaşımın getirdiği yukarıda bahsedilen sınırlamalardan dolayı, yazılım geliştirme mühendisleri daha iyi bir yol aramaya başlamışlardır.
Nesne Yönelimli Dillerin Evrimi Hemen hemen bütün nesne tabanlı ve nesne yönelimli dillerin atası, 1960’larda geliştirilen Simula 5 dilidir. Simula, ALGOL fikri üzerine inşa edilmiş bir dildir; ancak üzerine kapsülleme (encapsulation) ve kalıtım (inheritance) konseptlerini eklemişir. Belki daha da önemlisi, karmaşık sistemleri tanımlama ve simülasyon geliştirme dili olarak kullanılan Simula’nın, programlama dünyasını spesifik problem alanına özel kod yazma disiplini ile tanıştırmasıdır (Nesne taslaklarını hazırlayan kod kümelerinin yazılıp daha sonra kullanılması). Ardından Smalltalk dili ortaya çıkmıştır. Simula, Smalltalk dilinin birincil esin kaynağıdır. Bu fikir sonraları farklı geliştirmelerle desteklenmiştir. Smalltalk hem bir dili, hem de bir uyguma geliştirme ortamını temsil eder. Saf nesne yönelimli bir dil olan Smalltalk dilinde her şey bir nesne olarak ele alınır; integer bile bir sınıftır. Smalltalk, günümüzde yaygın olmasa da kendisini takip eden bütün nesne yönelimli diller onun konseptinden etkilenmiştir. Bu yüzden önemli bir kilometre taşıdır.
Orijinal adı C with Classes olan C++ dili, Bjarne Stroustrup adlı bir akademisyen tarafından, doktora tezinde uygulama geliştirirken karşılaştığı sorunlardan yola çıkarak 1979’da geliştirilmeye başlanmış, 1985’de ilk versiyonu yayınlanmıştır. Bu yeni dilin geliştirilme aşamasında başta C ve Simula olmak üzere ALGOL 68, Ada gibi dillerden esinlenilmiştir. Genellikle C dilinin daha gelişmiş hali olarak nitelendirilir (Tip kontrolü, aşırı yüklenmiş fonksiyonlar, statik fonksiyonlar, abstract sınıf, protected üyeler gibi dil yenilikleri gelmiştir). Ancak en önemlisi C++, C diline nesne yönelimli programlama özelliğini eklemiştir. C++ dili, donanım seviyesinde uygulama geliştirirken (işletim sistemi, sürücü vb.) alt seviye bir dil olması sebebiyle halen tercih edilmektedir. Doksanlı yılların hemen başında Sun Microsystems çalışanlarından James Gosling ve küçük grubu eğlence platformları, mikrodalga fırınlar vb. dijital olarak kontrol edilen tüketici aletleri için ileri seviye uygulamalar geliştiriyorlardı. Bu programları C++ ile yazmak zor olduğu için 1991 yılında OAK adı verilen bir dil oluşturdular. Geliştirme aşamasında OAK, genel amaçlı bir programlama dili haline geldi ve 1995 yılında adı değiştirilerek Java 1.0 sürümü ile duyuruldu. “Write Once, Run Anywhere” (Bir kez yaz, her yerde çalıştır ) yani platform bağımsız uygulama geliştirmek hedefinde olan java dilinin esas hedefi, yaygınlaşan internet erişimine ayak uydurarak web uygulamalarının kolayca geliştirileceği bir platform haline gelmekti. Java, günümüzde halen yaygın olarak kullanılmaktadır. Bir programlama dilinden öte Smalltalk gibi bir uygulama geliştirme ortamı ve çalışma zamanıdır. Java teknolojisi ile uygulama geliştirmek ve çalıştırmak için Java Virtual Machine kullanılması
5 / 59
gereklidir ve derlenen kaynak kodlar, platform bağımsızlık hedefi için Java Byte Code adında ortak bir ara dile dönüştürülmektedir. Ayrıca Java platformu, zengin ve genişletilebilen bir kod kütüphanesi içermektedir. (5) ALGOL (ALGOrithmic Language), 1950’lerin ortalarında Fortran dilindeki problemleri gidermek için dizayn edilen bir dildir. Bunun yanında Algol, Pascal gibi bazı dillerin ortaya çıkmasında rol oynamıştır. ALGOL köşeli parantezli ifade bloklarını kullanır. Ayrıca onları sınırlandırmak için BEGIN END bloklarını kullanan ilk programlama dilidir.
6 / 59
EĞİTİM : ADO.NET
Bölüm : Nesne Yönelimli Programlamaya Giriş
Konu : Veri ve Veriye Erişim Teknolojileri
7 / 59
Veri ve Veriye Erişim Teknolojileri Birçok uygulama bazı bilgileri geçici olarak tutup, daha sonra o bilgileri kullanarak işlemler yapar. Ancak bu bilgiler sadece uygulama çalıştığı sürece erişilebilir durumdadır. Çünkü; bellek, uygulama çalışırken bilgileri geçici olarak tutmak için kullanılır. Bu şekilde çalışan uygulamalara hesap makinası örnek olarak verilebilir. Yani kullanıcıdan birkaç veri alıp bunları hemen işleyen ve bu bilgilerin daha sonra kullanılması gerekmediği için geçici olarak tutulmasında sakınca olmayan uygulamalardır. Ancak her uygulama bu şekilde geliştirilemez. Çünkü alınan bilgilere uygulama kapatıldıktan sonra da erişilmesi gerekebilir. Bu durumda bilgileri bellek dışında, veriyi kalıcı olarak tutabilecek bir ortama, örneğin; disklere kaydetmek gerekecektir. İşte bu ihtiyacı karşılamak için farklı yöntemler geliştirilmiş ve günümüzde çok büyük miktarlarda veri tutan sistemler tasarlanmıştır. Kısaca bu gelişime bakacak olursak; ilk olarak, günümüzde de halen çok sık kullanılan veri tutulabilecek belki de en basit yapısal olmayan sistemin “text dosya” olduğu söylenebilir. Ancak, bu yapılar da zamanla yetersiz kalmış ve yapısal fakat hiyerarşik olmayan çözümler geliştirilmiştir. Bu başlık altında Comma Separated Value (CSV) dosyalar, Microsoft Exchange dosyaları, Active Directory dosyaları örnek olarak verilebilir. Zamanla, sadece veriyi tutmuş olmak da yetersiz kalmış ve verileri hiyerarşik bir yapıda tutma gereksinimi doğmuştur. Bu yapıya da verilecek en güzel örnek XML teknolojisidir. Bu yapının da yetersiz kaldığı durumlar için ilişkisel (Relational) veri depolama sistemleri geliştirilmiştir. İlişkisel veri depolama modeli günümüzde yoğun olarak kullanılmaktadır. Bu modelle saklanan verileri yönetmek için geliştirilen uygulamalara da ilişkisel veritabanı yönetim sistemi (Relational Database Management System, RDBMS) adı verilmektedir. Böylece veritabanını (Database), veriyi daha sonra kullanılabilmesi ya da sadece depolanması amacıyla belli bir formatta tutan sistem olarak açıklayabiliriz. Günümüzde büyük miktarlardaki verileri depolamak için ve bunları yönetmek için çeşitli firmaların geliştirmiş olduğu uygulamalar vardır. Bu uygulamalara örnek olarak Microsoft SQL Server, Oracle ve Microsoft Access verilebilir. Şimdi veriye erişim teknolojilerinden bahsedilmesi gerekmektedir. Peki nelerdir bu teknolojiler?
Veriye Erişim Teknolojileri ODBC (Open Database Connectivity) Microsoft ve diğer kuruluşların geliştirdiği bu teknoloji ile birçok veri kaynağına bağlanarak, veri alışverişi yapılabilmektedir. ODBC uygulama ortamlarına bir API sunmakta ve uygulamaların farklı sistemlere bağlanılabilmesini sağlamaktadır. ODBC teknolojisi 1990 öncesi geliştirilmiş olmasına rağmen ADO.NET platformunda da yer alan bir teknolojidir. ODBC hem yerel (local) hem de uzaktaki (Remote) veri kaynaklarına erişmek için kullanılabilecek bir veri erişim teknolojisidir.
DAO (Data Access Objects) 8 / 59
DAO, ODBC’nin aşağı seviye diller için (C,C++) geliştirilmiş olması ve kullanımının zor olması nedeniyle, Microsoft tarafından Visual Basic 3 ile geliştirilmiş ve kullanımı çok kolay bir teknolojidir. Microsoft’un, Microsoft Access veritabanlarına erişim standardı olan Jet için geliştirdiği bu model, halen VB6’dan MS Access’e erişim için en hızlı yöntemdir.
RDO (Remote Data Objects) Microsoft’un Visual Basic 4 ile duyurduğu RDO, DAO’nun ODBC sağlayıcısındaki eksikliğini gidermek için geliştirilmiştir. Uzak veri kaynaklarına erişimde ODBC’nin daha performanslı kullanılmasını sağlar.
OLE DB (Object Linking and Embedding DataBase) ODBC’de olduğu gibi driver (sürücü) mantığıyla çalışan OLE DB, arka tarafta COM arayüzünü kullanan bir tekniktir. Kaydettiği gelişme ile bir çok sisteme bağlantı için kullanılabilmektedir. Bu başarısından dolayı da ADO.NET içerisinde önemli bir yeri vardır.
ADO (ActiveX Data Objects) ADO aslında arka planda OLE DB teknolojisini kullanan ve veriye erişimi daha da kolaylaştıran, yüksek seviye programlama dilleri için veri erişiminde tercih edilen bir teknolojidir. ADO, ADO.NET’e temel oluşturduğu söylenen bir teknoloji olmasına rağmen bu tam olarak doğru değildir. Bunun en büyük nedenlerinden birisi de ADO’nun COM desteğine gereksinim duymasıdır. Günümüzde bazı platformlarda halen kullanılan bu teknoloji, XML standardına destek vermek için eklentilerle güçlendirilmesine rağmen, XML formatındaki veriyi sadece taşıyabilmektedir. Aksine, ADO.NET, XML standardı üzerine kurulmuş ve veriyi taşırken de XML formatını kulllanan bir teknoloji olarak geliştirilmiştir.
ADO.NET ADO.NET, Microsoft’un .NET platformunda çok önemli bir yere sahip, .NET uygulamalarından, her türlü veri kaynağına erişim için gerekli hazır tipleri olan, COM desteği gerektirmeyen, XML standardı üzerine kurulmuş ve en önemlisi de .NET Framework’ün imkanlarını kullanabilen, ADO’nun gelişmiş bir versiyonu olmaktan çok, yeni bir teknoloji olarak karşımıza çıkmaktadır. ADO.NET, .NET Framework’ ün bir parçası olarak 1.0’dan itibaren, .NET’in tüm versiyonlarında yer almaktadır. Ayrıca veri işlemlerini çok kolaylaştıran ve nesneye yönelik programlama (Object Oriented Programming) modeline uygun yapısı nedeniyle son derece kullanışlı bir platformdur.
9 / 59
Veri kaynakları ve Uygulamalar arasındaki bağlantı
10 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Programlamada Nesne ve Sınıf Kavramları
11 / 59
Nesne Şu ana kadar kavramsal olarak (ve günlük yaşamda) nesnenin ne olduğu saptanmaya çalışıldı. Şimdi ise daha kesin çizgilerle bu konseptin programlama ortamında nasıl temsil edileceği tartışılacaktır. Programatik anlamda nesne, nesne yönelimli programlamanın yapıtaşlarından birisidir. Nesne yönelimli programlama yaklaşımında tüm öğeler birer nesne olarak görülürler. Aşağıdaki öğeler, nesneye birer örnek olarak düşünülebilir: Araba Bebek Kedi Metinsel Bir İfade Yemek Kamera Nesne, bir varlık ya da konsepti programatik olarak açıklayan soyut ifadedir. Nesneler ayrıca, özellikleri ve davranışları hakkında da bilgi barındırırlar. C# dilinde aşağıdaki kod kullanılarak bir nesne örneklemesi yapılabilir. Nesne örneklenmesi, nesnenin program içerisinde bilgisayar hafızasına alınmasını ifade etmektedir. Baby b = new Baby(); Hafızaya yeni bir nesne çıkaran kod Nesneye ait tip bilgisi Hafızadaki nesneyi işaret edecek değişken adı Örneğimizdeki bebek nesnesinin özelliklerini düşündüğümüzde, adı, soyadı, boyu, ağırlığı, doğum tarihi gibi her bebeğin kendine özgü özellikleri olduğunu söyleyebiliriz. Nesnelerin özellikler dışında bir de davranışlara sahip olduğunu söylemiştik; bu davranışlar nesnenin dışarıya uyguladığı ya da dışarıdan nesneye uygulanan işlevsellikler olabilir. Örnek vermek gerekir ise; bebek nesnemiz, acıkır, ağlar, uyur, emekler, beslenir. Eğer daha önce Windows üzerinde programlama yaptıysanız; windows formu üzerine yerleştirilen metin kutusu (TextBox), liste kutusu (ListBox), buton (Button) vb. sayısız kontrollerin de birer nesne olduğunu biliyorsunuz. Microsoft geliştiricileri, bu kontrolleri biz uygulama geliştiricilerin kullanabilmeleri için yazmışlardır. Bir geliştirici bu kontrollerin belli işleri yapabildiğini bilir. Örneğin; ekranda yazı göstermek için Text özelliğine, metin kutusunun boyutlandırılması için Width özelliğine bilgi girişi yapılabilir.
Sınıf Programalama yaparken sınıf ve nesne kavramlarının birbirinden ayırt edilmesi gerekir. Sınıf, bir nesnenin tanımıdır; davranışlarını yansıtan karakteristiklerin ve nesnede olması gereken özelliklerin neler olduğunu belirleyen bir şablondur. Örneğin mimarın çizdiği kat planı, mühendis ve işçilerin gerçek hayata aktaracakları bina için bir taslaktır. Binaya çıkılacak toplam 10 kat için ortada tek bir tasarım vardır. Bu tasarımda belirtilen özelliklerin hepsi her katta yer almak zorundadır; ancak özelliklerin değerlerinin değişebileceği unutulmamalıdır. Mimar her katta 1 daire ve her dairede 4 oda olacağını belirtmesine rağmen katlardaki bu 4 odanın metrekareleri ya da her dairenin pencere boyu birbirinden farklı olabilir. Burada mimarın çizimi sınıf; işçi ve mühendislerin gerçek hayatta meydana getirdikleri (maddesel olarak yer kaplayan her kat) nesne olarak programlama ortamına aktarılabilir. Programlama ortamında sınıf bellekte yer kaplamaz, sadece bir kod tasarımıdır. Nesneler ise sınıfta belirtilen özelliklere ve davranışlara sahip bellekte yer kaplayan örneklerdir (instance). Çalışma zamanında oluşturulurlar.
Sınıf Üyeleri Nesnelerin iki yüzü vardır: Dış dünya tarafından bilinen ne yaptığı, ve genellikle dışarıya kapalı olan nasıl yaptığı. Nesnenin ne yapar kısmı (işlevselliği), metotlar ile temsil edilir. Metotlar, geliştirici için bir fonsiyonelliği temsil eden kod bloğudur. Nasıl çalışır kısmı ise, metotlar ve nesnenin özelliklerini sağlayan
12 / 59
değişkenler ile temsil edilir. Bu değişkenler C# terminolojisinde alan (field) olarak tanımlanmaktadırlar. Bu bilgilerden yola çıkarak bir genelleme yapılırsa nesne taslağı olan sınıf, metotlar ve alanlar ile tanımlanırlar. Ayrıca üye terimi, bir sınıfa ait olan herhangi bir parça için kullanılır. Bunlar bir alan ve bir metot olabileceği gibi ilerleyen konularda görülecek olan diğer programlama öğeleri de olabilir. C# sınıf üyelerinden bazıları aşağıda liste olarak yer almaktadır: Field (alan) Property (özellik) Method (metot) Event (olay)
13 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : C# Sınıf Tipini Anlamak
14 / 59
Basit anlamda bir sınıf, alan (field) ve metotlardan oluşan kullanıcı tanımlı bir veri tipinden başka bir şey değildir. Bir sınıfın alanları ortaklaşa bir sınıf nesne örneğinin durumunu (state) temsil ederler. Bir başka deyişle, yazılan bir sınıfın nesneleri, alanlara verilecek farklı değerler ile farklılık kazanırlar. Nesne yönelimli dillerin, veri ve fonksiyonellikleri tek bir kullanıcı tanımlı veri tipinde gruplayabilme yeteneği ve gücü, gerçek hayat varlıklarının yanında yazılım tiplerinin de modellenebilmelerini sağlar. Örneğin futbol ya da basketbol oyunu yazarken oyuncu profillerine birçok yerde ihtiyaç duyulabilir. Oyuncu adındaki sınıf ile programın bu ihtiyacını karşılayan veri tipini modellemeye çalıştığımızı varsayalım. Şu anda programın geri kalanını düşünmeyip bu sınıfı yazmaya odaklanalım. Sınıf, her oyuncu için ad, takım ve formaNo bilgilerini saklayan bir yapıda olsun. Ek olarak Oyuncu sınıfı, bir oyuncunun takımının değiştirilmesini sağlayan TakimDegistir metodu ve bir oyuncunun o anki durumu hakkında bilgi veren BilgiVer metodunu içersin. Aşağıdaki şekil, Oyuncu sınıfını göstermektedir.
Oyuncu sınıf (class) tipi Oyuncu sınıfı, nesne kullanıcısına bazı yetkiler sunmaktadır. Bunlar, yeni bir oyuncunun ad, forma no, oynadığı takım adını tutmak, oyuncunun takımını değiştirmek ve oyuncu profili hakkında genel bilgi vermektir. Sınıfı yazmak için gerekli C# kodu aşağıdaki gibidir: namespace SinifKullanimi { class Oyuncu { public string adi; public string takimi; public byte formaNo; public string BilgiVer() { return string.Format("{0} {1} - {2}",formaNo,adi,takimi); } public void TakimDegistir(string yeniTakim) { takimi = yeniTakim; } } } Sınıfa ait bütün kodlar, sınıf tanımlamasının ardından gelen süslü parantezler (curly brackets) arasında yer alır. Zorunluluk olmamasına rağmen genellikle her sınıf kendisine ait bir dosyada tutulur. Bu, belli bir sınıfın kodları incelenmek istendiğinde erişimi kolaylaştırır; çünkü dosyaya verilen isim çoğu zaman sınıfın ismi olur. Bir Sınıfın Dış Dünyaya Açık Arayüzünü Tanımlamak
15 / 59
Bir sınıfın içsel durum verilerine ve fonksiyonelliklerine karar verdikten sonra sıra sınıfın dış dünyaya açık arayüzünü ayrıntılandırmaya gelir. Literatürde public interface terimi ile ifade edilen bu kavram, oluşturulan nesne değişkeninin nokta operatörü ile erişilebilen üye kümesini işaret eder. Sınıfı inşa eden geliştirici -yazan- açısından dış dünyaya açık arayüz, sınıf içerisinde public erişim belirleyicisi ile tanımlanmış üyelerdir. Bir tipin dış dünyaya açık arayüzünde iç içe tip (nested types) tanımlaması yer alabilir. Sınıfı kullanan geliştirici ise nesne kullanıcısı (object user) olarak nitelendirilir. Oyuncu sınıfı, nesne kullanıcısı için public erişim belirleyicisi ile işaretlenmiş 3 tane alan (field) ve 2 tane metot içermektedir. Bu örnekte yer alan Oyuncu sınıfında bütün üyeler nesne kullanıcısına açıktır; ancak bir sınıf içerisinde yardımcı üyeler ya da değeri kontrollü olarak dış dünyaya açılmak istenen alanlar olabilir. Bu gibi senaryolar ilerleyen bölümlerde işlenecektir. namespace SinifKullanimi { class Program { public static void Main(string[] args) { Oyuncu o1 = new Oyuncu(); o1.adi = "Emre"; o1.takimi = "Newcastle United"; o1.formaNo = 5; Console.WriteLine(o1.BilgiVer()); Console.WriteLine("\n*****************************\n"); Oyuncu o2 = new Oyuncu(); o2.adi = "Thierry Henry"; o2.takimi = "Arsenal"; o2.formaNo = 14; o2.TakimDegistir("Barcelona"); Console.WriteLine(o2.BilgiVer()); Console.ReadLine(); } } } Programcı tarafından yeni bir sınıf tanımlandıktan sonra, aynı framework içerisindeki önceden tanımlanmış olan sınıflar gibi kullanılabilir. Bir başka deyişle yukarıdaki örnekte olduğu gibi bu tipten yeni değişkenler oluşturulabilir ya da aşağıdaki örnekte olduğu gibi tasarlanan sınıf tipinde parametre alan bir metot yazılabilir. namespace SinifKullanimi { class Program { public static void Main(string[] args) { }
16 / 59
public static void OyundanCikar(Oyuncu oyuncu) { //... } } } C# dilinde yeni bir nesne oluşturulurken new anahtar kelimesi kullanılır. namespace SinifKullanimi { class Program { public static void Main(string[] args) { //Hata!!! Başlangıç değeri verilmemiş lokal değişken! “new” kullanılmalı. MerhabaSinif s; s.BirMetot(); //... } } class MerhabaSinif { public void BirMetot() { //Kod bloğu } } } new anahtar kelimesi çalışma zamanında Oyuncu nesnesine bellek ayrılması için yol göstericilik yapar. Belirtilen nesnenin bellekte kaplayacağı byte miktarını hesaplar ve belleğin heap bölgesinden yeterli bellek alanı ayrılmasını sağlar. Aşağıda, MerhabaSinif sınıf tipinde iki tane nesnenin nasıl oluşturulduğu görülmektedir. namespace SinifKullanimi { class MerhabaSinif { public void BirMetot() { //Kod bloğu } } class Program { public static void Main(string[] args) { //Yeni bir nesne tek bir satırda tanımlanıp oluşturulabilir... MerhabaSinif s1 = new MerhabaSinif();
17 / 59
//Yada referans tanımlaması ile nesne oluşturulması iki satıra bölünebilir. Bu genelde nesneye farklı kod blokları içerisinden erişilmek istendiği durumlarda tercih edilir. MerhabaSinif s2; S2 = new MerhabaSinif(); //... } } } C#’da referans türünden değişkenler, bellekteki nesneler için hafıza adresi tutan gerçek birer referanstırlar. Nesnelerin heap bölgesindeki adreslerini tutarlar ve kendileri stack bölgesinde standart olarak 4 byte (64-Bit sistemlerde 8 byte) yer kaplarlar. Nesneye erişmek isteyen bir nesne kullanıcısı bu referanstan faydalanır. Bu bilgiler ışığında s1 ve s2 değişkenleri, belleğin heap bölgesinde MerhabaSinif tipinden alanları referans ederler. Nesneler Bellekten Nasıl Siliniyor? Bellekte nesneye yer açmak için yapılan bilinçli tanımlamanın aksine, bellek bölgesini temizlemek için herhangi bir operatör yoktur. Bunun yerine, nesne artık herhangi bir referans tarafından işaret edilmediğinde ya da uygulama kapatılmadan önce çalışma zamanı, bellek bölgesini sisteme iade eder. Otomatik bellek temizliğinden, çalışma zamanının entegre bir parçası olan garbage collector sorumludur. Garbage collector, belleğin heap bölgesindeki hangi nesnenin artık referans edilmediğini takip eder, bu nesneyi bir listeye dahil eder ve zamanı geldiğinde (belleğe ihtiyaç duyulduğunda) listeden temizlik yapar. Sonuç olarak, referans tipleri için ayrılan bellek alanının sisteme iade edilmesi için derleme zamanında belirlenen herhangi bir yer yoktur. Değer tiplerinde ise değişkenin tanımlandığı kod bloğunun (en içte kalan süslü parantez bloğunun) sonuna gelindiğinde değişken, bellekten otomatik olarak düşmektedir. Yani değer tipli bir değişkenin, belleğin stack bölgesinden ne zaman düşeceği derleme zamanında yazılan kod blokları ile belirlenir. Belirtmekte fayda var ki sınıflar(class) ile yapılar(struct) arasında kod yazımı açısından küçük değişiklikler dışında fark yoktur. Örneğin yukarıda yazılan Oyuncu sınıfında class anahtar kelimesi yerine struct kullanılabilir. Ancak bir çok yerde yapı yerine sınıf tercih edilir. Bunun başlıca sebebi, sınıfların referans tipli, yapıların değer tipli olmasıdır.
18 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Nesne Yönelimli Programlamanın Faydaları
19 / 59
Nesne Yönelimli Programlamanın faydaları nelerdir? Nesne Yönelimli Programlamada bir problemin çözümü parçalara ayrılarak ele alınabilir. Burada her bir parçanın ayrı bir sınıf olduğu söylenebilir. Nesnelerin de sınıflar sayesinde yaratıldıklarını zaten biliyorsunuz. Sınıflar ve nesneler, gerçek hayat problemlerini bilgisayar ortamına taşımada kolaylıklar sağlar. İstediğimiz herşeyi nesne olarak tanımlayarak üzerinde işlem yapabiliriz. Ayrıca sınıflar, kodlarımızın düzenli bir biçimde durmasını da sağlar. Bu sayede program üzerinde yapılacak olan değişiklikler için zaman kaybı olmaz. Nesne Yönelimli Programlama’nın özelliklerinden birisi de bir defa kodladıktan sonra, aynı işlemleri tekrar tekrar kodlamamaktır. Yani çok sık kullanacağımız bir işlevi her kullanışımızda tekrar yazmak yerine o işlevi bir sınıf haline getirerek yeri geldiğinde sınıfın nesnesini çağırarak, ihtiyaç duyulan işlevi rahatlıkla kullanabiliriz. Böylelikle projede harcanacak toplam mesai de kısalmış olur. Nesne Yönelimli Programlamada bir programcı kendi programında kullanmak üzere bir sınıf yazdığında, bu sınıfı başka programlarda da kullanabilmektedir. Bu özelliğiyle farklı projelerde de mesai süresi kısalmakta ve proje maliyetleri azalmaktadır. Kısaca Nesne Yönelimli Programlamanın büyük çaplı projelerde kodlamayı kolaylaştırdığını söyleyebiliriz. Yazılan kodların daha kolay güncellenebilir ve geliştirilebilir olması da ayrı bir avantajdır.
20 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Alanlar (Instance Fields)
21 / 59
Nesnenin Durum Verileri : Alanlar (Instance Fields) Nesne yönelimli dizaynın önemli amaçlarından birisi, herhangi bir objeyi temsil eden bütünlüğü sağlamak için, verileri gruplamaktır. Bu bölümde Oyuncu sınıfına eklenen veriler tartışılacaktır. Bir sınıf içerisinde veri saklayan değişken için nesne yönelimli literatürde kullanılan terim, üye değişken (member variable) terimidir. Bu C# için de geçerli bir isimlendirme iken daha doğru olan, alan (field) terimidir. Alan, kendisini içeren tip ile ilişkilendirilmiş, bir isimle erişilen bellek birimidir. Nesne alanları, nesne içerisinde veri depolamak için sınıf düzeyinde tanımlanan değişkenlerdir. Nesne alanları teriminin kullanılmasının sebebi birkaç bölüm sonra görülecek olan ve verinin saklanma yerleri konusunda nesne alanları ile aralarında temel farklılıklar bulunan statik alanlardır.
Alanları Tanımlamak Oyuncu sınıfı, 3 tane alan içermektedir : adi, takimi ve formaNo namespace SinifKullanimi { class Oyuncu { public string adi; public string takimi; public byte formaNo; //... } } Sınıf içerisinde tanımlanan bu alanlar ile, programın farklı yerlerinde ihtiyaç duyulacak her Oyuncu nesne örneği için temel bazı veriler saklamak mümkün olmaktadır. Bu durumda alanların önüne public erişim belirleyicisi konulmalıdır. Bir alanın önündeki public, alanın sakladığı verinin bu sınıf dışarısından erişilebilir olduğunu işaret eder. Lokal değişken tanımlamalarında olduğu gibi bir alan tanımlaması da alanın saklayacağı verinin tipini içerir. Alanların Varsayılan Değerleri Sınıf tipinin üye değişkenleri yani alanları, ilgili sınıftan bir nesne örneği oluşturulduğu anda otomatik olarak uygun varsayılan değerleri alırlar. Alınan bu değerler, alanın veri tipine bağlı olarak değişir:
bool tipi, false değer alır. int, long vb. tam sayı tipleri 0 değer alırlar. double vb. ondalıklı sayı tipleri 0.0 değer alırlar. string veri tipi null değer alır. char veri tipi ‘\0’ değerini alır. Referans tiplerinin tümü null değer alırlar.
Aşağıda bu kuralı ele alan kod parçası yer almaktadır. namespace SinifKullanimi { class Televizyon { public int ekran; 22 / 59
public string marka; public bool lcdMi; } class Program { public static void Main() { Televizyon tv = new Televizyon(); //0 değerini almıştır. Console.WriteLine("tv.ekran : {0}", tv.ekran); //boş (null) değer almıştır. Console.WriteLine("tv.marka : {0}", tv.marka); //false değer almıştır. Console.WriteLine("tv.lcdMi : {0}", tv.lcdMi); } } }
Alanların Varsayılan Değerleri
Varsayılan Değerler ve Lokal Değişkenler Bir tip üyesi içerisinde tanımlanan lokal değişken için ise senaryo oldukça farklıdır. Bir lokal değişken tanımlandığında kullanılması için öncelikle ona bilinçli olarak değer atamasının yapılması gerekir. Yani lokal değişkenlerde varsayılan değer ataması söz konusu değildir. Örneğin aşağıdaki kod bloğu derleme zamanı hatası ile sonuçlanır. //Hata!!! lokalInt isimli değişkene, kullanılmadan önce başlangıç değeri atanmalı. public static void Main() { int lokalInt; Console.WriteLine(lokalInt); } Bu problemi çözmenin yolu lokalInt isimli değişkene başlangıç değeri verilmesidir. //Böyle daha iyi; artık herkes mutlu. public static void Main() { int lokalInt = 0; Console.WriteLine(lokalInt); } Lokal değişkenleri kullanmak için başlangıç değeri verilmesi zorunluluğunun geçerli olmadığı tek bir istisna durum vardır: Eğer lokal değişken bir metoda output parametresi olarak aktarılmak 23 / 59
üzere kullanılırsa başlangıç değer ataması yapılmasına gerek yoktur. Çünkü zaten metot içerisinde kendisine mutlaka bir değer atanmak zorundadır. Nesne Alanlarına Erişmek Alanlara değer ataması yapılabilir ve ardından alanın taşıdığı veri elde edilebilir. Bir alan tanımlandığında önünde static niteleyicisinin kullanılmamış olması, alanın nesne alanı olduğunu gösterir. Yani alan, ilgili sınıftan oluşturulan her bir nesne için, nesnenin kendisine ait özel parçası olarak bellekte yerini alır. O yüzden bir nesne alanına sadece kendisini içeren sınıf nesne örneği üzerinden erişilebilir. Doğrudan sınıf adı üzerinden erişilemez. Bu da gösteriyor ki bir alanla çalışabilmek için öncelikle nesne örneğinin belleğe çıkarılmış olması gerekir. namespace SinifKullanimi { class Program { public static void Main(string[] args) { Oyuncu o1 = new Oyuncu(); o1.adi = "Emre"; o1.takimi = "Newcastle United"; o1.formaNo = 5; FormaDegistir(o1 , 9); Console.WriteLine("{0} {1}, {2}" , o1.formaNo , o1.adi , o1.takimi); Console.ReadLine(); } static void FormaDegistir(Oyuncu oyuncu,int yeniNo) { oyuncu.formaNo = yeniNo; } } }
Nesne alanına erişmek Yukarıdaki kodda öncelikle bir Oyuncu nesnesi örneklenmektedir. Ardından her bir alanının değeri verilir, forma numarasının değiştirilmesi için FormaDegistir() metodu çağrılır. Son olarak o1 referansının işaret ettiği Oyuncu nesnesi ile ilişkili olan her bir alanın değeri ekranda gösterilir. Görüldüğü gibi bir alana değeri verilmeden önce Oyuncu nesnesine ihtiyaç duyulmaktadır. Dolayısıyla alanlara değer atanırken ve değerlerine erişilirken o1 değişkeni alan isimlerinin ön eki gibi kullanılmaktadır.
24 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Metotlar ve Yapıcı Metot (Constructor)
25 / 59
Yapıcı Metot’ların Rolü (Constructor) Sınıf nesne örnekleri oluşturulurken tanımlamanın sonunda kullanılan metot parantezlerinin aslında bir anlamı vardır: Sınıfa ait yapıcı metodu çağırmak! Oyuncu o1 = new Oyuncu(); C# gibi nesne yönelimli dillerin en büyük avantajlarından birisi, bir sınıf nesne örneği oluşturulduğu anda otomatik olarak çağrılan özel bir metodun tanımlanabilmesidir. Bu metoda yapıcı metot (constructor) adı verilir. Varsayılan yapıcı metot, nesne içerisinde belleğe çıkarılacak olan bütün alanların doğru varsayılan değerleri almasını sağlar. (Bu bütün yapıcı metotlar için geçerlidir) Peki nesne oluşturulduğu anda çağrılan yapıcı metoda isim olarak ne verilir de derleyicinin bu işi otomatik olarak yapması sağlanır? Bu konuda C++ dilinin tasarımcısı Stroustrup takip edilmiştir. C# dilinde kullanılan yapıcı metotlar, bulundukları sınıfla aynı isimdedir. Aşağıda basit bir yapıcı metodu ile birlikte basit bir sınıf yer almaktadır: namespace YapiciMetotKullanimi { class YapiciliSinif { public YapiciliSinif() { Console.WriteLine("Yapıcı metot çalıştı..."); } } class Program { public static void Main(string[] args) { YapiciliSinif ctor = new YapiciliSinif(); } } } Yukarıda, içerisinde yapıcı metodu olan bir sınıf ve Main metodu içerisinde sadece bu sınıftan bir nesne örneğinin oluşturulduğu kodlar yer almaktadır. Yapıcı metotların nesne örneklendiği anda çalışması bu kod bloğunun çıktısından anlaşılabilir.
Yapıcı metotlar nesne örneklendiği anda otomatik olarak çalışır Yapıcı metot, geriye void dahil değer döndürmeyen, parametre alabilen özel bir çeşit metottur. Her sınıf mutlaka en az bir adet yapıcı metoda sahip olmak zorundadır. Eğer sınıf içerisine yukarıdaki gibi bilinçli olarak yazılmazsa, derleyici bizim için varsayılan bir yapıcı metot üretir. Derleyici tarafından üretilen varsayılan yapıcı metodun parametresi olmaz ve içerisinde alanların varsayılan değerleri verilir, bunun dışında hiçbir şey yoktur. Geliştirici olarak kendin yapıcı bir metot yazmak istediğinde bunu kolaylıkla gerçekleştirebilirsin. Geriye değer döndürmeyen (yani herhangi bir geri dönüş tipi belirtilmeyen), public erişim belirleyicisi ile işaretlenmiş ve içinde bulunduğu sınıf ile aynı isimde bir metot yazman yeterli olur. Metot içerisine, ilgili sınıftan bir nesne
26 / 59
oluştuğu anda çalışması istenen C# kodu yazılabilir. Sınıf içerisine bilinçli olarak yazılmadığı durumlarda her sınıfın varsayılan yapıcı metodu aşağıdaki gibi oluşturulmaktadır. namespace YapiciMetotKullanimi { class Oyuncu { public Oyuncu() { //Sınıfa ait alanların varsayılan değerleri otomatik olarak verilir. } public string adi; public string takimi; public byte formaNo; public string BilgiVer() { return string.Format("{0} {1} - {2}", formaNo, adi, takimi); } public void TakimDegistir(string yeniTakim) { takimi = yeniTakim; } } } Yapıcı Metotları Aşırı Yüklemek Nesne kullanıcısı nesne ile çalışmaya başladığında (yani nesne örneği oluşturulmasının hemen ardından) genellikle ilk iş sınıf içerisinde tanımlanmış olan alanların değerlerinin verilmesidir. Oyuncu sınıfı gibi birçok sınıf ile çalışılırken bir tane nesne için değeri gerekli bazı alanlara atama yapılmadan o nesne ile çalışmanın fazla bir anlamı olmayabilir. Bu sebeple nesne referansı üzerinden ilgili değişkenler çağrılır ve sırayla değerleri verilir. Öyleyse, daha nesne örneği oluşturulurken bu alanların alacağı değerlerin belli olması yerinde bir hareket olacaktır. Bu durumda nesne, alanlarının varsayılan değerleri ile değil, çalışılacak gerçek değerleri ile belleğe çıkar. Sınıflar bu iş için, varsayılan yerine kendi yapıcı metotlarımızın yazılması imkanını sunarlar. Bu şekilde nesne kullanıcısına nesneyi oluşturma anında durum verilerine başlangıç değerlerinin verilmesi için kolay bir yol sunulmuş olur. namespace YapiciMetotKullanimi { class Oyuncu { //Kendi yazdığımız yapıcı metot, nesnenin durum verilerine kullanıcıdan alınan parametre değerlerini aktarıyor public Oyuncu(string ad, string takim,byte formaNumarasi) { adi = ad; takimi = takim; formaNo = formaNumarasi; Console.WriteLine("Parametrik yapıcı metot çalıştı"); } //Varsayılan yapıcı metot 27 / 59
public Oyuncu() { Console.WriteLine("Varsayılan yapıcı metot çağrıldı..."); } public string adi; public string takimi; public byte formaNo; public string BilgiVer() { return string.Format("{0} {1} - {2}", formaNo, adi, takimi); } public void TakimDegistir(string yeniTakim) { takimi = yeniTakim; } } } Yukarıda kendi yazdığımız yapıcı metot ve yeniden tanımlanan varsayılan yapıcı metot ile düzenlenmiş Oyuncu sınıfı yer almaktadır. Bir sınıf içerisinde parametreli bir yapıcı metot tanımlandığında varsayılan yapıcı metot derleyici tarafından üretilmez. Çünkü alanların değerleri, yeni tanımlanan yapıcı metot içerisinde, sınıfın tasarlayıcısı tarafından verilmiştir. Ancak çalışma zamanı, nesne oluşturulduğu anda çağrılan özel yapıcı metodu yine de takip eder. Bunun sebebi sınıfı tasarlayan geliştiricinin yazdığı yapıcı metot içerisinde sınıfa ait bütün alanların başlangıç değerlerini bir şekilde atamak zorunda olmamasıdır, hatta istenirse alanlarla ilgili hiç kod yazılmayabilir. Kullanıcıdan parametre olarak alınan ya da kod bloğu içerisinde geliştirici tarafından değeri verilen alanların dışında kalanlara varsayılan değerlerini atamak özel yapıcı metodun görevidir. Parametrik yapıcı metodun yanında nesne kullanıcısına alanların değerleri verilmeden nesnenin tanımlanması imkanı verilmek istenirse, varsayılan yapıcı metot bilinçli olarak yazılmalıdır. Çünkü sınıf içerisine yazılan parametrik metot, derleyici tarafından üretileni ezmektedir. Son haliyle Oyuncu sınıfının kullanımı ve ekran çıktısı aşağıdaki gibidir: namespace YapiciMetotKullanimi { class Program { public static void Main(string[] args) { //Varsayılan yapıcı metot (constructor) çağrılır. Oyuncu o1 = new Oyuncu(); Console.WriteLine(o1.BilgiVer() + "\n"); //Parametreli yapıcı metot çağrılır. Oyuncu o2 = new Oyuncu("Tuncay", "Middlesbrough", 9); Console.WriteLine(o2.BilgiVer()); Console.ReadLine(); } } }
28 / 59
Parametrik yapıcı metot kullanımı Yapıcı metotlar aşırı yüklenebilirler. Yani farklı parametre sayıları ya da tipleri ile istenilen sayıda yazılabilirler. Bunun amacı nesne kullanıcısına farklı alan kümeleri içerisinden ihtiyaç duyduklarının değerlerini vererek nesneyi oluşturma esnekliğini sunmaktır. Aşağıda farklı parametre kombinasyonları ile yapıcı metodu üç defa aşırı yüklenmiş Oyuncu sınıfı yer almaktadır : namespace YapiciMetotKullanimi { class Oyuncu { public string adi; public string takimi; public byte formaNo; public Oyuncu(string ad) { adi = ad; } public Oyuncu(string ad, string takim) { adi = ad; takimi = takim; } public Oyuncu(string ad, string takim, byte formaNumarasi) { adi = ad; takimi = takim; formaNo = formaNumarasi; } public string BilgiVer() { return string.Format("{0} {1} - {2}", formaNo, adi, takimi); } public void TakimDegistir(string yeniTakim) { takimi = yeniTakim; } } }
29 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Alanlara Başlangıç Değeri Verme
30 / 59
Sınıf tipleri genellikle çok sayıda alana sahip olurlar. Bir sınıfın birden fazla yapıcı metot içerebilmesinden yola çıkarak, her bir yapıcı metot tanımlamasında, alanlar için yazılacak aynı başlangıç değerini atama kodu, rahatsız edici olabilir. Üyenin varsayılan başlangıç değerini alması istenmiyorsa, her bir yapıcı metot içerisinde alan atamalarını gerçekleştirmek doğru bir yaklaşımdır. Örneğin bir tam sayı verisinin yaşamına her zaman 3 değeri ile başladığından emin olunmak isteniyorsa, aşağıdaki gibi bir tasarım yapılabilir: namespace AlanBaslangicDegeri { //Kullanım tamam; ancak fazla kod var. class Daire { public string semti; public int odaSayisi; public Daire() { odaSayisi = 3; } public Daire(string semti) { odaSayisi = 3; this.semti = semti; } //... } } Bu tasarıma alternatif olarak, sınıfa private erişim belirleyicisine sahip yardımcı bir metot yazılıp, bütün yapıcı metotlar içerisinden çağrılması düşünülebilir. Bu şekilde tekrar edilen atama kodunun önüne geçilmiş olur. Ancak halen tasarımda gereksiz kod yer almaktadır: namespace AlanBaslangicDegeri { class Daire { public string semti; public int odaSayisi; public Daire() { this.OdaSayisiBelirle(); } public Daire(string semti) { this.OdaSayisiBelirle(); this.semti = semti; } private void OdaSayisiBelirle() { this.odaSayisi = 3; } 31 / 59
//... } } Her iki teknik de geçerli olmakla beraber C#, bir alanın sınıf içerisinde tanımlandığı anda başlangıç değerinin atanmasına izin verir. Derleme zamanında bir alana başlangıç değeri verilmek istendiğinde bu kullanımdan faydalanılabilir. Alanın varsayılan değerini almasının istenmediği ve dışardan alınan başlangıç değeri için atama kodunun her yapıcı metot içerisinde tekrar edilmesinin tercih edilmediği durumlarda bu teknik kullanışlıdır. Alanın değeri değiştirilmek istenirse, nesne oluşturulduktan sonra bunun yapılması mümkündür. namespace AlanBaslangicDegeri { class Daire { public string semti; public int odaSayisi = 3; Oda o = new Oda(50, Oda.OdaCesidi.Salon); //... } class Oda { //Odanın çeşidini saklamak için tanımlanan iç-içe tip public enum OdaCesidi { Salon, ÇocukOdası, MisafirOdası, YatakOdası, OturmaOdası, ÇalışmaOdası, } public int metrekare; public OdaCesidi odaCesit; public Oda(int m2, OdaCesidi odaCesidi) { this.metrekare = m2; this.odacesit = odaCesidi; } } } Bir emlak programı için ihtiyaç duyulabilecek Daire sınıfı ile çalışıldığını varsayalım. Bir dairenin hangi semtte yer aldığı string tipinde bir alanla, evin kaç odası olduğu int tipte bir alanla, evdeki her bir oda ise Oda tipinde bir alanla temsil edilsin. Daire sınıfının tasarımı incelendiğinde odaSayisi ve o Oda nesnesinin başlangıç değerlerinin tanımlama anında verildiği görülmektedir. semti alanının ise başlangıç değeri verilmemiş, değerinin nesne kullanıcısından ya da yazılabilecek bir yapıcı metot yardımıyla alınması uygun görülmüştür. Bu kod örneğinde ayrıca dikkat edilmesi gereken birkaç nokta vardır. Daire sınıfında yer alan o alanının ait olduğu tip tasarımı incelendiğinde iç-içe tip kullanıldığı görülmektedir. Oda sınıfı içerisinde sadece bu sınıfın kullanımı için OdaCesidi isimli bir numaralandırıcı tipi tanımlanmaktadır. Oda sınıfı yapıcı metodu, alan gibi kendi üyeleri içerisinde bu tipi kullandığında sadece kendi adıyla erişebilir. Daire sınıfı bu tipte bir parametre ile çalışırken, tipi içeren sınıfla birlikte tam adını (Oda.OdaCesidi) yazmak durumundadır. Bunun sebebi daha önce bahsedildiği üzere OdaCesidi tipinin dış dünya tarafından Oda sınıfın bir üyesi olarak görülmesidir.
32 / 59
Yukarıdaki örnekte yapıldığı gibi üye atamaları, her zaman yapıcı metot iş mantığından önce çalışır. Dolayısıyla yapıcı metot içerisinde bir alana değer ataması yapılırsa önceki değer atmaları iptal edilmiş olur.
33 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : const ve readonly Veri Tanımlamak
34 / 59
Sabit Veri Tanımlamak : const Sınıf değişkenlerinin (alanlar) nasıl tanımlandığını önceki bölümlerde gördün. Şimdi, üzerine yeniden atama yapılamayan verilerin nasıl tanımlanacağını inceleyeceğiz. C#, sabit bir veri saklayan değişken tanımlamak için const anahtar kelimesini sunmaktadır. Bu şekilde oluşturulan değişkenlerin değerleri sabittir ve sonradan değiştirilemez. Tanımlandığı anda değeri atanmak zorunda olan sabit verilerin değeri değiştirilmeye çalışıldığında derleme zamanı hatası alınır. const anahtar kelimesi, hem lokal değişken (metot, özellik vb. içerisinde tanımlanan değişkenler) hem de nesne örneği düzeyinde (alanlar için) kullanılabilir. Sabit (constant) değişken değeri derleme zamanında belirtilmelidir. Bu yüzden bir nesne referansı üzerinden sabit alana atama yapılamaz; çünkü nesne referansı üzerinden yapılan atama çalışma zamanında ele alınır. Sabit veriye yapılan atama ise derleme zamanında assembly içerisinde CIL koduna yazılır. const anahtar kelimesinin örnek kullanımı için yazılan sınıf aşağıdadır: namespace SabitVeriKullanimi { class Sabit { //Bir sabite atanan değer, derleme zamanında bilinmelidir. public const short birKilometreKacMetre = 1000; public const float pi = 3.14f; public const string baskent = "Ankara"; } } Bütün sabit verilerin değerleri derleme zamanında bellidir. ildasm.exe yardımıyla bu sabitler ve değerlerinin assembly içerisine yazılmış olduğu görülebilir.
Sabit verilerin değerleri derleme zamanında belli olur ve CIL’e yazılır. Sabit Veriyi Kullanmak Dışarıdaki bir tip tarafından tanımlanan sabit bir veri referans edilmek istendiğinde önüne tipin adı konulmalıdır. Bu, sabit alanların bilinçsiz olarak statik olmasından ileri gelir. statik üyelere erişirken nesne referansı üzerinden değil üyenin ait olduğu tip üzerinden işlem yapılır. Eğer sabit üye, erişen tipin ya da erişen üyenin içerisinde ise sabitin önüne tip adı verilmek zorunda değildir. Bu noktaları biraz daha netleştirmek için aşağıdaki kodu inceleyelim. namespace SabitVeriKullanimi { partial class Program { const string sirketAdi = "Microsoft Corp."; public static void Main(string[] args) { //Başka tip tarafından tanımlanan sabitlere erişmek Console.WriteLine("Başkent : {0}",Sabit.baskent); Console.WriteLine("Bir kilometre = {0} metre", Sabit.birKilometreKacMetre); Console.WriteLine("Pi sayısı : {0}", Sabit.pi); 35 / 59
//Bu sınıfa ait sabit bir üyeye erişmek Console.WriteLine("Şirket : {0}",sirketAdi); //Lokal bir sabit değişkene erişmek const int lokalSabitDegisken = 23; Console.WriteLine("Lokal sabit : {0}",lokalSabitDegisken); Console.ReadLine(); } } } Dikkat edilirse Program sınıfının, Sabit isimli sınıf içerisinde yer alan sabit verilere erişirken öncesinde tip adını kullandığı görülebilir. Ancak kendi sınıf kapsama alanı içerisinde yer alan sirketAdi sabitine direkt erişebilir. Ayrıca Main() metodu içerisindeki lokalSabitDegisken adlı lokal değişkene tabii ki doğrudan erişim sağlanabilmektedir. Bir sınıf içerisinde tanımlanan alanlar, bu sınıftan oluşturulan n sayıda nesnenin her biri içerisinde ayrı ayrı yer tutarlarken; const anahtar kelimesi ile sabit olarak tanımlanmış alanlar, nesnelerin üyesi olmazlar. Daha global olarak sınıfa ait bir üye olarak yaşamlarını sürdürürler. const ile işaretlenmiş sabitlerin bu özelliği, statik olmalarından kaynaklanır. Statik üyeler, her bir nesnenin üye ailelerine değil, bütün ailelerin tanıdığı tek bir aileye dahil olurlar. Dahil olunan bu aileye bütün nesneler erişebilir. Sabit alanlar da bu paralelde çalışırlar.
Salt Okunur Veri Tanımlamak : readonly Daha önce bahsedildiği gibi bir sabite atanan değerin derleme zamanında bilinmesi gerekir; zira bu değer CIL içerisinde yerini almaktadır. Öyleyse başlangıç değeri çalışma zamanına kadar bilinmeyen bir sabit veri tanımlanmak istendiğinde ne olacak? Bu senaryoyu ele almak için egitimKodu isimli alanı olan Egitim isimli bir sınıf tanımlandığını varsayalım. Ayrıca bu tip, değerleri değişmeyecek olan iki tane Egitim tipinden alana sahip olsun. Eğer bu alanların değerlerinin değişmemesi için const anahtar kelimesi kullanılırsa, derleme zamanı hatası alınır. Bunun sebebi bir nesnenin adresinin ancak çalışma zamanında belli olmasıdır, derleme zamanında elde edilemez. namespace SaltOkunurVeriKullanmi { public class Egitim { //Nesenin adresi çalışma zamanında belirlendiği //için 'const' anahtar kelimesi burada kullanılamaz! //Hata!!! public const Egitim cSharp = new Egitim(2124); //Hata!!! public const Egitim sql = new Egitim(2779); public int egitimKodu; public Egitim() { } public Egitim(int egitimKodu) { this.egitimKodu = egitimKodu; } } 36 / 59
} Salt okunur alanların değeri derleme zamanında bilinmez, ancak çalışma zamanında bir kere atandıktan sonra, tekrar değiştirilemez. Salt okunur bir alan tanımlamak için readonly anahtar kelimesini kullanabilirsin. namespace SaltOkunurVeriKullanmi { public class Egitim { public readonly Egitim cSharp = new Egitim(2124); public readonly Egitim sql = new Egitim(2779); public int egitimKodu; public Egitim() { } public Egitim(int egitimKodu) { this.egitimKodu = egitimKodu; } } } Bu şekilde bir güncellemenin ardından artık derleme zamanı hatası alınmaz. Aynı zamanda cSharp ve sql alanlarının değerlerinin nesne kullanıcısı tarafından değiştirilemeyeceği garanti altına alınmış olur. namespace SaltOkunurVeriKullanmi { partial class Program { public static void Main(string[] args) { //Hata!!! Salt okunur alanlara atama sadece yapıcı metot içerisinde ya da alanın tanımlandığı yerde yapılmaktadır. Egitim e = new Egitim(); e.cSharp = new Egitim(2125); //Hata } } } const ile işaretlenmiş sabit verilerin aksine readonly sabitler, bilinçsiz olarak statik değillerdir. Bu yüzden değerleri bir nesneden diğerine değişebilir. Eğer değişmesi istenmiyorsa yani verinin sınıf düzeyinde olması gerekiyorsa static anahtar kelimesinin eklenmesi yeterlidir. Salt okunur bir alana atama sadece iki yerde yapılabilir. Bunlardan birincisi alanın tanımlandığı yerdir. İkincisi ise yapıcı metot içerisinde yapılan atamadır. Bu durum salt okunur alan değerinin bir dış kaynaktan elde edilmesi gereken durumlarda faydalı olur. Salt okunur bir çalışan numarasına sahip Egitmen adında başka bir sınıf olduğunu varsayalım. Nesne kullanıcısının salt okunur egitmenId alanının değerine yapıcı metot yardımıyla atama yapabildiği aşağıdaki kod bloğu ile görülebilir: namespace SaltOkunurVeriKullanmi { public class Egitmen { public readonly string egitmenId; public Egitmen(string egitmenId) 37 / 59
{ this.egitmenId = egitmenId; } public void EgitmenIdBelirle(string id) { //Hata!!! Salt okunur alanlara atama sadece yapıcı metot içerisinde ya da alanın tanımlandığı yerde yapılmaktadır. this.egitmenId = id; } } } egitmenId salt okunur olduğu için bu alana sadece tanımlandığı anda ya da yapıcı metot içerisinde atama yapılabilir. Eğer metot vb. başka bir üye içerisinde değeri değiştirilmeye çalışılırsa derleyici buna izin vermez. namespace SaltOkunurVeriKullanmi { partial class Program { public static void Main(string[] args) { Egitmen t1 = new Egitmen("y1000"); Egitmen t2 = new Egitmen("y1001"); //Hata!!! Salt okunur alanlara atama sadece yapıcı metot içerisinde ya da alanın tanımlandığı yerde yapılmaktadır. t2.egitmenId = "y1002"; } } } Yukarıdaki kod bloğunda, salt okunur bir alan değerinin nesneden nesneye değişebileceğini görebilirsin. Ayrıca nesne kullanıcısı, yapıcı metot ile egitmenId değerini verdikten sonra herhangi bir şekilde bu alanının değerini değiştiremez. Salt okunur veriler lokal değişkenler için kullanılamaz, sadece sınıf ya da yapı üyesi olan alanlar için kullanılabilir.
38 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Public & Private & Internal Erişim Belirleyicileri (Access Modifiers)
39 / 59
Sınıf (class) ya da yapı (struct) üyelerinin (metotlar, alanlar, yapıcı metotlar vb.) erişim düzeyleri, tanımlandıklarında belirtilmek zorundadır. Eğer üye, bir erişim belirleyici anahtar kelimesi kullanılmadan tanımlanırsa otomatik olarak private ataması yapılır. Aşağıda, C# dilindeki bazı erişim belirleyicileri ve açıklamaları görülmektedir.
private, bir üyeyi sadece tanımlandığı sınıf içerisinden erişilebilir olarak işaretler. Bu üyeyi içeren sınıftan oluşturulan bir nesne örneği üzerinden private üyeler erişilemezlerdir. C# dilinde yazılan bütün üyeler varsayılan olarak private erişim belirleyicisine sahiptir.
internal, aynı assembly içerisindeki herhangi bir tip tarafından erişilebilir üyeler tanımlar. Başka bir *.dll ya da *.exe içerisindeki bir tip ve üyeleri bu üyeye erişemez.
public, bir üyeyi herhangi bir nesne değişkeni üzerinden erişilebilir olarak işaretler (Üyeyi içeren sınıftan türeyen sınıfın nesnesi dahil). Ayrıca hem üyenin tanımlandığı sınıf içerisinden hem kendisinden türeyen sınıf içerisinden bu üye çağrılıp kullanılabilir. Farklı bir assembly üzerinden de bu üyeye erişim mümkündür.
Üye Erişim Düzeyi Bilindiği gibi public olarak tanımlanan üyeler, bir nesne referansı üzerinden nokta operatörü ile direkt olarak erişilebilirler. private olarak tanımlanan üyeler ise nesne referansı üzerinden erişilemezler. Bunun yerine nesnenin fonksiyonelliklerine yardımcı olmak için tanımlandığı sınıf içerisinden çağrılabilirler (örneğin yardımcı metotlar). protected üyeler, sınıf hiyerarşisi yani kalıtım söz konusu olduğunda faydalıdırlar. internal ya da protected internal üyeler ise .NET kod kütüphaneleri (*.dll, *.exe gibi) ile çalışıldığında kullanılırlar. Bu anahtar kelimelerin anlamlarını biraz daha ortaya çıkarmak için öğrendiğin erişim belirleyicilerini kullanan Uzak adında bir sınıf yazalım: namespace UyeErisimDuzeyleri { //Üye erişim seçenekleri class Uzak { //Her yerden erişilebilir. public void PublicMethod() { } //Sadece Uzak sınıfı içerisinden erişilebilir. private void PrivateMethod() { } //Aynı assembly içerisinden erişilebilir. internal void InternalMethod() { } //C# 2.0'dan itibaren varsayılan erişim belirleyicisi : private void BirMetot() { } } } Şimdi de Uzak sınıfından bir nesne örneği oluşturalım ve nokta operatörü ile nesne referansı üzerinden bütün metotları çağıralım.
public static void Main(string[] args) 40 / 59
{ //Uzak veri tipinden bir nesne örneği oluşturulur ve üyeler çağrılır. Uzak remote = new Uzak(); remote.PublicMethod(); remote.InternalMethod(); remote.PrivateMethod(); //Hata!!! remote.BirMetot(); //Hata!!! } Eğer bu program derlenirse private olarak işaretlenen üyelerin nesne üzerinden erişilemedikleri ve hata alındığı görülür. Tip Erişim Düzeyi Tipler (sınıf (class), yapı (struct), numaralandırıcı (enumeration), temsilci (delegate), arayüz (interface)) tasarlanırken erişim belirleyicisi alırlar. İsim alanı düzeyinde tasarlanan tipler, üyelerin aksine sadece public ve internal ile sınırlıdır. public erişim belirleyicisine sahip bir tip oluşturulduğunda tipin içerisinde bulunduğu assembly ya da tipi kullanan başka bir assembly içerisinden bu tipe erişileceği garanti edilmiş olur. //Bu tip herhangi bir assembly içerisinden kullanılabilir public class AcikSınif { } internal erişim belirleyicisine sahip bir tip, sadece tanımlandığı assembly içerisindeki diğer tipler ve üyeleri tarafından kullanılabilir. Yani içerisinde internal bir tip yer alan .NET kod kütüphanesini referans edip kullanan bir assembly, bu tipi göremez, dolayısıyla nesne örneği oluşturamaz. C# 2.0’dan itibaren bir tip için varsayılan erişim belirleyicisi internal olduğu için bilinçli olarak public erişim düzeyi kullanılmazsa internal bir tip oluşturulur. //Bu sınıflar sadece tanımlandıkları assembly içerisinde kullanılabilirler. internal class YardimciSinif { } //C#’da tipler için varsayılan erişim belirleyicisi : internal class KontrolSinifi { } C#’da iç içe tipler (nested types), isim alanı düzeyinde değil bir sınıf ya da yapı içerisinde tanımlanan tipleri ifade etmek için kullanılır. Bunlar tip olmasına rağmen tanımlandıkları sınıf ya da yapının üyesi gibidirler ve dolayısıyla iç içe tipler için bütün erişim düzeyleri geçerlidir.
41 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Kapsülleme (Encapsulation)
42 / 59
Kapsülleme (Encapsulation) Nesne yönelimli programlamanın ilk prensibi kapsülleme (encapsulation) olarak adlandırılır. Bu özellik, dilin, nesne kullanıcısından gereksiz uygulama ayrıntılarını saklayabilme yeteneği olarak ifade edilir. Örnek olarak .Net Framework temel sınıf kütüphanesi içerisinde yer alan, Open() ve Close() metotlarına sahip SqlConnection sınıfını ele alalım. //... //SqlConnection, veritabanına yapılan bağlantının detaylarını kapsüllemektedir. SqlConnection baglanti = new SqlConnection("server = London; database = AdventureWorks; integrated security = true"); baglanti.Open(); //Veritabanından veri çekilir. baglanti.Close(); //... SqlConnection sınıfı, veritabanına açılacak bağlantının sağlanması, yüklenmesi, yönetimi, kapanması gibi içsel detayları kapsüllemiştir. Nesne kullanıcısı kapsüllemeyi sever; çünkü programlama görevlerini daha kolay hale getirir. SqlConnection sınıfında olduğu gibi, nesnenin görevini yerine getirmesi için arka tarafta çalışan onlarca satır kodu düşünmeye gerek yoktur. Tek yapılması gereken nesne örneğinin oluşturulması ve uygun metotların çağrılmasıdır. Kapsüllemenin diğer bir işlevi ise veri korumadır. Kitapta buraya kadar kullanılan sınıfların alanları, public erişim düzeyi ile dış dünyaya açık bırakılmıştır. Ancak doğru olan, nesnelerin durum verilerini private erişim belirleyicisi ile koruma altına almaktır. Bu yolla dışarıdan alanın değeri elde edilmek ya da değiştirilmek istendiğinde bir kontrolden geçmesi zorunlu olur. Kapsüllemeye neden ihtiyaç duyulduğunu bir örnekle inceleyelim: namespace KapsullemeyeNedenIhtiyacDuyulur { //İki adet nesne alanından oluşan Kitap sınıfı class Kitap { public string kitapAdi; public double fiyat; } } Dış dünyaya açık yani public erişim belirleyicisine sahip alanların sorunu, ait oldukları sınıfın iş mantıklarını algılayabilme yeteneklerinin olmamasıdır. Dolayısıyla atanan değerin iş mantığına (business logic) uygun olup olmadığına dair herhangi bir kontrolün sınıf içerisinde yapılabilmesi mümkün olmamaktadır. Nesne kullanıcısı genelde sınıfı yazan kişi olmakla birlikte, harici birisi de olabilir. Dolayısıyla böyle bir kontrolü nesne kullanıcısı tarafında yapmak akla gelse de, bu çok efektif bir yol olmaz. Bilindiği gibi C# double değişkeni, maksimum yaklaşık 310 basamaklı tam sayı ya da ondalıklı bir değeri taşıyabilir. Bu yüzden derleyici bir kitabın fiyatı için aşağıdaki atamanın yapılmasına izin verir:
namespace KapsullemeyeNedenIhtiyacDuyulur 43 / 59
{ class Program { //İlginç... Bir cd, 120 milyar ytl!!! public static void Main(string[] args) { Kitap ktp = new Kitap(); ktp.kitapAdi = "Kızıl Nehirler"; ktp.fiyat = 120000000000; } } } Yukarıdaki kullanımda 120 milyar değeri, double tipinin sınırları içerisinde kalsa da gerçek hayatta bir kitap fiyatının bu rakamda olması imkansızdır. Görüldüğü gibi public alanlar, veri onaylama kurallarına zorlamak için herhangi bir yol sunmamaktadır. Eğer Kitap sınıfı, bir kitap fiyatının 0 ile 25 ytl olması gerektiğini belirten bir iş kuralına (business rule) sahip olsaydı, veri nesneye aktarılırken bu kural işlerdi. Sınıfların durum verileri olarak anılan alanlar, private erişim belirleyicisi alarak dış dünyaya kapatılabilir. Başlangıç değerleri ise, sınıf içerisinde tanımlandığı yerde ya da yapıcı metot yardımıyla verilebilir. Böyle bir kapsülleme tercih edildiğinde bazı durumlarda alanın değerinin dışardan okunması ve sadece içsel olarak sınıf üyeleri tarafından atanması gerekebilir. Alternatif olarak durum verileri üzerinde yapılacak değişiklikler de dışarıya açılmak istenebilir. Ancak değişiklikle birlikte gelen verinin kontrol edilerek istenmeyen durumların önüne geçilmesi gerekliliği ortadadır. Kapsülleme, durum verilerinin tutarlılığı için bir yol sunmaktadır. public alanlar tanımlamaktansa, alan verilerini private olarak tanımlamak alışkanlık haline getirilmelidir (Herhangi bir iş kuralı olmasa da). Bu durumda alanlar nesne kullanıcısına iki teknikle açılabilir:
Sınıf içerisinde nesne yönelimli diğer diller için geleneksel yol olan Erişen ve Değiştiren (Accessor and Mutator) metotlar tanımlamak. Sınıf içerisinde bir özellik (property) tanımlamak.
Hangi teknik kullanılırsa kullanılsın iyi kapsüllenmiş bir sınıf, ham verisini (yani alanlarını) ve onu nasıl yönettiğinin ayrıntılarını dış dünyanın kem gözlerinden gizlemelidir. Bu yaklaşımın güzel ve faydalı olan yanı, sınıfı yazan kişinin, çalışan kodu bozmadan kapalı kapılar ardında metodun uygulanışını değiştirmekte serbest olmasıdır (Bu yapılırken metodun imzasının değişmemesi gerekir). Geleneksel yol izlenerek yapılan kapsülleme için kullanılan metotlara literatürde Getter/Seter metotlar adı da verilmektedir.
Erişen ve Değiştiren (Getter and Setter) Metotlar Tanımlamak Bu tekniği UyeKontrol adında bir sınıf ile ele alalım. Bu sınıf içerisinde kullanıcı adı ve şifre için iki tane alan ve bunların doğruluğunu ilgili veri kaynağından kontrol eden bir metot yer alsın. Burada sadece kullaniciAdi alanına atanacak değerin kontrolüne odaklanılacağı için sınıfın diğer üyeleri yazılmamıştır.
namespace Kapsulleme { 44 / 59
class UyeKontrol { private string _kullaniciAdi; private string _sifre; //Getter method public string KullaniciAdiniIste() { return _kullaniciAdi; } //Setter method public void KullaniciAdiniBelirle(string kullaniciAdi) { //illegal karakterleri kontrol et, harf olmayan bir karakter olması durumunda değer ataması yapma //Max. uzunluk ya da buyuk-kucuk harf kontrolleri de yapılabilir. bool isOk = true; for (int i = 0; i < kullaniciAdi.Length; i++) { if (!char.IsLetter(kullaniciAdi[i])) { isOk = false; break; } } if (isOk) _kullaniciAdi = kullaniciAdi; else throw new ArgumentException("Kullanıcı adı içerisinde harf dışında başka karakter olamaz"); } //Diğer üyeler } } Bu geleneksel yolda alanlar private olarak işaretlenir. Nesne kullanıcısı alanın değerini görmek istediğinde geriye string veri tipinde değer döndüren public olarak işaretlenmiş KullaniciAdiniIste() metodunu çağırır. Bu metot _kullancıAdi alanında saklanan değeri geriye döndürür. Bu metodun içerisinde herhangi bir kontrol kodu yoktur fakat istenirse yazılabilir. Nesneye ait alanın değeri dışarıdan değiştirilmek istendiğinde ise direkt olarak değiştirilmemesi için KullaniciAdiniBelirle() metoduna yeni değer parametre olarak aktarılır. Bu metot alana atamayı hemen yapmaz, önce yeni değeri sistemin iş kuralına göre kontrol eder. Örnekte, bu iş kuralı, kullanıcı adının içerisinde harf dışında herhangi bir karakter kullanılmamasıdır. Parametre bu iş kuralını yerine getirebilirse, alana atama yapılır. Eğer parametrenin içerisinde harf dışında bir karakter varsa sisteme bir çalışma zamanı hatası fırlatılır. Böylece nesne, iş kuralını ihlal eden bir veri ile çalışmaya devam etmemiş olur. Aşağıda nesne kullanıcısına ait kod yer almaktadır:
namespace Kapsulleme { class Program 45 / 59
{ public static void Main(string[] args) { try { UyeKontrol uye = new UyeKontrol(); uye.KullaniciAdiniBelirle("emrahuslu"); Console.WriteLine("Üye adınız : {0}", uye.KullaniciAdiniIste()); } catch (ArgumentException exc) { //Sınıf içerisinden çalışma zamanı hatası fırlatılırken gönderilen mesaj ekrana basılır. Console.WriteLine(exc.Message); } Console.ReadLine(); } } }
Özellik (Property) Tanımlamak Geleneksel yolun aksine C#, kapsülleme için tip özelliklerini (type properties) tercih eder. Bir özellik; alan, metot vb. üyeler gibi sınıfa ait bir üyedir ve alanlarda saklı olan veriye erişim sunan bir veri gibi davranır. Nesne kullanıcısı özellikleri kullanarak alandaki değere erişmek ve atama yapmak için iki ayrı metot çağırmak yerine bunu sanki alanın kendisi ile çalışıyormuş gibi tek bir üye ile gerçekleştirir. Bu durum özelliklerin tanımlanması sırasında iki ayrı blok kullanarak sağlanır. Bunlardan get bloğu Erişen (accessor) metodun yerini tutarken, set bloğu Belirleyen (Mutator) metodun yerini tutar. namespace Kapsulleme { class UyeKontrol { private string _kullaniciAdi; private string _sifre; public string Sifre { get { return _sifre; } set { _sifre = value; } } public string KullaniciAdi { get { return _kullaniciAdi; } set { //Burada atama öncesi gerekli kontroller yapılır. Herşey yolunda ise atama gerçekleştirilir. _kullaniciAdi = value; } } 46 / 59
//Diğer üyeler } Özellikler ele alındığında, temelde Erişen/Belirleyen metotların çalışma mantığından farklı hiçbir şey yoktur. Aradaki tek fark özelliklerin iki metot uygulamasını tek bir çatı altında toplamasıdır. Özellikler, koruma altına aldığı alanı ile aynı veri tipinde olmak durumundadır. İsmi tamamen farklı olabilir; ancak genel kullanım, alan isminin büyük harfle başlayan şeklidir. Dışarıdan erişilmesi gerektiği için, erişim belirleyicisi, public veya duruma göre erişim sağlamaya imkan veren uygun bir belirleyici olmalıdır. İçerisindeki get bloğu, özelliğe erişilmeye çalışıldığında otomatik olarak çağrılır. Bu durumda set bloğu kesinlikle çalışmaz. Eğer nesne kullanıcısı, özelliği atama operatörün sol tarafında alana yeni bir değer vermek için kullanırsa bu sefer otomatik olarak set bloğu çalışır; get bloğu çalışmaz. set bloğunun geleneksel versiyonu olan Belirleyen metotta, alana atanmak istenen değer metoda parametre olarak aktarılıyordu ve bu parametre metot içerisinde kontrol için kullanılıyordu. Özelliklerde ise atama yapıldığında nesne kullanıcısından gelen değer parametre olarak gelmez; bunun yerine atanan değer set bloğu içerisinde value olarak ele alınmaktadır. value değeri nesne üzerinden yapılan atamada eşitliğin sağ tarafındaki değerdir. Veri tipi her zaman için özelliğin veri tipi ile uyumlu olmalıdır.
Özellikler, tek başlarına hem değer ataması hem de değere erişim imkanı sunarlar. Aşağıda özelliklerin nesne kullanıcısı tarafından ele alınışı yer almaktadır. namespace Kapsulleme { class Program { public static void Main(string[] args) { try { UyeKontrol uye = new UyeKontrol(); uye.KullaniciAdi = "emrahuslu"; Console.WriteLine("Üye adınız : {0}", uye.KullaniciAdi); } catch (ArgumentException exc) { //Sınıf içerisinden çalışma zamanı hatası fırlatılırken gönderilen mesaj ekrana basılır. Console.WriteLine(exc.Message); }
47 / 59
Console.ReadLine(); } } } Alana değer vermek için bir metot çağırmak yerine, programlamanın doğasına daha uygun olan atama operatöründen faydalanmak, özelliklerin geleneksel yola göre avantajlarından biri olarak sayılabilir. Ayrıca sayısal bir değer döndüren erişen metot üzerinden atama kısa yollarını kullanmak mümkün değilken, bi özellik üzerinden örneğin ++ operatörü kullanılabilir. UyeKontrol sınıfının, KullaniciAdi özelliğinin tam uygulanmış ve yapıcı metot desteği kazandırılmış versiyonunu yazalım. Böylece yapıcı metotlara alınan parametrelerin alanlara mı yoksa özelliklere mi atanacağı konusuna da açıklık getirelim. namespace Kapsulleme { class UyeKontrol { private string _kullaniciAdi; private string _sifre; // _kullaniciAdi alanı için kodlanan özellik public string KullaniciAdi { get { return _kullaniciAdi; } set { //illegal karakterleri kontrol et, harf olmayan bir karakter olması durumunda değer ataması yapma //Max. uzunluk ya da buyuk-kucuk harf kontrolleri de yapılbilir. bool isOk = true; for (int i = 0; i < value.Length; i++) { if (!char.IsLetter(value[i])) { isOk = false; break; } } if (isOk) _kullaniciAdi = value; else throw new ArgumentException("Kullanıcı adı içerisinde harf dışında başka karakter olamaz"); } } // _sifre alanı için kodlanan özellik public string Sifre { get { return _sifre; } set { _sifre = value; } 48 / 59
} //Yapıcı metottan alınan durum verileri, doğrudan _alanlara değil, varsa kontrolden geçmek üzere Özelliklere yönlendirilir. public UyeKontrol(string kullaniciAdi , string sifre) { this.KullaniciAdi = kullaniciAdi; this.Sifre = sifre; } public UyeKontrol() { } //Diğer üyeler } } Kod içerisinde yer alan notlarda da belirtildiği üzere özelliklerin içerisine (genelde set bloğuna) kodlanmış olan iş mantıklarının çalışması için yapıcı metoda alınan parametrelerin alanlara aktarılabilmelerine rağmen özelliklere aktarılmaları daha efektif olur. Ancak yapıcı metot üzerinden yapılan ilk atamada farklı bir iş mantığı uygulanmak istenebilir. Bu gibi durumlarda yapıcı metottan alınan değerler bu farklı iş kuralından geçirilerek doğrudan alana atanır. .NET Framework temel sınıf kütüphanesi, geleneksel Erişen/Belirleyen metotlar yerine tip özelliklerini tercih eder. Bu yüzden sınıf kütüphanesi ile sağlıklı bir şekilde etkileşen programlar için kendi tiplerimizi yazarken özelliklerden faydalanmak doğru bir davranış olacaktır. Özelliklerin, bir alanın değerini işaret edip, alana değer atamaları zorunlu değildir. Bir alan olmadan da özelliklerden faydalanılabilir.
Özelliklerin get/set Bloklarının Erişim Düzeyini Kontrol Etmek C# 2.0 öncesinde get ve set bloklarının erişim düzeyleri ayrı ayrı belirlenemezdi. Her ikisi de, özelliğin tanımlamasında belirtilen erişim düzeyine sahip olurdu. //get ve set bloklarının erişim düzeyi Sifre özelliğinin erişim düzeyi olan public oldu. public string Sifre { get { return _sifre; } set { _sifre = value; } } Bazı durumlarda get ve set bloklarının erişim düzeylerin ayrı ayrı belirlemek faydalı olabilmektedir. Bunu yapmak için get ya da set anahtar kelimelerine ön ek olarak erişim belirleyicisi yazılabilir. Erişim belirleyicisi belirtilmeyen blok, özelliğin erişim düzeyine sahip olur. //Nesne kullanıcısı sadece Sifre özelliğinin değerini görebilir. Ancak sadece bu sınıfın diğer üyeleri, değerini değiştirebilir. public string Sifre { get { return _sifre; } private set { _sifre = value; } }
49 / 59
Bu durumda Sifre özelliğinin set bloğu sadece içinde bulunduğu sınıftan çağrılabilmekte; nesne örneği üzerinden çağrılamamaktadır. Özelliklerin get ve set bloklarına koyulacak erişim belirleyicisi, özelliğin erişim düzeyinden daha kısıtlayıcı olmalıdır. Bu yüzden public erişim düzeyi bilinçli olarak yazılamamaktadır.
50 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Kalıtım (Inheritance)
51 / 59
Kalıtım (Inheritance) Kapsüllenmiş, tek başına bir sınıf geliştirme tekniğini öğrendikten sonra, şimdi sıra birbirleriyle ilişkili sınıf aileleri oluşturmaya geldi. Nesne yönelimli programlamanın ikinci prensibi olan kalıtım (inheritance), dilin varolan sınıf tanımlamalarının üzerine yeni bir sınıf tanımlaması inşa edilmesine izin verme yeteneğidir. Kalıtım, bir temel sınıfın (base class ya da parent class) özellik ve davranışlarını, bir alt sınıfın miras alarak sanki kendi üyeleriymiş gibi kullanmasıdır.
Sınıflar arası ilişki Bu şekil, diagramdan şu şekilde okunabilir : Müdür bir çalışandır, çalışan ise bir object’dir. Sınıflar arasında bu tarz bir ilişki oluşturulduğu zaman, tipler arasında bir bağlılık inşa edilmiş olur. Kalıtım fikrinin altında yatan en basit fikir, yeni bir sınıfın başka sınıfların fonksiyonelliklerine sahip olması ve muhtemelen genişletmesidir. .NET dünyasında her tip, eninde sonunda ortak bir temel sınıftan (base class) türer: System.Object. Object sınıfı, .NET ekosisteminde yer alan bütün tipler tarafından desteklenen üyeler kümesini tanımlamaktadır. Herhangi bir temel sınıf belirtmeden tanımlanan bir sınıf, Object sınıfından türetilmiş olur. namespace Kalitim { //Otomatik olarak System.Object sınıfından türer. class Calisan { } } Eğer yapılan kalıtımın, dikkat çekmesi istenirse, türeyen sınıftan (derived class) sonra koyulan iki nokta üst üste (:), bir tipin temel sınıfının ne olduğunun belirtilmesine izin verir: namespace Kalitim { //Her iki kullanım da doğrudur. class Calisan : System.Object { } //ya da class Calisan : object { } } Object sınıfı, nesne düzeyinde ve sınıf düzeyinde (statik) birçok üye tanımlamaktadır.
52 / 59
Var olan bir tipe yeni fonksiyonellikler eklenmek istendiğinde veya birbiriyle ilişkili sınıfların birçok ortak üyesi bulunması durumunda kalıtıma başvurulabilir. Kalıtımın uygulandığı yerde, bir temel sınıftan türeyen sınıflar, bütün temel sınıf üyelerini miras alırlar. .NET Framework içerisinden örnek vermek gerekirse System.Windows.Forms isimalanı altında yer alan Control sınıfı, kendi yazacağımız bir windows kontrolünün ve yine aynı isimalanı altında yer alan Button, TextBox gibi var olan windows kontrollerinin temel sınıfı olarak kullanılmaktadır. Kendi kalıtım hiyararşimizi kullanmak için şöyle bir örneği ele alalım: Calisan adındaki bir sınıf, bir şirkette çalışan bütün personel çeşitlerinin tasarlanmasında temel sınıf (base class) olarak kullanılsın. Bu örnekte, personel çeşitlerinden sadece Mudur ve SatisElemani ele alınacaktır. Bütün personeller için ortak olacak üyeler; personel sigorta no, personel adı ve maaşı olarak belirlenebilir. Müdür, bir personel için düşünülen bu 3 üyeye kalıtım yoluyla sahip olurken, departmanının yaptığı kar miktarı bilgisini bünyesinde barındırarak, tasarlanan Calisan sınıfını genişletmiş olacak. Benzer şekilde satış elemanı ise yaptığı satış miktarını tutuyor olacak. namespace Kalitim { class Calisan { private int _sskNo; private string _adi; private double _maasi; public int SskNo { get { return _sskNo; } set { _sskNo = value; } } public string Adi { get { return _adi; } set { _adi = value; } } public double Maasi { get { return _maasi; } set { _maasi = value; } } } } C# dilinde bir sınıften türetmek, sınıf tanımlamasında sınıf adının ardından gelen ikinokta üst üstenin ardından temel sınıf adının yazılması ile gerçekleşir.
namespace Kalitim 53 / 59
{ class Mudur : Calisan { //Bir müdür, departmanın ne kadar kar yaptığını bilmelidir. private double _departmanKar; public double DepartmanKar { get { return _departmanKar; } set { _departmanKar = value; } } } class SatisElemani : Calisan { //Bir satış elemanı ne kadar satış yaptığını bilmelidir. private int _satisSayisi; public int SatisSayisi { get { return _satisSayisi; } set { _satisSayisi = value; } } } }
Artık sınıflar arasında bir ilişki kurulmuş oldu. Mudur ve SatisElemani sınıfları, otomatik olarak, Calisan sınıfının bütün public ve protected üyelerine miras yoluyla sahip oldular. Şimdi bir örnekle, türeyen sınıflardan nesne örnekleri oluşturup, temel sınıf üyelerine erişebildiklerini görelim:
Türeyen sınıflar otomatik olarak temel sınıflarının üyelerine erişirler.
namespace Kalitim 54 / 59
{ //Bir türeyen sınıf nesnesi oluşturulup temel sınıf üyelerine erişilir. class Program { public static void Main(string[] args) { //Yeni bir Mudur nesnesi oluşturulur. Mudur cem = new Mudur(); //Calisan temel sınıfından miras alınan üyeler cem.SskNo = 82194482; cem.Adi = "Cem Suskun"; cem.Maasi = 1000; //Mudur sınıfı tarafından tanımlanan üyeler cem.DepartmanKar = 115000; Console.ReadLine(); } } }
55 / 59
EĞİTİM : NESNE YÖNELİMLİ PROGRAMLAMA (OBJECT ORIENTED PROGRAMMING)
Bölüm : Nesne Yönelimli Programlamaya Giriş Konu : Çok Biçimlilik (Polymorphism)
56 / 59
Çok Biçimlilik (Polymorphism) Nesne yönelimli programlamanın son prensibi çok biçimlilik (polymorphism) olarak adlandırılır. Çok biçimliliği incelemek için Calisan temel sınıfına ZamYap adında bir metot ekleyelim. namespace CokBicimlilik { class Calisan { //Diğer üyeler public void ZamYap(double zamMiktari) { //Atama, doğrudan temel sınıftaki üyeye yapılır. _maasi += zamMiktari; } } } Bu üye public olarak tanımlandığı için hem müdürlere hem de satış elemanlarına zam yapılabilir! namespace CokBicimlilik { class Program { public static void Main(string[] args) { //Her çalışana zam yapılsın. Mudur mdr = new Mudur(82119944, "Tarık Ali", 1000, 20000); mdr.ZamYap(250); Console.WriteLine("mdr.Maasi : {0}", mdr.Maasi); SatisElemani sts = new SatisElemani(80332398, "Sena Sevim", 750, 220); sts.ZamYap(150); Console.WriteLine("sts.Maasi : {0}", sts.Maasi); Console.ReadLine(); } } } Bu tasarımdaki problem, türeyenlerin kalıtarak kullandığı ZamYap() metodunun uygulanışının bütün türeyenler için benzer olmasıdır. Yani Calisan sınıfında yer alan ZamYap() metodunun uygulanışı hem Mudur hem de SatisElemani için aynıdır. Halbuki pozisyona ve performansa göre zam stratejisinin belirlenmesi daha anlamlıdır. Örneğin; müdürlere yapılacak zam, departmanının yaptığı kar miktarına bağlı olarak yüzdelerle belirlenebilir ya da satış elemanına verilecek zam miktarı yaptığı satış sayısı ile doğru orantılı olarak belirlenebilir.
57 / 59
Temel Sınıfın Davranışlarını Ezmek : virtual ve override Çok biçimlilik, türeyen sınıf tarafından temel sınıfta yer alan bir üyenin (metot, özellik, veya olay) uygulanışının nasıl değiştirileceğine dair bir yol sunmaktadır. Bu yoldan ilerlemek için öncelikle virtual ve override anahtar kelimelerinin çalışma mekanizmalarının anlaşılması gerekir. Bir temel sınıf içerisinde tanımlanan üyenin uygulanışının -yani üyenin içerisindeki kodların-, kendisinden türeyen sınıflar tarafından değiştirilebilmesi isteniyorsa, bu üye virtual anahtar kelimesi ile işaretlenmelidir. namespace CokBicimlilik { class Calisan { //ZamYap() metodu varsayılan olarak, bir uygulama mantığına sahip. Türeyen sınıflar bu uygulamayı değiştirmek ve aynen kullanmak konusunda özgürler. public virtual void ZamYap(double zamMiktari) { this._maasi += zamMiktari; } public virtual string BilgiVer() { return this.SskNo + " / " + this.Adi + " / " + this.Maasi; } //Diğer üyeler } } Türeyen bir sınıf, sanal (virtual) bir üyenin uygulanışını kendi sınıfına ait bir iş mantığıyla değiştirmek isteyebilir; ancak zorunda değildir. İstenirse temel sınıftaki uygulanış biçimi aynen miras alınıp kullanabilir. Üyenin başına override anahtar kelimesi yazılarak yeniden kodlanması ile böyle bir değişiklik sağlanır. Örneğin SatisElemani sınıfı ZamYap() ve BilgiVer() metodunun uygulamasını değiştirsin, Mudur ise Calisan sınıfındaki uygulamayı aynen değiştirmeden kullansın. namespace CokBicimlilik { class SatisElemani : Calisan { //Diğer üyeler public override void ZamYap(double zamMiktari) { double ekstraZam = 0; if ((SatisSayisi > 0) && (SatisSayisi < 50)) { ekstraZam = 30; } this._maasi += (zamMiktari + ekstraZam); } public override string BilgiVer() { 58 / 59
string temelBilgi = base.BilgiVer(); temelBilgi += " / " + this.SatisSayisi; return temelBilgi; } } }
Bu ders notu, Açık Akademi projesi çerçevesinde TCM tarafından Microsoft Türkiye için hazırlanmıştır. Tüm hakları Microsoft Türkiye’ ye aittir. İzinsiz çoğaltılamaz, para ile satılamaz. 59 / 59