重构Java代码

6

好的,我来翻译一下。以下是需要翻译的内容:

好的,看起来这个问题很像:

什么是替换或代替程序中if..else if..else树的最佳方法?

考虑这个问题已经解决!


我想重构类似于这样的代码:

String input; // input from client socket.
if (input.equals(x)) {
  doX();
} else if (input.equals(y)) {
  doY();
} else {
  unknown_command();
}

这是一段代码,用于检查来自套接字的输入以执行某些操作,但我不喜欢if else的结构,因为每次向服务器(代码)添加新命令时,都必须添加一个新的if else语句,这很丑陋。另外,在删除命令时,必须修改if else语句。


“Map”? 其实那只是推迟问题。我会将函数放在不同的实例中,然后或许加入一个“接口”。 - Tom Hawtin - tackline
2
(嘘,第2行和第4行缺少一个右括号。) - Jim Kiley
哎呀 :). 你说得对。已经修复了。 - Alfred
可能是 https://dev59.com/UHRB5IYBdhLWcg3wxZ7Y 的重复问题。 - Esko
可能是重复的问题,链接为https://dev59.com/h3M_5IYBdhLWcg3w2XHw。 - dfa
5个回答

8

将这些命令收集到一个 Map<String, Command> 中,其中 Command 是一个带有 execute() 方法的 interface

Map<String, Command> commands = new HashMap<String, Command>();
// Fill it with concrete Command implementations with `x`, `y` and so on as keys.

// Then do:
Command command = commands.get(input);
if (command != null) {
    command.execute();
} else {
    // unknown command.
}

为了更进一步,您可以考虑通过扫描实现特定接口(在本例中为Command)或类路径中的特定注释来动态填充地图。 Google Reflections 可以大有帮助。 更新(来自评论)您还可以考虑将我的答案与Instantsoup的答案结合起来。在buildExecutor()方法中,首先从Map获取命令,如果命令不存在于Map中,则尝试加载关联类并将其放入Map中。这是一种懒加载方式。这比在我的答案中扫描整个类路径并像Instantsoup的答案一样每次创建更有效。

但是每次添加/删除新命令时,我都必须修改地图吗?我不能动态地或其他方式来完成这个任务吗? - Alfred
@Alfred。你可以尝试反射,但这几乎不是最好的选择。选择使用映射,将其保存在安全的位置,以便轻松添加新命令。 - Tom
2
你可以使用Spring这样的工具将实现注入到映射中,但无论是if()语句块、map.add(new Command())还是在Spring XML文件中,你都必须要接触一些代码才能添加新分支逻辑。 - user177800
我认为你的实现非常不错,但我并不确定这是最佳解决方案。 - Alfred
你可以考虑将instantsoup的答案与我的答案结合起来。在buildExecutor()方法中,首先从Map中获取命令,如果该命令不存在于映射中,则尝试加载相关类并将其放入映射中。这有点像懒加载。这比在我的答案中扫描整个类路径并像instantsoup的答案一样每次都创建它更有效率。 - BalusC
显示剩余3条评论

3

一种方法是创建一个接口ICommand,作为命令的通用契约,例如:

public interface ICommand {
    /** @param context The command's execution context */
    public void execute(final Object context);
    public String getKeyword();
}

然后,您可以使用Java的SPI机制自动发现各种实现并将它们注册到Map<String,ICommand>中,然后执行knownCommandsMap.get(input).execute(ctx)或类似的操作。

这实际上使您能够将服务与命令实现分离,从而使它们可插拔。

通过添加一个名为ICommand类的完全限定名称的文件(因此,如果它在包dummy中,则文件将是META-INF/dummy.ICommand),来向SPI注册实现类,然后您将加载并注册它们:

final ServiceLoader<ICommand> spi = ServiceLoader.load(ICommand.class);
for(final ICommand commandImpl : spi)
    knownCommandsMap.put(commandImpl.getKeyword(), commandImpl);

这只能在JDK 6中使用。 - user177800
你能更详细地解释一下 SPI 部分吗? - Alfred
javadocs说“自1.6以来”,不应使用sun.*中的任何内容。 - user177800
最好将此与注释混合使用。您可以在提供命令实现的类上使用注释来说明该实现是用于哪个命令,然后使用注释处理器在构建阶段构建命令列表。(我不是注释处理方面的专家; 从未按照我描述的方式构建过系统,但确实知道Web服务容器会执行此类操作。) - Donal Fellows
@donal:确实 :)@fuzzy lollipop:我同意。不过这在1.5的草案规范中已经有了。 - Romain
显示剩余2条评论

3
那接口、工厂和一点反射怎么样?当然,如果输入不良,您仍然需要处理异常,但您总是需要这样做。使用此方法,只需为新输入添加一个Executor的新实现即可。
public class ExecutorFactory
{
    public static Executor buildExecutor(String input) throws Exception
    {
        Class<Executor> forName = (Class<Executor>) Class.forName(input);
        return (Executor) executorClass.newInstance();
    }
}

public interface Executor
{
    public void execute();
}


public class InputA implements Executor
{
    public void execute()
    {
        // do A stuff
    }
}

public class InputB implements Executor
{
    public void execute()
    {
        // do B stuff
    }
}

您的代码示例现在变成了:
String input;
ExecutorFactory.buildExecutor(input).execute();

1
这也是一个不错的想法,它只需要每次创建一个新实例的成本。 - BalusC
1
执行器的名称来自哪里?如果它是客户端/用户输入,那么恶意用户可以在系统上实例化任何类。如果类初始化更改系统状态,则攻击者将获得对系统的某种程度的控制。然后,在实例化和抛出异常之间会存在竞争条件(假设该类不是Executor的子类),在此期间攻击者可以利用其他类开放的任何功能。 - atk
@atk 确定了。我认为还涉及到一些错误处理和输入检查,但这并未被表示出来。我将添加一个检查,以确保命令至少是一个Executor。 - Instantsoup

2

在枚举类上构建命令模式可以减少一些样板代码。假设input.equals(x)中的x是"XX",input.equals(y)中的y是"YY"。

enum Commands {
   XX {
     public void execute() { doX(); }        
   },
   YY {
     public void execute() { doY(); }        
   };

   public abstract void execute();
}

String input = ...; // Get it from somewhere

try {
  Commands.valueOf(input).execute();
}
catch(IllegalArgumentException e) {
   unknown_command();
}

这也是一个不错的想法,但它非常紧密耦合。你不能再从“外部”提供命令了。 - BalusC

1

你说你正在处理来自套接字的输入。输入量有多大?它有多复杂?结构化程度如何?

根据这些问题的答案,你可能最好编写一个语法,并让解析器生成器(例如ANTLR)生成输入处理代码。


只是一个简单的协议。例如memcached。 - Alfred
1
@Alfred - 对于memcached,它是一个非常简单的协议,只有六个操作且不太可能改变,我会使用if-else结构。没有理由使您的代码变得更加复杂,只为了"对象化" 它。但是,我会创建一个对象来包装命令和/或响应,并处理所有解析。并且可能会创建一个枚举来表示命令(这将把if-else链转换为switch)。 - Anon
1
如果我想要轻松地替换行为,我可以使用模板方法模式,为每个操作创建抽象方法,并让我的应用程序反射实例化子类。 - Anon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接