Müşteriler arasında değişiklik gösterebilen tek bir yöntemle bir sınıf için uygun tasarım


12

Müşteri ödemelerini işleme koymak için kullanılan bir sınıfım var. Bu sınıfın yöntemlerinden biri hariç tümü, her müşterinin kullanıcı için ne kadar borçlu olduğunu hesaplayan (örneğin) her müşteri için aynıdır. Bu, müşteriden müşteriye büyük ölçüde değişebilir ve herhangi bir sayıda özel faktör olabileceğinden, özellikler dosyası gibi bir şeydeki hesaplamaların mantığını yakalamanın kolay bir yolu yoktur.

CustomerID dayalı anahtarları çirkin kod yazabilirim:

switch(customerID) {
 case 101:
  .. do calculations for customer 101
 case 102:
  .. do calculations for customer 102
 case 103:
  .. do calculations for customer 103
 etc
}

ancak bu, her yeni müşteri aldığımızda sınıfın yeniden oluşturulmasını gerektirir. Daha iyi yol nedir?

[Düzenle] "Yinelenen" makale tamamen farklı. Bir anahtar ifadesinden nasıl kaçınacağımı sormuyorum , bu durum için en iyi olan modern tasarımı istiyorum - dinozor kodunu yazmak istersem bir anahtar ifadesiyle çözebilirim. Burada verilen örnekler geneldir ve yardımcı değildir, çünkü aslında "Hey, anahtar bazı durumlarda oldukça iyi çalışır, bazılarında değil."


[Düzenle] Aşağıdaki nedenlerle en üst sıradaki cevaba (standart bir arayüz uygulayan her müşteri için ayrı bir "Müşteri" sınıfı oluşturmaya) karar verdim:

  1. Tutarlılık: Başka bir geliştirici tarafından oluşturulmuş olsa bile, tüm Müşteri sınıflarının aynı çıktıyı almasını ve döndürmesini sağlayan bir arabirim oluşturabilirim

  2. Sürdürülebilirlik: Tüm kodlar aynı dilde (Java) yazılmıştır, bu nedenle son derece basit bir özellik olması için kimsenin ayrı bir kodlama dili öğrenmesine gerek yoktur.

  3. Yeniden kullanım: Kodda benzer bir sorunun ortaya çıkması durumunda, "özel" mantığı uygulamak için herhangi bir sayıda yöntemi tutmak için Müşteri sınıfını yeniden kullanabilirim.

  4. Tanıdıklık: Bunu nasıl yapacağımı zaten biliyorum, bu yüzden çabucak halledebilir ve diğer daha acil konulara geçebilirim.

Dezavantajları:

  1. Her yeni müşteri, değişiklikleri derleme ve uygulama şeklimize biraz karmaşıklık kazandırabilecek yeni Müşteri sınıfının derlenmesini gerektirir.

  2. Her yeni müşterinin bir geliştirici tarafından eklenmesi gerekir - bir destek personeli mantığı sadece özellikler dosyası gibi bir şeye ekleyemez. Bu ideal değil ... ama sonra da bir Destek görevlisinin gerekli iş mantığını nasıl yazabileceğinden emin değildim, özellikle de birçok istisna dışında karmaşıksa (muhtemelen).

  3. Birçok yeni müşteri eklersek iyi ölçeklenmez. Bu beklenmiyor, ancak eğer gerçekleşirse, kodun birçok parçasını ve bunun yanı sıra yeniden düşünmek zorundayız.

İlgilenenler için, bir sınıfı ada göre çağırmak için Java Reflection uygulamasını kullanabilirsiniz:

Payment payment = getPaymentFromSomewhere();

try {
    String nameOfCustomClass = propertiesFile.get("customClassName");
    Class<?> cpp = Class.forName(nameOfCustomClass);
    CustomPaymentProcess pp = (CustomPaymentProcess) cpp.newInstance();

    payment = pp.processPayment(payment);
} catch (Exception e) {
    //handle the various exceptions
} 

