Có cách nào thông minh hơn để làm điều này ngoài một chuỗi dài các câu lệnh if hoặc switch không?


20

Tôi đang triển khai bot IRC nhận được tin nhắn và tôi đang kiểm tra tin nhắn đó để xác định chức năng nào cần gọi. Có cách nào thông minh hơn để làm điều này? Có vẻ như nó sẽ nhanh chóng thoát khỏi tầm tay sau khi tôi nhận được tới 20 lệnh.

Có lẽ có một cách tốt hơn để trừu tượng này?

 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
Ngôn ngữ gì, loại quan trọng của nó ở cấp độ chi tiết này.
mattnz

3
@mattnz bất cứ ai quen thuộc với Java sẽ nhận ra nó trong mẫu mã mà anh ta cung cấp.
jwenting

6
@jwenting: Đó cũng là cú pháp C # hợp lệ và tôi cá là có nhiều ngôn ngữ hơn.
phresnel

4
@phresnel có, nhưng những cái đó có cùng API tiêu chuẩn chính xác cho Chuỗi không?
jwenting

3
@jwenting: Có liên quan không? Nhưng ngay cả khi: Người ta có thể xây dựng các ví dụ hợp lệ, ví dụ như Thư viện trợ giúp Java / C # -Interop hoặc xem Java cho .net: ikvm.net. Ngôn ngữ luôn có liên quan. Người hỏi có thể không tìm kiếm các ngôn ngữ cụ thể, anh ấy / cô ấy có thể đã mắc lỗi cú pháp (vô tình chuyển đổi Java sang C #, ví dụ), các ngôn ngữ mới có thể phát sinh (hoặc đã nổi lên trong tự nhiên, nơi có rồng) - chỉnh sửa : Nhận xét trước đây của tôi là tinh ranh, xin lỗi.
phresnel

Câu trả lời:


45

Sử dụng một bảng công văn . Đây là bảng chứa các cặp ("phần thông báo", pointer-to-function). Bộ điều phối sau đó sẽ trông như thế này (bằng mã giả):

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

(các equalsIgnoreCase có thể được xử lý như một trường hợp đặc biệt ở đâu đó trước đây hoặc nếu bạn có nhiều thử nghiệm trong số đó, với bảng công văn thứ hai).

Tất nhiên, những gì pointer-to-functionphải trông giống như phụ thuộc vào ngôn ngữ lập trình của bạn. Đây là một ví dụ trong C hoặc C ++. Trong Java hoặc C #, bạn có thể sẽ sử dụng các biểu thức lambda cho mục đích đó hoặc bạn mô phỏng "con trỏ tới các hàm" bằng cách sử dụng mẫu lệnh. Cuốn sách trực tuyến miễn phí " Thứ tự cao hơn Perl " có một chương hoàn chỉnh về các bảng điều khiển sử dụng Perl.


4
Tuy nhiên, vấn đề với điều này là bạn sẽ không thể kiểm soát cơ chế khớp. Trong ví dụ của OP, anh ấy sử dụng equalsIgnoreCasecho "bây giờ đang chơi" nhưng toLowerCase().startsWithcho những người khác.
mrjink

5
@mrjink: Tôi không xem đây là một "vấn đề", nó chỉ là một cách tiếp cận khác với những ưu và nhược điểm khác nhau. "Pro" của giải pháp của bạn: Các lệnh riêng lẻ có thể có các cơ chế khớp riêng lẻ. "Pro" của tôi: Các lệnh không phải cung cấp cơ chế khớp riêng. OP phải quyết định giải pháp nào phù hợp nhất với anh ấy. Nhân tiện, tôi cũng nêu lên câu trả lời của bạn.
Doc Brown

1
FYI - "Con trỏ hoạt động" là một tính năng ngôn ngữ C # được gọi là đại biểu. Lambda là một đối tượng biểu hiện có thể được thông qua - bạn không thể "gọi" một lambda như bạn có thể gọi một đại biểu.
dùng1068

1
Palăng toLowerCasehoạt động ra khỏi vòng lặp.
zwol

