if 문이나 스위치의 긴 체인 외에도이를 수행하는보다 지능적인 방법이 있습니까?


20

메시지를 수신하는 IRC 봇을 구현하고 있으며 호출 할 함수를 결정하기 위해 해당 메시지를 확인하고 있습니다. 이것을하는 더 영리한 방법이 있습니까? 내가 20 명령을 좋아하면 빨리 손을 get 것 같습니다.

아마도 이것을 추상화하는 더 좋은 방법이 있습니까?

 public void onMessage(String channel, String sender, String login, String hostname, String message){

        if (message.equalsIgnoreCase(".np")){
//            TODO: Use Last.fm API to find the now playing
        } else if (message.toLowerCase().startsWith(".register")) {
                cmd.registerLastNick(channel, sender, message);
        } else if (message.toLowerCase().startsWith("give us a countdown")) {
                cmd.countdown(channel, message);
        } else if (message.toLowerCase().startsWith("remember am routine")) {
                cmd.updateAmRoutine(channel, message, sender);
        }
    }

14
이 언어의 세부 수준에서 어떤 언어가 중요합니까?
mattnz

3
@mattnz Java에 익숙한 사람은 그가 제공 한 코드 샘플에서 Java를 인식합니다.
jwenting

6
@jwenting : 유효한 C # 구문이기도하며 더 많은 언어가있을 것입니다.
phresnel 2016 년

4
@ phresnel 예, 똑같은 표준 API API가 있습니까?
jwenting

3
@jwenting : 관련이 있습니까? 그러나 다음과 같은 경우에도 : Java / C # -Interop Helper Library와 같은 유효한 예제를 구성하거나 .net에 대한 Java를 살펴볼 수 있습니다 : ikvm.net. 언어는 항상 관련이 있습니다. 질문자가 특정 언어를 찾지 않았거나 구문 오류가 발생했을 수 있습니다 (예 : 실수로 Java를 C #으로 변환), 새로운 언어가 발생할 수 있습니다 (또는 야생이있는 곳에서 용이있는 곳)-편집 : 내 이전 의견은 미안했다.
phresnel

답변:


45

디스패치 테이블을 사용하십시오 . 쌍을 포함하는 테이블입니다 ( "메시지 부분", pointer-to-function). 그러면 디스패처가 의사 코드로 다음과 같이 표시됩니다.

for each (row in dispatchTable)
{
    if(message.toLowerCase().startsWith(row.messagePart))
    {
         row.theFunction(message);
         break;
    }
}

(이것은 equalsIgnoreCase이전의 어딘가에서 특별한 경우로 처리 할 수 ​​있거나 두 번째 디스패치 테이블을 사용하여 이러한 테스트가 많은 경우 처리 할 수 ​​있습니다).

물론, pointer-to-function모양은 프로그래밍 언어에 따라 다릅니다. 다음 은 C 또는 C ++의 예입니다. Java 또는 C #에서는 해당 목적으로 람다 식을 사용하거나 명령 패턴을 사용하여 "포인터 대 함수"를 시뮬레이션합니다. 무료 온라인 서적 " Higher Order Perl "에는 Perl을 사용한 디스패치 테이블에 대한 완전한 장이 있습니다.


4
그러나 이것의 문제점은 매칭 메커니즘을 제어 할 수 없다는 것입니다. OP의 예에서, 그는 equalsIgnoreCase"지금 재생"을 위해 사용 하지만 toLowerCase().startsWith다른 사람들을 위해 사용합니다.
mrjink 2016 년

5
@ mrjink : 나는 이것을 "문제"로 보지 않으며, 장단점이 다른 다른 접근법 일뿐입니다. 솔루션의 "프로": 개별 명령에는 개별 일치 메커니즘이있을 수 있습니다. 내 "프로": 사령부는 자체 매칭 메커니즘을 제공하지 않아도됩니다. OP는 자신에게 가장 적합한 솔루션을 결정해야합니다. 그건 그렇고, 나는 또한 당신의 대답을 상향 조정했습니다.
Doc Brown

1
참고로- "기능에 대한 포인터"는 델리게이트라는 C # 언어 기능입니다. Lambda는 전달할 수있는 표현식 객체입니다. 대리자를 호출 할 수있는 것처럼 람다를 "호출"할 수 없습니다.
user1068

1
toLowerCase루프 밖으로 작업을 들어 올리십시오 .
zwol

1
@HarrisonNguyen : docs.oracle.com/javase/tutorial/java/javaOO/…를 추천 합니다. 그러나 Java 8을 사용하지 않는 경우 "matchs"부분없이 mrink의 인터페이스 정의를 사용할 수 있습니다. 이는 100 % 동일합니다 (즉, "명령 패턴을 사용하여 포인터 대 함수 시뮬레이션"의 의미). 의견은 다른 프로그래밍 언어에서 같은 개념의 다른 변형에 대한 다른 용어에 대한 언급 일뿐입니다
Doc Brown