doSomethingElseWithThePayment(payment);

1
Belki de hesaplamaların türlerini ayrıntılandırmalısınız. Sadece hesaplanan bir indirim mi yoksa farklı bir iş akışı mı?
qwerty_so

@ThomasKilian Bu sadece bir örnek. Bir Ödeme nesnesi düşünün ve hesaplamalar "Payment.per yüzdesini Payment.total ile çarpın - ancak Payment.id 'R' ile başlıyorsa" gibi bir şey olabilir. Bu tür bir ayrıntı düzeyi. Her müşterinin kendi kuralları vardır.
Andrew


Eğer böyle bir şey denediniz bu . Her müşteri için farklı formüller ayarlama.
Laiv

1
Çeşitli yanıtlardan önerilen eklentiler yalnızca müşteri başına özel bir ürününüz varsa çalışır. Müşteri kimliğini dinamik olarak kontrol ettiğiniz bir sunucu çözümünüz varsa, çalışmaz. Peki ortamınız nedir?
qwerty_so

Yanıtlar:


14

Müşteri ödemelerini işleme koymak için kullanılan bir sınıfım var. Bu sınıfın yöntemlerinden biri hariç tümü, her müşterinin kullanıcı için ne kadar borçlu olduğunu hesaplayan (örneğin) her müşteri için aynıdır.

Aklıma iki seçenek geliyor.

Seçenek 1: Sınıfınızı soyut bir sınıf yapın, burada müşteriler arasında değişen yöntem soyut bir yöntemdir. Ardından her müşteri için bir alt sınıf oluşturun.

Seçenek 2: Müşteriye bağlı tüm mantığı içeren bir Customersınıf veya ICustomerarabirim oluşturun . Ödeme işleme sınıfınızın bir müşteri kimliğini kabul etmesi yerine, bir Customerveya ICustomernesnesini kabul etmesini sağlayın . Müşteriye bağlı bir şey yapması gerektiğinde, uygun yöntemi çağırır.


Müşteri sınıfı kötü bir fikir değildir. Her Müşteri için bir tür özel nesne olması gerekecek, ancak bunun bir DB kaydı, özellikler dosyası (bir tür) veya başka bir sınıf olması gerektiği açık değildi.
Andrew

10

Özel hesaplamaları uygulamanız için "eklentiler" olarak yazmak isteyebilirsiniz. Ardından, hangi müşteri için hangi hesaplama eklentisinin kullanılması gerektiğini programlamak için bir yapılandırma dosyası kullanırsınız. Bu şekilde, ana uygulamanızın her yeni müşteri için yeniden derlenmesi gerekmez; yalnızca bir yapılandırma dosyasını okuması (veya yeniden okuması) ve yeni eklentileri yüklemesi gerekir.


Teşekkürler. Bir eklenti Java ortamında nasıl çalışır? Örneğin, "sonuç = if (Payment.id.startsWith (" R "))? Payment.perprint * Payment.total: Payment.otherValue" formülünü yazmak için bunu müşteri için bir özellik olarak kaydedin ve ekleyin uygun yöntemde?
Andrew

@Andrew, bir eklentinin çalışabileceği yollardan biri, bir sınıfa adıyla yüklemek için yansıma kullanmaktır. Yapılandırma dosyası, müşteriler için sınıfların adlarını listeler. Tabii ki, hangi eklentinin hangi müşteri için olduğunu belirlemeniz için bir yol bulmanız gerekir (örneğin, adına veya bir yerde depolanan bazı meta verilere).
Kat

@Kat Teşekkür ederim, düzenlenmiş sorumu okuduysanız, aslında böyle yapıyorum. Dezavantajı bir geliştirici ölçeklenebilirliği sınırlayan yeni bir sınıf oluşturmak zorunda - bir destek kişinin sadece bir özellikler dosyasını düzenleyebilir tercih ediyorum ama çok fazla karmaşıklık olduğunu düşünüyorum.
Andrew

