是否有一种设计模式可以消除检查标志的需要?


28

我将在数据库中保存一些字符串有效负载。我有两个全局配置:

  • 加密
  • 压缩

可以使用配置启用或禁用这些功能,方法是仅启用其中一个,两个都启用或两个都禁用。

我当前的实现是这样的:

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

我在考虑装饰器模式。是正确的选择,还是有更好的选择?


5
您目前拥有的是什么问题?此功能的要求可能会改变吗?IE,可能会有新的if陈述吗?
达伦·杨

不,我正在寻找其他解决方案来改进代码。
Damith Ganegoda '16

46
您正在向后退一步。您找不到模式,然后编写代码以适合该模式。您编写符合您要求的代码,然后选择使用模式来描述您的代码。
与莫妮卡(Monica)进行的轻度比赛

1
注意:如果你认为你的问题确实是一个重复的这一个,然后作为提问者,你有一个选项,“覆盖”最近重新开放,并一手接近它是这样。我对自己的一些问题做了此操作,它就像一种魅力。这是我的操作方式,只需3个简单的步骤 -与“指令”的唯一区别在于,由于您的代表人数少于3K,因此您将必须通过标志对话框来获得“重复”选项
gnat

8
@LightnessRacesinOrbit:您说的话有些道理,但问是否有更好的结构化代码的方式是完全合理的,调用设计模式来描述建议的更好的结构是完全合理的。(不过,我仍然同意,当您想要的是设计时,可能会(也可能不会)严格遵循任何众所周知的模式,要求一种设计模式是一个XY问题。)此外,将“模式”用于稍微影响您的代码,因为如果您使用的是众所周知的模式,则相应地命名组件通常很有意义。
ruakh

Answers:


15

设计代码时,您总有两个选择。

  1. 只要完成它,在这种情况下,几乎所有解决方案都可以为您工作
  2. 迷恋并设计一种解决方案,该解决方案利用语言的怪癖及其意识形态(在这种情况下为OO语言-使用多态性作为提供决策的手段)

我不会集中讨论这两者中的第一个,因为实际上没有什么可说的。如果您只是想使其工作,可以将代码保持原样。

但是,如果您选择按照传统的方式进行设计并实际解决了设计模式中的问题,将会发生什么?

您可能正在看以下过程:

设计OO代码时,代码中的大多数ifs都不必在那里。自然地,如果要比较两个标量类型(例如ints或floats),则可能会有一个if,但是如果要基于配置更改过程,则可以使用多态来实现所需的结果,然后移动决策(ifs)从您的业务逻辑到实例化对象的地方- 工厂

到目前为止,您的过程可以通过4条单独的路径:

  1. data既未加密也未压缩(不调用,返回data
  2. data被压缩(调用compress(data)并返回)
  3. data已加密(调用encrypt(data)并返回)
  4. data被压缩和加密(调用encrypt(compress(data))并返回)

仅通过查看4条路径,您就会发现问题。

您有一个进程调用不同的方法来处理数据,然后将其返回,该过程调用3(理论上为4,如果您算不上调用任何东西)。这些方法具有不同的名称,即所谓的公共API(方法通过其传达其行为的方式)不同。

使用适配器模式,我们可以解决已经出现的名称colision(我们可以统一公共API)。简而言之,适配器可帮助两个不兼容的接口一起工作。另外,适配器通过定义新的适配器接口来工作,该接口试图将其API实现统一。

这不是具体的语言。这是一种通用方法,其中的any关键字表示它可以是任何类型,在C#这样的语言中,您可以将其替换为通用(<T>)。

我要假设,现在您可以拥有两个负责压缩和加密的类。

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

在企业环境中,即使是这些特定的类也很可能会被接口替换,例如class关键字将替换为interface(应该使用C#,Java和/或PHP之类的语言)或class关键字将保留,但是如果您使用C ++编写代码Compress,则Encrypt方法将被定义为纯虚函数。

为了制作适配器,我们定义了一个公共接口。

interface DataProcessing
{
    Process(data : any) : any;
}

然后,我们必须提供接口的实现以使其有用。

// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
    public Process(data : any) : any
    {
        return data;
    }
}

// when only compression is enabled
class CompressionAdapter : DataProcessing
{
    private compression : Compression;

    public Process(data : any) : any
    {
        return this.compression.Compress(data);
    }
}

// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(data);
    }
}

// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
    private compression : Compression;
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(
            this.compression.Compress(data)
        );
    }
}

这样,您最终得到4个类,每个类做的事情完全不同,但是每个类提供相同的公共API。该Process方法。

在业务逻辑中,在处理无/加密/压缩/两者决策的情况下,将设计对象以使其依赖于DataProcessing我们之前设计的接口。

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

然后,过程本身可以像这样简单:

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

没有更多的条件。该类DataService不知道将数据传递给dataProcessing成员时将如何处理数据,它并不真正在乎它,这不是它的责任。

