Bunu if ifadelerinin veya anahtarların uzun bir zincirinin yanında yapmanın daha akıllı bir yolu var mı?


20

İleti alan bir IRC botu uyguluyorum ve hangi işlevleri arayacağınızı belirlemek için bu iletiyi kontrol ediyorum. Bunu yapmanın daha akıllıca bir yolu var mı? 20 komuta çıktıktan sonra çabucak kontrolden çıkmış gibi görünüyor.

Belki de bunu soyutlamanın daha iyi bir yolu var mı?

 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
Hangi dil, bu ayrıntı düzeyinde önemli.
mattnz

3
@mattnz Java'ya aşina olan herkes bunu sağladığı kod örneğinde tanıyacaktır.
jwenting

6
@jwenting: Aynı zamanda geçerli C # sözdizimi ve daha fazla dil var.
phresnel

4
@phresnel evet, ancak bunlar String için aynı standart API'ye sahip mi?
jwenting

3
@jwenting: İlgili mı? Ancak, şöyle olsa bile: Örneğin Java / C # -Interop Yardımcı Kütüphanesi gibi geçerli örnekler oluşturabilir veya .net: ikvm.net için Java'ya bakabilirsiniz. Dil her zaman önemlidir. Sorgulayıcı belirli dilleri arıyor olmayabilir, sözdizimi hataları yapmış olabilir (yanlışlıkla Java'yı C # 'a dönüştürür), yeni diller ortaya çıkabilir (veya ejderhaların olduğu vahşi doğada yükselmiş olabilir) - düzenle : Önceki yorumlarım dicky, üzgünüm.
phresnel

Yanıtlar:


45

Bir dağıtım tablosu kullanın . Bu, çiftleri içeren bir tablodur ("mesaj bölümü", pointer-to-function). Ardından dağıtım programı şu şekilde görünecektir (sözde kodda):

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

(daha equalsIgnoreCaseönce bir yerde veya bu testlerin çoğuna sahipseniz, ikinci bir sevk tablosu ile özel bir durum olarak ele alınabilir).

Tabii ki, neye pointer-to-functionbenzemesi gerektiği programlama dilinize bağlıdır. İşte C veya C ++ 'da bir örnek. Java veya C # 'da muhtemelen bu amaç için lambda ifadeleri kullanacaksınız ya da komut modelini kullanarak "işaretçi işlevlerini" simüle edeceksiniz. Ücretsiz çevrimiçi kitap " Yüksek Sipariş Perl " Perl kullanarak sevk tabloları hakkında tam bir bölüm var.


4
Bununla birlikte sorun, eşleşen mekanizmayı kontrol edemeyeceğinizdir. OP örneğinde, equalsIgnoreCase"şimdi oynuyor" için ama toLowerCase().startsWithdiğerleri için kullanıyor.
mrjink

5
@mrjink: Bunu bir "problem" olarak görmüyorum, bu sadece farklı artı ve eksilerle farklı bir yaklaşım. Çözümünüzün "yanlısı": bireysel Komutların bireysel eşleştirme mekanizmaları olabilir. Benim “profesyonel ”im: Komutların kendi eşleşme mekanizmalarını sağlamaları gerekmez. OP hangi çözümün kendisine en uygun olduğuna karar vermek zorundadır. Bu arada, cevabını da iptal ettim.
Doc Brown

1
FYI - "İşaretçi işlevi" delege adı verilen bir C # dil özelliğidir. Lambda daha çok etrafından geçirilebilen bir ifade nesnesidir - delege diyebildiğiniz gibi lambda'yı "arayamazsınız".
user1068

1
Kaldırma toLowerCasedöngünün dışında çalışma.
14:09

1
@HarrisonNguyen: Ben tavsiye docs.oracle.com/javase/tutorial/java/javaOO/... . Ancak Java 8 kullanmıyorsanız, mrink'in arayüz tanımını "eşleşmeler" bölümü olmadan kullanabilirsiniz, bu% 100 eşdeğerdir (bu, kastettiğim, komut desenini kullanarak işaretçi-işlevi simüle eder). yorum, aynı kavramın farklı programlama dillerindeki farklı varyantları için farklı terimler hakkında bir açıklamadır
Doc Brown