@Andrew: Formüllerinizi bir özellikler dosyasına yazabilirsiniz, ancak bir tür ifade ayrıştırıcısına ihtiyacınız olacaktır. Ve bir gün, bir özellikler dosyasında tek satırlık bir ifade olarak (kolayca) yazmak için çok karmaşık olan bir formülle karşılaşabilirsiniz. Eğer varsa gerçekten olmayan programcılar bu çalışma yapabilmek istiyorum, uygulamanız için kod oluşturur kullanımına onlar için kullanıcı dostu ifadesi Editör çeşit gerekir. Bu yapılabilir (gördüm), ama önemsiz değil.
FrustratedWithFormsDesigner

4

Hesaplamaları tanımlamak için bir kural seti ile giderdim. Bu, herhangi bir kalıcılık deposunda tutulabilir ve dinamik olarak değiştirilebilir.

Alternatif olarak şunu düşünün:

customerOps = [oper1, oper2, ..., operN]; // array with customer specific operations
index = customerOpsIndex(customer);
customerOps[index](parms);

Nerede customerOpsIndexdoğru operasyon endeksi hesaplanır (hangi tedavi hangi müşteri ihtiyaçlarını biliyoruz).


Eğer kapsamlı bir kural seti oluşturabilseydim, yapardım. Ancak her yeni müşterinin kendi kuralları olabilir. Ayrıca bunu yapmak için bir tasarım deseni varsa, her şeyi kodunda yer almasını tercih ederim. Anahtarım {} örneğim bunu çok iyi yapıyor, ancak çok esnek değil.
Andrew

Bir müşteri için çağrılacak işlemleri bir dizide saklayabilir ve dizine eklemek için müşteri kimliğini kullanabilirsiniz.
qwerty_so

Bu daha umut verici görünüyor. :)
Andrew

3

Aşağıdaki gibi bir şey:

repo'da hala switch deyiminiz olduğunu unutmayın. Ancak bununla ilgili gerçek bir şey yok, bir noktada bir müşteri kimliğini gerekli mantığa eşlemeniz gerekiyor. Akıllıca bir yapılandırma dosyasına taşıyabilir, eşlemeyi bir sözlüğe koyabilir veya mantık derlemelerine dinamik olarak yükleyebilirsiniz, ancak temel olarak geçiş yapmak için kaynar.

public interface ICustomer
{
    int Calculate();
}
public class CustomerLogic101 : ICustomer
{
    public int Calculate() { return 101; }
}
public class CustomerLogic102 : ICustomer
{
    public int Calculate() { return 102; }
}

public class CustomerRepo
{
    public ICustomer GetCustomerById(
        string id)
    {
        var data;//get data from db
        if (data.logicType == "101")
        {
            return new CustomerLogic101();
        }
        if (data.logicType == "102")
        {
            return new CustomerLogic102();
        }
    }
}
public class Calculator
{
    public int CalculateCustomer(string custId)
    {
        CustomerRepo repo = new CustomerRepo();
        var cust = repo.GetCustomerById(custId);
        return cust.Calculate();
    }
}

2

Müşterilerden özel koda 1'e 1 eşleme yaptığınız ve derlenmiş bir dil kullandığınızdan, her yeni müşteri aldığınızda sisteminizi yeniden oluşturmanız gerekir.

Katıştırılmış bir komut dosyası dili deneyin.

Örneğin, sisteminiz Java'da ise JRuby'yi gömebilir ve ardından her müşteri için karşılık gelen bir Ruby kod snippet'i depolayabilirsiniz. İdeal olarak sürüm kontrolü altında bir yerde, aynı ya da ayrı bir git repo. Ardından bu snippet'i Java uygulamanızın bağlamında değerlendirin. JRuby herhangi bir Java işlevini çağırabilir ve herhangi bir Java nesnesine erişebilir.