理想情况下,您将进行单元测试来测试您创建的4个适配器类,以确保它们起作用,并通过测试。而且,如果它们通过了,那么您可以肯定,无论您在代码中的什么位置调用它们,它们都将起作用。

因此,以这种方式进行操作后if,我的代码中将永远不会再包含s了?

不。您在业务逻辑中不太可能有条件,但条件仍然必须存在。这个地方是你的工厂。

这很好。您将创建和实际使用代码的关注分开了。如果您使工厂可靠(在Java中甚至可以使用Google 的Guice框架之类的东西),那么在业务逻辑中,您就不必担心选择合适的注入类。因为您知道您的工厂在工作,并将按要求交付。

是否需要所有这些类,接口等?

这使我们回到了开始。

在OOP中,如果您选择使用多态性的路径,那么真的要使用设计模式,想要利用语言的功能和/或想要遵循一切都是对象意识形态,那么它就是。即便如此,此示例甚至都没有显示您将需要的所有工厂,如果要重构CompressionEncryption类并使其成为接口,则还必须包括其实现。

最后,您将获得数百个小类和接口,它们专注于非常具体的事情。如果您要做的只是做两个数字相加之类的简单操作,那并不一定很糟糕,但对于您来说可能不是最佳解决方案。

如果您想快速完成它,可以使用Ixrec的解决方案,该解决方案至少设法消除了else ifand else块,我认为这甚至比普通的还要差if

考虑到这是进行良好OO设计的方式。在过去的几年中,这是对接口而不是对实现进行编码的方式,这是我最喜欢的方法。

我个人更喜欢if-less编程,并且会更欣赏在5行代码上的更长解决方案。这是我习惯于设计代码的方式,并且非常乐于阅读。


更新2:关于我的解决方案的第一个版本进行了激烈的讨论。讨论主要是由我引起的,对此我深表歉意。

我决定以某种方式编辑答案,这是查看解决方案的一种方式,但不是唯一的一种方式。我还删除了装饰器部分,而我指的是立面,最后我决定完全省略,因为适配器是立面的变体。


28
我没有拒绝投票,但是其理由可能是大量荒谬的新类/接口来完成原始代码在8行中执行的操作(而其他答案在5行中执行)。在我看来,它唯一要做的就是增加代码的学习曲线。
莫里西

6
@Maurycy OP要求的是尝试使用常见的设计模式来找到他的问题的解决方案(如果存在)。我的解决方案是否比他或Ixrec的代码长?它是。我承认。我的解决方案是否使用设计模式解决了他的问题,从而回答了他的问题,并且还有效地从流程中消除了所有必要的条件?是的 Ixrec没有。
安迪

26
我相信,编写清晰,可靠,简洁,高性能和可维护的代码是必经之路。如果我每次有人引用SOLID或引用一个软件模式而没有清楚阐明他们的目标和基本原理时,我每次都能赚到一美元,那我将是个有钱人。
罗伯特·哈维

12
我认为我在这里遇到两个问题。首先是Compressionand Encryption接口似乎完全多余。我不确定您是在暗示装修过程中某种程度上必需它们,还是仅暗示它们代表所提取的概念。第二个问题是,使像这样的类CompressionEncryptionDecorator导致与OP的条件条件相同的组合爆炸。我还没有在建议的代码中清楚地看到装饰器模式。
cbojar '16

5
关于SOLID与简单的争论有点遗漏了一点:这段代码既没有,也没有使用装饰器模式。代码不是自动的,只是因为它使用了很多接口。DataProcessing接口的依赖注入是一个不错的选择。其他一切都是多余的。SOLID是一种体系结构级别的关注点,旨在很好地处理更改。OP没有提供有关他的体系结构的信息,也没有提供他期望代码如何变化的信息,因此我们甚至无法在答案中讨论SOLID。
卡尔·莱斯

120

我在当前代码中看到的唯一问题是,当您添加更多设置时,组合爆炸的风险很大,可以通过将代码结构化为如下形式来轻松缓解:

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

我不知道可以将其视为示例的任何“设计模式”或“惯用语”。


18
@DamithGanegoda不,如果您仔细阅读我的代码,您会发现它在这种情况下完全一样。这就是为什么else我的两个if语句之间没有内容,以及为什么我data每次都分配给它的原因。如果两个标志都为true,则执行compress(),然后根据compress()的结果执行crypto(),就像您想要的那样。
Ixrec '16

14
@DavidPacker从技术上讲,每种编程语言中的每个if语句也是如此。我之所以简单起见,是因为这看起来像个问题,很简单的答案很合适。您的解决方案也是有效的,但就我个人而言,当我有两个以上的布尔标志需要担心时,我会保存它。
Ixrec '16

15
@DavidPacker:正确性不是由代码遵循某些作者关于某种编程思想的某些准则的定义来确定的。正确的是“代码是否在合理的时间内完成了应该执行的工作”。如果以“错误的方式”去做是有意义的,那么错误的方式就是正确的方式,因为时间就是金钱。
whatsisname 2016年

