Blog / Teknoloji

Ürün konfigüratörü tasarımı: kural motorlarıyla geçerli kombinasyonlar

MatbaaStore'da ebat × gramaj × selefon uzayını yöneten kural motoru: geçersiz kombinasyon diye bir şey yok.

MatbaaStore'da bir kullanıcı kartvizit sayfasını açtığında önünde masum görünen bir form durur: ebat, kağıt türü, gramaj, tek yüz mü çift yüz mü, selefon, yuvarlak köşe, adet, teslim süresi. Kutucuklar birer birer dolar. Ama bu formun arkasında matbaacının otuz yıllık kafasındaki bir bilgi vardır ve o bilgi şudur: bu seçeneklerin çoğu birbiriyle üretilemez. 350 gramlık sticker kağıda mat selefon çekilmez. Aynı gün teslim ile 2.500 adet katalog aynı cümlede geçmez. Kraft kağıda parlak lak vurmak, kağıdı çöpe atmaktır.

Naif bir konfigüratör bu bilgiyi taşımaz. Kullanıcının her kombinasyonu seçmesine izin verir, fiyatı hesaplar, siparişi alır — ve sorun üretim planlamasında, yani en pahalı yerde patlar. İşin acısı, alternatifi de çoğu ekibin ilk refleksidir: geçersiz kombinasyonları tek tek elle engellemek. Birkaç ürün için işe yarar; katalog büyüdükçe bu yaklaşım kendi ağırlığı altında çöker. Bu yazıda MatbaaStore konfigüratörünü kurarken verdiğimiz kararı anlatacağız: geçerli kombinasyonları saymak yerine geçersizliği doğuran kısıtları yazmak, bu kısıtları koda değil veriye koymak ve arayüzde kullanıcıya daha ilk tıklamada yalnızca üretilebilir olanı göstermek.

Bu, tek bir e-ticaret sitesinin hikâyesi değil. Bir sigorta poliçesi, bir bilgisayar sipariş formu, bir mutfak tasarım aracı — birbirini kısıtlayan seçeneklerin olduğu her yerde aynı problem vardır. Matbaa sadece bize onu çok net gösterdi: burada geçersiz kombinasyon soyut bir hata değil, çöpe giden bir tabaka kâğıttır.

Çarpım bizi yener: kombinasyon patlaması

Problemi görmek için tek bir çarpma yeter. Bir kartvizit ürününde 5 ebat, 4 kağıt türü, 5 gramaj, 3 selefon seçeneği, 6 adet basamağı ve 3 teslim süresi olsun. Bu, 5 × 4 × 5 × 3 × 6 × 3 = 5.400 kombinasyon demektir. Tek bir ürün için. Kataloğunuzda birkaç yüz ürün varsa, kombinasyon uzayı milyonlara çıkar. Kimse bu tabloyu elle yönetemez; matbaacının Excel'i tam da bu duvara tosladığı için bize geldi.

Kritik nokta şu: bu 5.400 kombinasyonun büyük çoğunluğu geçerlidir. Geçersiz olanlar bir avuçtur — sticker + selefon, kraft + lak, aynı gün + yüksek adet gibi. Yani problemin doğası aslında bize yol gösteriyor. Biz binlerce geçerli satırı tek tek onaylamaya çalışıyoruz; oysa asıl bilgi, geçersizliği doğuran bir düzine kuralda saklı. Yıllar bize şunu öğretti: bir problemi modellerken çok olanı değil, az olanı sayın.

Geçerli kombinasyonları tek tek listelemek, sonsuzu saymaya çalışmaktır. Geçersizi doğuran kuralı yazmak ise sonluyu tarif etmektir. İkisi aynı gerçeği anlatır ama biri bakılabilir, diğeri değil.

Çarpımın bir başka acımasız yanı, boyut eklemenin maliyetidir. Diyelim ürüne yeni bir "kesim şekli" seçeneği eklediniz, üç değerli. Kombinasyon sayınız üçe katlandı. Tablo tabanlı düşünüyorsanız, o an binlerce yeni satırı doğrulamak zorundasınız. Kural tabanlı düşünüyorsanız, yeni boyut mevcut kuralların hiçbirini bozmaz; yalnızca o boyuta dair birkaç yeni kısıt eklersiniz. Bu fark, projenin altıncı ayında ürün ekibinin hızını belirleyen fark oldu.

