고객마다 다를 수있는 한 가지 방법으로 수업에 적합한 디자인


12

고객 결제를 처리하는 데 사용되는 클래스가 있습니다. 이 클래스의 메소드 중 하나를 제외한 모든 메소드는 고객의 사용자 부담 정도를 계산하는 메소드를 제외하고 모든 고객에 대해 동일합니다. 이는 고객마다 크게 다를 수 있으며 사용자 지정 요인이 많을 수 있으므로 속성 파일과 같은 계산 논리를 쉽게 캡처 할 수있는 방법이 없습니다.

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
}

그러나 새로운 고객을 확보 할 때마다 수업을 재건해야합니다. 더 좋은 방법은 무엇입니까?

[편집] "중복"기사는 완전히 다릅니다. 나는 스위치 문장 을 피하는 방법을 묻지 않고 , 이 경우에 가장 적합한 현대적인 디자인을 요구하고 있습니다. 공룡 코드를 작성하려면 스위치 문장으로 해결할 수 있습니다. 기본적으로 "이 스위치는 다른 경우가 아니라 일부 경우에는 꽤 잘 작동합니다."라고 말했기 때문에 일반적이고 도움이되지 않는 예제가 제공됩니다.


[편집] 다음과 같은 이유로 최상위 답변 (표준 인터페이스를 구현하는 각 고객에 대해 별도의 "고객"클래스 만들기)으로 가기로 결정했습니다.

  1. 일관성 : 다른 개발자가 작성한 경우에도 모든 고객 클래스가 동일한 출력을 수신하고 리턴하도록하는 인터페이스를 작성할 수 있습니다.

  2. 유지 관리 성 : 모든 코드는 동일한 언어 (Java)로 작성되므로 다른 사람이 간단한 코딩 기능을 유지하기 위해 별도의 코딩 언어를 배울 필요가 없습니다.

  3. 재사용 : 코드에서 비슷한 문제가 발생하면 Customer 클래스를 재사용하여 "사용자 정의"논리를 구현하기 위해 여러 메소드를 보유 할 수 있습니다.

  4. 친숙 함 :이 작업을 수행하는 방법을 이미 알고 있으므로 신속하게 처리하고 더 시급한 다른 문제로 넘어갈 수 있습니다.

단점 :

  1. 새로운 고객마다 새로운 고객 클래스를 컴파일해야하므로 변경 사항을 컴파일하고 배포하는 방법이 다소 복잡해질 수 있습니다.

  2. 각각의 새로운 고객은 개발자가 추가해야합니다. 지원 담당자는 속성 파일과 같은 것에 로직을 추가 할 수 없습니다. 이것은 이상적이지 않지만 ...하지만 지원 담당자가 필요한 비즈니스 로직을 작성할 수있는 방법을 확신하지 못했습니다. 특히 많은 예외로 인해 복잡한 경우가 많습니다.

  3. 많은 신규 고객을 추가하면 확장 성이 떨어집니다. 이것은 예상되지 않지만, 발생하는 경우 코드의 다른 많은 부분과이 부분을 다시 생각해야합니다.

관심있는 사람들을 위해 Java Reflection을 사용하여 이름으로 클래스를 호출 할 수 있습니다.

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
계산 유형에 대해 자세히 설명해야 할 수도 있습니다. 리베이트 만 계산됩니까 아니면 다른 작업 흐름입니까?
qwerty_so

@ThomasKilian 예일뿐입니다. Payment 오브젝트를 상상해보십시오. 계산에 "Payment.percentage에 Payment.total을 곱하십시오. 그러나 Payment.id가 'R'로 시작하는 경우는 아닙니다." 그런 세분성. 모든 고객에게는 고유 한 규칙이 있습니다.
앤드류


이 같은 노력이 . 고객마다 다른 수식 설정
Laiv

1
다양한 답변에서 제안 된 플러그인은 고객 전용 제품이있는 경우에만 작동합니다. 고객 ID를 동적으로 확인하는 서버 솔루션이 있으면 작동하지 않습니다. 그래서 당신의 환경은 무엇입니까?
qwerty_so

답변:


14