9
@DavidPacker:如果我在OP的职位上,并问了这个问题,那么Orbit评论中的Lightness Race是我真正需要的。“使用设计模式查找解决方案”已经从错误的脚开始。
whatsisname 2016年

6
@DavidPacker实际上,如果您更仔细地阅读问题,它并不一定要遵循模式。它说:“我正在考虑装饰器模式。这是正确的选择,还是有更好的选择?” 。您在我引述中提到了第一句话,但第二句话却没有。其他人则认为不,这不是正确的选择。然后,您不能声称只有您自己回答了这个问题。
乔恩·本特利

12

我想您的问题不是在寻找实用性,在这种情况下,lxrec的答案是正确的,而是要了解设计模式。

显然,命令模式对于您提出的这样一个琐碎的问题是一个过大的杀伤力,但是为了说明起见,它使用了:

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

如您所见,将命令/转换放在列表中可以使它们按顺序执行。显然,它将执行两个操作,或者仅执行其中一个操作,取决于您在列表中输入的内容而没有if条件。

显然,条件语句将以某种将命令列表组合在一起的工厂结束。

编辑@texacre的评论:

有许多方法可以避免解决方案创建部分的if条件,让我们以桌面GUI应用程序为例。您可以具有“压缩”和“加密”选项的复选框。在on clic这些复选框的情况下,您实例化相应的命令,并将其添加到列表中,或者如果取消选项从列表中删除。


除非您能提供一个“某种将命令列表汇总在一起的工厂”的示例,而没有代码本质上看起来像Ixrec的答案,否则IMO不会回答这个问题。这为实现压缩和加密功能提供了更好的方法,但没有提供避免标记的方法。
thexacre

@thexacre我添加了一个示例。
TulainsCórdova'16

因此,在您的复选框事件监听器中,您有“如果checkbox.ticked然后添加命令”?在我看来,如果周围的声明,您只是在改组旗帜……
thexacre

@thexacre不,每个复选框一个侦听器。在单击事件只是commands.add(new EncryptCommand());commands.add(new CompressCommand());分别。
TulainsCórdova'16

取消选中该框怎么办?在我遇到的几乎每种语言/ UI工具箱中,您仍然需要检查事件侦听器中复选框的状态。我同意这是一个更好的模式,但是如果flag在某处做某事,它基本上不能避免。
thexacre

7

我认为“设计模式”不必要地面向“ oo模式”,并且完全避免了更简单的想法。我们在这里谈论的是一个(简单的)数据管道。

我会尝试Clojure来做。函数是一流的任何其他语言可能也可以。也许以后我可以举一个C#示例,但是它不是那么好。我解决这个问题的方式将是以下步骤,并对非clojurian进行一些解释:

1.表示一组转换。

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

这是一个从关键字到函数的映射,即查找表/字典/所有内容。另一个示例(字符串的关键字):

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

因此,编写(transformations :encrypt)(:encrypt transformations)将返回加密函数。((fn [data] ... )只是一个lambda函数。)

2.以关键字顺序获取选项:

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

3.使用提供的选项过滤所有转换。

(let [ transformations-to-run (map transformations options)] ... )

例:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4.将功能合并为一个:

(apply comp transformations-to-run)

例:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5.然后一起:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

如果要添加新功能(例如“ debug-print”),则仅更改如下:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this

如何将funcs填充为仅包含需要应用的函数,而基本上不以一种或另一种方式使用一系列if语句?
thexacre

该行funcs-to-run-here (map options funcs)正在进行过滤,因此选择了一组要应用的功能。也许我应该更新答案,然后再详细一点。
NiklasJ

5

[本质上,我的答案是上述@Ixrec答案的后续。]

一个重要的问题:您需要涵盖的不同组合的数量会增加吗?您会更好地了解自己的主题领域。这是您的判断。
变体的数量可能会增加吗?好吧,这不是不可想象的。例如,您可能需要容纳更多不同的加密算法。

如果您预计不同组合的数量将会增加,那么策略模式可以为您提供帮助。它旨在封装算法并为调用代码提供可互换的接口。为每个特定的字符串创建(实例化)适当的策略时,您仍然会有少量逻辑。

您在上面已经评论说,您不希望需求有所改变。如果您不希望变体的数量会增加(或者如果您可以推迟此重构),请保持逻辑不变。当前,您的逻辑数量很少且可管理。(也许在评论中对可能重构为战略模式发表了自己的评论。)


1

在scala中执行此操作的一种方法是:

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

使用装饰器模式来实现上述目标(各个处理逻辑的分离以及它们如何连接在一起)可能太冗长。

在需要一种设计模式以在OO编程范式中实现这些设计目标的地方,功能语言通过使用作为一等公民的功能(代码中的第1行和第2行)和功能组合(第3行)来提供本机支持。


为什么这比OP的方法更好(或更糟)?和/或您如何看待OP使用装饰器模式的想法?
卡斯珀范登伯格

此代码段更好,并且在排序方面很明确(加密之前的压缩);避免不必要的界面
抹布
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.