Desain yang tepat untuk kelas dengan satu metode yang dapat bervariasi antar pelanggan

12

Saya memiliki kelas yang digunakan untuk memproses pembayaran pelanggan. Semua kecuali satu dari metode kelas ini adalah sama untuk setiap pelanggan, kecuali satu yang menghitung (misalnya) berapa banyak pengguna berutang kepada pelanggan. Ini dapat sangat bervariasi dari pelanggan ke pelanggan dan tidak ada cara mudah untuk menangkap logika perhitungan dalam sesuatu seperti file properti, karena bisa ada sejumlah faktor khusus.

Saya bisa menulis kode jelek yang beralih berdasarkan pada customerID:

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

tetapi ini membutuhkan pembangunan kembali kelas setiap kali kita mendapatkan pelanggan baru. Apa cara yang lebih baik?

[Sunting] Artikel "duplikat" sama sekali berbeda. Saya tidak bertanya bagaimana cara menghindari pernyataan switch, saya meminta desain modern yang paling cocok untuk kasus ini - yang bisa saya pecahkan dengan pernyataan switch jika saya ingin menulis kode dinosaurus. Contoh-contoh yang diberikan ada generik, dan tidak membantu, karena pada dasarnya mereka mengatakan "Hei, saklar berfungsi cukup baik dalam beberapa kasus, tidak dalam beberapa kasus lain."


[Sunting] Saya memutuskan untuk memilih jawaban berperingkat teratas (buat kelas "Pelanggan" terpisah untuk setiap pelanggan yang mengimplementasikan antarmuka standar) karena alasan berikut:

  1. Konsistensi: Saya dapat membuat antarmuka yang memastikan semua kelas Pelanggan menerima dan mengembalikan output yang sama, bahkan jika dibuat oleh pengembang lain

  2. Maintainability: Semua kode ditulis dalam bahasa yang sama (Java) sehingga tidak perlu orang lain untuk belajar bahasa pengkodean yang terpisah untuk mempertahankan apa yang seharusnya menjadi fitur mati-sederhana.

  3. Penggunaan Kembali: Jika masalah serupa muncul dalam kode, saya dapat menggunakan kembali kelas Pelanggan untuk menampung sejumlah metode untuk menerapkan logika "khusus".

  4. Keakraban: Saya sudah tahu bagaimana melakukan ini, jadi saya bisa menyelesaikannya dengan cepat dan beralih ke masalah lain yang lebih mendesak.

Kekurangan:

  1. Setiap pelanggan baru membutuhkan kompilasi kelas Pelanggan baru, yang dapat menambah kompleksitas pada bagaimana kami menyusun dan menggunakan perubahan.

  2. Setiap pelanggan baru harus ditambahkan oleh pengembang - orang yang mendukung tidak bisa hanya menambahkan logika ke sesuatu seperti file properti. Ini tidak ideal ... tapi kemudian saya juga tidak yakin bagaimana orang yang mendukung dapat menulis logika bisnis yang diperlukan, terutama jika kompleks dengan banyak pengecualian (seperti yang mungkin terjadi).

  3. Ini tidak akan skala dengan baik jika kami menambahkan banyak, banyak pelanggan baru. Ini tidak diharapkan, tetapi jika itu terjadi kita harus memikirkan kembali banyak bagian lain dari kode serta yang ini.

Bagi Anda yang tertarik, Anda dapat menggunakan Java Reflection untuk memanggil kelas dengan nama:

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);
Andrew
sumber
1
Mungkin Anda harus menguraikan jenis perhitungan. Apakah itu hanya rabat yang dihitung atau itu alur kerja yang berbeda?
qwerty_so
@ ThomasKilian Ini hanya contoh. Bayangkan sebuah objek Pembayaran, dan kalkulasinya dapat berupa "gandakan Pembayaran. Persentase dengan Pembayaran.total - tetapi tidak jika Payment.id dimulai dengan 'R'". Granularitas semacam itu. Setiap pelanggan memiliki aturannya sendiri.
Andrew
Sudahkah Anda mencoba sesuatu seperti ini . Menetapkan formula berbeda untuk setiap pelanggan.
Laiv
1
Pengaya yang disarankan dari berbagai jawaban hanya akan berfungsi jika Anda memiliki produk khusus per pelanggan. Jika Anda memiliki solusi server tempat Anda memeriksa id pelanggan secara dinamis, itu tidak akan berfungsi. Jadi, bagaimana lingkungan Anda?
qwerty_so