1
@HarrisonNguyen: Tôi khuyên bạn nên docs.oracle.com/javase/tutorial/java/javaOO/ . Nhưng nếu bạn không sử dụng Java 8, bạn chỉ có thể sử dụng định nghĩa giao diện của mrink mà không có phần "khớp", tương đương 100% (đó là ý của tôi bằng cách "mô phỏng con trỏ đến chức năng bằng cách sử dụng mẫu lệnh). Và user1068 bình luận chỉ là một nhận xét về các thuật ngữ khác nhau cho các biến thể khác nhau của cùng một khái niệm trong các ngôn ngữ lập trình khác nhau.
Doc Brown

31

Có lẽ tôi sẽ làm một cái gì đó như thế này:

public interface Command {
  boolean matches(String message);

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

Sau đó, bạn có thể có mọi lệnh thực hiện giao diện này và trả về true khi nó khớp với thông báo.

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

Nếu các thông điệp không bao giờ cần phải được phân tích cú pháp ở nơi khác (tôi cho rằng trong câu trả lời của tôi) thì điều này thực sự thích hợp hơn với giải pháp của tôi vì Commandnó độc lập hơn nếu nó biết khi nào được gọi là chính nó. Nó không tạo ra một chi phí nhỏ nếu danh sách các lệnh rất lớn nhưng có lẽ không đáng kể.
jhr

1
Mẫu này có xu hướng phù hợp với lệnh điều khiển paradygm độc đáo, nhưng chỉ vì nó tách biệt gọn gàng logic lệnh khỏi bus sự kiện và bởi vì các lệnh mới có thể sẽ được thêm vào trong tương lai. Tôi nghĩ rằng cần lưu ý rằng đây không phải là một giải pháp cho bất kỳ trường hợp nào mà bạn có một chuỗi dài nếu ... khác
Neil

+1 để sử dụng mẫu Lệnh "nhẹ"! Cần lưu ý rằng khi bạn giao dịch với các chuỗi không phải là Chuỗi, bạn có thể sử dụng các ví dụ thông minh biết cách thực hiện logic của chúng. Điều này thậm chí giúp bạn tiết kiệm vòng lặp for.
LastFreeNickname

3
Thay vì vòng lặp, chúng ta có thể sử dụng nó như một bản đồ nếu bạn tạo cho Command một lớp trừu tượng ghi đè equalshashCodegiống như chuỗi đại diện cho lệnh
Cruncher

Điều này thật tuyệt vời và dễ hiểu. Cám ơn vì sự gợi ý. Đó chính xác là những gì tôi đang tìm kiếm và dường như có thể quản lý được để bổ sung các lệnh trong tương lai.
Harrison Nguyễn

15

Bạn đang sử dụng Java - vì vậy hãy làm cho nó đẹp ;-)

Tôi có thể sẽ làm điều này bằng cách sử dụng Chú thích:

  1. Tạo chú thích phương thức tùy chỉnh

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Thêm chú thích cho tất cả các Phương thức có liên quan trong Lớp, vd

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. Trong hàm tạo của bạn, hãy sử dụng Reflection để tạo HashMap của các Phương thức từ tất cả các Phương thức được chú thích trong lớp của bạn:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. Trong onMessagePhương thức của bạn , chỉ cần thực hiện một vòng lặp commandListcố gắng khớp chuỗi trên mỗi chuỗi và gọi method.invoke()nơi phù hợp.

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

Không rõ là anh ta đang sử dụng Java, mặc dù đó là một giải pháp tao nhã.
Neil

Bạn nói đúng - mã trông rất giống mã Java được định dạng tự động nhật thực ... Nhưng bạn có thể làm tương tự với C # và với C ++, bạn có thể mô phỏng các chú thích với một số Macros thông minh
Falco

Nó rất có thể là Java, tuy nhiên trong tương lai, tôi khuyên bạn nên tránh các giải pháp dành riêng cho ngôn ngữ nếu ngôn ngữ không được chỉ định rõ ràng. Chỉ cần lời khuyên thân thiện.
Neil

Cảm ơn giải pháp này - bạn đã đúng khi tôi sử dụng Java trong mã được cung cấp mặc dù tôi không chỉ định, giao diện này thực sự rất hay và tôi chắc chắn sẽ cung cấp cho nó một shot. Xin lỗi tôi không thể chọn hai câu trả lời hay nhất!
Harrison Nguyễn

5

Điều gì sẽ xảy ra nếu bạn xác định một giao diện, giả sử IChatBehaviourcó một phương thức được gọi là Executelấy trong một messagevà một cmdđối tượng:

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

Trong mã của bạn, sau đó bạn triển khai giao diện này và xác định các hành vi bạn muốn:

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

Và như vậy cho phần còn lại.

Trong lớp chính của bạn, sau đó bạn có một danh sách các hành vi ( List<IChatBehaviour>) mà bot IRC của bạn thực hiện. Sau đó, bạn có thể thay thế các iftuyên bố của mình bằng một cái gì đó như thế này:

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

Ở trên nên giảm số lượng mã bạn có. Cách tiếp cận trên cũng sẽ cho phép bạn cung cấp các hành vi bổ sung cho lớp bot của mình mà không cần sửa đổi chính lớp bot (theoStrategy Design Pattern ).

Nếu bạn chỉ muốn một hành vi kích hoạt bất cứ lúc nào, bạn có thể thay đổi chữ ký của executephương thức để mang lại true(hành vi đã kích hoạt) hoặc false(hành vi không kích hoạt) và thay thế vòng lặp trên bằng một thứ như thế này:

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

Ở trên sẽ tẻ nhạt hơn khi thực hiện và khởi tạo vì bạn cần tạo và vượt qua tất cả các lớp bổ sung, tuy nhiên, nó sẽ làm cho bot của bạn dễ dàng mở rộng và có thể sửa đổi vì tất cả các lớp hành vi của bạn sẽ được đóng gói và hy vọng độc lập với nhau.


1
Đã nơi ifs đi đâu? Tức là, làm thế nào để bạn quyết định một hành vi được thực thi cho một lệnh?
mrjink

2
@mrjink: Xin lỗi tôi xấu. Quyết định có thực hiện hay không được ủy thác cho hành vi (Tôi đã bỏ qua ifphần sai trong hành vi).
npinti

Tôi nghĩ rằng tôi thích kiểm tra xem liệu một lệnh nhất định IChatBehaviourcó thể xử lý một lệnh đã cho hay không, vì nó cho phép người gọi thực hiện nhiều hơn với nó, như xử lý lỗi nếu không có lệnh nào khớp, mặc dù đó thực sự chỉ là một sở thích cá nhân. Nếu điều đó là không cần thiết, thì không cần phải phức tạp mã.
Neil

bạn vẫn cần một cách để tạo ra thể hiện của lớp chính xác để thực thi nó, nó vẫn sẽ có cùng một chuỗi ifs hoặc câu lệnh chuyển đổi lớn ...
jwenting

1

"Thông minh" có thể (ít nhất) ba điều:

Hiệu suất cao hơn

Đề xuất Bảng công văn (và tương đương của nó) là một gợi ý tốt. Một bảng như vậy được gọi là "CADET" trong nhiều năm qua cho "Không thể thêm; thậm chí không thử." Tuy nhiên, hãy xem xét một nhận xét để hỗ trợ người duy trì người mới về cách quản lý bảng đã nói.

Bảo trì

"Làm cho nó đẹp" không phải là lời khuyên nhàn rỗi.

và, thường bị bỏ qua ...

Khả năng phục hồi

Việc sử dụng toLowerCase có một cạm bẫy ở chỗ một số văn bản trong một số ngôn ngữ phải trải qua quá trình tái cấu trúc đau đớn khi thay đổi giữa magiscule và miniscule. Thật không may, những cạm bẫy tương tự tồn tại cho toUpperCase. Chỉ cần lưu ý.


0

Bạn có thể có tất cả các lệnh thực hiện cùng một giao diện. Sau đó, một trình phân tích cú pháp thư có thể trả về cho bạn lệnh thích hợp mà bạn sẽ chỉ thực hiện.

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

Có vẻ như chỉ cần nhiều mã hơn. Có, bạn vẫn cần phân tích thông điệp để biết lệnh nào sẽ được thực thi nhưng bây giờ nó đã được xác định đúng. Nó có thể được tái sử dụng ở nơi khác. (Bạn có thể muốn tiêm MessageParser nhưng đó là một vấn đề khác. Ngoài ra, mẫu Flykg có thể là một ý tưởng hay cho các lệnh, tùy thuộc vào số lượng bạn dự kiến ​​sẽ được tạo.)


Tôi nghĩ rằng điều này tốt cho việc tách rời và tổ chức chương trình, mặc dù tôi không nghĩ rằng điều này trực tiếp giải quyết vấn đề với việc có quá nhiều nếu ... các tuyên bố khác.
Neil

0

Những gì tôi sẽ làm là đây:

  1. Nhóm các lệnh bạn có thành các nhóm. (bạn có ít nhất 20 cái ngay bây giờ)
  2. Ở cấp độ đầu tiên, phân loại theo nhóm, do đó bạn có lệnh liên quan đến tên người dùng, lệnh bài hát, lệnh đếm, v.v.
  3. Sau đó, bạn đi theo phương pháp của mỗi nhóm, lần này bạn sẽ nhận được lệnh ban đầu.

Điều này sẽ làm cho điều này dễ quản lý hơn. Lợi ích nhiều hơn khi số lượng 'khác nếu' tăng quá nhiều.

Tất nhiên, đôi khi có những 'nếu khác' sẽ không phải là một vấn đề lớn. Tôi không nghĩ 20 là xấu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.