如何避免在工厂中使用条件语句?

huangapple go评论75阅读模式
英文:

How to avoid conditional statements in Factory?

问题

Sure, here's the translated content:

假设我们有interface Button{ void Click(); }及其实现:AndroidButtonIOSButton。是否可以根据输入如string os = "ios"来创建适当的按钮,而不违反SOLID原则?就像当出现新类型的'os'时,我们不必更改现有的代码一样。

英文:

Imagine we have interface Button{ void Click(); } and its implementations: AndroidButton and IOSButton. Is it possible to create appropriate button based on input like string os = "ios" without violating the sOlid ? Like when a new type of 'os' appeared, we would not have to change the existing code

答案1

得分: 1

开放/关闭原则并不意味着您不能更改工厂代码。它要求您只能扩展工厂的接口和行为,而不是修改它们。

然而,像这样实现抽象工厂模式将最接近您的需求:

public interface ButtonFactory {
    Button createButton();
}

public class IosButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new IosButton();
    }
}

public class AndroidButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new AndroidButton();
    }
}

现在,如果您需要提供新的按钮类型,您只需要添加一个新的实现,就像这样:

public class WindowsButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new WindowsButton();
    }
}

这样,您的工厂就不再包含条件语句。

请注意,必须有一个地方,一个条件决定使用哪个具体的实现,与提供的 string os 相关。这可以在您的引导程序中,您在那里配置您的 IoC 容器,或者在您的主方法中,就像在这个非常整洁的示例中所示。

这种方式,您的工厂就不再包含条件语句,这是您最初提出的要求。

编辑1 关于 @LightSoul 的第一条评论

还有一些其他设计工厂模式的方法,但那时它不再是抽象工厂,而是一个简单工厂。

在这里,我们再次从接口开始:

public interface ButtonFactory {
    Button createButton(String token);
}

实现可能看起来像这样:

public class ButtonFactoryImpl implements ButtonFactory {
    private final Map<String, Supplier<Button>> factoryMethods = new HashMap<>();

    public ButtonFactoryImpl() {
        factoryMethods.put("ios", this::createIosButton);
        factoryMethods.put("android", this::createAndroidButton);
    }

    @Override
    public Button createButton(String token) {
        if (factoryMethods.containsKey(token)) {
            return factoryMethods.get(token).get();
        }

        throw new IllegalArgumentException("Token %s not configured".formatted(token));
    }

    private Button createIosButton() {
        return new IosButton();
    }

    private Button createAndroidButton() {
        return new AndroidButton();
    }
}

阅读这篇文章,了解为什么 token 必须是一个原始数据类型。

这种设计符合开放/封闭原则,因为一旦引入上面提到的 WindowsButton,您只需要扩展而不是修改代码。

如果您愿意,也不需要考虑速度和其他缺点,可以使用反射:

首先,创建一个新的注释:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Token {
    public String value();
}

然后我们的工厂可能如下所示:

public class ButtonFactoryImpl implements ButtonFactory {

    @Override
    public Button createButton(String token) {
        for(Method method : getType().getDeclaredMethods()) {
            Annotation annotation = method.getDeclaredAnnotation(Token.class);
            if (annotation instanceof Token t && t.value().equals(token)) {
               method.setAccessible(true);
               Button button = (Button) method.invoke(this, null);
               method.setAccessible(false);
               return button;
            }
        }

        throw new IllegalArgumentException("Token %s not configured".formatted(token));
    }


    @Token("ios")
    private Button createIosButton() {
        return new IosButton();
    }

    @Token("android")
    private Button createAndroidButton() {
        return new AndroidButton();
    }
}

现在,如果要扩展工厂以添加新的私有方法,以处理 @Token("windows"),就不再需要调整任何分发代码,如 switch/if 或 Map 初始化。但是,正如上面提到的,反射有一些严重的影响需要考虑。注意并祝好运。

(免责声明:上面的代码未经测试,可能需要一些小的更改才能运行。它为了易读和易理解没有包含错误处理。)

英文:

The Open/Close Principle does not mean that you must not change the factory code. It requests you to only extend, but not modify the factory's interface and behavior.

However, implementing Abstract Factory Pattern like this will come closest to your desire:

public interface ButtonFactory {
    Button createButton();
}

public class IosButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new IosButton();
    }
}

public class AndroidButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new AndroidButton();
    }
}

Now. if you have to provide a new button type, you only have to add a new implementation like this:

public class WindowsButtonFactory implements ButtonFactory {
    public Button createButton() {
        return new WindowsButton();
    }
}

This way, your factory is free of conditional statements.

Consider that there must be a location, where a condition decides, which of the concrete implementation is used related to the provided string os. This can be in your bootstrapper, where you configure your IoC container, or in your main method, as shown in this really neat sample.

This way, your Factory is free of conditional statements, as it was your request by your initial question.

EDIT1 Concerning @LightSoul 's 1st comment below

There are some other designs of the Factory Pattern, however, then, it's not longer an Abstract Factory, it's a simple Factory.

Here, once again, we start with the interface:

public interface ButtonFactory {
    Button createButton(String token);
}

The implementation might look something like:

public class ButtonFactoryImpl implements ButtonFactory {
    private final Map&lt;String, Supplier&lt;Button&gt;&gt; factoryMethods = new HashMap&lt;&gt;();

    public ButtonFactoryImpl() {
        factoryMethods.put(&quot;ios&quot;, this::createIosButton);
        factoryMethods.put(&quot;android&quot;, this::createAndroidButton);
    }

    @Override
    public Button createButton(String token) {
        if (factoryMethods.containsKey(token)) {
            return factoryMethods.get(token).get();
        }

        throw new IllegalArgumentException(&quot;Token %s not configured&quot;.formatted(token);
    }

    private Button createIosButton() {
        return new IosButton();
    }

    private Button createAndroidButton() {
        return new AndroidButton();
    }
}

Read this article to learn why token must be a primitive datatype.

This design fits to the OpenClosed Principle since you have only to extend, but not modify code, once you introduce the WindowsButton mentioned above.

If you like, and need not to consider speed and other drawbacks, you may want to use reflection:

First, create a new annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Token {
    public String value();
}

Then our factory might look like this:

public class ButtonFactoryImpl implements ButtonFactory {

    @Override
    public Button createButton(String token) {
        for(Method method : getType().getDeclaredMethods()) {
            Annotation annotation = method.getDeclaredAnnotation(Token.class);
            if (annotation instanceof Token t &amp;&amp; t.value().equals(token)) {
               method.setAccessible(true);
               Button button = (Button) method.invoke(this, null);
               method.setAccessible(false)
               return button;
            }
        }

        throw new IllegalArgumentException(&quot;Token %s not configured&quot;.formatted(token);
    }
    

    @Token(&quot;ios&quot;)
    private Button createIosButton() {
        return new IosButton();
    }

    @Token(&quot;android&quot;)
    private Button createAndroidButton() {
        return new AndroidButton();
    }
}

Now, you don't longer have a need to adjust any dispatch code, like switch/if or Map initializing, if you have to extend the factory with a new private method for the @Token("windows"). But, as mentioned above, reflection has some serious impacts to consider. Take care and good luck.

(Disclaimer: the code above is not tested and might require some small changes to run. It does not contain error handling for easy readability and understandability.)

huangapple
  • 本文由 发表于 2023年6月12日 03:39:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76452237.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定