Somut bir rakam verelim: MatbaaStore kataloğunda bugün on'dan fazla ürün kategorisi var ve bazı ürünlerin kombinasyon uzayı tek başına yüz bini aşıyor. Bütün bu uzayı toplasanız milyonlarca satırlık bir tablo eder. Buna karşılık sistemde tanımlı toplam kısıt sayısı üç haneli — yani birkaç yüz kural, milyonlarca satırın işini görüyor. Bir problemin doğru modeli, girdisini büyüttüğünüzde patlamayan modeldir. Tabloda satır sayısı çarpımla büyür; kuralda kısıt sayısı gerçek iş kurallarının sayısıyla büyür ve gerçek iş kuralları her zaman azdır. Matbaacının kafasında da binlerce satır yoktu; bir avuç "şu şununla olmaz" cümlesi vardı.

Tablo tabanlı yaklaşımın çekiciliği ve duvarı

Dürüst olalım: tablo tabanlı yaklaşımın bir cazibesi var. Her geçerli kombinasyonu bir satır olarak veritabanında tutmak, sorgulaması basittir — kullanıcının seçimini bir WHERE koşuluna çevirir, satır dönerse geçerli, dönmezse değil. Fiyatı bile aynı satıra yazabilirsiniz. Projelerin çoğu buradan başlar ve ilk demo hızlı gelir. Sorun, demodan sonra başlar.

İlk duvar üretimdir: o milyonlarca satırı kim üretecek? Birini elle giremezsiniz; bir betikle çarpımı açıp üretirsiniz. Ama çarpımı açan betik zaten kuralları biliyor demektir — yani kural mantığı sistemde vardır, sadece kötü bir yerde, tek seferlik bir üretim betiğinde saklıdır. İkinci duvar bakımdır: matbaacı yeni bir gramaj eklediğinde ya da bir kısıt değiştiğinde, bütün tabloyu yeniden üretmek gerekir. Üçüncü duvar tutarlılıktır: fiyat da aynı tabloda tutuluyorsa, kağıt zammında milyonlarca satırı güncellemeniz gerekir ve bu güncelleme yarıda kalırsa fiyatlar birbirini tutmaz.

En sinsi duvar ise anlamın kaybolmasıdır. Tabloda "kraft + parlak lak" satırının olmaması size bir şey söylemez — bu kombinasyon yasak olduğu için mi yok, yoksa birisi üretmeyi mi unuttu? Tablo, kararın gerekçesini taşımaz. Altı ay sonra kimse o satırın neden orada olmadığını hatırlamaz. Kural ise gerekçesiyle birlikte durur: "kraft kağıt lakı tutmaz." Kod tabanının en pahalı borcu eksik satırlar değil, kaybolmuş gerekçelerdir.

Bir de performans yanılgısı var, çünkü tablo taraftarları hep hızı öne sürer: "satır sorgusu kural değerlendirmesinden hızlıdır." Ölçtük ve bu doğru değil. Bir düzine kısıtı bir seçim üzerinde koşturmak mikrosaniyeler alıyor — istemcide bile fark edilmez. Milyonlarca satırlık bir tabloda indeksli sorgu ise, tablo diske taştığında kural değerlendirmesinden yavaş olabilir. Hızın kaynağı burada tablo değil, verinin küçüklüğü. Kural verisi bellekte durur, çünkü küçüktür; tablo diske iner, çünkü çarpım onu şişirir. İronik olan şu ki, tablo tabanlı yaklaşım hem daha az esnek hem de çoğu senaryoda daha hızlı bile değil.

Tablo tabanlı yaklaşımı tümden reddetmiyoruz. Önhesaplanmış fiyat matrisi gibi, okuması sık ve değişmesi seyrek olan yerlerde tablo mükemmel bir önbellektir; anlık fiyatlandırma motorumuzda popüler kombinasyonları tam da böyle Redis'e yazıyoruz. Ama tablo bir önbellektir, doğruluğun kaynağı değil. Doğruluğun kaynağı kuraldır; tablo o kuraldan türetilir. Bu ikisini karıştırmak, bakımı imkânsız kılan klasik anti-pattern'dir. Önbelleği doğruluk kaynağı sanan ekipler, günün birinde önbellek ile gerçeğin ayrıştığı ve hangisinin doğru olduğunu kimsenin bilemediği o karanlık öğleden sonrayı yaşar.

Kısıtları yazın, kombinasyonları saymayın

