Bir yapılandırma (build) işleminin ardından derlenen kodu ve kodla ilgili diğer dosyaları ihtiva eden bir dosya oluşur. Bu dosyaya yapı (artifakt) ismi verilir. Yapı bünyesinde uygulamanın çalışır şekli yer alır. Java dünyasında yapılar jar, war, ear gibi ZIP kökenli dosyalar içinde yer alır. Microsoft dünyasında yapılar dll, Linux dünyasında rpm, tar ya da gz formatındadır.
Bu yazımda oluşturulan yapıların nasıl versiyonlandırıldığını ve bunun neden gerekli olduğunu örnekler üzerinden göstermek istiyorum. Öncelikle yapının ne olduğunu ve ne ihtiva ettiğini inceleyelim.
Java dilinde aşağıdaki sınıfı oluşturduğumuzu düşünelim:
[sourcecode language=”java”]
import java.io.File;
public class FileUtils {
public static void copy(File file, File dir){
// …
}
}
[/sourcecode]
FileUtils sınıfında yer alan copy metodunu bir dosyayı bir dizine kopyalamak için kullanabiliriz. Bu çok temel bir fonksiyon gibi görünüyor, yani bu fonksiyonu projemizde ya da başka bir proje bünyesinde dosya kopyalama işlemleri yapmak için kullanabiliriz. Başka bir projede bu sınıfı kullanmak istediğimizde, bu sınıfı ihtiva eden bir jar dosyası oluşturmamız gerekiyor. Jar dosyası aşağıdaki formatta olacaktır:
[sourcecode language=”java”]
fileutils.jar
[/sourcecode]
Eger .Net dünyasında olsaydık, oluşturduğumuz kütüphaneyi şu şekilde başkalarına verirdik:
[sourcecode language=”java”]
fileutils.dll
[/sourcecode]
Linux dünyasında bu kütüphaneyi şu şekilde başkalarıyla paylaşırdık:
[sourcecode language=”java”]
fileutils.so
[/sourcecode]
fileutils.jar/dll/so dosyasını tekrar kullanılabilir yapıda bir kod birimi olarak düşünebiliriz. Bu kütüphaneyi kendi projemize ekledikten sonra, FileUtils sınıfında yer alan copy fonksiyonunu dosya kopyalama işlemleri için kullanabiliriz. Başka birisi tarafından oluşturulan fonksiyon kütüphanelerini bu şekilde tekerleği tekrar icat etmek zorunda kalmadan kullanabiliriz. Nitekim bir Java uygulamasının çok büyük bir bölümü (Hibarnete, Spring, Jdbc…) tekrar kullanılabilir fonksiyon kütüphanelerinden oluşmaktadır.
Şimdi şu senaryoyu hayal edelim. fileutils.jar fonksiyon kütüphanesini kullanılmak üzere başka projelerle paylaştık. Bize copy ya da sunduğumuz başka bir fonksiyon bünyesinde bir hata olduğu bilgisi ulaştı. Bu durumda hatayı giderdikten sonra, fileutils.jar dosyasını tekrar oluşturup, kullanıcı projelerle tekrar paylaşmamız gerekiyor. Bu çok zahmetli bir iş gibi görünse de, Maven ya da Ivy yapı araçlarıyla kütüphane dağıtımını otomatize edebiliriz.
FileUtils sınıfını oluşturup, fileutils.jar dosyasında dağıtmaya başladığımız andan itibaren bir uygulama arayüzü (API – Application Programming Interface) oluşturmuş olduk. FileUtils sınıfında yer alan her public fonksiyon bu arayüzün bir parçasıdır. Herkes bu arayüzü kullanarak, mevcut kodun ve kütüphanenin tekrar kullanılmasını sağlayabilir. Bunun yanı sıra fileutils.jar bünyesinde yer alan her public fonksiyon ya da metot oluşturduğumuz bu uygulama arayüzüne dahildir.
Başka programcılar kendi projelerinde fileutils.jar kütüphanesinde yer alan herhangi bir fonksiyon ya da metodu kullanmaya başladıklarında, oluşturduğumuz uygulama arayüzüne bağımlı hale gelirler. Bu noktadan itibaren fileutils.jar kütüphanesinden sorumlu programcı olarak, bu kütüphane bünyesinde yer alan public fonksiyon ya da metotlar üzerinde istediğimiz şekilde değişiklik yapamayız. Bunu yaparsak, çalışmaz hale gelen diger projelerdeki yazılımcıların ne kadar sinirleneceği sanırım tahmin edilebilir. Örneğin copy fonksiyonunu aşağıdaki şekilde değiştirmemiz mümkün değildir. Eğer bunu yaparsak, yeni fileutils.jar sürümünü alan ve copy fonksiyonunu kullanan her uygulama çalışmaz hale gelecektir, çünkü copy fonksiyonunun eski haline göre derlenmişlerdir.
[sourcecode language=”java”]
import java.io.File;
public class FileUtils {
public static void copy(File[] files, File dir){
// …
}
}
[/sourcecode]
Bu noktada şu sonuçları çıkarmak mümkün:
- Programcı public fonksiyon ya da metot oluşturmamaya dikkat etmelidir. Public olan bir fonksiyon ya da metodu herkes kullanabilir. Bu fonksiyon ya da metot sahibinin oluşturduğu fonksiyon ya da metodu yaşam süresi boyunca desteklemesi gerektiği anlamına gelmektedir. Java metotlara erişimi public ve private gibi direktiflerle desteklemektedir. Bu desteği sağlamayan dillerde tanımlanan tüm fonksiyon ve metotlar public konumundadır. Bu durumda kütüphane oluşturularak, bu kod birimlerinin dağıtılması, programcısının destek sağlaması anlamına gelmektedir.
- Kod birimlerinin tekrar kullanılabilir yapıda olmalarınını ya da kullanıcı ile kullanılan arasındaki ilişkiyi tanımlamak için bir uygulama arayüzü (API) oluşturulmalıdır. Bu çoğu zaman fonksiyon ve metotların public olmaları gerektiği anlamına gelmektedir. Bir uygulama arayüzü oluşturma niyeti, bu arayüzün bakımı ve geliştirilmesi için gerekli desteğin sağlanmasını da kapsamalıdır.
- Oluşturulan API’nin başka uygulamalar bünyesinde kullanılmasını sağlamak amacıyla jar ya da herhangi başka bir formatta bir kütüphane dosyasına yerleştirilmesi gerekmektedir. Sadece bu şekilde derlenmiş kod başka projelerde doğrudan kullanılabilir.
Uygulama arayüzleri oluşturulması görüldüğü gibi beraberinde bazı sorumluluklar getirmektedir. Bu arayüzleri kullanıcı ile kullanılan arasında bir nevi anlaşma metni olarak düşünebiliriz. Kullanılan taraf neyin, nasıl kullanılacağını oluşturduğu arayüzler aracılığı ile ifade etmektedir. Kullanıcı taraf bu arayüzleri kullanarak, bağımlı hale gelmekte birlikte, anlaşma metnine dayanarak anlaşmanın bozulmamasını arzu etmektedir. Bu anlaşma metnine göre kullanılan taraf kullanım şeklinde arayüzde tanımlananın haricinde değişiklik yapamaz. Şimdi hangi durumlarda böyle değişikliklerin yapılabileceğini inceleyelim.
Bir fonksiyon kütüphanesin versiyon kullanılmadan dağıtılması durumunda, API’lerde meydana gelen değişikliklerden dolayı kullanıcılarını olumsuz etkileyebileceğini gördük. Bunun önüne geçmek için versiyon numaraları kullanılabilir. Örneğin FileUtils sınıfını ihtiva eden bir fonksiyon kütüphanesinin şu şekilde bir sürümünü oluşturabiliriz:
[sourcecode language=”java”]
fileutils-0.1.0.jar
[/sourcecode]
fileutils-0.1.0.jar FileUtils sınıfının ilk sürümünü ihtiva etmektedir. Herhangi bir kullanıcı fileutils-0.1.0.jar kütüphanesini alarak, kendi projesinde kullanabilir. Şimdi FileUtils sınıfı üzerinde bir değişiklik yaptığımızı düşünelim. Örneğin copy fonksiyonunu değiştirmiş ya da başka bir fonksiyon eklemiş olalım. Bu durumda aşağıdaki sürümü oluşturmamız gerekmektedir:
[sourcecode language=”java”]
fileutils-0.2.0.jar
[/sourcecode]
Bu durumda iki değişik fonksiyon kütüphanesi sürümüne sahip olduk:
[sourcecode language=”java”]
fileutils-0.1.0.jar
fileutils-0.2.0.jar
[/sourcecode]
İsteyen 0.1.0 ya da 0.2.0 sürümünü kullanabilir. Bu yöntemin sağladığı avantajlar şunlardır:
- fileutils-0.1.0.jar kullanıcısı fileutils-0.2.0.jar bünyesinde meydana gelen değişikliklerden etkilenmez, çünkü fileutils-0.1.0.jar sürümüne sahiptir. Eğer duyduğu bağımlılık fileutils.jar formatında olsaydı, yeni bir fileutils.jar sürümü ile değişikliklerden etkilenebilirdi. Bu şekilde fileutils-0.1.0.jar isimli sürümü kullanarak, hayatını sürdürebilir ve diğer değişikliklerden etkilenmez.
- Kullanıcı istediği zaman başka bir sürüme geçme avantajına sahiptir. Örneğin fileutils-0.1.0.jar kullanıcısı fileutils-0.2.0.jar ile gelen yeni copy metodunu kullanmak isterse, bağımlılığını fileutils-0.2.0.jar şeklinde değiştirerek, yeni copy fonksiyonunu kullanmaya başlayabilir.
- fileutils-0.1.0.jar kütüphanesini oluşturan programcı, fileutils.jar kullanıcılarını olumsuz bir şekilde etkilemek zorunda kalmadan oluşturduğu API üzerinde değişiklik yapabilir.
- Versiyon numaraları sayesinde aynı API’nin değişik sürümlerini kullanmak mümkün hale gelmektedir.
Versiyonlamış sürümlerin sağladığı avantajları gördük. Sürümlerin neden versiyonlanmaları gerektiğine Tekrar Kullanım ve Sürüm Eşitliği (REP – Reuse-Release Equivalence Principle) tasarım prensibi işaret etmektedir. Buna göre tekrar kullanımı kolaylaştırmak için paket sürümlerinin oluşturulması şarttır. REP’e göre tekrar kullanılabilirlik (reuse) sürüm (release) ile direk orantılıdır. Sürüm ne ihtiva ediyorsa, o tekrar kullanılabilir.
Şimdi sürüm numaralarının nasıl yönetilmesi gerektiği konusuna değinmek istiyorum. Şimdiye kadar verdiğim örneklerde üç rakamdan oluşan bir versiyon numarası kullandım. Versiyon numarası şu bölümlerden oluşmaktadır:
[sourcecode language=”java”]
MAJOR.MINOR.BUGFIX
[/sourcecode]
- MAJOR: Büyük versiyon numarasıdır. Sadece API üzerinde değişiklikler yapıldığında artırılır.
- MINOR: Küçük versiyon numarasıdır. Uygulamaya yeni özellikler (feature) eklendiğini artırılır
- BUGFIX: Giderilen hataları gösteren versiyon numarasıdır. Her bugfix işlemi ardından artırılır.
Versiyon numaralarının nasıl artırıldığını bir örnek üzerinde birlikte inceleyelim. Kısa bir zaman önce fileutils-0.1.0.jar sürümünü oluşturduğumuzu düşünelim. İsteyen her proje fileutils-0.1.0.jar dosyasını bağımlılık olarak kullanmaya başlamış olsun. Kısa bir zaman sona kullanıcılarımızdan birisi bir hata keşfeder ve bize durumu bildirir. Bu durumda hatayı giderdikten sonra, yeni bir sürüm oluşturmamız gerekmektedir. Sadece yeni sürüm ile kullanıcılarımız hatanın giderilmiş haliyle çalışabilirler. Hatayı giderdikten sonra oluşturmamız gereken sürüm şu şekilde olmalıdır:
[sourcecode language=”java”]
fileutils-0.1.1.jar
[/sourcecode]
Tekrar bir hatanın keşfedildiği haberini alıyoruz. Bu durumda yeni sürüm şöyle olur:
[sourcecode language=”java”]
fileutils-0.1.2.jar
[/sourcecode]
Görüldüğü gibi hatalar giderildiğinde sadece BUGFIX numarası artırılmaktadır. Kütüphanemize yeni bir fonksiyon eklediğimizde, sürüm numarası şu şekilde olmalıdır:
[sourcecode language=”java”]
fileutils-0.2.0.jar
[/sourcecode]
Kütüphanemize yeni bir özellik (örneğin yeni bir copy fonksiyonu) ekledikten sonra, MINOR numarasını artırdık ve BUGFIX numarasını sıfırladık. Oluşturduğumuz bu yeni sürümde bir hata olursa, bu hatayı düzelttikten sonra aşağıdaki sürümü oluşturmamız gerekir:
[sourcecode language=”java”]
fileutils-0.2.1.jar
[/sourcecode]
Yeni bir fonksiyon ile sürüm aşağıdaki versiyon numarasını taşımaya başlar:
[sourcecode language=”java”]
fileutils-0.3.0.jar
[/sourcecode]
Şimdi MAJOR versiyon numarasının ne zaman değiştirilmesi gerektiği konusunu inceleyelim. Eğer mevcut bir API’yi bir önceki sürümünde yer aldığı hali ile uyuşmaz hale getirirsek, yani API’yi değiştirirsek, bu durumdan kullanıcılarımızı haberdar etmemiz gerekir. Bunu MAJOR versiyon numarasını yükselterek yapabiliriz. fileutils-0.1.0.jar sürümünde copy fonksiyonu şu şekilde yer almaktadır:
[sourcecode language=”java”]
import java.io.File;
public class FileUtils {
public static void copy(File file, File dir){
// …
}
}
[/sourcecode]
Ne yazık ki başka alternatifimiz olmadığı için copy fonksiyonunu şu şekilde değiştirmemiz gerekiyor:
[sourcecode language=”java”]
import java.io.File;
public class FileUtils {
public static void copy(File[] file, File dir){
// …
}
}
[/sourcecode]
Görüldüğü gibi API’mizi oluşturan FileUtils.copy() metodunun parametre listesi değişti. Bu durumda sürüm numarası şu şekilde olmamalıdır:
[sourcecode language=”java”]
fileutils-0.2.0.jar
[/sourcecode]
fileutils-0.1.0.jar sürümünü kullanan bir proje fileutils-0.2.0.jar sürümüne baktığında, kütüphanenin sahip oldugu API’de bir değişiklik olmadığını, kütüphaneye sadece yeni bir özelliğin eklendiğini düşünecektir. Proje fileutils-0.2.0.jar sürümüne geçtiği zaman yazılımcılar copy fonksiyonunun düşündükleri gibi çalışmadığını görürler, çünkü bu fonksiyon değişikliğe uğramıştır. API değişmiştir. kullanıcılarımıza API değişikliklerini göstermek için MAJOR numarasını artırmamız gerekmektedir. API değişikliğinden sonra sürüm numarası ku şekilde olmalıdır:
[sourcecode language=”java”]
fileutils-1.0.0.jar
[/sourcecode]
Bu durumda fileutils-0.1.0.jar kullanıcısı fileutils-1.0.0.jar sürümüne baktığında, kullandığı API’nin değişmiş olabileceğini görebilir. Eğer isterse fileutils-1.0.0.jar sürümüne geçebilir. Ama bu durumda gerekli değişiklikleri yaparak, uygulamasını yeniden derlemesi gerekmektedir. Eğer bu değişiklikleri yapmak istemezse ya fileutils-0.1.0.jar sürümünde kalabilir ya da bu sürüm ile uyumlu olan başka bir sürüm bakar. fileutils-0.1.0.jar ile aşağıdaki sürümler uyumludur:
[sourcecode language=”java”]
fileutils-0.1.1.jar
fileutils-0.1.2.jar
fileutils-0.1.3.jar
fileutils-0.2.1.jar
fileutils-0.2.2.jar
fileutils-0.3.0.jar
fileutils-0.4.0.jar
fileutils-0.5.0.jar
fileutils-0.6.0.jar
[/sourcecode]
Bu sürümlerin hepsinde fileutils-0.1.0.jar bünyesinde yer alan copy fonksiyonu değişmemiştir. Kütüphaneye sadece yeni özellikler eklenmiştir. Sürüm numarasına bakarak, yapılan değişiklikleri kabaca görmek mümkündür. fileutils-1.0.0.jar dosyasına baktığımızda, kullandığımız API’nin değişmiş olabileceğini görüyoruz. Bu değişiklik doğrudan kullandığımız copy fonksiyonu ile ilişkili olmayabilir. Örneğin Java dilinde interface sınıfları üzerinde yapılan değişiklikler API değişikliği olarak sınıflandırılır ve yeni sürümlerde mutlaka MAJOR sürüm numarası artırılır.
Şimdi kısaca sürüm oluşturulurken dikkat edilmesi gereken konulara değinmek istiyorum. Bunlar:
- fileutils-1.0.0.jar şeklinde yeni bir sürüm oluşturulduktan sonra, bu sürüm dosyasının değişikliğe uğramaması için gerekli tüm tedbirler alınmalıdır. Örneğin birisi fileutils-2.1.0.jar sürümünü alıp, fileutils-1.0.0.jar şeklinde degistirdikten sonra, bu sürümü fileutils-1.0.0.jar olarak deklare edememelidir. Kullanıcılar ilk etapta versiyon numarasına bakarak, hangi sürümü kullanmaları gerektiğine karar verirler. Sürüm numarasının sürüm içinde olan fonksiyonları ve değişiklikleri yansıtmaması durumunda, kullanıcılar potansiyel API değişikliklerinden dolayi çalışmaz hale gelebilirler.
- Yeni bir kütüphanenin sürüm numarası 0.1.0 ile başlamalıdır. 0.0.1 sürüm numarası oluşan yeni kütüphane için bugfix yapıldığı anlamına gelir ki bu doğru değildir.
- Bir fonksiyon kütüphanesinin değişik versiyonları bir yazılım deposu (repository) bünyesinde kullanım için tutulmalıdır. Bu şekilde kullanıcılar API değişikliklerinde yeni sürümü kullanmak zorunda bırakılmazlar. Hangi sürümü kullanacağına kullanıcı karar verebilmelidir.
Bir sürüm dosyasına baktığımızda, içinde neler olduğunu ilk bakışta görmek mümkün degildir. Versiyon numaraları kullanılarak, hangi sürüm dosyasının kullanılabileceğini kestirmek mümkün hale gelmektedir.
EOF (End Of Fun)
Özcan Acar
Yorumlar
“Versiyon ve Sürüm Numaraları Nasıl Oluşturulur?” için 4 yanıt
Ellerinize sağlık, anlaşılır, sade ve oldukça bilgilendirici bir yazı olmuş.
Teşekkür ediyorum.
Harika bir yazı yeni bir genel kültür edindim sağolasın abicim.
Bu yazı bir harika dostum 🙂
Kıymetli bilgileriniz için teşekkürler hocam.