มีวิธีที่ชาญฉลาดกว่านี้นอกเหนือจากการใช้คำสั่งหรือการสลับสายยาว ๆ หรือไม่?


20

ฉันกำลังใช้บอต IRC ที่ได้รับข้อความและฉันกำลังตรวจสอบข้อความนั้นเพื่อพิจารณาว่าจะเรียกใช้ฟังก์ชันใด มีวิธีที่ฉลาดกว่านี้ในการทำเช่นนี้? ดูเหมือนว่ามันจะออกจากมือได้อย่างรวดเร็วหลังจากที่ฉันได้รับคำสั่งถึง 20 คำสั่ง

บางทีอาจมีวิธีที่ดีกว่าที่จะสรุปสิ่งนี้ใช่ไหม

 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 จะรับรู้ได้ในตัวอย่างโค้ดที่เขาให้
jwenting

6
@jwenting: เป็นไวยากรณ์ C # ที่ถูกต้องและฉันคิดว่ามีภาษาอื่นอีกมาก
phresnel

4
@phresnel ใช่ แต่มี API มาตรฐานเดียวกันสำหรับ String หรือไม่
jwenting

3
@ jwenting: มันเกี่ยวข้องหรือไม่ แต่แม้ว่า: หนึ่งสามารถสร้างตัวอย่างที่ถูกต้องเช่นเช่น Java / C # -Interop Helper Library หรือดู Java สำหรับ. net: ikvm.net ภาษามีความเกี่ยวข้องเสมอ ผู้ถามอาจไม่มองหาภาษาที่เฉพาะเจาะจงเขา / เธออาจก่อให้เกิดข้อผิดพลาดทางไวยากรณ์ (โดยไม่ได้ตั้งใจแปลง Java เป็น C # เช่น), ภาษาใหม่อาจเกิดขึ้น (หรือเพิ่มขึ้นในป่าที่มีมังกร) - แก้ไข : ความคิดเห็นก่อนหน้าของฉันคือ dicky ขอโทษ
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

5
@ mrjink: ฉันไม่เห็นว่านี่เป็น "ปัญหา" มันเป็นเพียงแนวทางที่แตกต่างกับข้อดีและข้อเสียที่แตกต่างกัน "มืออาชีพ" ของการแก้ปัญหาของคุณ: คำสั่งแต่ละคำสั่งสามารถมีกลไกการจับคู่ส่วนบุคคล "โปร" ของฉัน: คำสั่งไม่จำเป็นต้องให้กลไกการจับคู่ของตัวเอง OP ต้องตัดสินใจเลือกวิธีที่เหมาะสมกับเขาที่สุด โดยวิธีการที่ฉัน upvoted คำตอบของคุณ
Doc Brown

1
FYI - "Pointer to function" เป็นคุณสมบัติภาษา C # ชื่อผู้ได้รับมอบหมาย แลมบ์ดาเป็นวัตถุนิพจน์ที่สามารถผ่านไปได้ - คุณไม่สามารถ "เรียก" แลมบ์ดาอย่างที่คุณสามารถเรียกผู้แทนได้
user1068

1
ยกtoLowerCaseการดำเนินการออกจากวง
zwol

