Gözlemci (Observer) tasarım kalıbı
November 15, 2007 von ilkerium
Bu yazıda tasarım kalıplarından gözlemci kalıbını tanıtmaya çalışacağım. Yazıya teorik bilgilerden çok yazılım hayatından alınmış, sadeleştirilmiş bir örnek olay üzerinden bir probleme çözüm bulma çabası hakim olacak. O yüzden lafı fazla uzatmadan, teorik tanımlamalara girişmeden yazıya temel oluşturacak durumu tanıtalım:
Başlangıç noktası: Dernek yazılımı
Yazılım yolunda yeni emeklemeye başlamış kişiler olarak bir arkadaşımızın hatırı için işlettiği derneğe bir yazılım paketi sunduk. Bu yazılım paketinde istenen önemli bir özellik de dernek üyelerini planlanan bir eğitim programından haberdar etmesi. Derneğin internet sitesinde yaptıkları kendi kişisel ayarlarına göre üyeler planlanan kurslara reaksiyon gösterme imkanına sahip (yeni duyurulan kurslar hakkındaki genel bilgileri otomatik olarak e-postalarına gönderme gibi). Derneğin kullanımına sunduğumuz paketteki bu fonksiyonaliteyi aşağıdaki (basitleştirilmiş) model ile geliştirdik:
İzlediğimiz yol aslında basitti: Realitedeki ögeleri bire bir modelimize yansıttık ve tasarladığımız ögeleri gerekli fonksiyonalite ile donattık: Club nesnesi addMember yordamı ile bir üye ekleme yeteneğine sahip. Örneğin basit kalması için üyeleri bir listede (ArrayList) depoluyoruz. Bu yordamın bir sonucu olarak ortaya çıkan Member nesnesi parametrelerde verilen özellikleri yansıtmanın yanında coursePlanned yordamıyla planlanmış bir kurs durumunda ayarlara göre çeşitli işler yapma özelliğine sahip. Bu yordam ile planlanan kursların üyelere duyurulması da Club nesnesindeki pronounceCourse yordamında geliştirilmiş durumda:
...
public class Club{
private ArrayList<Member> members;
...
private void pronounceCourse(Course course){
for(Member member : members){
member.coursePlanned(course);
}
}
...
}
Kullanıma sunulan paketteki eksikliklere teker teker girmeyeceğiz. Kendimizi pronounceCourse yordamıyla sınırlandırıp, geliştirmemiz istenen yeni bir fonksiyonalite hakkında kafa yorarken modeldeki diğer bazı eksikleri de giderme fırsatımız olacak.
İstekler hiçbir zaman bitmez
Sunduğumuz yazılım paketinden dernek memnun kaldı. Fakat bir süre sonra dernekte eğitim hizmetlerini üye olmayan insanlara da açma kararı aldılar. Bu da sistemde bir değişiklik gerektiriyordu. İsteyen herkes internet sitesi üzerinden kendisini sisteme kaydedebilmeli ve planlanan kurslardan e-posta yoluyla haberdar edilmeliydi. Ayrıca eğitim hizmetlerinin duyurulması eklentiler yoluyla ilerletilebilmeliydi. Bu yolla ileride yeni kitleler tanımlayıp, bu sisteme onları da entegre etmeyi planlıyorlardı (mesela belirli firmaları da sisteme entegre edip kursları duyurma gibi). İleride planladıkları bu eklentilerin bütünüyle içeriğini bilemedikleri için de sistemin bu konuda ilerletilebilir olmasını istiyorlardı.
Bunun üzerine geliştirdiğimiz yazılım üzerine daha ayrıntılı düşünmeye başladık. Aşağıdaki noktalar ilk etapta göze çarpanlar:
pronounceCourseyordamında belirli bir somut sınıfı (Member) hedef almakta ve kullanmaktayız. Kursların duyurulması istenen her yeni nesne tipinde kaynak kodunu değiştirmek zorundayız.- Her yeni eklenen ve kurslardan haberdar edilmesi istenen nesne tipinde bu nesnelerin toplanması için
Clubsınıfı içinde yeni birArrayListbelirlememiz ve ekleme/silme (addXYZ/removeXYZ) yordamları sunmamız gerekir. - Yeni bir kitle belirlendiğinde
Membersınıfında bulunan bazı temel özellikleri (isim, adres, eposta vs.) o sınıfta da sıfırdan geliştirmemiz gerekecek.
Aslında tespit ettiğimiz kusurların hepsinin çözümü bağlaşımı kesme (decoupling) ile gevşek bağlaşım (loose coupling) prensiplerini uygulamaktan geçiyor. Bunun da yolu kabaca tarif etmek gerekirse, kaynak kodumuzda sınıfları değil de belirli bir fonksiyonaliteyi öngören ara yüzleri (interface) esas almak ve bunun için de şu ana kadar geliştirdiğimiz nesneleri öncelikle arayüzlere dökmek. O yüzden pronounceCourse yordamına el atmadan önce modeldeki nesnelere bir çekidüzen verelim:
Önce arayüzeyleri belirleyim
İlk olarak Member sınfına bir göz atalım. Uygulamamızda bir dernek üyesinin (diğer özellikleri yanında) üç temel özelliği var: İsim, adres ve eposta. Fakat bildiğimiz gibi bu bir dernek üyesine has özellikler değil. O yüzden bu özellikleri (daha doğrusu bu özellikleri sunan yordamları) ilk etapta daha genel bir arayüzde tanımlıyoruz:
public interface IPerson{
public String getForename();
public String getSurname();
public String getAddress();
public String getPhone();
public String getEmail();
}
Bu arayüzeyin bize anlattığı, bir nesnenin kendisini bir kişi olarak tanıtabilmesi için arayüzün belirlediği yordamları sunması gerektiği. Set yordamlarına veya bu özelliklerin nasıl doldurulabileceği (assignment) konusuna örnekleri basit tutmak için değinmiyoruz. Aynı şekilde bir de dernek üyesi için arayüz oluşturalım:
public interface IMember{
public String getMemberId();
public String getJob();
public String[] getInterests();
public int getMembershipFee();
public String[] getAccountInformations();
}
Bu arayüzde de bir dernek üyesinin belirli özelliklerini (meslek, ilgi alanları, aidat ve konto bilgileri, üye no) tanımladık. Fakat dikkatinizi çektiyse bu arayüzde kurslar konusunda bilgilendirilmesi ile ilgili bir yordam yok. Bunun nedeni de kurslara ilgi göstermenin sadece üyeler ile sınırlı bir özellik olmaması. O yüzden bu özelliği özel bir arayüzde tanımlıyoruz:
public interface IInterestedPartyForCourse{
public void coursePlanned(ICourse course);
}
Kursla ilgililer için arayüz olur da kurs için olmaz mı (basitleştirilmiş):
public interface ICourse{
public String getCourseId();
public String getTitle();
public String getLocation();
public String getTutorName();
public String[] getAgenda();
}
Şimdi sınıflara el atalım
Bu değişikliklerden sonra, şu ana kadar kullandığımız sınıfları (Member, Course) yeni arayüzlere uygun şekilde tekrar elden geçirmemiz lazım. Fakat Member sınıfını değiştiriken dikkat etmemiz gereken bir husus var: Şu ana kadar bu sınıfta programlanmış özellikler şu anda iki arayüz arasında bölünmüş durumda: IPerson ve IMember. Ve IPerson tarafından istenen özellikler başka nesneler için de gerekli. Bu yüzden en mantıklısı IPerson arayüzünü yorumlayan (implements) bir sınıf (Person) geliştirip, Member sınıfını hem Person sınfından türeyen (extends), hem de IMember ve IInterestedPartyForCourse arayüzlerini yorumlayan (implements) bir şekilde tanımlamamız.
public class Member extends Person implements IMember, IInterestedPartyForCourse {
...
}
Bundan sonraki adımda, üye olmayan, fakat kurslara ilgi gösteren kişileri Person sınfından türeyen ve IInterestedPartyForCourse arayüzünü yorumlayan bir sınıfla (InterestedPersonForCourse) realize ediyoruz:
public class InterestedPersonForCourse extends Person implements IInterestedPartyForCourse {
...
}
Tabii bu değişikliklerden sonra dernek sınfını da yeni arayüz ve sınıflara göre değiştirmemiz lazım:
...
public class Club {
private ArrayList<IMember> memberList;
private ArrayList<IInterestedPartyForCourse> courseNotifyList;
...
private void pronounceCourse(ICourse course) {
for (IMember member : memberList) {
if (member instanceof IInterestedPartyForCourse) {
((IInterestedPartyForCourse) member).coursePlanned(course);
}
}
for (IInterestedPartyForCourse interested : courseNotifyList) {
interested.coursePlanned(course);
}
}
public void addInterestedPartyForCourse(
IInterestedPartyForCourse interested) {
courseNotifyList.add(interested);
}
public void removeInterestedPartyForCourse(
IInterestedPartyForCourse interested) {
int i = courseNotifyList.indexOf(interested);
if(i >= 0){
courseNotifyList.remove(interested);
}
}
public String addMember(String address, String email, String forename,
String phone, String surname, String[] interests, String job,
int membershipFee, String[] accountInformations) {
String id = createId();
Member member = new Member(address, email, forename, phone, surname,
id, interests, job, membershipFee, accountInformations);
memberList.add(member);
return id;
}
private String createId() {
return “id” + memberList.size();
}
…
}
Kursa ilgi gösterenleri ekstra bir listede (courseNotifyList) depoluyoruz. Bu listeyi beslemek ve temizlemek için de ayrıca iki yordam realize ettik (addInterestedPartyForCourse ve removeInterestedPartyForCourse). Üye numarası oluşturulmasını (createId) örnekte basit tuttuk.
Yine de mükemmel değil
Göze hoş gelmeyen bir nokta var son uygulamada: Üyeleri (IMember) ve kurslara ilgi gösteren fakat üye olmayanları (IInterestedPartyForCourse) pronounceCourse yordamında ayrı ayrı ele alıyoruz. Peki bir üye illa ki kurslardan haberdar olmak isteyecek mi? Üyelere bu imkanı kullanmama hakkı vermek daha doğru olmaz mı? Bu yüzden pronounceCourse yordamında sadece kurslara ilgi gösterenlerin (üye olsun veya olmasın) listesini esas almalıyız:
...
private void pronounceCourse(ICourse course) {
for (IInterestedPartyForCourse interested : courseNotifyList) {
interested.coursePlanned(course);
}
}
...
Member sınfının artık kendini listeye eklettirip, sildirtebilmesi gerekir ki üyeler de haberdar edilebilsin. Bunun için ise Club sınıfındaki yordamlara (addInterestedPartyForCourse, removeInterestedPartyForCourse) ulaşabilmesi gerekir. Burada yeni bir soru akla geliyor: Yararlandığı yordamlardan bağımsız olarak Club sınıfı ile bağlantısı bulunan her öge sınfın bütününü mü görmeli? Mesela Club sınıfını Member sınıfına aktarmak yerine bir arayüz tanımlayıp Club sınfını bir arayüz olarak Member sınfının kullanımına sunmamız, programı daha esnek kılacaktır. Böylece ilerideki bazı değişiklikler (mesela kursların ayrı bir sınıfta yönetilmesi) Member sınfı ile olan bağlantıya etki etmez.
public interface ICourseService {
public void addInterestedPartyForCourse(IInterestedPartyForCourse interested);
public void removeInterestedPartyForCourse(IInterestedPartyForCourse interested);
}
public class Club implements ICourseService{
public String addMember(String address, String email, String forename,
String phone, String surname, String[] interests, String job,
int membershipFee, String[] accountInformations) {
…
Member member = new Member(address, email, forename, phone,
surname, id, interests, job, membershipFee, accountInformations, this);
}
}
Member sınıfında ise kullanıma sunulan arayüz üzerinden üyeyi listeye kaydettirecek veya sildirecek bir yordam ekleyelim:
public class Member extends Person implements IMember, IInterestedPartyForCourse {
...
private ICourseService _courseService = null;
/**
*
*/
public Member(String address, String email, String forename, String phone,
String surname, String memberId, String[] interests, String job,
int membershipFee, String[] accountInformations, ICourseService courseService) {
…
this._courseService = courseService;
}
…
public void takeInterestForCourse(boolean interested){
if(interested){
_courseService.addInterestedPartyForCourse(this);
} else{
_courseService.removeInterestedPartyForCourse(this);
}
}
…
}
Yeni modelimiz ve bir test
Bütün bu değişikliklerden sonra ortaya çıkan modele bir göz atalım (basitleştirilmiş):
Gevşek bağlaşım yoluyla esnek bir model ortaya çıkmış durumda. Bu modele dayanan bir mimaride IInterestedPartyForCourse arayüzü sayesinde kursların duyurulması özelliğinden başka nesnelerin de yararlanması mümkün. Sadece bu arayüzü yorumlamaları gerekiyor. Böyle bir nesne mimariye eklendiğinde Club sınıfında ve diğer sınıflarda bir değişiklik gerekli olmayacak.
Ortaya çıkan sonucu aşağıdaki JUnit-testi ile test etmemiz de mümkün:
/**
* 18:37:01
* ClubTest.java
*/
package org.jtpd.observer;
import org.jtpd.observer.core.Club;
import org.jtpd.observer.impl.InterestedPersonForCourse;
import org.jtpd.observer.impl.Member;
import org.jtpd.observer.impl.ProgrammingCourse;
import junit.framework.TestCase;
/**
* @author Ilker Yumsek
*
*/
public class ClubTest extends TestCase {
public void testCourseService() throws Exception {
Club club = new Club();
IMember member = club.addMember("adres", "ahmet333@mynet.com.tr", "Ahmet",
"(216) 987 65 43", "Aydin", new String[] { “yazilim”,
“bilisim”, “tasarim kaliplari” }, “Programlamaci”, 1,
new String[] { “banka”, “konto” });
InterestedPersonForCourse interested = new InterestedPersonForCourse(
“adres”, “fowler@acm.org”, “Martin”, “(123) 123 45 67″,
“Fowler”);
((Member) member).takeInterestForCourse(true);
club.addInterestedPartyForCourse(interested);
ProgrammingCourse course = new ProgrammingCourse(new String[] {
“Servlet”, “JSP”, “Etiket kullanimlari”, “Struts”,
“Java Server Faces”, “Facelets”, “Ajax4JSF”, “Mootools”,
“Hibernate-Annotations”, “Filters”, “Log4J”, “Ant” },
“egitim1″, “Istanbul”, “Java Web Teknolojileri Egitimi”,
“Ali Ozan CİL”);
club.pronounceCourse(course);
}
}
Testi çalıştırdığımızda aşağıdaki çıktıyı alıyoruz:
Kurs planlaniyor, gitsem mi?: Java Web Teknolojileri Egitimi Istanbul Ali Ozan CİL Servlet JSP Etiket kullanimlari Struts Java Server Faces Facelets Ajax4JSF Mootools Hibernate-Annotations Filters Log4J Ant Uye degilim ama kurslardan haberdar ediliyorum: Java Web Teknolojileri Egitimi Istanbul Ali Ozan CİL Servlet JSP Etiket kullanimlari Struts Java Server Faces Facelets Ajax4JSF Mootools Hibernate-Annotations Filters Log4J Ant
Gözlemci (Observer) tasarım kalıbı üzerine
Terimler
- Gözlemci tasarım kalıbı
- Bir grup nesnenin, gözlemciler, gözlem altındaki bir nesnede olan değişimlerden otomatik olarak haberdar olmasına olanak sağlar. Gözlem altındaki nesne, kimler tarafından izlendiğinden bağımsız olarak işlevini sürdürür. Zaman içinde yeni gözlemcilerin katılımı ya da ayrılması mümkündür. Bu sayede uygulama zaman içinde davranış değiştirebilir.
- Gevşek bağlaşım
- Bir yazılımın realitedeki nesnelerin özelliklerini ve aralarındaki münasebetleri esas alan arayüz tanımlamaları üzerinden realize edilmesi ile sağlanır. Böylece nesneler birbirleri ile iletişim kurarken, birbirleri hakkında sadece belirli özellikleri tanırlar. Gevşek bağlaşım sayesinde uygulamadaki nesnelerin birbirlerine bağımlılıkları en düşük seviyede tutulmuş olur. Nesnelerde yapılan değişiklikler (iletişimin sağlandığı arayüzdeki değişiklikler hariç) diğer nesneleri etkilemez. Bu da uygulamada değişikliklerin daha çabuk ve en az zahmet ile realize edilmesini sağlar.
Java Standart sürümü (Java SE) bu kalıp için bir sınıf ve bir arayüzü kullanıma sunmaktadır: java.util.Observable ve java.util.Observer. Observable sınıfı gözlenebilir bir nesnenin realize edilmiş halidir. Gözleyen nesnelerin eklenmesi, silinmesi ve değişikliklerden haberdar edilmesini sağlayan fonksiyonaliteyi kalıtım (inheritance) üzerinden kullanıma sunar. Observer arayüzünü yorumlayan nesneler kendilerini gözlemci olarak ekleyebilir, sildirebilir ve değişikliklerden haberdar olabilirler. Gözlenmesi gereken sınıfın da sadece Observable sınıfından türemesi gerekir. Fakat tam da bu nokta kullanıma sunulan fonksiyonalitenin zaaf noktasını oluşturur. Gözlenmesi gereken sınıf bu şekilde sınırılanmış olur. Ayrıca kullanıma sunulmuş sınıfı geliştirme şansı da sunulmamıştır. Fakat bu nesneler çok kompleks olmayan bir uygulamada yine de kullanılabilirler
Gözlemci tasarım kalıbı Java Standart sürümü (Java SE) içerisinde birçok yerde kullanılır. Bunlardan belki de en çok bilineni Swing-API’dir. Bu API’deki birçok nesne (JButton, JTextArea gibi) gözlemci (Listener) ekleme ve silme olanağı sunar. Böylece kullanıcı yüzeyindeki değişikliklere (kullanıcının ‘gönder’ butonuna basması gibi) reaksiyon gösterilmesi sağlanır.
Kaynaklar
- Vikipedi: http://en.wikipedia.org/wiki/Observer_pattern
- Swing API ve Observer (An inside view of Observer): http://www.javaworld.com/javaworld/jw-03-2003/jw-0328-designpatterns.html
- Head First Design Patterns: http://www.oreilly.com/catalog/hfdesignpat/