고객 결제를 처리하는 데 사용되는 클래스가 있습니다. 이 클래스의 메소드 중 하나를 제외한 모든 메소드는 고객의 사용자 부담 정도를 계산하는 메소드를 제외하고 모든 고객에 대해 동일합니다.

두 가지 옵션이 떠 오릅니다.

옵션 1 : 클래스를 추상 클래스로 만듭니다. 여기서 고객마다 다른 방법이 추상 메서드입니다. 그런 다음 각 고객에 대한 서브 클래스를 작성하십시오.

옵션 2 : 모든 고객 종속 로직을 포함 하는 Customer클래스 또는 ICustomer인터페이스를 작성하십시오 . 결제 처리 클래스가 고객 ID를 받도록하는 대신 Customer또는 ICustomer객체를 받도록하십시오 . 고객 종속적 인 작업을 수행해야 할 때마다 적절한 메소드를 호출합니다.


Customer 클래스는 나쁜 생각이 아닙니다. 각 고객마다 일종의 사용자 정의 객체가 있어야하지만 이것이 DB 레코드, 속성 파일 (일종의) 또는 다른 클래스인지는 확실하지 않았습니다.
앤드류

10

사용자 정의 계산을 애플리케이션의 "플러그인"으로 작성하려고 할 수 있습니다. 그런 다음 구성 파일을 사용하여 어떤 계산 플러그인을 어떤 고객에게 사용해야하는지 프로그램에 알려줍니다. 이 방법으로 모든 새로운 고객에 대해 기본 응용 프로그램을 다시 컴파일 할 필요가 없습니다. 구성 파일을 읽거나 다시 읽어서 새 플러그인을로드하기 만하면됩니다.


감사. Java 환경에서 플러그인은 어떻게 작동합니까? 예를 들어 "result = if (Payment.id.startsWith ("R "))? Payment.percentage * Payment.total : Payment.otherValue"수식을 작성하여 고객의 속성으로 저장하고 삽입합니다. 적절한 방법으로?
Andrew

@Andrew, 플러그인이 작동하는 한 가지 방법은 리플렉션을 사용하여 이름으로 클래스에로드하는 것입니다. 구성 파일에는 고객의 클래스 이름이 나열됩니다. 물론 어떤 플러그인이 어떤 고객인지를 식별 할 수있는 방법이 필요합니다 (예 : 이름이나 메타 데이터로 저장).
Kat

@Kat 편집 한 질문을 읽으면 실제로 어떻게하는지 감사합니다. 단점은 개발자가 확장 성을 제한하는 새로운 클래스를 만들어야한다는 것입니다. 지원 담당자가 속성 파일을 편집 할 수 있기를 원하지만 너무 복잡하다고 생각합니다.
Andrew

@Andrew : 속성 파일에 수식을 쓸 수는 있지만 일종의 식 파서가 필요합니다. 그리고 언젠가 속성 파일에서 한 줄짜리 식으로 작성하기에 너무 복잡한 수식을 사용할 수도 있습니다. 당신이 경우 정말 프로그래머가 아닌 사람이 작업 할 수 있도록하려면, 당신은 당신의 응용 프로그램에 대한 코드를 생성합니다 사용에 대한 사용자 친화적 인 표현 편집기의 일종이 필요합니다. 이 작업을 수행 할 수 있지만 (보았습니다) 사소한 것은 아닙니다.
FrustratedWithFormsDesigner

4

계산을 설명하기 위해 규칙 세트를 사용합니다. 이것은 모든 지속성 저장소에 보유되고 동적으로 수정 될 수 있습니다.

대안으로 이것을 고려하십시오 :

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

어디 customerOpsIndex올바른 동작 지수를 계산 (당신은 어떤 치료를하는 고객의 요구를 알고있다).


포괄적 인 규칙 집합을 만들 수 있다면 그렇게 할 것입니다. 그러나 새로운 고객마다 고유 한 규칙 집합이있을 수 있습니다. 또한이 작업을 수행하는 디자인 패턴이 있으면 코드에 포함 된 전체 내용을 선호합니다. 내 switch {} 예제는이 작업을 훌륭하게 수행하지만 매우 유연하지는 않습니다.
앤드류

