Apakah ada cara yang lebih cerdas untuk melakukan ini selain rantai panjang jika pernyataan atau beralih?

20

Saya menerapkan bot IRC yang menerima pesan dan saya memeriksa pesan itu untuk menentukan fungsi mana yang harus dihubungi. Apakah ada cara yang lebih pintar untuk melakukan ini? Sepertinya akan cepat lepas kendali setelah aku bangun untuk menyukai 20 perintah.

Mungkin ada cara yang lebih baik untuk abstrak ini?

 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);
        }
    }
Harrison Nguyen
sumber
14
Bahasa apa, jenisnya yang penting pada tingkat detail ini.
mattnz
3
@ mattnz siapa pun yang akrab dengan Java akan mengenalinya dalam contoh kode yang diberikannya.
jwenting
6
@jwenting: Ini juga sintaks C # yang valid, dan saya yakin ada lebih banyak bahasa.
phresnel
4
@ respir ya, tetapi apakah mereka memiliki API standar yang sama persis untuk String?
jwenting
3
@jwenting: Apakah ini relevan? Tetapi bahkan jika: Seseorang dapat membuat contoh yang valid, seperti misalnya Java / C # -Interop Helper Library, atau melihat Java untuk .net: ikvm.net. Bahasa selalu relevan. Si penanya mungkin tidak mencari bahasa tertentu, ia mungkin telah melakukan kesalahan sintaksis (secara tidak sengaja mengubah Java ke C #, mis.), Bahasa baru mungkin muncul (atau muncul di alam liar, di mana ada naga) - sunting : Komentar saya sebelumnya adalah dicky, maaf.
phresnel

Jawaban:

45

Gunakan tabel pengiriman . Ini adalah tabel yang berisi pasangan ("bagian pesan", pointer-to-function). Operator kemudian akan terlihat seperti ini (dalam kode semu):

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

( equalsIgnoreCasedapat ditangani sebagai kasus khusus di suatu tempat sebelumnya, atau jika Anda memiliki banyak tes tersebut, dengan tabel pengiriman kedua).

Tentu saja, apa yang pointer-to-functionharus terlihat tergantung pada bahasa pemrograman Anda. Berikut ini adalah contoh dalam C atau C ++. Di Java atau C # Anda mungkin akan menggunakan ekspresi lambda untuk tujuan itu, atau Anda mensimulasikan "pointer-to-functions" dengan menggunakan pola perintah. Buku online gratis " Perl Pesanan Tinggi " memiliki bab lengkap tentang tabel pengiriman menggunakan Perl.