1
@HarrisonNguyen: ผมขอแนะนำให้docs.oracle.com/javase/tutorial/java/javaOO/... แต่ถ้าคุณไม่ได้ใช้ Java 8 คุณสามารถใช้นิยามอินเตอร์เฟสของ mrink โดยไม่มีส่วน "ที่ตรงกัน" นั่นเทียบเท่า 100% (นั่นคือสิ่งที่ฉันหมายถึงโดย "จำลองตัวชี้ไปยังฟังก์ชันโดยใช้รูปแบบคำสั่ง) และของ user1068 ความคิดเห็นเป็นเพียงข้อสังเกตเกี่ยวกับเงื่อนไขที่แตกต่างกันสำหรับตัวแปรที่แตกต่างของแนวคิดเดียวกันในภาษาการเขียนโปรแกรมที่แตกต่างกัน
Doc Brown

31

ฉันอาจทำสิ่งนี้:

public interface Command {
  boolean matches(String message);

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

จากนั้นคุณสามารถให้ทุกคำสั่งใช้อินเทอร์เฟซนี้และคืนค่าจริงเมื่อตรงกับข้อความ

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
รูปแบบนี้มีแนวโน้มที่จะพอดีกับคำสั่งคอนโซล paradygm อย่างสวยงาม แต่เพียงเพราะมันแยกตรรกะคำสั่งออกจาก Event Bus อย่างเป็นระเบียบและเนื่องจากคำสั่งใหม่มีแนวโน้มที่จะถูกเพิ่มในอนาคต ฉันคิดว่ามันควรจะสังเกตว่านี่ไม่ใช่วิธีแก้ปัญหาสำหรับทุกสถานการณ์ที่คุณมีสายยาวถ้า ... elseif
Neil

+1 สำหรับการใช้รูปแบบคำสั่ง "แสง"! ควรสังเกตว่าเมื่อคุณจัดการกับสายที่ไม่ใช่คุณสามารถใช้ประโยชน์เช่น enums อัจฉริยะที่รู้วิธีการดำเนินการตรรกะของพวกเขา สิ่งนี้ยังช่วยให้คุณประหยัดสำหรับวง
LastFreeNickname

3
แทนที่จะวนซ้ำเราสามารถใช้สิ่งนี้เป็นแผนที่ได้หากคุณกำหนดให้คำสั่งเป็นคลาสนามธรรมที่แทนที่equalsและhashCodeเหมือนกับสตริงที่แสดงถึงคำสั่ง
Cruncher

มันยอดเยี่ยมและเข้าใจง่าย ขอบคุณสำหรับคำแนะนำ มันเป็นสิ่งที่ฉันกำลังมองหาอยู่และดูเหมือนว่าจะจัดการได้สำหรับการเพิ่มคำสั่งในอนาคต
แฮร์ริสันเหงียน

15

คุณกำลังใช้ Java - ทำให้มันสวยงาม ;-)

ฉันอาจใช้คำอธิบายประกอบ:

  1. สร้างคำอธิบายประกอบวิธีที่กำหนดเอง

    @IRCCommand( String command, boolean perfectmatch = false )
  2. เพิ่มคำอธิบายประกอบให้กับวิธีการที่เกี่ยวข้องทั้งหมดในคลาสเช่น

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. ใน Constructor ของคุณให้ใช้ Reflections เพื่อสร้าง HashMap of Methods จากวิธีการทำหมายเหตุประกอบทั้งหมดในคลาสของคุณ:

    ...
    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-Code ที่จัดรูปแบบอัตโนมัติ ... แต่คุณสามารถทำเช่นเดียวกันกับ C # และด้วย C ++ คุณสามารถจำลองคำอธิบายประกอบด้วยมาโครที่ฉลาดบางตัว
Falco

อาจเป็น Java ได้เป็นอย่างดีอย่างไรก็ตามในอนาคตฉันขอแนะนำให้คุณหลีกเลี่ยงการแก้ปัญหาเฉพาะภาษาหากไม่ได้ระบุภาษาอย่างชัดเจน คำแนะนำที่เป็นมิตรเพียง
Neil

ขอบคุณสำหรับโซลูชันนี้ - คุณพูดถูกว่าฉันใช้ Java ในโค้ดที่ให้แม้ว่าฉันจะไม่ได้ระบุมันก็ดูดีมากและฉันจะต้องแน่ใจว่าได้ยิง ขอโทษฉันไม่สามารถเลือกคำตอบที่ดีที่สุดสองคำได้!
แฮร์ริสันเหงียน

5

ถ้าคุณนิยามอินเตอร์เฟสให้บอกว่าIChatBehaviourมีวิธีใดที่เรียกว่าExecuteซึ่งใช้ใน a messageและcmdวัตถุ:

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

และอื่น ๆ สำหรับส่วนที่เหลือ

ในชั้นเรียนหลักของคุณคุณจะมีรายการพฤติกรรม ( List<IChatBehaviour>) ซึ่งบอต IRC ของคุณใช้ จากนั้นคุณสามารถแทนที่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
ไม่อยู่ที่ไหนifs ไป? เช่นคุณจะตัดสินว่าจะมีการใช้งานคำสั่งอย่างไร
mrjink

2
@mrjink: ขอโทษฉันไม่ดี การตัดสินใจว่าจะดำเนินการหรือไม่นั้นมอบให้กับพฤติกรรม (ฉันละเว้นifส่วนหนึ่งในพฤติกรรมโดยไม่ตั้งใจ)
npinti

ฉันคิดว่าฉันชอบการตรวจสอบว่ามีให้IChatBehaviourสามารถจัดการคำสั่งที่กำหนดเพราะมันช่วยให้ผู้โทรทำมากขึ้นเช่นการประมวลผลข้อผิดพลาดหากไม่มีคำสั่งที่ตรงกันแม้ว่าจริงๆแล้วมันเป็นเพียงการตั้งค่าส่วนตัว หากไม่จำเป็นต้องใช้รหัสนั้นจะไม่ซับซ้อน
Neil

คุณยังคงต้องมีวิธีการสร้างอินสแตนซ์ของชนชั้นที่ถูกต้องในการที่จะดำเนินการได้ซึ่งยังคงมีห่วงโซ่ยาวเดียวกันของไอเอฟเอสวิทช์หรือใบใหญ่ ...
jwenting

1

"ฉลาด" สามารถ (อย่างน้อย) สามสิ่ง:

ประสิทธิภาพที่สูงขึ้น

ข้อเสนอแนะตารางการจัดส่ง (และรายการเทียบเท่า) เป็นสิ่งที่ดี ตารางดังกล่าวถูกเรียกว่า "CADET" ในหลายปีที่ผ่านมาสำหรับ "ไม่สามารถเพิ่มได้หรือไม่ลอง" อย่างไรก็ตามพิจารณาความคิดเห็นเพื่อช่วยผู้ดูแลมือใหม่ในการจัดการตารางดังกล่าว

การบำรุงรักษา

"ทำให้สวย" ไม่ใช่คำเตือนที่ไม่ใช้งาน

และมักถูกมองข้าม ...

ความยืดหยุ่น

การใช้ toLowerCase มีข้อผิดพลาดในการที่ข้อความบางภาษาในบางภาษาจะต้องได้รับการปรับโครงสร้างที่เจ็บปวดเมื่อมีการเปลี่ยนแปลงระหว่าง magiscule และ miniscule น่าเสียดายที่มีข้อผิดพลาดเดียวกันสำหรับ 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อาจเป็นแนวคิดที่ดีสำหรับคำสั่งขึ้นอยู่กับจำนวนที่คุณคาดว่าจะสร้าง)