31

아마 다음과 같이 할 것입니다 :

public interface Command {
  boolean matches(String message);

  void execute(String channel, String sender, String login,
               String hostname, String message);
}

그런 다음 모든 명령이이 인터페이스를 구현하도록하고 메시지와 일치하면 true를 리턴 할 수 있습니다.

List<Command> activeCommands = new ArrayList<>();
activeCommands.add(new LastFMCommand());
activeCommands.add(new RegisterLastNickCommand());
// etc.

for (Command command : activeCommands) {
    if (command.matches(message)) {
        command.execute(channel, sender, login, hostname, message);
        break; // handle the first matching command only
    }
}

메시지를 다른 곳에서 파싱 할 필요가 없다면 (내 대답에서) 이것이 Command스스로 호출 될 때를 알면 내 솔루션 보다 더 바람직 합니다. 명령 목록이 크지 만 무시할 수있는 경우 약간의 오버 헤드가 발생합니다.
jhr

1
이 패턴은 콘솔 명령 패러다임에 잘 맞는 경향이 있지만, 명령 로직을 이벤트 버스와 깔끔하게 분리하고 향후 새로운 명령이 추가 될 가능성이 있기 때문입니다. 나는 이것이 당신이 긴 체인을 가지고있는 어떤 상황에서도 해결책이 아니라고 생각합니다 ... elseif
Neil

"light"명령 패턴을 사용하는 +1! 문자열이 아닌 문자열을 다룰 때 논리를 실행하는 방법을 알고있는 지능형 열거 형을 사용할 수 있습니다. 이것은 심지어 for-loop를 저장합니다.
LastFreeNickname

3
재정의 것을 추상 클래스 명령을 할 경우 오히려 루프보다, 우리는 맵으로 사용할 수 equalshashCode명령을 나타내는 문자열로 동일하게
Cruncher의

이것은 훌륭하고 이해하기 쉽습니다. 제안 해 주셔서 감사합니다. 그것은 내가 찾던 것과 거의 똑같으며 나중에 명령을 추가 할 수있는 것처럼 보입니다.
해리슨 응 우옌

15

Java를 사용하고 있으므로 아름답게 만드십시오. ;-)

아마도 주석을 사용 하여이 작업을 수행 할 것입니다.

  1. 사용자 정의 메소드 어노테이션 작성

    @IRCCommand( String command, boolean perfectmatch = false )
  2. 클래스의 모든 관련 메소드에 주석 추가

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. 생성자에서 Reflections를 사용하여 클래스의 모든 주석이 달린 메소드에서 메소드의 HashMap을 만듭니다.

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. 당신에 onMessage방법, 그냥 루프 할 commandList각각의 문자열과 일치하는 것을 시도하고 호출 method.invoke()이 맞는 곳을.

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );

그가 Java를 사용하고 있는지는 확실하지 않지만 우아한 솔루션입니다.
Neil

당신 말이 맞아-코드는 방금 자동 형식화 된 Java 코드처럼 보였습니다 ... 그러나 C #으로도 똑같이 할 수 있고 C ++로 영리한 매크로로 주석을 시뮬레이트 할 수 있습니다
Falco

Java 일 수도 있지만 나중에 언어가 명확하게 표시되지 않으면 언어 별 솔루션을 사용하지 않는 것이 좋습니다. 친절한 조언.
Neil

이 솔루션에 감사드립니다. 내가 지정하지 않았지만 제공된 코드에서 Java를 사용하고 있다는 것이 옳았습니다. 이것은 정말 멋져 보입니다. 두 가지 최고의 답변을 선택할 수 없습니다.
해리슨 응 우옌

5

인터페이스를 정의하면 a 와 객체 를 취하는 IChatBehaviour하나의 메소드가 있습니다 .Executemessagecmd

public Interface IChatBehaviour
{
    public void execute(String message, CMD cmd);
}

그런 다음 코드에서이 인터페이스를 구현하고 원하는 동작을 정의하십시오.

public class RegisterLastNick implements IChatBehaviour
{
    public void execute(String message, CMD cmd)
    {
        if (message.toLowerCase().startsWith(".register"))
        {
            cmd.registerLastNick(channel, sender, message);
        }
    }
}

나머지도 마찬가지입니다.

메인 클래스에는 IRC 봇이 구현하는 동작 목록 ( List<IChatBehaviour>)이 있습니다. 그런 다음 if진술을 다음과 같이 바꿀 수 있습니다 .

for(IChatBehaviour behaviour : this.behaviours)
{
    behaviour.execute(message, cmd);
}