Doc Brown
sumber
4
Masalahnya, Anda tidak akan bisa mengendalikan mekanisme pencocokan. Dalam contoh OP, ia menggunakan equalsIgnoreCaseuntuk "bermain sekarang" tetapi toLowerCase().startsWithuntuk yang lain.
mrjink
5
@ mrjink: Saya tidak melihat ini sebagai "masalah", itu hanya pendekatan yang berbeda dengan pro dan kontra yang berbeda. The "pro" dari solusi Anda: Perintah individu dapat memiliki mekanisme pencocokan individu. "Pro" milik saya: Perintah tidak harus menyediakan mekanisme pencocokan mereka sendiri. OP harus memutuskan solusi mana yang paling cocok untuknya. Omong-omong, saya juga mengangkat jawaban Anda.
Doc Brown
1
FYI - "Pointer to function" adalah fitur bahasa C # yang disebut delegate. Lambda lebih merupakan objek ekspresi yang dapat diedarkan - Anda tidak dapat "memanggil" lambda seperti Anda dapat memanggil delegasi.
user1068
1
Angkat toLowerCaseoperasi keluar dari loop.
zwol
1
@ HarrisonNguyen: Saya merekomendasikan docs.oracle.com/javase/tutorial/java/javaOO/… . Tetapi jika Anda tidak menggunakan Java 8, Anda bisa menggunakan definisi antarmuka mrink tanpa bagian "cocok", itu setara 100% (itulah yang saya maksudkan dengan "mensimulasikan pointer-to-function dengan menggunakan pola perintah). Dan user1068's komentar hanyalah komentar tentang perbedaan istilah untuk varian berbeda dari konsep yang sama dalam bahasa pemrograman yang berbeda
Doc Brown
31

Saya mungkin akan melakukan sesuatu seperti ini:

public interface Command {
  boolean matches(String message);

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

Kemudian Anda dapat meminta setiap perintah mengimplementasikan antarmuka ini, dan mengembalikan true ketika cocok dengan pesan.

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
    }
}
mrjink
sumber
Jika pesan tidak perlu diurai di tempat lain (saya berasumsi bahwa dalam jawaban saya) ini memang lebih disukai daripada solusi saya karena Commandlebih mandiri jika tahu kapan harus dipanggil sendiri. Memang menghasilkan sedikit overhead jika daftar perintah sangat besar tapi itu mungkin diabaikan.
jhr
1
Pola ini cenderung cocok dengan paradygm perintah konsol dengan baik, tetapi hanya karena ia dengan rapi memisahkan logika perintah dari bus peristiwa dan karena perintah baru kemungkinan akan ditambahkan di masa depan. Saya pikir harus dicatat bahwa ini bukan solusi untuk keadaan apa pun di mana Anda memiliki rantai panjang jika ... elseif
Neil
+1 untuk menggunakan pola perintah "ringan"! Perlu dicatat bahwa ketika Anda berurusan dengan non-String Anda dapat menggunakan misalnya enum cerdas yang tahu bagaimana menjalankan logika mereka. Ini bahkan menghemat for-loop.
LastFreeNickname
3
Daripada loop, kita bisa menggunakan ini sebagai peta jika Anda membuat Command kelas abstrak yang menimpa equalsdan hashCodeharus sama dengan string yang mewakili perintah
Cruncher
Ini luar biasa dan mudah dimengerti. Terima kasih untuk sarannya. Ini persis seperti apa yang saya cari dan tampaknya dapat dikelola untuk penambahan perintah di masa depan.
Harrison Nguyen
15

Anda menggunakan Java - jadi buatlah itu indah ;-)

Saya mungkin akan melakukan ini menggunakan Anotasi:

  1. Buat Anotasi Metode kustom

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Tambahkan Anotasi ke semua Metode yang relevan di Kelas misalnya

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. Di konstruktor Anda gunakan Refleksi untuk membuat HashMap Metode dari semua Metode yang dijelaskan di kelas Anda:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. Dalam onMessageMetode Anda , lakukan saja perulangan commandListmencoba mencocokkan String pada masing-masing dan memanggil di method.invoke()mana cocok.

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );
Falco
sumber
Tidak jelas bahwa dia menggunakan Java, meskipun itu adalah solusi yang elegan.
Neil
Anda benar - kodenya hanya mirip sekali dengan Java-Code yang diformat dengan gerhana-otomatis ... Tetapi Anda dapat melakukan hal yang sama dengan C # dan dengan C ++ Anda dapat mensimulasikan anotasi dengan beberapa Makro pintar
Falco
Bisa jadi Jawa, namun di masa depan, saya sarankan Anda menghindari solusi khusus bahasa jika bahasa tidak ditunjukkan dengan jelas. Hanya saran yang ramah.
Neil
Terima kasih atas solusi ini - Anda benar bahwa saya menggunakan Java dalam kode yang diberikan meskipun saya tidak menentukan, ini terlihat sangat bagus dan saya pasti akan mencobanya. Maaf saya tidak dapat memilih dua jawaban terbaik!
Harrison Nguyen
5

Bagaimana jika Anda mendefinisikan antarmuka, katakanlah IChatBehaviouryang memiliki satu metode yang disebut Executeyang mengambil dalam messagedan cmdobjek:

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

Di kode Anda, Anda kemudian mengimplementasikan antarmuka ini dan menentukan perilaku yang Anda inginkan:

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

Dan seterusnya untuk yang lainnya.

Di kelas utama Anda, Anda kemudian memiliki daftar perilaku ( List<IChatBehaviour>) yang diterapkan bot IRC Anda. Anda kemudian dapat mengganti ifpernyataan Anda dengan sesuatu seperti ini:

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

Di atas akan mengurangi jumlah kode yang Anda miliki. Pendekatan di atas juga akan memungkinkan Anda untuk memasok perilaku tambahan ke kelas bot Anda tanpa memodifikasi kelas bot itu sendiri (sesuai dengan Strategy Design Pattern).

