重构Java工厂方法

时间:2020-03-06 14:32:35  来源:igfitidea点击:

这段代码有些令人不满意:

/*
Given a command string in which the first 8 characters are the command name
padded on the right with whitespace, construct the appropriate kind of 
Command object.
*/
public class CommandFactory {
     public Command getCommand(String cmd) {
         cmdName = cmd.subString(0,8).trim();

         if(cmdName.equals("START")) {
             return new StartCommand(cmd);
         }
         if(cmdName.equals("END")) {
             return new EndCommand(cmd);
         }
         // ... more commands in more if blocks here
         // else it's a bad command.
         return new InvalidCommand(cmd);
     }
}

对于结构清晰的多个出口点,我毫不pent悔。但是我对一系列几乎相同的if语句不满意。我考虑过将字符串映射为命令:

commandMap = new HashMap();
commandMap.put("START",StartCommand.class);
// ... etc.

...然后使用Reflection使相应类的实例从Map查找。但是,尽管从概念上讲是优雅的,但是这涉及到大量的反射代码,尽管成本可能会被收益抵消,但是继承此代码的人可能不会欣赏。硬编码到commandMap中的所有行的气味几乎与if块一样糟糕。

如果工厂的构造函数可以扫描类路径中Command的子类,查询它们的String表示形式,然后将它们自动添加到其库中,那就更好了。

那么我应该如何重构呢?

我猜那里的一些框架免费为我提供了这种东西。假设我无法将这些内容迁移到这样的框架中。

解决方案

至少,命令应具有getCommandString()-其中,StartCommand会被覆盖以返回" START"。然后,我们可以注册或者发现课程。

采用"对流与配置"方法,并使用反射来扫描可用的Command对象并将其加载到地图中,将是理想之选。然后,我们无需重新编译即可公开新命令。

它不是直接回答问题,而是为什么不引发InvalidCommandException(或者类似的东西),而是返回InvalidCommand类型的对象?

我认为字符串到命令的映射很好。我们甚至可以将字符串命令名分解为构造函数(即StartCommand是否不应该知道其命令为" START"?)如果可以这样做,则命令对象的实例化要简单得多:

Class c = commandMap.get(cmdName);
if (c != null)
    return c.newInstance();
else
    throw new IllegalArgumentException(cmdName + " is not as valid command");

另一个选择是用链接到类的所有命令创建一个"枚举"(假设所有命令对象都实现" CommandInterface"):

public enum Command
{
    START(StartCommand.class),
    END(EndCommand.class);

    private Class<? extends CommandInterface> mappedClass;
    private Command(Class<? extends CommandInterface> c) { mappedClass = c; }
    public CommandInterface getInstance()
    {
        return mappedClass.newInstance();
    }
}

由于枚举的toString是它的名称,因此我们可以使用EnumSet找到正确的对象并从内部获取该类。

将此重复的对象创建代码全部隐藏在工厂中还算不错。如果必须在某处完成它,至少到这里都可以了,所以我不必担心太多。

如果我们真的想对它做点什么,也许可以选择Map,但是从属性文件中对其进行配置,然后从该props文件中构建地图。

不使用类路径发现路径(我不知道该路径),我们将始终在2个地方进行修改:编写一个类,然后在某个地方(工厂,地图初始化或者属性文件)添加一个映射。

除了

cmd.subString(0,8).trim();

部分,这对我来说似乎还不错。我们可以使用"地图"并使用反射,但是,根据我们添加/更改命令的频率,这可能不会给我们带来很多好处。

我们可能应该记录为什么只想要前8个字符,或者更改协议,以便更容易找出该字符串的哪一部分是命令(例如,在命令键后加上':'或者';'等标记,单词)。

我喜欢想法,但是如果我们想避免反射,则可以向HashMap中添加实例:

commandMap = new HashMap();
commandMap.put("START",new StartCommand());

每当需要命令时,只需克隆即可:

command = ((Command) commandMap.get(cmdName)).clone();

然后,设置命令字符串:

command.setCommandString(cmdName);