Jawaban:

14

Saya memiliki kelas yang digunakan untuk memproses pembayaran pelanggan. Semua kecuali satu dari metode kelas ini adalah sama untuk setiap pelanggan, kecuali satu yang menghitung (misalnya) berapa banyak pengguna berutang kepada pelanggan.

Dua pilihan muncul di benak saya.

Opsi 1: Jadikan kelas Anda kelas abstrak, di mana metode yang bervariasi di antara pelanggan adalah metode abstrak. Kemudian buat subkelas untuk setiap pelanggan.

Opsi 2: Buat Customerkelas, atau ICustomerantarmuka, yang berisi semua logika yang bergantung pada pelanggan. Alih-alih memiliki pembayaran kelas pengolahan Anda menerima ID pelanggan, telah itu menerima Customeratau ICustomerobjek. Kapan pun ia perlu melakukan sesuatu yang bergantung pada pelanggan, ia memanggil metode yang tepat.

Tanner Swett
sumber
Kelas Pelanggan bukanlah ide yang buruk. Harus ada semacam objek kustom untuk setiap Pelanggan, tapi saya tidak jelas apakah ini harus menjadi catatan DB, file properti (semacam), atau kelas lain.
Andrew
10

Anda mungkin ingin melihat penulisan perhitungan khusus sebagai "plug-in" untuk aplikasi Anda. Kemudian, Anda akan menggunakan file konfigurasi untuk memberi tahu Anda program plugin penghitung mana yang harus digunakan untuk pelanggan mana. Dengan cara ini, aplikasi utama Anda tidak perlu dikompilasi ulang untuk setiap pelanggan baru - hanya perlu membaca (atau membaca kembali) file konfigurasi dan memuat plug-in baru.

FrustratedWithFormsDesigner
sumber
Terima kasih. Bagaimana sebuah plug-in bekerja di lingkungan Java? Misalnya, saya ingin menulis rumus "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" simpan itu sebagai properti untuk pelanggan, dan masukkan dalam metode yang sesuai?
Andrew
@Andrew, Salah satu cara plugin dapat bekerja adalah dengan menggunakan refleksi untuk memuat di kelas dengan nama. File config akan mencantumkan nama-nama kelas untuk pelanggan. Tentu saja, Anda harus memiliki beberapa cara untuk mengidentifikasi plugin mana untuk pelanggan yang mana (mis. Dengan namanya atau beberapa metadata yang disimpan di suatu tempat).
Kat
@ Kat Terima kasih, jika Anda membaca pertanyaan saya yang diedit, itulah sebenarnya yang saya lakukan. Kekurangannya adalah bahwa pengembang harus membuat kelas baru yang membatasi skalabilitas - Saya lebih suka orang yang mendukung bisa mengedit file properti tapi saya pikir ada terlalu banyak kerumitan.
Andrew
@Andrew: Anda mungkin bisa menulis rumus Anda di file properti, tetapi kemudian Anda membutuhkan semacam pengurai ekspresi. Dan Anda mungkin suatu hari akan menemukan formula yang terlalu rumit untuk (mudah) menulis sebagai ekspresi satu baris dalam file properti. Jika Anda benar - benar ingin non-programer dapat bekerja pada ini, Anda akan memerlukan semacam editor ekspresi yang ramah pengguna untuk mereka gunakan yang akan menghasilkan kode untuk aplikasi Anda. Ini bisa dilakukan (saya sudah melihatnya), tapi itu tidak sepele.
FrustratedWithFormsDesigner
4

Saya akan pergi dengan aturan yang ditetapkan untuk menggambarkan perhitungan. Ini dapat disimpan di toko persistensi apa pun dan dimodifikasi secara dinamis.

Sebagai alternatif pertimbangkan ini:

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

Di mana customerOpsIndexdihitung indeks operasi yang tepat (Anda tahu pelanggan mana yang membutuhkan perawatan mana).

qwerty_so
sumber
Jika saya bisa membuat seperangkat aturan komprehensif saya akan. Tetapi setiap pelanggan baru mungkin memiliki seperangkat aturan mereka sendiri. Saya juga lebih suka seluruh hal terkandung dalam kode, jika ada pola desain untuk melakukan ini. Contoh switch saya {} melakukan ini dengan cukup baik, tetapi tidak terlalu fleksibel.
Andrew
Anda bisa menyimpan operasi yang dipanggil untuk pelanggan dalam array dan menggunakan ID pelanggan untuk mengindeksnya.
qwerty_so
Itu terlihat lebih menjanjikan. :)
Andrew
3