31

Muhtemelen böyle bir şey yapardım:

public interface Command {
  boolean matches(String message);

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

Daha sonra her komutun bu arabirimi uygulamasını sağlayabilir ve iletiyle eşleştiğinde true değerini döndürebilirsiniz.

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
    }
}

Eğer mesajlar asla başka bir yerde ayrıştırılmaya ihtiyaç duymazsa (cevabımda bu varsayımı benimsediğimde) bu benim çözümüm için Commanddaha çok tercih edilir . Komutların listesi çok büyükse hafif bir ek yük oluşturur, ancak bu muhtemelen ihmal edilebilir.
jhr

1
Bu desen, konsol komut parametresine güzelce uyma eğilimindedir, ancak yalnızca komut mantığını olay veriyolundan düzgün bir şekilde ayırması ve gelecekte yeni komutların eklenmesi muhtemeldir. Sanırım bu, eğer uzun bir zinciriniz varsa herhangi bir durum için bir çözüm değildir ... elseif
Neil

+1 "hafif" bir Komut deseni kullanmak için! Dizeler olmayanlarla uğraşırken, mantıklarının nasıl yürütüleceğini bilen akıllı numaralandırmalardan yararlanabileceğinizi belirtmek gerekir. Bu, for-loop'u bile kurtarır.
LastFreeNickname

3
Eğer geçersiz kılar o soyut sınıf Command yaparsanız Aksine döngü yerine, bir harita olarak kullanabiliriz equalsve hashCodekomutu temsil dize aynı olacak şekilde
Cruncher

Bu harika ve anlaşılması kolay. Önerin için teşekkürler. Tam olarak aradığım şey buydu ve gelecekteki komutların eklenmesi için yönetilebilir gibi görünüyor.
Harrison Nguyen

15

Java kullanıyorsunuz - çok güzel yapın ;-)

Muhtemelen Ek Açıklamaları kullanarak bunu yapardım:

  1. Özel bir Yöntem Açıklaması oluşturma

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Ek Açıklamaları Sınıftaki tüm ilgili Yöntemlere ekleyin;

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. Yapıcısında, sınıfınızdaki tüm açıklamalı Yöntemlerden bir HashMap Yöntemleri oluşturmak için Yansımaları kullanın:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. Senin içinde onMessageYöntem, biraz üzerinde bir döngü yapmak commandListher birinde Dize maç çalışıyor ve çağıran method.invoke()nerede uygun.

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

Java kullandığı belli değil, ama bu zarif bir çözüm.
Neil

Haklısın - kod sadece eclipse-auto-formatted Java-Code'a çok benziyordu ... Ama aynı şeyi C # ile yapabilirsin ve C ++ ile bazı akıllı Makrolarla ek açıklamaları simüle edebilirsin
Falco

Java çok iyi olabilir, ancak gelecekte dil açıkça belirtilmemişse dile özgü çözümlerden kaçınmanızı öneririm. Sadece dostça tavsiye.
Neil

Bu çözüm için teşekkürler - Ben belirtmedim bile sağlanan kodda Java kullanıyorum haklıydı, bu gerçekten güzel görünüyor ve ben bir şans vereceğim. Üzgünüm en iyi iki cevabı seçemiyorum!
Harrison Nguyen

5

Bir arabirim tanımlarsanız, IChatBehaviourhangisinin Executebir messageve bir cmdnesneyi alan bir yöntem olduğunu söyleyin :

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

Kodunuzda, bu arabirimi uygular ve istediğiniz davranışları tanımlarsınız:

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

Ve diğerleri için.

Ana sınıfınızda, IRC botunuzun uyguladığı davranışların ( List<IChatBehaviour>) bir listesine sahip olursunuz. Daha sonra ififadelerinizi şu şekilde değiştirebilirsiniz :

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

Yukarıdakiler sahip olduğunuz kod miktarını azaltmalıdır. Yukarıdaki yaklaşım, bot sınıfının kendisini değiştirmeden bot sınıfınıza ek davranışlar sağlamanıza da izin verecektir (göre Strategy Design Pattern).