但是使用clone()听起来不如使用反射:(

考虑到这一点,我们可以创建一些小的实例化类,例如:

class CreateStartCommands implements CommandCreator {
    public bool is_fitting_commandstring(String identifier) {
        return identifier == "START"
    }
    public Startcommand create_instance(cmd) {
        return StartCommand(cmd);
    }
}

当然,如果微小的类只能说"是的,那就开始,给我那个"或者"不,不喜欢那个",这会增加很多,但是,我们现在可以将工厂重新设计包含这些CommandCreator的列表,然后仅询问它们:"我们喜欢此命令吗?"并返回第一个接受的CommandCreator的create_instance结果。当然,现在从CommandCreator外部提取前8个字符看起来有些尴尬,因此我将进行重新设计,以便将整个命令字符串传递到CommandCreator中。

我想我在这里应用了一些"多态替换开关"-重构,以防万一有人对此感到奇怪。

我想通过反射来制作地图和创作。如果扫描类路径太慢,则可以始终向该类添加自定义注释,使注释处理器在编译时运行,并将所有类名称存储在jar元数据中。

然后,我们唯一可以做的错误就是忘记注释。

我前一阵子用maven和APT做过这样的事情。

除非有某种原因,否则我总是尝试使命令实现变为无状态。如果是这种情况,我们可以在命令界面中添加一个方法boolean identifier(String id)方法,该方法将告诉此实例是否可用于给定的字符串标识符。然后工厂可能看起来像这样(注意:我没有编译或者测试它):

public class CommandFactory {
    private static List<Command> commands = new ArrayList<Command>();       

    public static void registerCommand(Command cmd) {
        commands.add(cmd);
    }

    public Command getCommand(String cmd) {
        for(Command instance : commands) {
            if(instance.identifier(cmd)) {
                return cmd;
            }
        }
        throw new CommandNotRegisteredException(cmd);
    }
}

动态查找要加载的类的另一种方法是省略显式映射,而仅尝试从命令字符串构建类名。标题大小写和连接算法可以将" START"->" com.mypackage.commands.StartCommand"转换为仅使用反射来实例化它。如果找不到该类,则以某种方式失败(InvalidCommand实例或者我们自己的异常)。

然后,只需添加一个对象并开始使用它就可以添加命令。

下面的代码如何:

public enum CommandFactory {
    START {
        @Override
        Command create(String cmd) {
            return new StartCommand(cmd);
        }
    },
    END {
        @Override
        Command create(String cmd) {
            return new EndCommand(cmd);
        }
    };

    abstract Command create(String cmd);

    public static Command getCommand(String cmd) {
        String cmdName = cmd.substring(0, 8).trim();

        CommandFactory factory;
        try {
            factory = valueOf(cmdName);
        }
        catch (IllegalArgumentException e) {
            return new InvalidCommand(cmd);
        }
        return factory.create(cmd);
    }
}

枚举的" valueOf(String)"用于查找正确的工厂方法。如果工厂不存在,它将抛出一个" IllegalArgumentException"。我们可以以此为信号来创建InvalidCommand对象。

一个额外的好处是,如果我们还可以使方法create(String cmd)成为公共方法,并且我们还将使这种构造Command对象编译时间的方式对其余代码可用,则该方法是可用的。然后,我们可以使用CommandFactory.START.create(String cmd`)创建一个Command对象。

最后一个好处是我们可以轻松地在Javadoc文档中创建所有可用命令的列表。

一种选择是使每种命令类型都有其自己的工厂。这给我们带来两个好处:

1)通用工厂不会调用new。因此,每种命令类型将来都可以根据字符串中空格后面的参数返回不同类的对象。

2)在HashMap方案中,可以通过对每个命令类映射到实现SpecializedCommandFactory接口的对象而不是映射到类本身来避免反射。实际上,该对象可能是单例对象,但不必这样指定。然后,通用getCommand会调用专门的getCommand。

就是说,工厂扩散可能会一发不可收拾,而拥有的代码就是最简单的方法。我个人可能会保持原样:我们可以在源代码和规范中比较命令列表,而无需非本地考虑,例如以前称为CommandFactory.registerCommand的内容,或者通过反射发现的类。不会混淆。少于一千个命令的速度很慢。唯一的问题是,如果不修改工厂,就无法添加新的命令类型。但是我们要进行的修改是简单且重复的,并且如果我们忘记进行修改,则对于包含新类型的命令行会出现明显的错误,因此操作并不繁琐。

我这样做的方法是没有通用的Factory方法。

我喜欢将域对象用作命令对象。自从我使用Spring MVC以来,这是一个很好的方法,因为DataBinder.setAllowedFields方法为我提供了很大的灵活性,可以将单个域对象用于几种不同的形式。

为了获得命令对象,我在Domain对象类上有一个静态工厂方法。例如,在成员类中,我将使用类似-

public static Member getCommandObjectForRegistration();
public static Member getCommandObjectForChangePassword();

等等。

我不确定这是否是个好方法,我从没在任何地方看到它的建议,只是我自己b / c提出了这个想法,我喜欢将这样的东西放在一个地方的想法。如果有人发现任何反对的理由,请在评论中让我知道。

在思考建议上+1,它将使课堂结构更加理智。

实际上,我们可以执行以下操作(如果我们还没有考虑过的话)
创建与我们希望作为getCommand()工厂方法的参数的String相对应的方法,那么我们所要做的就是反射和invoke()这些方法并返回正确的对象。

我建议尽可能避免反射。这有点邪恶。

我们可以使用三元运算符使代码更简洁:

return 
     cmdName.equals("START") ? new StartCommand  (cmd) :
     cmdName.equals("END"  ) ? new EndCommand    (cmd) :
                               new InvalidCommand(cmd);

我们可以介绍一个枚举。将每个枚举常量设置为一个工厂很冗长,并且还需要花费一些运行时内存。但是我们可以轻松地查找一个枚举,然后将其与==或者switch一起使用。

import xx.example.Command.*;

 Command command = Command.valueOf(commandStr);
 return 
     command == START ? new StartCommand  (commandLine) :
     command == END   ? new EndCommand    (commandLine) :
                        new InvalidCommand(commandLine);

勇往直前,反思。但是,在此解决方案中,现在假定Command接口具有可访问setCommandString(String s)方法,因此newInstance易于使用。另外,commandMap是任何具有字符串键(cmd)的映射,它们对应于它们所对应的Command类实例。

public class CommandFactory {
     public Command getCommand(String cmd) {
        if(cmd == null) {
            return new InvalidCommand(cmd);
        }

        Class commandClass = (Class) commandMap.get(cmd);

        if(commandClass == null) {
            return new InvalidCommand(cmd);
        }

        try {
            Command newCommand = (Command) commandClass.newInstance();
            newCommand.setCommandString(cmd);
            return newCommand;
        }
        catch(Exception e) {
            return new InvalidCommand(cmd);
     }
}

嗯,浏览,只是碰到了这一点。我还能发表评论吗?

恕我直言,原始的if / else块代码没有任何问题。这很简单,而简单性必须始终是我们设计中的首要任务(http://c2.com/cgi/wiki?DoTheSimplestThingThatCouldPossibleWork)

这似乎尤其正确,因为所提供的所有解决方案都比原始代码少了自我记录功能……我的意思是我们不应该为阅读而不是翻译编写代码……