Sesuatu seperti di bawah ini:

perhatikan Anda masih memiliki pernyataan beralih di repo. Tidak ada yang benar-benar menyelesaikan masalah ini, pada titik tertentu Anda perlu memetakan id pelanggan ke logika yang diperlukan. Anda bisa menjadi pintar dan memindahkannya ke file config, meletakkan pemetaan dalam kamus atau memuat secara dinamis di majelis logika, tetapi pada dasarnya bermuara untuk beralih.

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();
    }
}
Ewan
sumber
2

Kedengarannya Anda memiliki pemetaan 1 banding 1 dari pelanggan ke kode khusus dan karena Anda menggunakan bahasa yang dikompilasi, Anda harus membangun kembali sistem Anda setiap kali Anda mendapatkan pelanggan baru

Coba bahasa skrip tertanam.

Misalnya, jika sistem Anda di Jawa Anda bisa menanamkan JRuby dan kemudian untuk setiap pelanggan menyimpan potongan kode Ruby yang sesuai. Idealnya di suatu tempat di bawah kontrol versi, baik dalam repo git yang sama atau dalam yang terpisah. Dan kemudian evaluasi potongan itu dalam konteks aplikasi Java Anda. JRuby dapat memanggil fungsi Java apa saja dan mengakses objek Java apa saja.

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!";
    }

}

Ini adalah pola yang sangat umum. Sebagai contoh, banyak game komputer ditulis dalam C ++ tetapi menggunakan skrip Lua yang tertanam untuk mendefinisikan perilaku pelanggan dari setiap lawan dalam game.

Di sisi lain, jika Anda memiliki banyak ke 1 pemetaan dari pelanggan ke kode kustom, cukup gunakan pola "Strategi" seperti yang sudah disarankan.

Jika pemetaan tidak didasarkan pada ID pengguna buat, tambahkan matchfungsi ke setiap objek strategi dan buat pilihan yang terurut strategi yang akan digunakan.

Berikut ini beberapa kode semu

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
sumber
2
Saya akan menghindari pendekatan ini. Kecenderungannya adalah untuk menempatkan semua cuplikan skrip itu dalam db dan kemudian Anda kehilangan kontrol sumber, versi dan pengujian.
Ewan
Cukup adil. Mereka menyimpannya dalam git repo yang terpisah atau di mana pun. Memperbarui jawaban saya untuk mengatakan bahwa cuplikan idealnya berada di bawah kendali versi.
akuhn
Bisakah mereka disimpan dalam sesuatu seperti file properti? Saya mencoba menghindari agar properti Pelanggan tetap ada di lebih dari satu lokasi, jika tidak mudah untuk beralih ke spageti yang tidak dapat dipelihara.
Andrew
2

Aku akan berenang melawan arus.

Saya akan mencoba menerapkan bahasa ekspresi saya sendiri dengan ANTLR .

Sejauh ini, semua jawaban sangat didasarkan pada kustomisasi kode. Menerapkan kelas-kelas konkret untuk setiap pelanggan tampaknya bagi saya bahwa, di beberapa titik di masa depan, tidak akan skala dengan baik. Perawatannya akan mahal dan menyakitkan.

Jadi, dengan Antlr, idenya adalah mendefinisikan bahasa Anda sendiri. Anda dapat mengizinkan pengguna (atau pengembang) untuk menulis aturan bisnis dalam bahasa tersebut.

Mengambil komentar Anda sebagai contoh:

Saya ingin menulis rumus "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

Dengan EL Anda, seharusnya memungkinkan bagi Anda untuk menyatakan kalimat seperti:

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

Kemudian...

simpan itu sebagai properti untuk pelanggan, dan masukkan dalam metode yang sesuai?

Anda bisa. Ini sebuah string, Anda dapat menyimpannya sebagai properti atau atribut.

Saya tidak akan berbohong Cukup rumit dan sulit. Lebih sulit lagi jika aturan bisnisnya rumit juga.

Berikut beberapa pertanyaan SO yang mungkin menarik bagi Anda:


Catatan: ANTLR menghasilkan kode untuk Python dan Javascript juga. Itu mungkin membantu untuk menulis bukti konsep tanpa terlalu banyak biaya tambahan.

Jika Anda menemukan Antlr terlalu sulit, Anda dapat mencoba dengan lib seperti Expr4J, JEval, Parsii. Ini bekerja dengan tingkat abstraksi yang lebih tinggi.

Laiv
sumber
Itu ide yang bagus tetapi sakit kepala pemeliharaan untuk orang berikutnya di telepon. Tetapi saya tidak akan membuang ide apa pun sampai saya mengevaluasi dan menimbang semua variabel.
Andrew
1
Banyak pilihan yang Anda miliki semakin baik. Saya hanya ingin memberi satu lagi
Laiv
Tidak dapat disangkal bahwa ini adalah pendekatan yang valid, lihat saja websphere atau lainnya dari sistem rak perusahaan bahwa 'pengguna akhir dapat menyesuaikan' Tapi seperti @akuhn menjawab downside adalah bahwa sekarang Anda pemrograman dalam 2 bahasa dan kehilangan versi Anda / kontrol sumber / kontrol perubahan
Ewan
@ Ewan maaf, saya takut saya tidak mengerti argumen Anda. Deskriptor bahasa dan kelas yang dibuat secara otomatis dapat menjadi bagian atau tidak dari proyek. Tetapi bagaimanapun juga saya tidak melihat mengapa saya bisa kehilangan manajemen kode versi / sumber. Di sisi lain, mungkin tampak bahwa ada 2 bahasa yang berbeda, tetapi itu bersifat sementara. Setelah bahasa selesai, Anda hanya mengatur string. Suka ekspresi Cron. Pada akhirnya ada sedikit hal yang perlu dikhawatirkan.
Laiv
"Jika pembayaranID dimulai dengan 'R' lalu (pembayaran / persentase pembayaran) pembayaran lain" di mana Anda menyimpan ini? versi apa itu? bahasa apa itu ditulis? tes unit apa yang Anda punya untuk itu?
Ewan
1

Anda setidaknya dapat mengeksternalkan algoritme sehingga kelas Pelanggan tidak perlu berubah ketika pelanggan baru ditambahkan dengan menggunakan pola desain yang disebut Pola Strategi (ada di Gang Empat).

Dari cuplikan yang Anda berikan, dapat diperdebatkan apakah pola strategi akan kurang dapat dipertahankan atau lebih dapat dipelihara, tetapi setidaknya akan menghilangkan pengetahuan kelas Pelanggan tentang apa yang perlu dilakukan (dan akan menghilangkan kasus sakelar Anda).

Objek StrategyFactory akan membuat pointer StrategyIntf (atau referensi) berdasarkan pada CustomerID. Pabrik dapat mengembalikan implementasi default untuk pelanggan yang tidak spesial.

Kelas Pelanggan hanya perlu meminta Pabrik untuk strategi yang benar dan kemudian menyebutnya.

Ini adalah pseudo C ++ yang sangat singkat untuk menunjukkan kepada Anda apa yang saya maksud.

class Customer
{
public:

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


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

Kelemahan dari solusi ini adalah bahwa, untuk setiap pelanggan baru yang memerlukan logika khusus, Anda harus membuat implementasi baru dari CalculationsStrategyIntf dan memperbarui pabrik untuk mengembalikannya untuk pelanggan yang tepat. Ini juga membutuhkan kompilasi. Tetapi Anda setidaknya akan menghindari kode spageti yang terus tumbuh di kelas pelanggan.

Matthew James Briggs
sumber
Ya, saya pikir seseorang menyebutkan pola yang sama dalam jawaban lain, untuk setiap pelanggan untuk merangkum logika dalam kelas terpisah yang mengimplementasikan antarmuka Pelanggan dan kemudian memanggil kelas itu dari kelas PaymentProcessor. Ini menjanjikan tetapi artinya setiap kali kita menghasilkan Pelanggan baru kita harus menulis kelas baru - bukan masalah besar, tetapi bukan sesuatu yang bisa dilakukan oleh orang yang Mendukung. Masih merupakan pilihan.
Andrew
-1

Buat antarmuka dengan metode tunggal dan gunakan lamdas di setiap kelas implementasi. Atau Anda dapat kelas anonim untuk menerapkan metode untuk klien yang berbeda

Zubair A.
sumber