package com.example;

import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;

public class Main {

    private ScriptingContainer ruby;

    public static void main(String[] args) {
        new Main().run();
    }

    public void run() {
        ruby = new ScriptingContainer(LocalVariableBehavior.PERSISTENT);
        // Assign the Java objects that you want to share
        ruby.put("main", this);
        // Execute a script (can be of any length, and taken from a file)
        Object result = ruby.runScriptlet("main.hello_world");
        // Use the result as if it were a Java object
        System.out.println(result);
    }

    public String getHelloWorld() {
        return "Hello, worlds!";
    }

}

Bu çok yaygın bir örüntüdür. Örneğin, birçok bilgisayar oyunu C ++ ile yazılır, ancak oyundaki her rakibin müşteri davranışını tanımlamak için gömülü Lua komut dosyaları kullanır.

Öte yandan, müşterilerden özel koda çok sayıda 1'e eşleme varsa, daha önce önerildiği gibi "Strateji" şablonunu kullanın.

Eşleme kullanıcı kimliği markasına dayalı değilse, matchher strateji nesnesine bir işlev ekleyin ve hangi stratejinin kullanılacağına dair sıralı bir seçim yapın.

İşte bazı sahte kod

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)

2
Bu yaklaşımdan kaçınırım. Eğilim, tüm bu kod snippet'lerini bir db'ye koymaktır ve daha sonra kaynak kontrolünü, sürümlendirmesini ve testini kaybedersiniz.
Ewan

Yeterince adil. Onları ayrı bir git deposunda veya her yerde saklarlar. Snippet'lerin ideal olarak sürüm kontrolü altında olması gerektiğini söyleyen cevabım güncellendi.
akuhn

Özellikler dosyası gibi bir yerde saklanabilirler mi? Birden fazla konumda sabit Müşteri özelliklerine sahip olmaktan kaçınmaya çalışıyorum, aksi takdirde sürdürülemez spagetti'ye dönüşmek kolaydır.
Andrew

2

Akıntıya karşı yüzeceğim.

ANTLR ile kendi ifade dilimi uygulamayı denerdim .

Şimdiye kadar, tüm cevaplar kesinlikle kod özelleştirmesine dayanmaktadır. Her müşteri için somut sınıflar uygulamak bana gelecekteki bir noktada iyi ölçeklenmeyecek gibi geliyor. Bakım pahalı ve acı verici olacaktır.

Yani, Antlr ile fikir kendi dilinizi tanımlamaktır. Kullanıcıların (veya geliştiricilerin) bu dilde iş kuralları yazmasına izin verebilirsiniz.

Yorumunuzu örnek olarak alalım:

Formülü yazmak istiyorum "sonuç = if (Payment.id.startsWith (" R "))? Payment.perprint * Payment.total: Payment.otherValue"

EL'inizle, şu cümleleri belirtmeniz mümkün olmalıdır:

If paymentID startWith 'R' then (paymentPercentage / paymentTotal) else paymentOther

Sonra...

bunu müşteri için bir mülk olarak kaydetmek ve uygun yönteme eklemek?

Yapabilirdin. Bu bir dize, onu özellik veya özellik olarak kaydedebilirsiniz.

Yalan söylemeyeceğim. Oldukça karmaşık ve zor. İş kurallarının da karmaşık olması daha da zor.

Burada ilginizi çekebilecek bazı SO soruları:


Not: ANTLR, Python ve Javascript için de kod üretir. Bu, çok fazla yük olmadan kavram kanıtları yazmaya yardımcı olabilir.

Antlr'ın çok zor olduğunu fark ederseniz, Expr4J, JEval, Parsii gibi kütüphaneleri deneyebilirsiniz. Bunlar daha yüksek bir soyutlama seviyesiyle çalışır.


İyi bir fikir ama bir sonraki adam için bir bakım baş ağrısı. Ama tüm değişkenleri değerlendirip tartana kadar hiçbir fikri atmayacağım.
Andrew