ฉันคิดว่านี่เป็นสิ่งที่ดีสำหรับการแยกกลุ่มและการจัดโปรแกรม แต่ฉันไม่คิดว่าจะแก้ปัญหาโดยตรงกับการมีงบมากเกินไปถ้า ... elseif
Neil

0

สิ่งที่ฉันจะทำคือ:

  1. จัดกลุ่มคำสั่งที่คุณมีไว้เป็นกลุ่ม (คุณมีอย่างน้อย 20 ตอนนี้)
  2. ในระดับแรกจัดหมวดหมู่ตามกลุ่มดังนั้นคุณมีคำสั่งที่เกี่ยวข้องกับชื่อผู้ใช้คำสั่งเพลงคำสั่งนับเป็นต้น
  3. จากนั้นคุณไปในวิธีการของแต่ละกลุ่มคราวนี้คุณจะได้รับคำสั่งดั้งเดิม

สิ่งนี้จะทำให้จัดการได้ง่ายขึ้น ได้ประโยชน์มากขึ้นเมื่อจำนวน 'อื่นถ้า' เติบโตมากเกินไป

แน่นอนว่าบางครั้งการมี 'ถ้าอย่างอื่น' เหล่านี้จะไม่ใช่ปัญหาใหญ่ ฉันไม่คิดว่า 20 เป็นสิ่งที่ไม่ดี

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.