65.9K
CodeProject 正在变化。 阅读更多。
Home

设计原则:更好的解决方案的启示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (19投票s)

2010年5月10日

CPOL

10分钟阅读

viewsIcon

42399

downloadIcon

103

从我最早的编程记忆开始,我就被教导在编码之前应该进行一定程度的设计。后来我开始听到“依赖注入”、“IoC”等术语,但当我问人们为什么需要这些模式时,我很少得到令我满意的答案……

引言

本文不适合专家阅读。相反,在这个画布上,我试图为那些对各种设计原则术语感到困惑的人们展示良好设计的力量。

在这里,我将从一个简单的陈述问题开始……然后提供一个基本解决方案(不考虑任何设计原则)……讨论解决方案中的问题……最后,逐一应用设计原则,开发一个更好的解决方案。

致读者的一封信

在阅读每个解决方案之前,请进行一些头脑风暴。也许你能够为这个问题找到一个更好的解决方案。

其他文章

对于原始的AJAX,点击这里

场景

NS-Corporation(Nano Soft Corporation)是一家领先的解决方案提供商,为全球各个领域提供解决方案。一位著名的分析师Flemming先生访问了他们的潜在客户“Best Insurance Ltd.”,以了解他们现有的业务场景。我们将看到NS-Corporation团队如何得出设计结论。

问题陈述

来自客户的角度——“我们是一家保险公司。我们有许多股东,需要为他们准备月度报表。我们以PDF格式向股东提供月度报表。用于生成这些报表的输入是我们需要由我们的合作伙伴公司提供的、特定格式的Excel文件。目前,我们的员工阅读这些Excel文件并进行一些计算和业务处理,最后为我们的每一位股东准备PDF文件。

我们需要一个自动化系统来完成这些工作。

解决方案

解决方案将以迭代的方式开发。在每次迭代结束时,将讨论上述解决方案的局限性。

在我们开始之前,让我们看看Flemming先生是如何抽象这个问题的……

他说,“抽象是读取 - 业务处理 - 写入。

第一轮迭代

当报表的抽象呈现给NS-Corporation的首席程序员之一“Manoj”时,他立即回答说,“嘿,这可能是我编程生涯中遇到的最简单的问题……只需编写一个类,包含三个方法 - 读取、处理和写入。

所以他提供的解决方案如图所示

Fig1.png

Manoj的上级Bannhi,虽然个子不高……但她对设计原则有很深的见解,她挠了挠头争辩说,“我们不应该将业务处理与其他非业务操作混为一谈。因为那样的话,任何业务/非业务领域的小变动都可能迫使其他部分发生改变,而我们需要为每次更改运行整个测试周期。”——她得到了测试人员Sonali和Ambuja的大力支持。

第二轮迭代

于是她提出了她的模型

Fig2.png

按照她的观点,完整的逻辑如下所示

  1. 将功能性(业务)需求与非功能性需求分离,这样在一个区域的更改就不会迫使运行完整的测试周期。
  2. 尝试设计具有单一职责的类或函数(单一职责原则 - SRP)。这就是设计三个具有读取、处理和写入职责的类的原因。

[SRP的动机:该原则指出,一个类应该只有一个改变的理由。例如,如果我们有一个类有两个改变的理由,我们应该将功能分成两个类。]

现在大家看起来都很高兴。

咖啡厅会议

NS-Corporation的数据库专家Malini和JD在咖啡厅会议上祝项目团队好运,但看起来有点失望,因为这个项目没有数据库交互。

JD是个很酷的家伙,幽默感十足,最近被经济衰退的阴影笼罩。他还拥有宽阔的前额,几乎能碰到衣领!!!

Malini以她冷静的性格闻名……非常冷静,以至于应该永远避免冒险让她失望!!!

Aditya是整个项目的中心,**项目经理**。

作为项目经理,Aditya被教导要预感即将到来的风险……
他预见到,“Malini长期失望可能会是灾难性的……”所以他决定揭示更多关于项目的事实。
团队,有个好消息……”——Aditya立即引起了大家的注意。
我们将为这个项目增加数据库交互功能。”——他宣布。

现在咖啡厅里的团队成员们互相祝贺,庆祝这一时刻。除了一个人,其他人都看起来很高兴。

第三轮迭代

Sayan,总是沉浸在自己的世界里,突然醒来说,“我们需要改变我们的设计。”Bannhi皱着眉头看着他。但Manoj看起来很高兴。

