JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

只需三分钟,深度解析Java设计模式六大原则之开闭原则!确实简单

wys521 2024-11-02 14:55:24 精选教程 26 ℃ 0 评论

卡车是一种多功能的设备。他们可以根据车厢挂载的零件类型来执行不同的任务,如果载重允许的话,甚至可以装载多个车厢。

简而言之,卡车可以通过不同的零件以进行扩展,但不能改装每个零件的内部单元(例如发动机或机舱)。可拓展的代码也应该像卡车挂载零件一样得心应手。

内容涉及到:Java相关问题Java学习资料+电子书籍+学习视频,免费打包分享,私信【Java】即可获取。

制冰机问题

假设我们必须要编写一个程序来为 Ted&Kelly 公司制作可可冰激凌。

public class IceCreamMachine
{
    public void MakeIceCream()
    {
        Console.WriteLine("Cacao ice-cream"); //logic to create cacao ice cream
    }
 }

我们将程序交付给客户,虽然这段代码很简单,但是看上去没有什么问题,而且没有违反任何原则。客户目前是非常满意的。

但是事情并不是到这里就结束了,客户总是会有一些新的需求。这就意味着我们需要不断调整我们的代码。

添加新的口味

没想到程序刚上线就如此火爆,于是Ted&Kelly 希望扩大规模。他们要求我们还要能够生产香草味的冰激凌。

短暂思考之后,我们给出了以下解决方案:

public class IceCreamMachine
{
    public void MakeIceCream(string flavor)
    {
        if(flavor == "cacao")
        {
            Console.WriteLine("Cacao ice-cream"); //logic to create cacao ice-cream
        }
        else if (flavor == "vanilla")
        {
            Console.WriteLine("Vanilla ice-cream"); //logic to create vanilla ice-cream
        }
        else
        {
            throw new Exception("Flavor not supported");
        }
    }
}

我们修改了最初的代码,添加了一个参数以指定所需的口味,并添加了 if 语句以在逻辑之间切换。由于我们已经改变了最初的函数签名,因此调用我们代码的方法将被破坏,但是至少从现在开始,我们应该在不破坏更改的情况下支持其他口味的生产。

创建可可和香草组合

业务进展顺利,但是很快客户就提出了第二个需求,希望我们能够生产由可可和香草制成的冰淇淋。事情开始变得有点复杂,但是我们仍然能够处理。

public class IceCreamMachine
{
    public void MakeIceCream(string flavor)
    {
        if(flavor == "cacao")
        {
            Console.WriteLine("Cacao ice-cream"); //logic to create cacao ice-cream
        }
        else if (flavor == "vanilla")
        {
            Console.WriteLine("Vanilla ice-cream"); //logic to create vanilla ice-cream
        }
        else if (flavor == "cacao-vanilla")
        {
            //copy & paste the cacao ice-cream logic
            Console.WriteLine("Cacao ice-cream");

            //copy & paste the vanilla ice-cream logic
            Console.WriteLine("Vanilla ice-cream");
        }
        else
        {
            throw new Exception("Flavor not supported");
        }
    }
}

我们又添加了一个if语句,在这种情况下,生产冰淇淋的逻辑被复制到每个 if 内部。在实际的应用程序中,我可能会在单独的服务中提取生产的逻辑。但是,正如我们将看到的那样,提取服务并不总是最好的方案。

Ted&Kelly 要求支持生产更多口味时,会发生什么。如果他想进一步将它们结合起来怎么办?仅添加 if 子句并不是理想的解决方案。

这种解决方案产生的问题

每次我们添加新的口味或组合时,我们都必须更新 IceCreamMachine 类,这就意味着:

  • 我们更新已经部署的代码
  • 这个类变得越来越复杂,易读性变得越来越差。假设我们有 100 种口味,这个类很容易膨胀到 5000+ 行代码
  • 我们可能会破坏现有的单元测试

现在回想一下开头卡车的类比,当你每次更换车厢挂载的零件时,你会每次都更换引擎吗?显然不会,看我们如何解决这个问题。

传统的可拓展性方法

Bertrand Meyer 是最早提出开闭原则这一术语的人,他定义 “软件实体(类,模块,函数,等) 应当对扩展开放,对修改关闭。”

换句话说,每当我们需要向旧对象添加新行为时,都可以根据需要继承和更新它们。开闭原则是那些易于理解却难以应用的原则之一。

让我们遵循这种方法重写刚刚的代码:

public abstract class BaseIceCream
{
    public abstract void MakeIceCream();
}

public class CacaoIceCream : BaseIceCream
{
    public override void MakeIceCream()
    {
        Console.WriteLine("Cacao ice-cream");
    }
}

public class VanillaIceCream : BaseIceCream
{
    public override void MakeIceCream()
    {
        Console.WriteLine("Vanilla ice-cream");
    }
}

public class CacaoAndVanilla : CacaoIceCream
{
    public override void MakeIceCream()
    {
        base.MakeIceCream();
        Console.WriteLine("Vanilla ice-cream"); //duplicate vanilla logic because we can't iherit both cacao and vanilla
    }
}

原始类分为了四个类,每个类代表一种生产口味。通过此解决方案,我们解决了所有最初的问题:

  • 每个类都短小精悍,可维护性和易读性都很高
  • 当需要添加新口味时,我们只需要添加一个新的类
  • 不会影响现有的单元测试

代码看起来已经没有任何问题了,我们完全可以到这里就停止思考,但是其实这种解决方案还是存在一些问题的:

  • 不能继承多个类,因此对于两种口味的结合,我们必须复制一些代码或将逻辑提取到服务中
  • 如果基类中的代码被更新,所有子类都会受到影响。假设基类通过构造函数注入了一些依赖关系,每当我们添加新的依赖关系时,所有子代都必须将该参数解析为基本构造函数

现在可拓展性的方法

Robert C. Martin 重申 Meyer 的开闭原则时,他做了一些更新。偏爱继承而不是继承

组合对象时,我们可以自由地随意组合任意数量和所需的组合。而且,如果我们针对抽象类(接口)进行编程,则可以修改已有代码的行为而无需进行修改代码。让我们看看最终的解决方案:

public interface IMakeIceCream
{
    void Make();
}

public class CacaoIceCream : IMakeIceCream
{
    private IMakeIceCream iceCreamMaker;

    public CacaoIceCream(IMakeIceCream iceCreamMaker = null)  //optional argument, another flavor isn't required
    {
        this.iceCreamMaker = iceCreamMaker;
    }

    public void Make()
    {
        if(iceCreamMaker != null) //if flavor passed in, make that first
        {
            iceCreamMaker.Make();
        }

        Console.WriteLine("Cacao ice-cream");
    }
}

public class VanillaIceCream : IMakeIceCream
{
    private IMakeIceCream iceCreamMaker;

    public VanillaIceCream(IMakeIceCream iceCreamMaker = null) //optional argument, another flavor isn't required
    {
        this.iceCreamMaker = iceCreamMaker;
    }

    public void Make()
    {
        if (iceCreamMaker != null) //if flavor passed in, make that first
        {
            iceCreamMaker.Make();
        }

        Console.WriteLine("Vanilla ice-cream");
    }
}

现在将原始的类分成了三个对象:

  • IMakeIceCream 接口定义了制作冰淇淋的通用抽象
  • CacaoIceCream 实现了 IMakeIceCream
  • VanillaIceCream 实现了 IMakeIceCream

通过使用接口,我们将类和实现解耦。接口是封闭不可修改的,因此一旦我们定义了对象,就无法更改。但是我们可以为新的功能定义新接口并继承它们。这使得代码可扩展。

为什么要向每个构造函数添加“ IMakeIceCream”参数?新代码是否具有旧方法或旧方法的所有功能?


答案是肯定的。可可香草组合仍在这里,但我们不需要if子句或专门针对它的类。我们可以利用构造函数参数。类似这样:

var cacaoVanillaIceCream = new CacaoIceCream(new VanillaIceCream());
cacaoVanillaIceCream.Make();

组合的好处在于,我们可以根据需要合成任意数量的对象。需要4种口味?只需编写一个构造函数链即可。这称为装饰器模式。你可以在 这里 读更多关于它的内容。

请注意, IMakeIceCream 参数是可选的。这使我可以组合使用或单独使用该类。

像这样编写代码会实现一个可拔插式的体系结构。这是非常 nice 的,因为我们可以通过编写代码来添加新功能,而不更改现有功能。任务完成。

注意一下咯:由于篇幅原因,小编已将JAVA以及JAVA相关的知识都集结整理出来了,有需要的私信我关键词 “Java”,回复获取免费下载原文件的方式。

作者:maybelence
链接:https://juejin.cn/post/6952789900114690078
来源:掘金

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表