Herhangi bir anda yalnızca bir davranışın tetiklenmesini istiyorsanız, executeyöntemin imzasını true(davranış tetiklendi) veya false(davranış tetiklenmedi) verecek ve yukarıdaki döngüyü aşağıdaki gibi bir şeyle değiştirebilirsiniz:

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

Yukarıdakilerin uygulanması ve başlatılması daha fazla yorucu olacaktır, çünkü tüm ekstra sınıfları oluşturmanız ve geçmeniz gerekir, ancak tüm davranış sınıflarınız kapsüllenecek ve umarım birbirinden bağımsız olacağı için botunuzu kolayca genişletilebilir ve değiştirilebilir hale getirmelidir .


1
Nereye gitti if? Yani, bir komut için bir davranışın yürütülmesine nasıl karar veriyorsunuz?
mrjink

2
@mrjink: Özür dilerim. Yürütülüp yürütülmeyeceği kararı davranışa devredilir (Yanlışlıkla ifdavranıştaki bölümü atladım).
npinti

IChatBehaviourBelirli bir komutu işleyip işleyemeyeceğini kontrol etmeyi düşünüyorum , çünkü arayanın komutla eşleşmemesi durumunda hataların işlenmesi gibi, onunla daha fazlasını yapmasına izin veriyor, ancak gerçekten sadece kişisel bir tercih. Bu gerekli değilse, gereksiz yere kodu karmaşık bir noktaya gerek yok.
Neil

hala aynı sınıf ifs veya büyük anahtar deyimine sahip olacak doğru sınıf örneğini oluşturmak için bir yol gerekir ...
jwenting

1

"Akıllı" (en azından) üç şey olabilir:

Daha Yüksek Performans

Dağıtım Tablosu (ve eşdeğerleri) önerisi iyi bir öneridir. Böyle bir tablo geçmiş yıllarda "Ekleyemiyorum; Denemiyor bile" için "CADET" olarak adlandırıldı. Bununla birlikte, acemi bir bakıcıya söz konusu tablonun nasıl yönetileceği konusunda bir yorum yapmayı düşünün.

İdame

"Güzel kılın" boş bir uyarı değildir.

ve genellikle gözden kaçan ...

Esneklik

ToLowerCase kullanımı, bazı dillerdeki bazı metinlerin magiscule ve miniscule arasında geçiş yaparken ağrılı bir yeniden yapılandırmaya girmesi gerektiği konusunda tuzaklara sahiptir. Ne yazık ki, toUpperCase için aynı tuzaklar var. Sadece farkında ol.


0

Tüm komutların aynı arayüzü kullanmasını sağlayabilirsiniz. Daha sonra bir mesaj ayrıştırıcı size yalnızca yürüteceğiniz uygun komutu döndürebilir.

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);
    }
}

Daha fazla kod gibi görünüyor. Evet, hangi komutu çalıştıracağını bilmek için mesajı ayrıştırmanız gerekiyor, ancak şimdi doğru tanımlanmış bir noktada. Başka bir yerde tekrar kullanılabilir. (MessageParser'ı enjekte etmek isteyebilirsiniz, ancak bu başka bir konudur. Ayrıca, Flyweight deseni , kaç tane oluşturmayı beklediğinize bağlı olarak komutlar için iyi bir fikir olabilir.)


Ben bunun ayrıştırma ve program organizasyonu için iyi olduğunu düşünüyorum, ancak bunun doğrudan eğer ... elseif ifadeleri ile ilgili sorunu doğrudan ele almıyor sanmıyorum.
Neil

0

Ne yapacağım şudur:

  1. Sahip olduğunuz komutları gruplara ayırın. (şu anda en az 20 tane var)
  2. İlk düzeyde, gruba göre kategorilere ayırın, böylece kullanıcı adıyla ilgili komut, şarkı komutları, sayma komutları vb.
  3. Daha sonra her grubun yöntemine girersiniz, bu sefer orijinal komutu alırsınız.

Bu, bunu daha yönetilebilir hale getirecektir. 'If if' sayısı çok arttığında daha fazla fayda sağlar.

Tabii ki, bazen bu 'başka' duruma sahip olmak büyük bir sorun olmazdı. 20 kadar kötü olduğunu sanmıyorum.

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.