고객에 대해 호출 할 오퍼레이션을 배열로 저장하고 고객 ID를 사용하여 색인을 작성할 수 있습니다.
qwerty_so

더 유망 해 보인다. :)
Andrew

3

아래와 같은 것 :

여전히 repo에 switch 문이 있습니다. 그러나이 문제를 실제로 해결할 수는 없습니다. 어떤 시점에서 고객 ID를 필요한 논리에 매핑해야합니다. 영리하고 구성 파일로 옮길 수 있으며, 사전에 매핑을 넣거나 논리 어셈블리에 동적으로로드 할 수 있지만 기본적으로 전환됩니다.

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

고객에서 사용자 지정 코드로 일대일로 매핑되는 것처럼 들리고 컴파일 된 언어를 사용하기 때문에 새로운 고객을 확보 할 때마다 시스템을 재 구축해야합니다

내장 된 스크립팅 언어를 사용해보십시오.

예를 들어, 시스템이 Java 인 경우 JRuby를 임베드 한 다음 각 고객 상점에 해당하는 Ruby 코드 스 니펫을 저장할 수 있습니다. 동일하거나 별도의 git repo에서 버전 제어하에 이상적입니다. 그런 다음 Java 애플리케이션의 컨텍스트에서 해당 스 니펫을 평가하십시오. JRuby는 모든 Java 함수를 호출하고 모든 Java 객체에 액세스 할 수 있습니다.

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

}

이것은 매우 일반적인 패턴입니다. 예를 들어, 많은 컴퓨터 게임은 C ++로 작성되었지만 내장 Lua 스크립트를 사용하여 게임에서 각 상대방의 고객 행동을 정의합니다.

반면 고객에서 사용자 지정 코드로 다 대일 매핑하는 경우 이미 제안 된대로 "전략"패턴을 사용하십시오.

맵핑이 사용자 ID 작성을 기반으로하지 않는 경우 match각 전략 오브젝트에 기능을 추가하고 사용할 전략을 순서대로 선택하십시오.

여기 의사 코드가 있습니다

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

2
나는이 접근법을 피할 것입니다. 경향은 모든 스크립트 스 니펫을 db에 넣고 소스 제어, 버전 관리 및 테스트를 잃는 것입니다.
Ewan

그럴 수 있지. 그들은 별도의 git repo 또는 어디에서나 저장하는 것이 가장 좋습니다. 스 니펫이 이상적으로 버전 관리하에 있어야한다고 내 대답을 업데이트했습니다.
akuhn

속성 파일과 같은 곳에 저장할 수 있습니까? 고정 고객 속성이 둘 이상의 위치에 존재하지 않도록 노력하고 있습니다. 그렇지 않으면 유지 관리 할 수없는 스파게티로 쉽게 전환 할 수 있습니다.
앤드류

2

나는 현재에 맞서 헤엄 칠 것입니다.

ANTLR을 사용하여 자체 표현 언어를 구현하려고합니다 .

지금까지 모든 답변은 코드 사용자 정의를 기반으로합니다. 각 고객에 대한 구체적인 수업을 구현하는 것은 나에게 어느 시점에서 확장 성이 떨어질 것 같습니다. 유지 보수는 비싸고 고통 스러울 것입니다.

따라서 Antlr을 사용하면 자신의 언어를 정의하는 것이 좋습니다. 사용자 (또는 개발자)가 그러한 언어로 비즈니스 규칙을 작성하도록 허용 할 수 있습니다.

귀하의 의견을 예로 들어 보자 :

"result = if (Payment.id.startsWith ("R "))? Payment.percentage * Payment.total : Payment.otherValue"공식을 작성하고 싶습니다.

EL로 다음과 같은 문장을 진술 할 수 있어야합니다.

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

그때...

고객의 자산으로 저장하고 적절한 방법으로 삽입 하시겠습니까?

당신은 할 수 있습니다. 문자열이므로 속성 또는 속성으로 저장할 수 있습니다.

거짓말하지 않겠습니다. 꽤 복잡하고 어렵다. 비즈니스 규칙도 복잡하면 더 어려워집니다.

관심있는 몇 가지 질문이 있습니다.


