Yıllar önce bir SAP projesinde, üretim planlama modülü için karmaşık bir fiyatlandırma motoru geliştirmem gerekiyordu. Müşterinin talebi netti: onlarca farklı fiyatlandırma kuralı, farklı ülkeler için farklı vergi hesaplamaları ve her çeyrek değişebilecek iş mantığı. Procedural ABAP ile başladım—sonuç? Yüzlerce satır CASE deyimi, her yeni kural geldiğinde büyüyen bir bakım cehennemi. O günden bu yana OOP tasarım desenleri benim için bir kurtarıcı oldu.
Bu makalede, ABAP’ta nesne yönelimli programlamanın gerçek gücünü ortaya koyan iki temel tasarım desenini—Factory Method ve Strategy Pattern—elle tutulur örneklerle inceliyoruz. SAP geliştirme dünyasında bu desenleri ne zaman, neden ve nasıl kullanmanız gerektiğini, hazır ABAP kod örnekleriyle birlikte ele alacağız.
“Tasarım desenleri, daha önce çözülmüş problemlerin kanıtlanmış çözümleridir. Tekerleği yeniden icat etmek yerine, atölye pratiğini mirasınıza dahil edin.”
Neden ABAP’ta OOP Tasarım Desenleri Kullanmalısınız?
ABAP geliştiricilerinin önemli bir kısmı hâlâ procedural yaklaşımla kod yazıyor. Bu anlaşılabilir bir durum—SAP’ın kökleri FM (Function Module) ve Report programlara dayanıyor. Ancak S/4HANA geçişleriyle birlikte, iş mantığının karmaşıklığı ve bakım yükü artık OOP’u zorunlu hale getiriyor.
Tasarım desenlerinin size kazandırdıkları:
- Esneklik: Mevcut kodu bozmadan yeni davranışlar ekleyebilirsiniz
- Test Edilebilirlik: Bağımlılıkları soyutladığınızda unit test yazmak kolaylaşır
- Okunabilirlik: Ekip üyeleri niyetinizi kod yapısından anlayabilir
- SAP BTP Uyumu: Clean Core prensipleriyle doğal uyum sağlar
Eğer temiz ve sürdürülebilir ABAP kod yazımı konusunu daha kapsamlı ele almak istiyorsanız, ABAP Clean Code: Okunabilir ve Sürdürülebilir SAP Kodu Yazmanın 10 Altın Kuralı makalemize göz atmanızı öneririm—bu makale onun pratik bir devamı niteliğinde.
Factory Method Pattern: Nesne Yaratımını Soyutlayın
Problem: Doğrudan NEW Operatörü Kullanmanın Riskleri
Şöyle bir senaryo düşünün: Farklı belge türleri (fatura, sipariş, iade) için farklı işlemci sınıfları yazıyorsunuz. Naif yaklaşım şu olurdu:
" KÖTÜ YAKLAŞIM - Bağımlılıkları doğrudan bağlamak
CASE lv_doc_type.
WHEN 'INVOICE'.
lo_processor = NEW zcl_invoice_processor( ).
WHEN 'ORDER'.
lo_processor = NEW zcl_order_processor( ).
WHEN 'RETURN'.
lo_processor = NEW zcl_return_processor( ).
ENDCASE.
Sorun şu: Yeni bir belge türü her geldiğinde bu CASE deyimini aramak ve değiştirmek zorundasınız. Açık/Kapalı prensibini (Open/Closed Principle) ihlal ediyorsunuz.
Factory Method ile Çözüm
Önce bir arayüz tanımlayalım:
" Arayüz tanımı
INTERFACE zif_doc_processor.
METHODS:
process
IMPORTING
is_document TYPE zs_document
RETURNING
VALUE(rv_result) TYPE string,
validate
IMPORTING
is_document TYPE zs_document
RETURNING
VALUE(rv_is_valid) TYPE abap_bool.
ENDINTERFACE.
Her belge türü için concrete implementation:
" Fatura işlemcisi
CLASS zcl_invoice_processor DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_doc_processor.
ENDCLASS.
CLASS zcl_invoice_processor IMPLEMENTATION.
METHOD zif_doc_processor~process.
" Fatura işleme mantığı
rv_result = |Invoice { is_document-doc_number } processed|.
ENDMETHOD.
METHOD zif_doc_processor~validate.
" Fatura doğrulama mantığı
rv_is_valid = xsdbool( is_document-amount > 0 ).
ENDMETHOD.
ENDCLASS.
Ve şimdi Factory sınıfı:
" Factory sınıfı
CLASS zcl_doc_processor_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS:
create
IMPORTING
iv_doc_type TYPE string
RETURNING
VALUE(ro_processor) TYPE REF TO zif_doc_processor
RAISING
zcx_unknown_doc_type.
PRIVATE SECTION.
" Processor registry - yeni tipler buraya eklenir
CLASS-DATA:
gt_registry TYPE TABLE OF REF TO zif_doc_processor.
ENDCLASS.
CLASS zcl_doc_processor_factory IMPLEMENTATION.
METHOD create.
CASE iv_doc_type.
WHEN 'INVOICE'.
ro_processor = NEW zcl_invoice_processor( ).
WHEN 'ORDER'.
ro_processor = NEW zcl_order_processor( ).
WHEN 'RETURN'.
ro_processor = NEW zcl_return_processor( ).
WHEN OTHERS.
RAISE EXCEPTION TYPE zcx_unknown_doc_type
EXPORTING
mv_doc_type = iv_doc_type.
ENDCASE.
ENDMETHOD.
ENDCLASS.
Kullanımı artık son derece temiz:
" Kullanım - İstemci kodu
TRY.
DATA(lo_processor) = zcl_doc_processor_factory=>create(
iv_doc_type = ls_header-doc_type
).
IF lo_processor->validate( is_document = ls_document ).
DATA(lv_result) = lo_processor->process( is_document = ls_document ).
WRITE: / lv_result.
ENDIF.
CATCH zcx_unknown_doc_type INTO DATA(lx_error).
" Hata yönetimi - bilinmeyen belge türü
MESSAGE lx_error->get_text( ) TYPE 'E'.
ENDTRY.
Artık yeni bir belge türü eklemeniz gerektiğinde yalnızca yeni bir sınıf yazıp factory’ye bir satır ekliyorsunuz. İstemci kodu hiç değişmiyor.
Strategy Pattern: Değişen Davranışı Kapsülleyin
Problem: İş Mantığı Değişiyor, Kod Değişmemeli
SAP projelerinde sık karşılaştığım bir senaryo: Vergi hesaplama mantığı ülkeye, müşteri tipine veya ürün kategorisine göre farklılık gösteriyor. Her kombinasyon için IF/CASE deyimleri yazmak hem tehlikeli hem de bakımı imkânsız hale getiriyor.
Strategy Pattern, bir algoritma ailesini tanımlamanıza, her birini ayrı bir sınıfa koymanıza ve birbirinin yerine kullanılabilir hale getirmenize olanak tanır.
Gerçek Dünya: Fiyatlandırma Stratejileri
" Strategy arayüzü
INTERFACE zif_pricing_strategy.
METHODS:
calculate_price
IMPORTING
iv_base_price TYPE p DECIMALS 2
iv_quantity TYPE i
is_customer TYPE zs_customer
RETURNING
VALUE(rv_final_price) TYPE p DECIMALS 2.
get_strategy_name
RETURNING
VALUE(rv_name) TYPE string.
ENDINTERFACE.
Concrete stratejiler:
" Standart fiyatlandırma
CLASS zcl_standard_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
ENDCLASS.
CLASS zcl_standard_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
rv_final_price = iv_base_price * iv_quantity.
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = 'Standard Pricing'.
ENDMETHOD.
ENDCLASS.
" VIP müşteri indirimli fiyatlandırma
CLASS zcl_vip_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
PRIVATE SECTION.
CONSTANTS: c_discount_rate TYPE p DECIMALS 2 VALUE '0.15'. " %15 indirim
ENDCLASS.
CLASS zcl_vip_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
DATA(lv_gross) = iv_base_price * iv_quantity.
rv_final_price = lv_gross * ( 1 - c_discount_rate ).
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = 'VIP Customer Pricing (15% Discount)'.
ENDMETHOD.
ENDCLASS.
" Toplu alım fiyatlandırması
CLASS zcl_bulk_pricing DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_strategy.
PRIVATE SECTION.
CONSTANTS:
c_bulk_threshold TYPE i VALUE 100,
c_bulk_discount TYPE p DECIMALS 2 VALUE '0.20'. " %20 indirim
ENDCLASS.
CLASS zcl_bulk_pricing IMPLEMENTATION.
METHOD zif_pricing_strategy~calculate_price.
DATA(lv_gross) = iv_base_price * iv_quantity.
IF iv_quantity >= c_bulk_threshold.
rv_final_price = lv_gross * ( 1 - c_bulk_discount ).
ELSE.
rv_final_price = lv_gross.
ENDIF.
ENDMETHOD.
METHOD zif_pricing_strategy~get_strategy_name.
rv_name = |Bulk Pricing (>{ c_bulk_threshold } units: 20% off)|.
ENDMETHOD.
ENDCLASS.
Context sınıfı—stratejiyi kullanan motor:
" Context sınıfı
CLASS zcl_price_calculator DEFINITION PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING
io_strategy TYPE REF TO zif_pricing_strategy,
set_strategy
IMPORTING
io_strategy TYPE REF TO zif_pricing_strategy,
calculate
IMPORTING
iv_base_price TYPE p DECIMALS 2
iv_quantity TYPE i
is_customer TYPE zs_customer
RETURNING
VALUE(rv_price) TYPE p DECIMALS 2.
PRIVATE SECTION.
DATA: mo_strategy TYPE REF TO zif_pricing_strategy.
ENDCLASS.
CLASS zcl_price_calculator IMPLEMENTATION.
METHOD constructor.
mo_strategy = io_strategy.
ENDMETHOD.
METHOD set_strategy.
" Runtime'da strateji değiştirilebilir!
mo_strategy = io_strategy.
ENDMETHOD.
METHOD calculate.
rv_price = mo_strategy->calculate_price(
iv_base_price = iv_base_price
iv_quantity = iv_quantity
is_customer = is_customer
).
ENDMETHOD.
ENDCLASS.
Kullanımı—runtime’da strateji seçimi:
" Stratejiyi müşteri tipine göre belirle
DATA(lo_strategy) = COND #(
WHEN ls_customer-type = 'VIP' THEN NEW zcl_vip_pricing( )
WHEN ls_order-quantity >= 100 THEN NEW zcl_bulk_pricing( )
ELSE NEW zcl_standard_pricing( )
).
" Calculator oluştur
DATA(lo_calculator) = NEW zcl_price_calculator(
io_strategy = lo_strategy
).
" Fiyatı hesapla
DATA(lv_price) = lo_calculator->calculate(
iv_base_price = ls_product-price
iv_quantity = ls_order-quantity
is_customer = ls_customer
).
WRITE: / |Final Price: { lv_price CURRENCY 'TRY' }|.
İki Deseni Birlikte Kullanmak: Gerçek Dünya Senaryosu
En güçlü kullanım, bu iki deseni birleştirmektir. Factory, doğru stratejiyi yaratır; Strategy, davranışı kapsüller.
" Pricing Strategy Factory
CLASS zcl_pricing_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS:
create_strategy
IMPORTING
is_customer TYPE zs_customer
iv_quantity TYPE i
RETURNING
VALUE(ro_strategy) TYPE REF TO zif_pricing_strategy.
ENDCLASS.
CLASS zcl_pricing_factory IMPLEMENTATION.
METHOD create_strategy.
" İş kurallarına göre otomatik strateji seçimi
IF is_customer-type = 'VIP' AND is_customer-years_active >= 5.
" Sadık VIP müşteriler en iyi fiyatı alır
ro_strategy = NEW zcl_vip_pricing( ).
ELSEIF iv_quantity >= 100.
ro_strategy = NEW zcl_bulk_pricing( ).
ELSE.
ro_strategy = NEW zcl_standard_pricing( ).
ENDIF.
ENDMETHOD.
ENDCLASS.
" Artık çağıran kod son derece sade:
DATA(lo_strategy) = zcl_pricing_factory=>create_strategy(
is_customer = ls_customer
iv_quantity = ls_order-quantity
).
DATA(lo_calc) = NEW zcl_price_calculator( io_strategy = lo_strategy ).
DATA(lv_final) = lo_calc->calculate(
iv_base_price = ls_product-price
iv_quantity = ls_order-quantity
is_customer = ls_customer
).
Unit Test Yazımı: Tasarım Desenlerinin Test Edilebilirliği
Bu yaklaşımın en büyük faydalarından biri test edilebilirliktir. Her strateji bağımsız olarak test edilebilir:
" ABAP Unit Test
CLASS zcl_pricing_test DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
METHODS:
test_vip_discount FOR TESTING,
test_bulk_threshold FOR TESTING,
test_standard_pricing FOR TESTING.
ENDCLASS.
CLASS zcl_pricing_test IMPLEMENTATION.
METHOD test_vip_discount.
DATA(lo_strategy) = NEW zcl_vip_pricing( ).
DATA(ls_customer) = VALUE zs_customer( type = 'VIP' ).
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '100'
iv_quantity = 1
is_customer = ls_customer
).
" VIP indirim sonrası 85 olmalı (%15 indirim)
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '85'
msg = 'VIP discount calculation failed'
).
ENDMETHOD.
METHOD test_bulk_threshold.
DATA(lo_strategy) = NEW zcl_bulk_pricing( ).
DATA(ls_customer) = VALUE zs_customer( ).
" 100 adet - bulk threshold eşiğinde
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '10'
iv_quantity = 100
is_customer = ls_customer
).
" 1000 * 0.80 = 800 olmalı
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '800'
msg = 'Bulk pricing threshold test failed'
).
ENDMETHOD.
METHOD test_standard_pricing.
DATA(lo_strategy) = NEW zcl_standard_pricing( ).
DATA(ls_customer) = VALUE zs_customer( ).
DATA(lv_price) = lo_strategy->calculate_price(
iv_base_price = '50'
iv_quantity = 3
is_customer = ls_customer
).
cl_abap_unit_assert=>assert_equals(
act = lv_price
exp = '150'
msg = 'Standard pricing calculation failed'
).
ENDMETHOD.
ENDCLASS.
S/4HANA Clean Core Perspektifinden Değerlendirme
SAP’ın Clean Core stratejisi, özelleştirmelerin SAP standart koduna dokunmamasını öngörüyor. Bu tasarım desenleri bu prensibi doğal olarak destekliyor:
- BAdI Entegrasyonu: Factory pattern, BAdI implementasyonlarını seçmek için ideal
- SAP BTP Uyumu: Soyutlanmış iş mantığını side-by-side extensibility ile buluta taşımak kolaylaşır
- Upgrade Güvenliği: Standart SAP nesnelerine dokunmadığınız için yükseltmeler risksizdir
Yaygın Hatalar ve Kaçınma Yolları
Bu desenleri uygularken en sık karşılaştığım hatalar:
1. Over-engineering: Her şey için factory yazmak gerekmez. Eğer yalnızca bir implementasyon varsa ve değişmesi beklenmiyorsa, basit tutun.
2. Anemic Strategy: Strategy sınıfı yalnızca bir getter/setter içeriyorsa, muhtemelen yanlış deseni uygulamışsınızdır.
3. Gereksiz Singleton: Factory sınıflarınızı Singleton yapmaktan kaçının—state taşıyan bir factory test etmek son derece güçtür.
4. Exception Yönetimini Atlamamak: Factory her zaman bilinmeyen tip için anlamlı bir exception fırlatmalı.
Sonuç: Mimarın Perspektifi
ABAP’ta Factory Method ve Strategy Pattern, karmaşık SAP projelerinde iş mantığını yönetilebilir hale getirmenin en etkili yollarından ikisi. Yıllar içinde edindiğim deneyimle şunu söyleyebilirim: Bu desenleri öğrenmek bir haftanızı alır, ama getirisi yıllarca sürer.
Başlangıç için şu adımları öneririm:
- Mevcut projenizdeki büyük CASE deyimlerini belirleyin
- Bunları arayüz arkasına alın
- Her dal için concrete sınıf yazın
- Factory ile seçimi merkezileştirin
- ABAP Unit testlerinizi yazın
Bu yaklaşımı bir kez içselleştirdiğinizde, diğer tasarım desenlerini—Observer, Decorator, Command—de ABAP’a adapte etmek çok daha kolay gelecek. Bir sonraki makalede bu desenleri de ele alacağım.
Bu makaledeki kod örnekleri SAP NetWeaver 7.50 ve üzeri ile S/4HANA ortamlarında test edilmiştir. Production kullanımı öncesinde kendi geliştirme sisteminizde doğrulamanızı öneririm.
Sizin Deneyimleriniz?
SAP projelerinizde OOP tasarım desenlerini kullandınız mı? Hangi deseni en çok faydalı buldunuz? Yorumlarınızı bekliyorum—özellikle gerçek proje deneyimlerinizi duymak isterim. Eğer bu yaklaşımı denemek isterseniz ve takıldığınız bir nokta olursa, soru sormaktan çekinmeyin.