让我们从“当事人口中”了解“原因”。

Bannhi建议的设计似乎不错。但现在我们需要着眼于未来。由于我们的客户承诺将项目的第二阶段也包含数据库交互,所以我们应该考虑我们解决方案的扩展能力。
让我们以Read()操作为例。现在看来,我们需要从Excel文件和数据库表中读取。
”——Sayan停顿了一下。

但是我们可以通过在Read()方法中设置一个输入参数‘readFrom’轻松实现。并相应地实现该方法。客户会调用带有合适参数的方法!”——Bannhi争辩道。

更正……”——Sayan惊呼道!!!“如果明天客户要求我们从Word文件读取,然后从PDF文件……然后从DJVW文件……怎么办?每一次我们都需要修改‘ReadPartnerDocument’类并进行测试。此外,这在某种程度上是否违反了‘开闭原则’设计原则?”——这次测试女孩们转而支持Sayan。

于是Sayan提出了他的设计

Fig3.png

为简洁起见,省略了Process和Write部分

设计图中有两个显著的部分。一个是接口的引入(通常称为依赖倒置),第二个是“DocumentUtility”类的引入。此设计的好处如下所示

  1. 对于读取每种类型的文档,我们无需干扰已有的经过测试的功能。例如,假设在“第一阶段”,我们构建并测试了“ReadPartnerXLDocument”。在“第二阶段”,我们也需要从数据库读取。在目前的情况下,我们无需干扰现有的经过测试的实现“ReadPartnerXLDocument”。相反,我们将实现一个新类“ReadPartnerDBTable”,它将负责从数据库读取,依此类推……因此,我们不干扰现有的实现,也无需重新测试该部分。
  2. 引入了一个名为“DocumentUtility”的新类,其中包含各种文档读取器之间的通用功能。
    实际上有两种思想流派。一种倾向于将通用功能放在基类中并强制继承关系。另一种则喜欢将通用功能实现为实用类并强制聚合关系。我个人支持Sayan的聚合模型,因为继承模型本质上是紧密耦合的。但是,如果有人确信将来不会有深度继承,那么她/他可以通过继承来实现。

[开闭原则的动机:该原则指出,类的设计应以可以通过不修改现有代码来添加新功能的方式进行。这意味着设计应该对采纳新功能持开放态度,但要关闭以修改现有功能来采纳新功能。总之,为了在那里纳入新功能,无需修改现有功能。]

我明白了……”——Aditya说,也许并没有看懂。

但这需要外部团队进行一些审查……”——他建议。

但他的建议 somehow 激怒了Manoj!!!
这个人总是对我们信心不足……”——Manoj咕哝道。

ManoOj……”——Aditya选择了他的唯一牺牲者。

是的 AAA..Aditya-Da”——Manoj困惑地回答,Aditya怎么会知道他的评论。

请将设计文档发给Nilanjan和Sudipta审查……”——Aditya命令道。

Manoj带着他所有的沮丧,遵从了他的折磨者的命令,并通知了Nilanjan和Sudipta。

第四轮迭代

Nilanjan认为设计似乎很完美。但他对客户端程序中Reader对象的创建表示担忧。他争辩说,“假设我们一个客户端程序目前正在读取Excel文档。明天它可能需要读取PDF文档而不是Excel文档。在这种情况下,如果我们硬编码方式在客户端程序中直接创建一个对象,那么它将迫使我们重新编译我们的客户端。所以我们应该有一个对象创建容器,我们可以从外部注入所需的,也许是从配置文件中。

因此,他将一个DependencyResolver与客户端关联起来。

Fig4.png

他建议,“……对象不应该直接在客户端中创建,而应该通过DependencyResolver创建,以便我们可以从外部(例如,从配置文件)控制它的创建。”

致读者