참고 : ANTLR은 Python 및 Javascript 용 코드도 생성합니다. 너무 많은 오버 헤드없이 개념 증명을 작성하는 데 도움이 될 수 있습니다.

Antlr이 너무 어렵다면 Expr4J, JEval, Parsii와 같은 라이브러리를 사용해 볼 수 있습니다. 이것들은 더 높은 수준의 추상화로 작동합니다.


다음 사람에게 줄을서는 것이 좋은 생각이지만 유지 보수의 골칫거리입니다. 그러나 모든 변수를 평가하고 계량 할 때까지 어떤 아이디어도 버리지 않을 것입니다.
Andrew

1
많은 옵션이 더 좋습니다. 방금 하나 더주고 싶었습니다
Laiv

이것이 올바른 접근 방법이라는 것을 부인할 필요는 없습니다. Websphere 또는 '최종 사용자가 사용자 정의 할 수있는'기타 엔터프라이즈 시스템을 살펴보십시오. @ akuhn의 대답과 같이 단점은 이제 2 개 언어로 프로그래밍하고 버전 관리를 잃어 버린다는 것입니다 / source control / change control
Ewan

@Ewan 죄송합니다, 나는 당신의 주장을 이해하지 못할 것을 두려워합니다. 언어 설명과 자동 생성 된 클래스는 프로젝트의 일부일 수도 있고 아닐 수도 있습니다. 그러나 어쨌든 버전 관리 / 소스 코드 관리를 잃을 수있는 이유를 알 수 없습니다. 반면에 2 개의 다른 언어가있는 것처럼 보이지만 일시적입니다. 언어가 완료되면 문자열 만 설정합니다. Cron 식처럼. 장기적으로 걱정할 것이 적습니다.
Laiv

"paymentID가 'R'로 시작하면 (paymentPercentage / paymentTotal) else paymentOther) 어디에 저장합니까? 어떤 버전입니까? 어떤 언어로 작성 되었습니까? 어떤 단위 테스트를 받았습니까?
Ewan

1

전략 패턴이라는 디자인 패턴 (4의 갱에 있음)을 사용하여 새 고객을 추가 할 때 Customer 클래스를 변경할 필요가 없도록 최소한 알고리즘을 외부화 할 수 있습니다.

당신이 준 스 니펫에서 전략 패턴이 유지 보수가 용이하지 않거나 유지 보수가 용이한지 여부는 논쟁의 여지가 있지만 적어도 수행해야 할 사항에 대한 고객 클래스의 지식을 제거하고 스위치 케이스를 제거합니다.

StrategyFactory 오브젝트는 CustomerID를 기반으로 StrategyIntf 포인터 (또는 참조)를 작성합니다. 팩토리는 특별하지 않은 고객에 대한 기본 구현을 반환 할 수 있습니다.

고객 클래스는 팩토리에 올바른 전략을 요청한 후 호출하면됩니다.

이것은 내가 의미하는 바를 보여주는 매우 간단한 의사 C ++입니다.

class Customer
{
public:

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


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

이 솔루션의 단점은 특별한 논리가 필요한 각 신규 고객에 대해 CalculationsStrategyIntf의 새로운 구현을 작성하고 해당 고객에 대해이를 반환하도록 팩토리를 업데이트해야한다는 것입니다. 컴파일도 필요합니다. 그러나 적어도 고객 클래스에서 점점 커지는 스파게티 코드는 피해야합니다.


예, 누군가 다른 답변에서 비슷한 패턴을 언급했다고 생각합니다. 각 고객은 고객 인터페이스를 구현하는 별도의 클래스에서 논리를 캡슐화 한 다음 PaymentProcessor 클래스에서 해당 클래스를 호출해야합니다. 유망하지만 새로운 고객을 유치 할 때마다 새로운 클래스를 작성해야합니다. 실제로 큰 일이 아니라 지원 담당자가 할 수있는 일이 아닙니다. 여전히 옵션입니다.
Andrew

-1

단일 메소드로 인터페이스를 작성하고 각 구현 클래스에서 라마다를 사용하십시오. 또는 익명의 클래스를 사용하여 다른 클라이언트에 대한 메소드를 구현할 수 있습니까?

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.