위의 코드는 줄어 듭니다. 위의 접근 방식을 사용하면 봇 클래스 자체를 수정하지 않고도 봇 클래스에 추가 동작을 제공 할 수 있습니다 Strategy Design Pattern.

한 번에 하나의 동작 만 실행하려면 execute메소드 의 서명을 변경하여 true(동작이 발생 했음) 또는 false( 동작이 발생 하지 않음) 위의 루프를 다음과 같이 바꾸십시오.

for(IChatBehaviour behaviour : this.behaviours)
{
    if(behaviour.execute(message, cmd))
    { 
         break;
    }
}

위의 내용은 모든 추가 클래스를 생성하고 전달해야하기 때문에 구현하고 초기화하는 것이 더 지루하지만, 모든 행동 클래스가 캡슐화되고 희망적 으로 서로 독립적 이기 때문에 봇을 쉽게 확장하고 수정할 수 있어야합니다 .


1
의 어디로 if갔습니까? 즉, 명령에 대한 동작이 어떻게 실행되는지 어떻게 결정합니까?
mrjink

2
@mrjink : 미안 내 나쁜. 실행 여부에 대한 결정은 행동에 위임됩니다 (실수로 행동의 if일부를 생략했습니다 ).
npinti

IChatBehaviour주어진 명령이 주어진 명령을 처리 할 수 있는지 여부를 확인하는 것이 바람직하다고 생각합니다. 왜냐하면 명령이 일치하지 않으면 오류를 처리하는 것처럼 호출자가 더 많은 작업을 수행 할 수 있기 때문입니다. 그것이 필요하지 않다면, 코드를 불필요하게 복잡하게 만들 필요가 없습니다.
Neil

당신은 여전히 ​​똑같은 긴 ifs 체인이나 대규모 스위치 문장을 가질 수있는 올바른 클래스의 인스턴스를 생성하는 방법이 필요합니다.
jwenting

1

"지능형"은 (적어도) 세 가지가 될 수 있습니다.

더 높은 성능

디스패치 테이블 (및 해당 항목) 제안은 좋은 것입니다. 이러한 표는 과거에 "추가 할 수없고 시도조차하지 않음"으로 "CADET"이라고 불 렸습니다. 그러나 초보자 유지 관리자가 해당 테이블을 관리하는 방법에 도움이되는 의견을 고려하십시오.

유지 보수성

"아름답게 만드십시오"는 유휴 훈계가 아닙니다.

그리고 종종 간과 ...

복원력

toLowerCase를 사용하면 일부 언어의 일부 텍스트가 자기와 작은 부분 사이를 바꿀 때 고통스러운 구조 조정을 거쳐야한다는 단점이 있습니다. 불행히도, toUpperCase에도 동일한 함정이 존재합니다. 그냥 알아 둬


0

모든 명령이 동일한 인터페이스를 구현하도록 할 수 있습니다. 그런 다음 메시지 파서는 사용자에게만 적합한 명령을 반환 할 수 있습니다.

public interface Command {
    public void execute(String channel, String message, String sender) throws Exception;
}

public class MessageParser {
    public Command parseCommandFromMessage(String message) {
        // TODO Put your if/switch or something more clever here
        // e.g. return new CountdownCommand();
    }
}

public class Whatever {
    public void onMessage(String channel, String sender, String login, String hostname, String message) {
        Command c = new MessageParser().parseCommandFromMessage(message);
        c.execute(channel, message, sender);
    }
}

더 많은 코드처럼 보입니다. 예, 실행할 명령을 알기 위해 메시지를 구문 분석해야하지만 이제는 올바르게 정의 된 지점에 있습니다. 다른 곳에서 재사용 할 수 있습니다. MessageParser를 주입하고 싶을 수도 있지만 또 다른 문제입니다. 또한 Flyweight 패턴 은 생성 할 것으로 예상되는 수에 따라 명령에 적합합니다.


나는 이것이 분리 및 프로그램 구성에 좋다고 생각하지만 if ... elseif 문이 너무 많은 문제를 직접적으로 다루지는 않는다고 생각합니다.
Neil

0

내가 할 일은 이것입니다 :

  1. 가지고있는 명령을 그룹으로 그룹화하십시오. (지금은 20 이상입니다)
  2. 첫 번째 수준에서는 그룹별로 분류되므로 사용자 이름 관련 명령, 노래 명령, 카운트 명령 등이 있습니다.
  3. 그런 다음 각 그룹의 방법으로 이동하여 이번에는 원래 명령을 얻습니다.

이렇게하면 관리가 더 쉬워집니다. 'else if'의 수가 너무 많아지면 더 많은 이점이 있습니다.

물론, 때때로 '다른 경우'를 갖는 것은 큰 문제가되지 않습니다. 나는 20이 그렇게 나쁘지 않다고 생각합니다.

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