Kural motorunun fikri sade: bir seçimin geçerliliğini, o seçimi bir kısıt listesinden geçirerek belirlersiniz. Her kısıt bir "koşul → sonuç" çiftidir. Koşul seçime uyuyorsa, sonucun da sağlanması gerekir; sağlanmıyorsa bir ihlal doğar. Konfigüratörün işi geçerliyi ispatlamak değil, ihlal aramaktır. İhlal yoksa kombinasyon geçerlidir. Bu ters mantık, kombinasyon patlamasını tamamen ortadan kaldırır: 5.400 satır yerine bir düzine kısıtla çalışırsınız.

Kısıtları üç temel biçime indirgeyebildik ve pratikte bunlar matbaanın bütün kurallarını karşıladı:

  • Engelle: koşul sağlandığında belirli değerler yasaktır. "Kağıt sticker ise selefon mat/parlak olamaz." En sık kullandığımız biçim buydu.
  • Gerektir: koşul sağlandığında başka bir kısıt zorunlu olur. "Selefon mat ise gramaj en az 300 olmalı." Bir seçim, başka bir seçimin alt sınırını dayatır.
  • Kapsa: koşul sağlandığında seçenek kümesi daralır. "Aynı gün teslim ise adet yalnızca 100, 250, 500 olabilir." Üretim kapasitesinin kurala döküldüğü yer.

Bu üç biçim, görünürde çok daha karmaşık kuralları da ifade eder, çünkü kısıtlar birleşir. "Kraft kağıt hem lakı hem selefonu engeller, ayrıca çift yüz baskıda minimum gramajı yükseltir" cümlesi, üç ayrı kısıt olarak yazılır ve motor hepsini aynı seçime uygular. Kuralları birbirinden bağımsız, küçük ve tek sorumluluklu tutmak — yazılımın her yerinde işe yarayan o eski ilke — burada da altın kuraldı. Tek bir dev "eğer-değilse" ağacı yazan ekipler, üçüncü ayında o ağacın içinde kaybolur.

Değerlendiricinin çekirdeği utanç verecek kadar basittir, ve olması gereken de budur. Go tarafında saf bir fonksiyon olarak yazdık: girdi bir seçim ile kural listesi, çıktı ihlaller. Hiçbir global duruma dokunmaz, deterministiktir, dolayısıyla hem test edilmesi hem de yüksek eşzamanlılıkta kilitsiz çalışması kolaydır.

// Bir seçim geçerli mi? Yalnızca ihlalleri döndür.
func Denetle(secim Secim, kurallar []Kural) []Ihlal {
    ihlaller := make([]Ihlal, 0)
    for _, k := range kurallar {
        if k.Kosul.Eslesir(secim) && !k.Sonuc.Saglanir(secim) {
            ihlaller = append(ihlaller, Ihlal{
                Kural: k.Ad,
                Mesaj: k.Mesaj,   // kullanıcıya gösterilecek gerekçe
            })
        }
    }
    return ihlaller
}

Bu fonksiyonun on satırdan ibaret olması bir kazadan değil, tasarım tercihinden kaynaklanır. Zekâyı motora değil, veriye koyduk. Motor aptal olmalı; kurallar akıllı. Motorun aptallığı sayesinde onu bir kez yazdık, iki yıldır de neredeyse hiç dokunmadık. Değişen tek şey kurallar oldu ve onlar koddan çıkıp veriye taşındığı için, kural değişikliği kod dağıtımı gerektirmedi. Bir sonraki bölümün konusu bu.

Kuralları koda değil veriye taşımak

Konfigüratör tasarımında verdiğimiz en değerli karar, kuralları koddan çıkarmaktı. İlk taslakta kısıtlar Go içinde "if" bloklarıydı; çalışıyordu ama her kural değişikliği bir pull request, bir kod incelemesi ve bir dağıtım demekti. Matbaacı yeni bir kağıt eklemek istediğinde bizi aramak zorunda kalıyordu. Bu, ürün ekibini yazılım ekibine bağımlı kılan sessiz bir darboğazdı. Kuralları veriye taşıdığımız gün bu bağımlılık koptu.

Bugün kısıtlar veritabanında, matbaa ekibinin bir yönetim panelinden düzenleyebildiği yapısal kayıtlar olarak duruyor. Motor bunları başlangıçta yükleyip bellekte tutuyor, değişiklik olduğunda bir bildirimle tazeliyor — tıpkı fiyat tarifelerini yönettiğimiz gibi. Bir kuralın veri hali kabaca şöyle görünür; dikkat edin, burada hiçbir program mantığı yok, yalnızca beyan var:

{
  "urun": "kartvizit",
  "boyutlar": {
    "kagit":   ["kuse", "bristol", "kraft", "sticker"],
    "gramaj":  [90, 170, 300, 350, 400],
    "selefon": ["yok", "mat", "parlak"],
    "adet":    [100, 250, 500, 1000, 2500]
  },
  "kisitlar": [
    { "kosul": {"kagit": "sticker"},   "engelle": {"selefon": ["mat", "parlak"]},
      "mesaj": "Sticker kağıda selefon uygulanamaz." },
    { "kosul": {"selefon": "mat"},      "gerektir": {"gramaj_min": 300},
      "mesaj": "Mat selefon için en az 300 gr gerekir." },
    { "kosul": {"teslim": "ayni_gun"}, "kapsa": {"adet": [100, 250, 500]},
      "mesaj": "Aynı gün teslim en fazla 500 adet içindir." }
  ]
}

Bu formatın taşıdığı en büyük değer, her kuralın yanında bir "mesaj" alanı olması. Kural yalnızca ne olduğunu değil, kullanıcıya neyin neden yasak olduğunu da söylüyor. Kod tabanlı yaklaşımda bu mesajlar arayüzde bir yerlere serpiştirilir ve kuraldan kopar; veri tabanlı yaklaşımda kural ile gerekçesi tek bir kayıtta yaşar. Bir kısıt değiştiğinde mesajı da onunla birlikte değişir. Kullanıcıya doğru gerekçeyi göstermek, konfigüratörün en çok küçümsenen ama en çok fark yaratan detayıydı — "bu seçenek kapalı" ile "sticker kağıda selefon uygulanamaz" arasındaki fark, bir destek çağrısıdır.

Veriye taşımanın ikinci büyük kazancı denetlenebilirlik oldu. Kurallar veri olduğu için her değişiklik sürümlenebilir bir kayıttır: kim, ne zaman, hangi kısıtı ekledi ya da kaldırdı — hepsi izlenebilir. Bir müşteri "geçen ay bu kombinasyonu sipariş edebiliyordum, şimdi neden kapalı" dediğinde, kural geçmişine bakıp o kısıtın ne zaman ve neden eklendiğini saniyeler içinde görüyoruz. Kod tabanlı yaklaşımda bu bilgi commit geçmişinde, teknik ekibin ancak ulaşabileceği bir yerde kalırdı; veri tabanlı yaklaşımda ürün ekibi kendi kararlarının izini kendisi sürebiliyor. Bir kural yanlış eklendiyse geri almak da bir dağıtım değil, panelden tek bir işlem.

Veriye taşımanın bir bedeli de var, onu da söyleyelim: artık kuralları doğrulayan bir katmana ihtiyacınız olur. Serbest metin gibi girilen bir kural, çelişki üretebilir ya da hiçbir kombinasyonun geçerli kalmadığı bir çıkmaz doğurabilir. Bu yüzden panelde kural kaydedilmeden önce bir tutarlılık kontrolü çalıştırıyoruz: yeni kural kümesiyle en az bir geçerli kombinasyon kalıyor mu, kurallar birbiriyle çelişiyor mu, bir kısıt başka bir kısıtı tümden gereksiz kılıyor mu. Kuralları veriye taşımak sorumluluğu ekibe verir; bu sorumluluğu güvenli kılmak da bizim işimizdir. Özgürlük olmadan bakım kolaylığı yoktur; korkuluk olmadan da özgürlük tehlikelidir.

Geçersizi UI'da anında elemek: reaktif konfigüratör

Doğru kural motoru dünyanın en iyi arka ucunu verir, ama kullanıcı onu göremez. Kullanıcının gördüğü tek şey formdur ve iyi bir konfigüratörün altın kuralı şudur: geçersiz seçeneği kullanıcıya hiç seçtirmeyin. Kötü konfigüratör, kullanıcı her şeyi seçtikten sonra "bu kombinasyon üretilemez" der; iyi konfigüratör, kullanıcı sticker'ı seçtiği anda selefon seçeneklerini soluklaştırıp devre dışı bırakır. Birincisi kullanıcıyı geri adıma zorlar, ikincisi onu görünmez bir elle doğru yola sokar.

Svelte'i tam da bu yüzden sevdik. Reaktif model, "seçim değişti → hangi seçenekler artık geçersiz → arayüzü güncelle" zincirini neredeyse bedava kuruyor. Her seçim değiştiğinde kural motorunu çalıştırıp geçersiz seçenek kümesini türetiyoruz; bu küme değişince de seçenek düğmeleri kendiliğinden devre dışı hale geliyor. Elle DOM güncellemesi, elle olay dinleyicisi, elle senkronizasyon yok. Reaktif bildirimler tam olarak şuna benziyor:

// her seçim değiştiğinde: hangi seçenekler artık geçersiz?
$: gecersizler = kurallariUygula(secim, kurallar);

// seçenekleri işaretle — devre dışı olanlar arayüzde soluklaşır
$: secenekler = tumSecenekler.map((o) => ({
    ...o,
    devreDisi: gecersizler.has(o.id)
}));

// geçerli değilse fiyat motorunu hiç çağırma
$: fiyat = gecersizler.size ? null : hesapla(secim);

Bu üç reaktif bildirimin altında önemli bir tasarım kararı yatıyor. Kural değerlendirmesini hem istemcide hem sunucuda çalıştırıyoruz. İstemcide, çünkü anlık geri bildirim ağ gecikmesini kaldırmaz; kullanıcı bir seçeneğe tıkladığında sonucu milisaniyede görmeli. Sunucuda, çünkü istemciye asla güvenilmez — sipariş kaydedilmeden önce aynı kurallar arka uçta bir kez daha koşar. Aynı kural verisini iki tarafın da okuması, bu ikili doğrulamayı çelişkisiz kılan şey. Kurallar kodda olsaydı, istemci ve sunucu kopyalarını senkron tutmak başlı başına bir kâbus olurdu; veride oldukları için tek kaynaktan besleniyorlar.

Geçersiz seçeneği gizlemek yerine soluklaştırmayı tercih ettik, ve bu bilinçli bir karardı. Seçeneği tümden kaldırırsanız kullanıcı onun var olduğunu bile bilmez; soluk ama görünür bırakırsanız, üstüne geldiğinde "sticker kağıda selefon uygulanamaz" mesajını gösterebilirsiniz. Kullanıcı böylece hem sınırı öğrenir hem de o sınırı aşmak için ne yapması gerektiğini — kağıdı değiştirmesi gerektiğini. Konfigüratör bir engel değil, bir öğretmen olmalı.

Reaktif konfigüratörde beklemediğimiz en zorlu problem, sonradan geçersizleşen seçim oldu. Kullanıcı önce mat selefon seçer, sonra gramajı 170'e düşürür — oysa mat selefon en az 300 gr ister. Seçim artık kendisiyle çelişir. Burada üç seçenek var: değişikliği tümden engellemek, önceki seçimi sessizce sıfırlamak, ya da çelişkiyi göstermek. İlkini denedik ve terk ettik; kullanıcıyı seçim yapmaktan alıkoymak, onu neden alıkoyduğunuzu anlatamadığınız için sinir bozucuydu. Sessiz sıfırlama daha da kötüydü — kullanıcının bilerek yaptığı bir seçim kaybolunca güven de kayboluyordu. Yerleştiğimiz çözüm üçüncüsü: yeni seçime izin ver, çelişen eski seçimi görünür biçimde işaretle ve "gramajı düşürdünüz, mat selefon artık uygulanamıyor; selefonu güncelleyin" mesajını göster. Kullanıcı hangi iki seçimin çeliştiğini görür ve hangisinden vazgeçeceğine kendisi karar verir. Anti-pattern, kullanıcı adına karar vermek; doğru desen, çelişkiyi şeffaf kılıp kararı ona bırakmaktır.

Bu geri bildirimin bedeli, kural motorunun her tuş vuruşunda koşması. Ölçtük: bir seçim üzerinde bütün kısıt kümesini değerlendirmek istemcide yarım milisaniyenin altında kalıyor, çünkü kural sayısı küçük ve her kural yalnızca birkaç alan karşılaştırması. Reaktif zincirin tamamı — kuralları koştur, seçenekleri işaretle, fiyatı çağır — kullanıcının gözünde tek bir kare içinde tamamlanıyor. Bu hız pazarlık konusu değildi; bir konfigüratörde geri bildirim gecikirse kullanıcı hangi tıklamanın neyi değiştirdiğini kaybeder ve forma güvenini yitirir.

Fiyat motoruyla tek doğruluk kaynağında buluşmak