依赖注入的动机:依赖注入是一个通用概念。要详细了解它,应参考Martin Fowler的(https://martinfowler.com.cn/articles/injection.html.)文章。这个概念的目的是使用一个依赖类,而无需直接引用该类。如迭代四中所述,依赖注入允许我们以松散耦合的方式关联对象。完整的实现代码可在下载中找到。

思考题:想象一下,在现实生活中,哪些场景下这个解决方案可能有用/过时。

Sudipta似乎对设计有点不满(没人介意,因为他大多数时候都对自己工作不满意!!!)。
但他指出了一个有效的问题:“如果出于某种原因,我们需要在执行任何方法之前或之后插入一个策略,那么我们就需要修改我们现有的代码库。例如,如果我们为了性能调优需要计算Read()方法执行了多长时间,那么我们就需要更改代码库。此外,如何控制创建的对象的使用寿命?是否可以从外部更改可配置对象的使用寿命?如何将日志记录等横切关注点与主要应用程序代码分开?

第五轮迭代

于是他将IoC容器引入代替“DependencyResolver”到设计中。因此,他提出的设计如下所示

Fig5.png

控制反转的动机 - IoC的维基定义如下:“控制反转,或IoC,是一个抽象原则,描述了某些软件架构设计中,与过程式编程相比,系统的控制流被反转了。”通常我们通过调用相应的库函数来使用库代码,例如Console.WriteLine().但有时框架需要回调我们。例如,使用IComparable 接口编写自定义数组排序算法。代码如下所示:

static void Main(string[] args)
{
            // ComplexNumber class defines custom sort order where sort will 
            // takes place by real numbers and then by imaginary numbers.
            ComplexNumber[] arr = new ComplexNumber[11];

            arr[0] = new ComplexNumber(-1, 2);
            arr[1] = new ComplexNumber(1, 3.5);
            arr[2] = new ComplexNumber(2.5, 2);
            arr[3] = new ComplexNumber(-2.5, 4);
            arr[4] = new ComplexNumber(-3, 6);
            arr[5] = new ComplexNumber(3, 6);
            arr[6] = new ComplexNumber(0, 5);
            arr[7] = new ComplexNumber(0, 5);
            arr[8] = new ComplexNumber(0, -5);
            arr[9] = new ComplexNumber(2, 0);
            arr[10] = new ComplexNumber(0.5, -25);

            // Array.Sort is calling the CompareTo method in ComplexNumber. 
            // Inversion of control happens here.

            Array.Sort(arr);

            foreach (ComplexNumber c in arr)
                Console.WriteLine(c.ToString());

            Console.ReadLine();
} 

internal class ComplexNumber : IComparable
{
            double _realPart = 0;
            double _imaginaryPart = 0;
            
            public ComplexNumber(double realPart, double imaginaryPart)
            {
                _realPart = realPart;
                _imaginaryPart = imaginaryPart;
            }

            // This function compares real parts and then imaginary parts
            public int CompareTo(object obj)
            {
                ComplexNumber otherValue = (ComplexNumber)obj;

                if (_realPart == otherValue._realPart)
                {
                    if (_imaginaryPart == otherValue._imaginaryPart)
                        return 0;
                    else if (_imaginaryPart > otherValue._imaginaryPart)
                        return 1;
                    else 
                        return -1;
                }
                else if (_realPart > otherValue._realPart)
                {
                    return 1;
                }
                else
                {
                    return -1;
                }
            }

            public override string ToString()
            {
                return _realPart.ToString() + " + " + _imaginaryPart.ToString() + "j";
            }
}

控制反转的基本原理已在上面介绍。IoC容器基于此原则,并结合了其他几个模式。在本例中,IoC的工作方式几乎类似于依赖注入,并包含一些上面提到的额外功能。
嗯……所以这就是你们都同意的设计,对吗?”——Aditya问道。
是的”——大家都点头。
伙计们,如果需要进一步完善,我们可以做到……我们还有时间。

NS-Corporation的没有人能做得更好了……但Aditya仍然有时间……他还在等待……

如果你们中的任何人能够完善这个设计,请发邮件给他,地址是:WaitingAditya@hotmail.com!!!

致谢

在这篇文章中,我试图演示如何构思一个优秀的设计。本文的主要目的是解释选择特定设计的逻辑原因。在我的下一篇文章中,我将深入探讨这里提到的模式。

这篇文章的参与者都是我现实生活中的朋友。所以在文章的许多地方,我都会提及他们……但这仅仅是为了好玩,我觉得我有权对我亲近的人这样做。

他们所有人都有不可动摇的效率水平,并且非常有能力一次性达成最终设计。我很幸运有这些先锋作为我的朋友。

我还要再说一件事,这篇文章非常受到Aditya的寻宝活动(一次传奇性的事件……我必须说)和他著名的白皮书(我们听过很多次……但很不幸都没见过!!!)的启发。

© . All rights reserved.