1
Birçok seçenek daha iyi. Ben sadece bir tane daha vermek istedim
Laiv

Bunun geçerli bir yaklaşım olduğunu inkar etmiyor, sadece son kullanıcıların özelleştirebileceği websphere'e veya diğer raf dışı sistemlere bakın. / kaynak kontrolü / değişiklik kontrolü
Ewan

@Herhangi bir üzgünüm, endişelerinizi anlamadığım için korkuyorum. Dil tanımlayıcıları ve otomatik oluşturulan sınıflar projenin bir parçası olabilir veya olmayabilir. Ama her durumda neden sürüm / kaynak kodu yönetimini kaybedebileceğimi anlamıyorum. Diğer yandan, 2 farklı dil var gibi görünebilir, ancak bu geçicidir. Dil tamamlandığında, yalnızca dizeler ayarlarsınız. Cron ifadeleri gibi. Uzun vadede endişelenecek daha az şey var.
Laiv

"Eğer ödemeKimliği 'R' ile başlarsa (paymentPer yüzdesi / paymentTotal) başka ödemeDiğer" bunu nerede saklıyorsunuz? hangi versiyon? hangi dilde yazılıyor? bunun için hangi birim testleriniz var?
Ewan

1

En azından algoritmayı dışa aktarabilirsiniz, böylece yeni bir müşteri eklendiğinde Strateji Deseni (Dörtlü Çete'de bulunur) adı verilen bir tasarım deseni kullanarak müşteri sınıfının değişmesi gerekmez.

Verdiğiniz snippet'ten, strateji modelinin daha az bakım yapılabilir mi yoksa daha sürdürülebilir mi olduğu tartışılabilir, ancak en azından Müşteri sınıfının ne yapılması gerektiği hakkındaki bilgisini ortadan kaldıracaktır (ve anahtar durumunuzu ortadan kaldıracaktır).

StrategyFactory nesnesi, CustomerID'ye dayalı bir StrategyIntf işaretçisi (veya başvurusu) oluşturur. Fabrika, özel olmayan müşteriler için varsayılan bir uygulama döndürebilir.

Müşteri sınıfı, Fabrikadan sadece doğru stratejiyi istemeli ve sonra çağırmalıdır.

Bu ne demek istediğimi göstermek için çok kısa bir sahte C ++.

class Customer
{
public:

    void doCalculations()
    {
        CalculationsStrategyIntf& strategy = CalculationsStrategyFactory::instance().getStrategy(*this);
        strategy.doCalculations();
    }
};


class CalculationsStrategyIntf
{
public:
    virtual void doCalculations() = 0;
};

Bu çözümün dezavantajı, özel mantığa ihtiyaç duyan her yeni müşteri için CalculationsStrategyIntf uygulamasının yeni bir uygulamasını oluşturmanız ve fabrikayı uygun müşteri (ler) için geri göndermeniz gerektiğidir. Bu ayrıca derleme gerektirir. Ancak en azından müşteri sınıfında sürekli büyüyen spagetti kodundan kaçınırsınız.


Evet, birisinin başka bir cevapta benzer bir kalıptan bahsettiğini düşünüyorum, her müşterinin mantığı Müşteri arabirimini uygulayan ayrı bir sınıfta kapsüllemesi ve daha sonra bu sınıfı PaymentProcessor sınıfından çağırması. Bu umut vericidir, ancak her yeni Müşteriyi her getirdiğimizde yeni bir sınıf yazmamız gerektiği anlamına gelir - gerçekten büyük bir anlaşma değil, bir Destek görevlisinin yapabileceği bir şey değil. Yine de bu bir seçenek.
Andrew

-1

Tek bir yöntemle bir arayüz oluşturun ve her uygulama sınıfında lamdas kullanın. Veya farklı istemciler için yöntemleri uygulamak için anonim sınıf yapabilirsiniz

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.