Konfigüratör ve fiyat motoru MatbaaStore'da iki ayrı sistem değil, aynı seçim nesnesini paylaşan iki katman. Bu ayrımı doğru kurmak, projenin en önemli mimari kararlarından biriydi. Kural motoru "bu kombinasyon üretilebilir mi" sorusuna cevap verir; fiyat motoru "üretilebilirse ne kadar" sorusuna. Sıralama önemlidir: geçersiz bir seçim için fiyat hesaplamak hem anlamsızdır hem de tehlikeli. Yukarıdaki reaktif bildirimde gördüğünüz gibi, fiyat motorunu yalnızca ihlal kalmadığında çağırıyoruz. Geçersiz bir kombinasyonun fiyatı yoktur; olması, kullanıcıya asla üretemeyeceğiniz bir rakam vaat etmektir.

İki motoru ayrık tutmanın kazancı, ikisinin de sade kalması. Fiyat motoru — ayrı bir yazıda anlattığımız katmanlı hesap — geçerlilikle uğraşmaz; girdisinin zaten geçerli olduğunu varsayar. Kural motoru fiyatla uğraşmaz; yalnızca üretilebilirliğe bakar. Bu tek sorumluluk ayrımı sayesinde her ikisini ayrı ayrı test edebiliyor, biri değiştiğinde diğerine dokunmuyoruz. Bir kural eklemek fiyat kodunu, bir tarife değişikliği kural kodunu ilgilendirmiyor.

Ama bu iki motorun ortak bir yerde buluşması gerekir, ve o yer boyut tanımıdır. Yukarıdaki JSON'da gördüğünüz "boyutlar" bölümü — kağıt, gramaj, selefon, adet — hem kural motorunun geçerlilik denetlediği alan, hem fiyat motorunun maliyet hesapladığı alandır. Bu tanımı tek bir yerde tutmak şarttı. İki ayrı listede tutup birini güncellemeyi unutursanız, kullanıcı fiyatı görünen ama üretilemeyen ya da tam tersi bir kombinasyona düşer. Tek doğruluk kaynağı ilkesi burada teorik bir zarafet değil, çöpe giden kağıt ile öfkeli müşteri arasındaki farktır.

Bir seçim nesnesi, iki soruya da aynı veriyle cevap vermeli: "üretilebilir mi" ve "ne kadar". Bu iki cevabın farklı kaynaklardan gelmesine izin verdiğiniz an, birbirini tutmayan fiyatlarla yaşamaya başlarsınız.

Pratikte akış şöyle işliyor: kullanıcı bir seçeneğe tıklar, Svelte seçimi günceller, kural motoru istemcide koşup geçersizleri işaretler, ihlal yoksa fiyat motoru tetiklenir ve fiyat — çoğu zaman Redis'teki önhesaplanmış matristen — milisaniyede döner. Kullanıcı bütün bu zinciri tek bir akıcı güncelleme olarak görür: seçti, bazı seçenekler soluklaştı, fiyat değişti. Arkadaki iki motor, bir tutkal olan seçim nesnesi ve onları besleyen tek kural verisi olmasa, bu akıcılık asla mümkün olmazdı.

Sonuç

Ürün konfigüratörü, yüzeyde bir formdur; derinde ise bir kombinasyon uzayını yönetme problemidir. Bu problemi kaybetmenin yolu bellidir: geçerli kombinasyonları tek tek saymaya çalışmak. Kazanmanın yolu da bellidir: geçersizliği doğuran kısıtları yazmak, onları koddan çıkarıp veriye koymak, arayüzde kullanıcıya yalnızca üretilebilir olanı sunmak ve geçerlilik ile fiyatı tek bir seçim nesnesi etrafında ayrık ama uyumlu tutmak. MatbaaStore'da bu dört karar, matbaacının kafasındaki otuz yıllık bilgiyi bir düzine bakımı yapılabilir kurala indirdi.

Benzer bir konfigüratör kuruyorsanız tavsiyemiz nettir. İlk günden çarpımı unutun; kısıtları yazın. Kuralları koda gömmeyin — onları ürün ekibinin gerekçesiyle birlikte düzenleyebileceği veri kayıtları yapın. Motorunuzu aptal, verinizi akıllı tutun; çünkü değişen hep veri olacak, motor değil. Ve son olarak, geçersiz seçeneği kullanıcıya seçtirip sonra reddetmeyin; daha ilk tıklamada, nazikçe, gerekçesiyle birlikte önüne koyun. İyi bir konfigüratör kullanıcıyı sınamaz; ona doğru olanı gösterir.

Benzer bir altyapı mı kuruyorsunuz? Deneyimimizi projenize taşıyalım — 1 iş gününde teklif.
Teklif Al
yazilim.tech © 2026 yazilim.tech — Fikirden ürüne. Bir diji.tech markasıdır.