Jika Anda hanya ingin satu perilaku diaktifkan pada satu waktu, Anda dapat mengubah tanda tangan executemetode untuk menghasilkan true(perilaku telah dipecat) atau false(perilaku tidak menyala) dan mengganti loop di atas dengan sesuatu seperti ini:

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

Hal di atas akan lebih membosankan untuk diimplementasikan dan diinisialisasi karena Anda perlu membuat dan lulus semua kelas tambahan, namun, itu akan membuat bot Anda mudah diperluas dan dimodifikasi karena semua kelas perilaku Anda akan dienkapsulasi dan semoga independen satu sama lain.

npinti
sumber
1
Kemana perginya if? Yaitu, bagaimana Anda memutuskan suatu perilaku dieksekusi untuk suatu perintah?
mrjink
2
@mrjink: Maaf, salahku. Keputusan untuk mengeksekusi atau tidak didelegasikan ke perilaku (saya keliru menghilangkan ifbagian dalam perilaku).
npinti
Saya pikir saya lebih suka cek tentang apakah yang diberikan IChatBehaviourdapat menangani perintah yang diberikan, karena memungkinkan penelepon untuk melakukan lebih banyak dengan itu, seperti memproses kesalahan jika tidak ada perintah yang cocok, meskipun itu benar-benar hanya preferensi pribadi. Jika itu tidak diperlukan, maka tidak ada gunanya rumit kode.
Neil
Anda masih memerlukan cara untuk membuat turunan dari kelas yang benar untuk menjalankannya, yang masih akan memiliki rantai panjang ifs atau pernyataan peralihan besar-besaran ...
jwenting
1

"Cerdas" dapat (setidaknya) tiga hal:

Performa Lebih Tinggi

Saran Tabel Pengiriman (dan yang setara) adalah saran yang bagus. Tabel seperti itu disebut "CADET" pada tahun-tahun yang lalu untuk "Tidak Dapat Menambahkan; Bahkan Tidak Mencoba." Namun, pertimbangkan komentar untuk membantu pengelola pemula tentang cara mengelola tabel tersebut.

Maintabilitas

"Jadikan indah" bukanlah peringatan kosong.

dan, sering diabaikan ...

Kegembiraan

Penggunaan toLowerCase memiliki jebakan di mana beberapa teks dalam beberapa bahasa harus mengalami restrukturisasi yang menyakitkan ketika mengubah antara magiscule dan miniscule. Sayangnya, jebakan yang sama ada untuk toUpperCase. Berhati-hatilah.

Kami B. Mars
sumber
0

Anda bisa meminta semua perintah mengimplementasikan antarmuka yang sama. Kemudian parser pesan dapat mengembalikan Anda perintah yang sesuai yang hanya akan Anda jalankan.

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

Sepertinya lebih banyak kode. Ya, Anda masih perlu mem-parsing pesan untuk mengetahui perintah mana yang harus dieksekusi tetapi sekarang sudah pada titik yang ditentukan dengan benar. Ini dapat digunakan kembali di tempat lain. (Anda mungkin ingin menyuntikkan MessageParser tapi itu masalah lain. Juga, pola Flyweight mungkin merupakan ide yang baik untuk perintah, tergantung pada berapa banyak yang Anda harapkan akan dibuat.)

jam
sumber
Saya pikir ini bagus untuk decoupling dan program organisasi, meskipun saya tidak berpikir ini secara langsung mengatasi masalah dengan memiliki terlalu banyak jika ... pernyataan ifif.
Neil
0

Apa yang akan saya lakukan adalah ini:

  1. Kelompokkan perintah yang Anda miliki ke dalam kelompok. (Anda memiliki setidaknya 20 sekarang)
  2. Pada tingkat pertama, kategorikan berdasarkan grup, sehingga Anda memiliki perintah terkait nama pengguna, perintah lagu, perintah hitungan, dll.
  3. Kemudian Anda masuk dalam metode masing-masing kelompok, kali ini Anda akan mendapatkan perintah asli.

Ini akan membuat ini lebih mudah dikelola. Lebih banyak manfaatnya ketika jumlah 'lain jika' tumbuh terlalu banyak.

Tentu saja, terkadang memiliki 'jika ada' ini tidak akan menjadi masalah besar. Saya kira 20 tidak seburuk itu.

InformedA
sumber