五、使用 DRY 原则
不要重复你自己(DRY)原则是另一个重要的原则,当一个专业的程序员为一个应用写一段代码时,他必须遵循这个原则。你在第四章学到的单一责任原则(SRP)和开/闭原则(OCP)原则都与 DRY 原则有关。我们从安迪·亨特和迪夫·托马斯的名著《务实的程序员中了解到这个原则。在这本书里,干原理是这样陈述的:
每一条知识都必须在一个系统内有一个单一的、明确的、权威的表述。
第一次看这个的时候可能会觉得很复杂。本章的目的是通过一个案例研究来帮助你理解这个原则。
DRY 的原因
代码重复会导致应用失败。程序员经常称这种情况为软件中的罪恶。现在的问题是——为什么我们会看到重复的代码?有多种原因。让我们用例子来看看其中的一些:
-
程序员无法抗拒简单的复制/粘贴,这对他来说似乎是成功的捷径。
-
项目的最后期限快到了。开发人员假设此时一定数量的副本是可以的。他计划在下一个版本中删除这些重复的内容,但是后来他忘记了。
-
代码注释中也会出现重复。考虑一个例子:一个开发人员非常了解代码。他不需要文档来理解代码的逻辑。一个新的需求迫使他更新部分代码。因此,他复制并粘贴了一段带有现有注释的现有代码,并开始处理该代码段。一旦更新完成,由于各种原因,他忘记更新相关评论。
-
测试人员可能需要传递相同的输入来验证测试套件中的各种方法。
-
有时重复是难以避免的。项目标准可能要求开发人员在代码中放置重复的信息。再举一个例子:假设您的软件面向使用不同编程语言和/或开发环境的多个平台。在这种情况下,您可能需要复制共享信息(例如方法)。
-
除此之外,编程语言可以有一个自身复制一些信息的结构。
等等。
在计算机科学中,遵循许多原则来避免代码重复。例如,数据库规范化技术试图消除重复数据。在第二章中,你看到了你可以把一个公共方法放在一个抽象基类中,以避免在派生类中复制该方法。
我必须说找到重复的代码并不总是容易的。例如,考虑下面的代码段(代码段 1),它有两个方法。
Code Segment 1
public void DisplayCost()
{
Console.WriteLine("Game name: SuperGame");
Console.WriteLine("Version:1.0");
Console.WriteLine("Actual cost is:$1000");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine("Game name: SuperGame");
Console.WriteLine("Version:1.0");
Console.WriteLine("The discounted price for festive season is:$800");
}
您可以很容易地看到,开始的两行是两种方法共有的。但是,如果重复项与其他代码或注释混杂在一起,那么检测重复项就不那么简单了。例如,考虑代码段 2。
Code Segment 2
public void DisplayCost()
{
Console.WriteLine("\AbcCompany SuperGame's price details:");
Console.WriteLine("Version:1.0 cost is:$1000");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine("\AbcCompany offers festive season discount.");
Console.WriteLine("Discounted price detail:");
Console.WriteLine("Game: SuperGame. Version: 1.0\. Discounted price:$800");
}
仔细观察,您会发现在两个代码段中,公司名称、游戏名称和软件的版本细节都是重复的。虽然在第一个代码段中很容易找到重复的代码,但在第二个代码段中,您需要仔细阅读代码。
这些代码段只包含两个方法。在真实的应用中,有许多方法,并且不是所有的方法都出现在同一个文件中。因此,如果您在文件中散布重复的信息,一个简单的更新就可能导致软件显示不一致的行为。
在一次更新操作中,如果你有 n 个副本,你需要 n 倍的修改,你不能错过任何一个。这就是为什么你需要小心他们。违反干原理导致你得到湿解。它通常代表“每次都写”、“每件事都写两遍”、“我们喜欢打字”或“浪费每个人的时间”。
像前几章一样,我将从一个开始看起来不错的程序开始。我们将分析这个程序,并通过消除冗余代码来使它变得更好。当你遇到类似的情况时,你可以遵循同样的方法。
初始程序
这里有一个简单的例子。该计划的主要假设如下:
-
有个游戏软件叫
SuperGame
。你创建一个类来代表这个游戏。 -
AboutGame()
方法告诉我们一些关于这个软件的有用信息。例如,它规定使用该软件的最低年龄是 10 岁。它还显示了游戏的当前版本。 -
DisplayCost()
显示该软件最新版本的价格。 -
买家可以得到高达 20%的折扣。
DisplayCostAfterDiscount()
显示最新软件的折扣价。
演示 1
假设有人编写了下面的程序。它成功编译并运行。让我们看看输出,然后按照分析部分。
using System;
namespace WithoutDRYDemo
{
class SuperGame
{
public void AboutGame()
{
Console.WriteLine("Game name: SuperGame");
Console.WriteLine("Minimum age: 10 years and above.");
Console.WriteLine("Current version: 1.0.");
Console.WriteLine("It is a AbcCompany product.");
}
public void DisplayCost()
{
Console.WriteLine("\AbcCompany SuperGame's price details:");
Console.WriteLine("Version:1.0 cost is:$1000");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine("\n AbcCompany offers a festive season discount.");
Console.WriteLine("Discounted price detail:");
Console.WriteLine("Game: SuperGame. Version: 1.0 Discounted price:$800");
}
}
class Program
{
static void Main()
{
Console.WriteLine("***A demo without DRY principle.***");
SuperGame superGame = new SuperGame();
superGame.AboutGame();
superGame.DisplayCost();
superGame.DisplayCostAfterDiscount();
Console.ReadKey();
}
}
}
输出
以下是输出:
***A demo without DRY principle.***
Game name: SuperGame
Minimum age: 10 years and above.
Current version: 1.0.
It is a AbcCompany product.
AbcCompany SuperGame's price details:
Version:1.0 cost is:$1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame. Version: 1.0 Discounted price:$800
分析
你能看出这个程序的问题吗?公司名称AbcCompany
和版本详情你看到多少次?我知道这是一个简单的程序,但是考虑一下我之前提到的例子。这些方法可以出现在不同的模块中,在这种情况下,如果您需要更新公司信息或版本详细信息,您需要在提供更新之前找出所有使用它的地方。这就是干原则的用武之地。
这个程序受到硬编码字符串使用的困扰。这个问题的解决方案很简单。您可以在一个位置包含那些出现在多个地方的字符串。然后你与程序的其他部分共享这个代码段。因此,当您更新共享位置中的字符串时,更改会正确地反映在它出现的每个地方。
所以,基本思想是,如果你在多个位置看到一个公共代码,你把公共部分从剩余部分中分离出来,把它们放在一个位置,从程序的其他部分调用这个公共代码。这样,您就避免了复制/粘贴技术,这种技术在开始时可能看起来很容易,也很吸引人。
一次且只有一次的原则类似于干。你可以把这个原理应用到一个功能行为上,你可以把它看成是干的子集。简单来说,干和一次只干一次的核心思想是一致的。
你能把演示 1 做得更好吗?让我们看看下面的程序。
更好的程序
这个程序使用一个构造函数来初始化这些值。您可以在类的实例方法中使用这些值。
演示 2
这是演示 1 的改进版本:
using System;
namespace ImprovedVersion
{
class SuperGame
{
readonly string companyName;
readonly string gameName;
readonly double minimumAge;
readonly string version;
readonly double actualCost;
readonly double discountedCost;
public SuperGame()
{
companyName = "AbcCompany";
gameName = "SuperGame";
version = "1.0";
minimumAge = 10;
actualCost = 1000;
discountedCost = 800;
}
public void AboutGame()
{
Console.WriteLine($"Game name: {gameName}");
Console.WriteLine($"Minimum age: {minimumAge} years and above.");
Console.WriteLine($"Current version: {version}.");
Console.WriteLine($"It is a {companyName} product.");
}
public void DisplayCost()
{
Console.WriteLine($"\n{companyName} SuperGame's price details:");
Console.WriteLine($"Version:{version} " +
$"cost is: {actualCost}");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine($"\n{companyName} offers festive season discount.");
Console.WriteLine("Discounted price detail:");
Console.WriteLine($"Game: {gameName}. " +
$"Version: {version}. " +
$"Discounted price:{discountedCost}");
}
}
class Program
{
static void Main()
{
Console.WriteLine("***An improved version using DRY principle.***");
SuperGame superGame = new SuperGame();
superGame.AboutGame();
superGame.DisplayCost();
superGame.DisplayCostAfterDiscount();
Console.ReadKey();
}
}
}
输出
下面是这个程序的输出:
***An improved version using DRY principle.***
Game name: SuperGame
Minimum age: 10 years and above.
Current version: 1.0.
It is a AbcCompany product.
AbcCompany SuperGame's price details:
Version:1.0 cost is:1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame. Version: 1.0\. Discounted price:800
您可以看到这个程序产生了相同的输出,除了第一行,它显示这是一个改进的版本。
分析
尽管如此,这个例子中还是有一些重复。请注意,公司名称显示在AboutGame(),DisplayCost()
和DisplayCostAfterDiscount()
中。这在这里是可以的,因为我希望客户在任何方法中显示公司名称。
但是你可以改进这个程序。对于不同的游戏(由同一家公司制作),软件的初始版本和公司名称可能不会改变,但是游戏的名称和价格细节可能会改变。所以,我想在这些方面改进程序逻辑。此外,如果你熟悉前一章(第四章)中的坚实原则,你知道这个程序不遵循 SRP。
简而言之,出于各种原因,您将来可能需要更新此程序,例如:
-
软件的成本是可以改变的。
-
折扣价可以改。
-
可以更改版本详细信息。
-
游戏的名字可以改。
-
还有,公司名称本身也可以改。
因此,我将company name, game name, version,
和age requirement
移到一个新的类GameInfo
中。实际价格和折扣价格被移到不同的类别中,GamePrice
。此外,这次我使用了属性,所以您可以在稍后使用这些属性将更改应用到初始值。
在这个即将到来的程序中,当你实例化一个GameInfo
实例时,你提供游戏的名称,但是在此之前,你初始化一个GameInfo
实例和一个GamePrice
实例。该活动帮助您用存储在GameInfo
和GamePrice
中的默认信息实例化一个游戏实例。如前所述,您可以使用这些类的各种属性来更改这些值。
现在,检查一个提议的改进。您可以遵循类似的结构,以最小的努力来合并更改。
演示 3
这是演示 2 的改进版本:
using System;
namespace DRYDemo
{
class GameInfo
{
public string CompanyName { get; set; }
public string GameName { get; set; }
public string Version { get; set; }
public double MinimumAge { get; set; }
public GameInfo(string gameName)
{
CompanyName = "AbcCompany";
GameName = gameName;
Version = "1.0";
MinimumAge = 10.5;
}
}
class GamePrice
{
public double Cost { get; set; }
public double DiscountedCost { get; set; }
public GamePrice()
{
Cost = 1000;
DiscountedCost = 800;
}
}
class Game
{
readonly string companyName;
readonly string gameName;
readonly double minimumAge;
readonly string version;
readonly double actualCost;
readonly double discountedCost;
public Game(
GameInfo gameInfo,
GamePrice gamePrice
)
{
companyName = gameInfo.CompanyName;
gameName = gameInfo.GameName;
version = gameInfo.Version;
minimumAge = gameInfo.MinimumAge;
actualCost = gamePrice.Cost;
discountedCost = gamePrice.DiscountedCost;
}
public void AboutGame()
{
Console.WriteLine($"Game name: {gameName}");
Console.WriteLine($"Minimum age: {minimumAge} years and above.");
Console.WriteLine($"Current version: {version}.");
Console.WriteLine($"It is a {companyName} product.");
}
public void DisplayCost()
{
Console.WriteLine($"\n{companyName} {gameName}'s price details:");
Console.WriteLine($"Version:{version},cost:${actualCost}");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine($"\n{companyName} offers a festive season discount.");
Console.WriteLine("Discounted price detail:");
Console.WriteLine($"Game: {gameName},Version: {version},Discounted price:${discountedCost}");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Another improved version following the DRY principle. ***");
// Initial setup
GameInfo gameInfo = new GameInfo("SuperGame");
GamePrice gamePrice = new GamePrice();
// Create the game instance with default setup
Game game = new Game(gameInfo, gamePrice);
// Display the default game detail.
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.WriteLine("------------");
Console.WriteLine("Changing the game version and price now.");
// Changing some of the game info
gameInfo.Version = "2.0";
gameInfo.MinimumAge = 9.5;
// Changing the game cost
gamePrice.Cost = 1500;
gamePrice.DiscountedCost = 1200;
// Updating the game instance
game = new Game(gameInfo, gamePrice);
// Display the latest detail
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.ReadKey();
}
}
}
输出
下面是新的输出,它反映了各个领域的变化:
*** Another improved version following the DRY principle. ***
Game name: SuperGame
Minimum age: 10.5 years and above.
Current version: 1.0.
It is a AbcCompany product.
AbcCompany SuperGame's price details:
Version:1.0,cost:$1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame,Version: 1.0,Discounted price:$800
------------
Changing the game version and price now.
Game name: SuperGame
Minimum age: 9.5 years and above.
Current version: 2.0.
It is a AbcCompany product.
AbcCompany SuperGame's price details:
Version:2.0,cost:$1500
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame,Version: 2.0,Discounted price:$1200
这是改进的终点吗?你知道答案。它们没有尽头;你总是可以改进你的代码。让我们从一个普遍的角度来思考。你知道,一家公司不会在只做了一款游戏后就结束了。它可以创建多个游戏,并且可以使用一个通用的格式来显示关于它们的信息。所以,如果明天公司想让你做一个新游戏,比如说,NewPokemonKid
,你该如何着手?应该复制/粘贴现有代码并开始编辑吗?你知道这个过程根本不推荐。
如果您将Game
、GameInfo,
和GamePrice
类移动到一个共享库中并相应地使用它们,您可以使这个程序变得更好。当你这样做的时候,你遵循了 DRY 原则,因为你没有复制/粘贴现有的代码来制作一个新的游戏/新的需求。相反,您可以重用一个运行良好的现有解决方案,通过使用它,您可以间接地节省测试时间。
因此,我创建了一个名为BasicGameInfo
的类库项目,然后将这些类移动到一个公共文件CommonLibrary.cs
(我从class1.cs
中将其重命名)。我创建这些类public
,这样我就可以从不同的文件中访问它们。
为了您的直接参考,请参见图 5-1 中的解决方案浏览器视图,这里我在DryDemoUsingDll
项目中使用了一个BasicGameInfo
项目参考。
图 5-1
DryDemoUsingDll 正在使用 BasicGameInfo 项目引用
在我创建了项目DryDemoUsingDll
之后,我添加了引用BasicGameInfo
。图 5-2 显示了当我右键单击项目依赖项,添加引用,并准备按下 OK 按钮时的示例快照。
图 5-2
向 C# 项目文件添加 BasicGameInfo 引用
现在我可以在新文件的开头添加using BasicGameInfo;
,这样可以减少输入。比如我可以直接用Game
代替BasicGameInfo.Game
。同样的评论也适用于GameInfo
和GamePrice
。
演示 4
在这个示例演示中,我更改了一些参数,如游戏名称、版本、价格细节等。我把所有的片段都放在这里供你参考。请注意,更新后的客户端代码类似于您在之前的演示中看到的客户端代码。
// The content of CommonLibrary.cs
using System;
namespace BasicGameInfo
{
public class Game
{
readonly string companyName;
readonly string gameName;
readonly double minimumAge;
readonly string version;
readonly double actualCost;
readonly double discountedCost;
public Game(
GameInfo gameInfo,
GamePrice gamePrice
)
{
companyName = gameInfo.CompanyName;
gameName = gameInfo.GameName;
version = gameInfo.Version;
minimumAge = gameInfo.MinimumAge;
actualCost = gamePrice.Cost;
discountedCost = gamePrice.DiscountedCost;
}
public void AboutGame()
{
Console.WriteLine($"Game name: {gameName}");
Console.WriteLine($"Minimum age: {minimumAge} years and above.");
Console.WriteLine($"Current version: {version}.");
Console.WriteLine($"It is a {companyName} product.");
}
public void DisplayCost()
{
Console.WriteLine($"\n{companyName} {gameName}'s price details:");
Console.WriteLine($"Version:{version},cost:${actualCost}");
}
public void DisplayCostAfterDiscount()
{
Console.WriteLine($"\n{companyName} offers a festive season discount.");
Console.WriteLine("Discounted price detail:");
Console.WriteLine($"Game: {gameName},Version: {version},Discounted price:${discountedCost}");
}
}
public class GameInfo
{
public string CompanyName { get; set; }
public string GameName { get; set; }
public string Version { get; set; }
public double MinimumAge { get; set; }
public GameInfo(string gameName)
{
CompanyName = "AbcCompany";
GameName = gameName;
Version = "1.0";
MinimumAge = 10.5;
}
}
public class GamePrice
{
public double Cost { get; set; }
public double DiscountedCost { get; set; }
public GamePrice()
{
Cost = 1000;
DiscountedCost = 800;
}
}
}
// The content of the new client code
using BasicGameInfo;
using System;
namespace DryDemoUsingDll
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Apply the DRY principle using DLLs. ***");
// Initial setup
GameInfo gameInfo = new GameInfo("NewPokemonKid");
GamePrice gamePrice = new GamePrice();
// Create the game instance with a
// default setup
Game game = new Game(gameInfo,gamePrice);
// Display the default game detail.
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.WriteLine("------------");
Console.WriteLine("Changing the game version and price now.");
// Changing some of the game info
gameInfo.Version = "2.1";
gameInfo.MinimumAge = 12.5;
// Changing the game cost
gamePrice.Cost = 3500;
gamePrice.DiscountedCost = 2000;
// Updating the game instance
game = new Game(gameInfo, gamePrice);
// Display the latest detail
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.ReadKey();
}
}
}
输出
当您运行这个程序时,您会得到以下输出:
*** Apply the DRY principle using DLLs. ***
Game name: NewPokemonKid
Minimum age: 10.5 years and above.
Current version: 1.0.
It is a AbcCompany product.
AbcCompany NewPokemonKid's price details:
Version:1.0,cost:$1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: NewPokemonKid,Version: 1.0,Discounted price:$800
------------
Changing the game version and price now.
Game name: NewPokemonKid
Minimum age: 12.5 years and above.
Current version: 2.1.
It is a AbcCompany product.
AbcCompany NewPokemonKid's price details:
Version:2.1,cost:$3500
AbcCompany offers a festive season discount.
Discounted price detail:
Game: NewPokemonKid,Version: 2.1,Discounted price:$2000
您可以看到我们得到了想要的输出,但是这次我使用了最少的代码。我通过遵循 DRY 原则和重用现有代码实现了这个结果。
我知道你在想什么。您可以看到Game
和GameInfo/GamePrice
之间的紧密耦合。你如何能去除这种耦合?既然你在前一章学了 DIP,那对你来说应该不是问题。我把这个练习留给你。
摘要
代码重复会导致软件中的严重问题。程序员经常称这种重复为软件中的罪恶。为什么我们会看到重复的代码?原因多种多样:有些是积极的,有些是难以避免的。通过删除多余的代码,您可以制作出更好的、更易于维护的软件。
这一章向你展示了 DRY 原则的应用。你看到了一个程序的初始版本可以被多次改进以使它变得更好。最后,您将公共代码移动到了共享库中。
这个原则不仅适用于代码,也适用于代码注释或测试用例。例如,您可以创建一个公共输入文件来测试各种方法,而不是在每个方法中重复传递相同的输入。当你考虑使用代码注释时,试着遵循《实用程序员》一书的建议,这本书告诉我们在代码中保留低级知识,使用注释进行高级解释。这完全符合干原则的哲学;否则,对于每次更新,您都需要更改代码和注释。
简而言之,这个原则帮助你写出更干净更好的代码,从而产生更好的软件。
六、使用工厂分离可变代码
开发人员的最终目标是开发出满足客户需求的应用。代码必须易于扩展、维护,并且足够稳定以满足未来的需求。第一次尝试就写出程序的最佳版本是很困难的。您可能需要分析代码并多次重构它。在本章中,您将看到这样一个场景,并学习如何使用工厂。为了使讨论简单,我从一个小程序开始。我们将继续分析该程序,并相应地进行修改。
为了制作一个稳定的应用,专业程序员的目标是使它松散耦合。他试图找出将来可能会发生变化的代码。一旦完成,他就将这部分代码从剩余的代码库中分离出来。工厂在这种情况下是最好的。
POINTS TO REMEMBER
显而易见的问题是:什么是工厂?简而言之,这是一段处理对象创建过程细节的代码。任何使用工厂的方法都被称为工厂的客户端。你应该注意到一个工厂可以有很多客户。这些客户端可以使用工厂来获取一个对象,然后,根据他们的需要,他们可以调用该对象的方法。因此,如何使用他从工厂收到的对象取决于客户。这就是为什么分离对象创建过程是有益的。否则,您将会在多个客户端中出现重复的代码。
问题陈述
假设你写了一个程序来演示两种不同动物的行为,比如老虎和猫。但是你有一个约束,说你不应该在你的 Main()
*方法中实例化动物对象。*你为什么会看到这种约束?以下是一些原因:
-
您希望对客户端隐藏实例化逻辑。你知道“变化”是编程世界中唯一不变的东西。让我们看看如果对象的实例化逻辑驻留在客户端会发生什么。当您增强应用以支持新类型的对象时,您也需要更新客户端代码。它还要求重新测试客户端代码。如何将实例化逻辑从客户端代码中分离出来?您将在接下来的演示中看到这一点。
-
可能有单独的类,它们的方法也可以创建猫或老虎。所以,最好将实例化一只猫或一只老虎的代码分离出来。在这种情况下,有多少客户端使用该代码并不重要。每个客户端都可以引用公共位置来实例化一个对象。
初始程序
我希望你明白这个要求。让我们假设您编写了下面的程序,如演示 1 所示。在您完成整个程序之前,请注意以下几点:
-
在这个程序中,
AnimalFactory
类负责实例化一个对象。它包含一个名为CreateAnimal()
的方法来创建一个 tiger 或 cat 实例。因此,AnimalFactory
像一个工厂类,而CreateAnimal()
像一个工厂方法。 -
CreateAnimal()
方法是非静态的,尽管您可以使它成为静态的。我将在本书的最后一章(第十一章)讨论使用静态方法的利弊。 -
在客户端代码中,实例化这个工厂类来获得一个动物。这就是为什么在
Main()
方法中,您会看到下面的代码:AnimalFactory animalFactory = new AnimalFactory(); IAnimal animal = animalFactory.CreateAnimal("cat"); animal.DisplayBehavior();
演示 1
以下是完整的程序:
using System;
namespace UsingSimpleFactory
{
interface IAnimal
{
void DisplayBehavior();
}
class Tiger : IAnimal
{
public Tiger()
{
Console.WriteLine("\nA tiger is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It roars.");
Console.WriteLine("It loves to roam in a jungle.");
}
}
class Cat : IAnimal
{
public Cat()
{
Console.WriteLine("\nA cat is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It meows.");
Console.WriteLine("It loves to stay at a home.");
}
}
class AnimalFactory
{
public IAnimal CreateAnimal(string animalType)
{
IAnimal animal;
if (animalType.Equals("cat"))
{
animal = new Cat();
}
else if (animalType.Equals("tiger"))
{
animal = new Tiger();
}
else
{
Console.WriteLine("You can create either a cat or a tiger. ");
throw new ApplicationException("Unknown animal cannot be instantiated.");
}
return animal;
}
}
class Program
{
static void Main()
{
Console.WriteLine("***Creating animals and learning about them.***");
AnimalFactory animalFactory = new AnimalFactory();
IAnimal animal = animalFactory.CreateAnimal("cat");
animal.DisplayBehavior();
animal = animalFactory.CreateAnimal("tiger");
animal.DisplayBehavior();
Console.ReadKey();
}
}
}
输出
以下是输出:
***Creating animals and learning about them.***
A cat is created.
It meows.
It loves to stay at a home.
A tiger is created.
It roars.
It loves to roam in a jungle.
分析
演示 1 中使用的方法在编程中很常见。它有一个名字: 简单工厂模式 。现在让我们来分析这个程序。
如果将来需要创建不同类型的动物,比如说猴子,您可能需要增强这个应用。你会怎么做?您需要修改AnimalFactory
类并扩展if-else
链来考虑猴子。但是如果你这样做,你将违反 OCP,结果,你将需要重新测试AnimalFactory
类。
POINTS TO REMEMBER
当您在类似的例子中看到switch
语句或if-else
链来创建不同类型的对象时,您会得到提示,您可能需要重新打开代码以适应未来的更改。在最坏的情况下,这些代码会在应用的几个部分重复出现。因此,您不断违反 OCP,这可能会在将来导致严重的维护问题。
在这个程序中,Visual Studio 向您显示一条消息,内容如下: CA1822:成员“CreateAnimal”不访问实例数据,可以标记为 static 。还口口声声说:不访问实例数据或调用实例方法的活动成员可以标记为静态。将方法标记为静态后,编译器将向这些成员发出非虚拟调用点。这可以为对性能敏感的代码带来可观的性能提升。我不建议现在采取这种方法。静态方法允许您在不实例化对象的情况下调用方法。但是静态方法也有缺点。例如,您不能在运行时更改静态方法的行为。如前所述,你会在第十一章中看到关于这个的详细讨论。
更好的程序
你明白通过遵循 OCP 原则,你可以使程序变得更好。因此,在接下来的演示中,您将看到一个新的层次结构。
#region Factory hierarchy
abstract class AnimalFactory
{
public abstract IAnimal CreateAnimal();
}
class CatFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Cat();
}
}
class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Tiger();
}
}
#endregion
为什么这很有帮助?我以这样一种方式使用这个结构,即整个代码段都是封闭的,以便进行修改。将来,如果您需要支持一种新的动物类型,比如说猴子,您将需要执行以下操作:
-
创建一个将实现
IAnimal
接口的Monkey
类。 -
创建一个
MonkeyFactory
,它将实现AnimalFactory
并为CreateAnimal()
方法提供实现。
因此,只测试新的类就足够了。 您现有的代码未被改动,关闭修改 。注意,在这个程序中有两个独立的继承层次:一个是动物层次,另一个是工厂层次。我在这段代码中标记了它们,供您参考。为了更清楚起见,我还包括了下面的类图(见图 6-1 )。
图 6-1
在演示 2 中,类图显示了两个不同的层次结构
演示 2
下面是完整的演示:
using System;
namespace SimpleFactoryModified
{
#region Animal hierarchy
interface IAnimal
{
void DisplayBehavior();
}
class Tiger : IAnimal
{
public Tiger()
{
Console.WriteLine("\nA tiger is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It roars.");
Console.WriteLine("It loves to roam in a jungle.");
}
}
class Cat : IAnimal
{
public Cat()
{
Console.WriteLine("\nA cat is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It meows.");
Console.WriteLine("It loves to stay at a home.");
}
}
#endregion
#region Factory hierarchy
abstract class AnimalFactory
{
public abstract IAnimal CreateAnimal();
}
class CatFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Cat();
}
}
class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Tiger();
}
}
#endregion
// Client
class Program
{
static void Main()
{
Console.WriteLine("***Modifying the simple factory in the demonstration 1.***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.CreateAnimal();
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.CreateAnimal();
animal.DisplayBehavior();
Console.ReadKey();
}
}
}
输出
除了第一行,该输出与前面的输出相同。
***Modifying the simple factory in the demonstration 1.***
A cat is created.
It meows.
It loves to stay at a home.
A tiger is created.
It roars.
It loves to roam in a jungle.
分析
您可以用以下几点来总结这个修改后的实现:
-
在
Main()
里面,你决定使用哪个动物工厂——是CatFactory
还是TigerFactory
? -
AnimalFactory
的子类创建一个Cat
实例或一个Tiger
实例。 -
这样,你就支持了 OCP 的概念。因此,您可以得到一个更好、更具可扩展性的解决方案。
新的要求
在第四章中,我说过:完全实现这个原则并不总是容易的,但是即使部分遵守 OCP 协议也是有益的。对于每一项要求来说,实现 OCP 并不容易。一个新的需求可能会要求应用进行许多更改。在这种情况下,根据情况,你必须选择一种技术。
例如,让我们假设您有一个额外的需求:您希望允许客户为动物选择一种颜色。你应该如何进行?一种选择是在构造函数中传递color
属性,并相应地更新程序。
演示 3
下面是一个满足要求的示例演示。我用粗体字做了重要的修改。
using System;
namespace UsingFactoryMethod
{
#region Animal hierarchy
interface IAnimal
{
void DisplayBehavior();
}
class Tiger : IAnimal
{
public Tiger(string color)
{
Console.WriteLine($"\nA {color} tiger is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It roars.");
Console.WriteLine("It loves to roam in a jungle.");
}
}
class Cat : IAnimal
{
public Cat(string color)
{
Console.WriteLine($"\nA {color} cat is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It meows.");
Console.WriteLine("It loves to stay at a home.");
}
}
#endregion
#region Factory hierarchy
abstract class AnimalFactory
{
public abstract IAnimal CreateAnimal(string color);
}
class CatFactory : AnimalFactory
{
public override IAnimal CreateAnimal(string color)
{
return new Cat(color);
}
}
class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal(string color)
{
return new Tiger(color);
}
}
#endregion
// Client
class Program
{
static void Main()
{
Console.WriteLine("***Modifying demonstration 2 now.***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.CreateAnimal("black");
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.CreateAnimal("white");
animal.DisplayBehavior();
Console.ReadKey();
}
}
}
输出
该程序产生以下输出:
***Modifying demonstration 2 now.***
A black cat is created.
It meows.
It loves to stay at a home.
A white tiger is created.
It roars.
It loves to roam in a jungle.
分析
你可以看到许多变化是必需的。有没有替代的方法?我也这么认为演示 4 就是为此目的而做的。
由于AnimalFactory
是一个抽象类,您可以修改这个类来适应这种变化。在这个替代演示中,我引入了一个新方法MakeAnimal()
,它在调用CreateAnimal()
方法创建动物实例之前接受color
属性。下面是代码:
abstract class AnimalFactory
{
public IAnimal MakeAnimal(string color)
{
Console.WriteLine($"\nThe following animal color is {color}.");
IAnimal animal= CreateAnimal();
return animal;
}
public abstract IAnimal CreateAnimal();
}
在客户端代码中,调用MakeAnimal()
方法而不是CreateAnimal()
来查看动物的颜色。
演示 4
下面是修改后的例子。
using System;
namespace FactoryMethodDemo2
{
#region Animal hierarchy
interface IAnimal
{
void DisplayBehavior();
}
class Tiger : IAnimal
{
public Tiger()
{
Console.WriteLine("\nA tiger is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It roars.");
Console.WriteLine("It loves to roam in a jungle.");
}
}
class Cat : IAnimal
{
public Cat()
{
Console.WriteLine("\nA cat is created.");
}
public void DisplayBehavior()
{
Console.WriteLine("It meows.");
Console.WriteLine("It loves to stay at a home.");
}
}
#endregion
#region Factory hierarchy
abstract class AnimalFactory
{
public IAnimal MakeAnimal(string color)
{
Console.WriteLine($"\nThe following animal color is {color}.");
IAnimal animal= CreateAnimal();
return animal;
}
public abstract IAnimal CreateAnimal();
}
class CatFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Cat();
}
}
class TigerFactory : AnimalFactory
{
public override IAnimal CreateAnimal()
{
return new Tiger();
}
}
#endregion
// Client
class Program
{
static void Main()
{
Console.WriteLine("***Modifying demonstration 2 now.***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.MakeAnimal("black");
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.MakeAnimal("white");
animal.DisplayBehavior();
Console.ReadKey();
}
}
}
输出
以下是输出:
***Modifying demonstration 2 now.***
The following animal color is black.
A cat is created.
It meows.
It loves to stay at a home.
The following animal color is white.
A tiger is created.
It roars.
It loves to roam in a jungle.
分析
在这一章的开始,我们看到了使用工厂的优势。从演示 2 开始,我们为工厂使用了一个新的层次结构,这样所有的具体工厂都继承自AnimalFactory
,我们将对象创建的细节传递给具体工厂(CatFactory
或TigerFactory
)。既然我们遵循 OCP 原则,我们可以添加一个新的混凝土工厂,比如说MonkeyFactory
,来创造猴子。如果我们实现了这个场景,我们就不需要重新打开现有的代码。相反,新的MonkeyFactory
类可以从AnimalFactory,
继承,并且遵循规则,它将创造猴子。在这种情况下,我们需要以创建Tiger
类或Cat
类的相同方式创建一个Monkey
类。请注意,我们永远不需要重新打开现有的代码。
我创建了演示 3 和 4 来支持一个新的需求。在当前结构中维护 OCP 以适应新的需求是非常困难的,因为在开始时没有考虑颜色属性。演示 4 向您展示了您仍然可以进行最少的更改。
摘要
工厂为对象创建提供了另一种方法。本章从一个简单的工厂类开始。它帮助您将可能与代码的其他部分不同的代码分离出来。您将实例化逻辑放在工厂类中,以便为对象创建提供统一的方式。
遵循 OCP 原则,您进一步修改了应用。您为工厂创建了另一个层次结构,并将实际的对象创建逻辑传递给了具体的工厂。
稍后,在演示 4 中,您看到了在抽象工厂类中,您可以设置一个所有派生的具体工厂都必须遵守的通用规则。这个过程可以帮助您以最小的变化适应特定的需求。
七、使用包装器添加功能
继承的另一种选择是合成。这在编程中很常见,通常会给你带来更好的回报。本章使用一些包装器向您展示了关于这个主题的一个有用的案例研究。
你想到的第一个问题可能是:什么是包装器?包装就像包裹着一个物体的顶层。在编程中,你经常使用一个包装器来动态添加一些功能 。这是一种强大的技术,因为您可以根据自己的需要添加或丢弃包装器,并且不会妨碍原始对象的功能。
考虑一个例子:你需要处理一段代码并添加一些新特性。先前有人对此进行了编码,您不能更改现有的代码。当您需要增强一个特性来吸引新客户,但是您不能改变软件的现有工作流时,这种情况在软件行业中很常见,因为您仍然必须支持现有的客户。在这种情况下,由于您不是编写软件第一个版本的团队的一员,您从一开始就没有独占控制权。包装器在这种情况下很有用。如前所述,在这种情况下,您可以在现有功能的基础上添加新功能来支持新客户。事实上,使用不同类型的包装器,您也可以添加不同类型的客户。接下来的例子会让你更清楚这个概念。
问题陈述
考虑一群人,他们每个人都想购买房产并拥有自己的家。每个人的预算和心态都不一样。所以,这群人去拜访一家房屋建筑商,以获得成本估算。为简单起见,我们假设他们有以下选择:
-
他们可以用最少的设施建造一个基本的家,也可以用更多的设施建造一个高级的家。为了便于说明,让我们将这些住宅分别称为
BasicHome
和AdvancedHome
。 -
房屋建筑商给他们提供了选择:顾客可以选择一个操场,或者一个游泳池,或者两者都选。让我们称这些为奢侈品。这些奢侈品都增加了购买者的额外成本。
基于预算限制,客户可以选择各种选项,最终价格会有所不同。最重要的是,今天选择BasicHome
的顾客可以通过增加一个操场或一个游泳池(或两者都有)来升级他的房子。你能为这个场景写一个程序吗?
使用子类化
如果您尝试使用继承来提供解决方案,您将会发现与之相关的问题。假设您从以下结构开始:
class Home
{
// Some code
}
class PlayGround : Home
{
// Some code
}
class SwimmingPool : PlayGround
{
// Some code
}
这不是一个推荐的方法,因为要得到一个游泳池,你首先必须得到一个操场,这可能是客户不想要的。由于类似的逻辑,下面的结构也不是一个好的选择:
class Home
{
// Some code
}
Class SwimmingPool : Home
{
// Some code
}
class PlayGround : SwimmingPool
{
// Some code
}
这是因为在这种情况下,要得到一个操场,你首先必须得到一个游泳池,而顾客可能不想要。 所以,实现多级继承,在这种情况下,并不是一个好主意!
现在,让我们假设你从一个层次继承开始,其中SwimmingPool
和PlayGround
都继承自Home
类,如图 7-1 所示。
图 7-1
等级继承
现在你会有一个有游泳池和操场的家。因此,你最终会得到如图 7-2 所示的设计。
图 7-2
一个类需要从多个基类继承。它导致了 C# 中的菱形问题
但是你知道在 C# 中不能有多个基类。 所以,任何像下面这样的构造也会引发编译错误:
class Home: SwimmingPool, PlayGround // Error
{
}
您可以看到,在这种情况下,使用简单的子类化并不是一个好主意。有哪些选择?让我们继续调查。
你可以继续进行奢侈品的界面。例如,您可以选择以下界面:
interface ILuxury
{
void AddPlayGround();
void AddSwimmingPool();
}
现在你需要一个可以实现这个接口的类。例如,下面是一个Home
类,它扩展了BasicHome
类并实现了ILuxury
接口:
class Home : BasicHome, ILuxury
{
public void AddPlayGround()
{
// Some code
}
public void AddSwimmingPool()
{
// Some code
}
}
但是,顾客可能会选择拥有其中一种奢侈品的房子,而不是两者都拥有。在这种情况下,如果不需要某个方法,您应该编写:
throw new NotImplementedException();
与此相关的问题将在第四章的 LSP 中讨论。为了避免这种情况,您可以遵循 ISP 并隔离ILuxury
接口。是的,这次能成功!既然你在第二章看到了一个类似的解决方案(当我将不同的能力放入一个单独的层次中时),我就不在这里重复了。
接下来,我们寻找另一种方法。本章就是为此而作的。
使用对象合成
让我们看看包装器如何帮助您。 使用包装器,你用另一个对象 包围一个对象。封闭对象通常被称为 装饰器 ,并且符合它所装饰的组件的接口。它将请求转发给原始组件,并可以在这些请求之前或之后执行附加操作。 你可以用这个概念 添加无限数量的责任。以下数字有助于您理解这一点。
图 7-3 显示家(初级或高级)被一个操场包围。
图 7-3
这个家被一个操场包围着
图 7-4 显示住宅被一个游泳池包围。
图 7-4
这个家被一个游泳池包围着
图 7-5 显示该住宅被一个操场和一个游泳池包围。在这里,你首先用一个操场把房子围起来,然后用一个游泳池把房子围起来。
图 7-5
这所房子周围有一个操场和一个游泳池
图 7-6 显示该住宅再次被一个游泳池和一个操场包围。但这次你改变了顺序;你首先在房子周围建一个游泳池,然后在周围建一个操场。
图 7-6
这个家被一个游泳池包围着。随后,你用一个操场包围这个建筑
Note
遵循同样的技术,你可以增加两个操场或游泳池。
让我们试着按照我们的要求来实现这个概念。
在接下来的演示中,涉及到六个玩家:Home, BasicHome, AdvancedHome, Luxury, PlayGround, SwimmingPool
。Home
定义如下:
abstract class Home
{
public double basePrice = 100000;
public double AdditionalCost { get; set; }
public abstract void BuildHome();
public virtual double GetPrice()
{
return basePrice + AdditionalCost;
}
}
以下是一些要点:
-
你可以看到,
Home
的具体实现者必须实现BuildHome()
和GetPrice()
方法。在这个例子中,BasicHome
和AdvancedHome
继承自Home.
-
我假设房屋的基本价格是 10 万美元。使用
AdditionalPrice
属性,可以设置一些额外的价格。我使用这个属性来设置高级住宅的附加成本。目前,对于一个基本家庭来说,这一成本是 0,而对于一个高级家庭来说,这一成本是 25,000 美元。 -
我认为一旦房子建好了,就不需要立即修改。人们可以在以后添加奢侈品。
-
一旦房子建好,你可以为现有的房子选择一个操场或一个游泳池,或者两者都要。因此,
PlayGround
和SwimmingPool
类出现在这个例子中。 -
尽管并不严格要求,但为了共享公共代码,
PlayGround
类和SwimmingPool
类都继承了抽象类Luxury
,其结构如下:abstract class Luxury : Home { protected Home home; public double LuxuryCost { get; set; } public Luxury(Home home) { this.home = home; } public override void BuildHome() { home.BuildHome(); } }
-
像
AdditionalPrice
属性一样,可以使用LuxuryCost
属性设置/更新奢侈品成本。 -
注意
Luxury
持有一个Home
的引用。因此,具体的装饰器(本例中的PlayGround
或SwimmingPool
)正在装饰Home
的一个实例。 -
现在让我们来看看一个混凝土装饰工的结构,比如说,
PlayGround
,如下所示:class PlayGround : Luxury { public PlayGround(Home home) : base(home) { this.LuxuryCost = 20000; } public override void BuildHome() { base.BuildHome(); AddPlayGround(); } private void AddPlayGround() { Console.WriteLine($"For a playground,you pay extra ${this.LuxuryCost}."); Console.WriteLine($"Now the total cost is: ${GetPrice()}."); } public override double GetPrice() { return home.GetPrice() + LuxuryCost; } }
-
你可以看到,通过使用
AddPlayGround()
方法,你可以添加一个操场。当你增加这项设施时,你必须额外支付 20,000 美元。我在构造函数中初始化这个值。最重要的是,注意在添加操场之前,它从基类Luxury
中调用BuildHome()
。该方法又从Home
的具体实现中调用BuildHome()
。 -
SwimmingPool
类的工作方式类似,但是你必须为此付出更多。(是的,我假设在这种情况下,一个游泳池比一个游乐场更贵)。
类图
图 7-7 显示了类图中最重要的部分。
图 7-7
类图显示了除客户端类之外的参与者
示范
这是给你的完整演示。在客户端代码中,您可以看到许多不同的场景来展示此应用的有效性。
using System;
namespace UsingWrappers
{
abstract class Home
{
public double basePrice = 100000;
public double AdditionalCost { get; set; }
public abstract void BuildHome();
public virtual double GetPrice()
{
return basePrice + AdditionalCost;
}
}
class BasicHome : Home
{
public BasicHome()
{
AdditionalCost = 0;
}
public override void BuildHome()
{
Console.WriteLine("A home with basic facilities is made.");
Console.WriteLine($"It costs ${GetPrice()}.");
}
}
class AdvancedHome : Home
{
public AdvancedHome()
{
AdditionalCost = 25000;
}
public override void BuildHome()
{
Console.WriteLine("A home with advanced facilities is made.");
Console.WriteLine($"It costs ${GetPrice()}.");
}
}
abstract class Luxury : Home
{
protected Home home;
public double LuxuryCost { get; set; }
public Luxury(Home home)
{
this.home = home;
}
public override void BuildHome()
{
home.BuildHome();
}
}
class PlayGround : Luxury
{
public PlayGround(Home home) : base(home)
{
this.LuxuryCost = 20000;
}
public override void BuildHome()
{
base.BuildHome();
AddPlayGround();
}
private void AddPlayGround()
{
Console.WriteLine($"For a playground, you pay an extra ${this.LuxuryCost}.");
Console.WriteLine($"Now the total cost is ${GetPrice()}.");
}
public override double GetPrice()
{
return home.GetPrice() + LuxuryCost;
}
}
class SwimmingPool : Luxury
{
public SwimmingPool(Home home) : base(home)
{
this.LuxuryCost = 55000;
}
public override void BuildHome()
{
base.BuildHome();
AddSwimmingPool();
}
private void AddSwimmingPool()
{
Console.WriteLine($"For a swimming pool, you pay an extra ${this.LuxuryCost}.");
Console.WriteLine($"Now the total cost is ${GetPrice()}.");
}
public override double GetPrice()
{
return home.GetPrice() + LuxuryCost;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using wrappers.***");
Console.WriteLine("Scenario-1: A basic home with basic facilities.");
Home home = new BasicHome();
home.BuildHome();
Console.WriteLine("\nScenario-2: A basic home with an additional playground.");
Luxury homeWithOnePlayGround = new PlayGround(home);
homeWithOnePlayGround.BuildHome();
Console.WriteLine("\nScenario-3: A basic home with two additional playgrounds.");
Luxury homeWithDoublePlayGrounds = new PlayGround(homeWithOnePlayGround);
homeWithDoublePlayGrounds.BuildHome();
Console.WriteLine("\nScenario-4: A basic home with one additional playground and swimming pool.");
Luxury homeWithOnePlayGroundAndOneSwimmingPool = new SwimmingPool(homeWithOnePlayGround);
homeWithOnePlayGroundAndOneSwimmingPool.BuildHome();
Console.WriteLine("\nScenario-5: Adding a swimming pool and then a playground to a basic home.");
Luxury homeWithOneSimmingPool = new SwimmingPool(home);
Luxury homeWithSwimmingPoolAndPlayground = new PlayGround(homeWithOneSimmingPool);
homeWithSwimmingPoolAndPlayground.BuildHome();
Console.WriteLine("\nScenario-6: An advanced home with some more facilities.");
home = new AdvancedHome();
home.BuildHome();
Console.WriteLine("\nScenario-7: An advanced home with an additional playground.");
homeWithOnePlayGround = new PlayGround(home);
homeWithOnePlayGround.BuildHome();
Console.ReadKey();
}
}
}
输出
以下是输出:
***Using wrappers.***
Scenario-1: A basic home with basic facilities.
A home with basic facilities is made.
It costs $100000.
Scenario-2: A basic home with an additional playground.
A home with basic facilities is made.
It costs $100000.
For a playground, you pay an extra $20000.
Now the total cost is $120000.
Scenario-3: A basic home with two additional playgrounds.
A home with basic facilities is made.
It costs $100000.
For a playground, you pay an extra $20000.
Now the total cost is $120000.
For a playground, you pay an extra $20000.
Now the total cost is $140000.
Scenario-4: A basic home with one additional playground and swimming pool.
A home with basic facilities is made.
It costs $100000.
For a playground, you pay an extra $20000.
Now the total cost is $120000.
For a swimming pool, you pay an extra $55000.
Now the total cost is $175000.
Scenario-5: Adding a swimming pool and then a playground to the basic home.
A home with basic facilities is made.
It costs $100000.
For a swimming pool, you pay an extra $55000.
Now the total cost is $155000.
For a playground, you pay an extra $20000.
Now the total cost is $175000.
Scenario-6: An advanced home with some more facilities.
A home with advanced facilities is made.
It costs $125000.
Scenario-7: An advanced home with an additional playground.
A home with advanced facilities is made.
It costs $125000.
For a playground, you pay an extra $20000.
Now the total cost is $145000.
分析
注意,这个实现遵循 OCP 原理。因此,当您创建一个不同类型的 home 时,您不需要打开现有的代码,而是可以创建一个继承自抽象类Home
的新类。
我想让你注意到我在这个例子中稍微违反了 SRP。这是因为我想给你看的是统一加了奢侈品后的最终价格。实际上,当我添加一个包装器时,我不需要计算总成本;相反,它足以显示增加的价格。但是我假设客户会希望看到总的估计价格。这就是为什么,每增加一件奢侈品,我都会显示总费用。另外,价格取决于你选择的房屋类型。因此,将GetPrice()
方法和BuildHome()
方法放在Home
类中是有意义的。
当您使用只做一项工作的包装器时,这个示例模式会更有效。因此,当您遵循 SRP 时,您可以使用这种包装器轻松地添加或删除行为。
我即将完成这一章。但是首先,我想告诉您,当您在。NET 框架和 Java,你会发现很多类似的实现。例如,BufferedStream
类继承自抽象基类Stream.
我从 Visual Studio IDE 中截取了一个快照来展示这个类的构造函数(见图 7-8 )。
图 7-8
Visual Studio IDE 中 BufferedStream 类的部分快照
您可以看到,BufferedStream
类构造函数可以接受一个Stream
类对象作为参数。注意,Luxury
构造函数也接受它的基类对象(Home
)作为参数。因此,您得到了一个线索,即BufferedStream
类遵循包装器模式。
但是现在请注意来自 Visual Studio IDE 的FileStream
类的部分快照(见图 7-9 )。
图 7-9
Visual Studio IDE 中 FileStream 类的部分快照
可以看到没有一个FileStream
类的构造函数可以接受一个Stream
类对象作为参数。因此,这个类没有遵循包装器/装饰器模式。
摘要
本章展示的模式被称为包装器或装饰器模式。本章向您展示了子类化技术的一种替代方法。你已经看到了多级继承和多重继承都不能解决我们在本章开始时提到的问题的原因。稍后,您看到了使用对象组合的实现。您使用了不同类型的包装器在这个应用中动态添加行为。回想一下,简单继承只促进编译时绑定,而不是动态绑定。
简而言之,以下是您在本章中学到的要点:
-
您可以在不修改现有继承层次结构的情况下添加状态和/或行为。
-
在演示 1 中,您定义了一个新的层次结构,它本身扩展了原始/现有层次结构的根。
-
要使用装饰器,首先要实例化一个 home,然后将它包装在装饰器中。
-
这个示例模式有一个名称。我们称之为装饰模式。它展示了一个例子,说明对象组合何时比普通继承执行得更好。
您还看到了. NET 中的一些内置示例。在那里,您了解到BufferedStream
类遵循类似的模式,但是FileStream
类不遵循。
八、使用挂钩的高效模板
本章将向你展示两种重要的技术。首先,您将学习使用模板方法。为什么这很重要?模板方法是代码重用的基本技术之一。假设你按照一个多步算法去完成一个任务。使用模板方法,您可以重新定义这些步骤中的一部分(但不是全部),而不改变它们的调用顺序。
本章从使用模板方法的演示开始。稍后,除了模板方法之外,您还将使用挂钩方法来增强这个应用。
问题陈述
经销商(或卖家)出售各种产品,如电视、洗衣机和音乐播放器。假设你认识这样一个经销商。你可以去他的陈列室买一台电视机。你可以参观同一个展厅,为你的家庭购买一台洗衣机。在每种情况下,您都可以按以下顺序总结整体活动:
1: You visit the dealer showroom.
2: You purchase a product.
3: The dealer generates a bill(or,invoice) for you.
4: The dealer delivers the product to you.
你能制作一个模拟这个场景的应用吗?
POINT TO NOTE
我们许多人把账单和发票区分开来。我建议你不要把重点放在区别上。请在本章中对它们一视同仁。
初始程序
在接下来的节目中,我假设你从同一个经销商那里购买了一台洗衣机和一台电视机。当您购买电视机时,您会看到以下输出:
1.The customer visits a dealer showroom.
2.The customer purchases a television.
3.The bill is printed.
4.The product is delivered.
当您购买洗衣机时,您会看到以下输出:
1.The customer visits a dealer showroom.
2.The customer purchases a washing machine.
3.The bill is printed.
4.The product is delivered.
请注意,步骤 1、3 和 4 对于这两种情况都是通用的。当你购买不同的产品时,你看不出这些步骤有什么不同。还要注意,这些步骤是固定的。例如,一旦你购买了一个产品,账单就生成了,然后产品就交付了。如果你没有首先参观陈列室或选择产品,系统不太可能生成账单并交付产品。(这种情况下我不考虑网购)。
在这种情况下,模板方法是理想的,在这种情况下,您不改变算法的基本步骤,但允许在某些步骤中进行一些小的修改。在我们的示例中,第 2 步略有变化,以显示您选择的产品,但当您购买任何产品时,其余步骤都是相同的。
当你点披萨时,你会注意到类似的场景。例如,你可以选择不同的配料,如培根、洋葱、额外的奶酪或蘑菇。厨师是怎么做比萨饼的?他首先按照他的传统方式准备比萨饼。就在送之前,他添加配料让你开心。你也可以在其他领域找到类似的情况。
现在的问题是:如何创建一个有多个步骤的应用,但其中只有几个步骤不同?答案是:您可以在父类中使用模板方法(由许多步骤组成)。然后,您可以将一些步骤委托给子类(代表特定的产品),并允许它们根据需要覆盖这些步骤。
Note
使用简单的多态,您可以通过在子类中重写父类的所有或大部分方法来带来彻底的改变。但是,当您使用模板方法时,您不会重写子类中的所有父类(或基类)方法。相反,您只覆盖有限数量的方法(或步骤)。这是这种方法和简单多态之间的关键区别。
在接下来的例子中,您可以看到下面的父类包含一个名为PurchaseProduct
的模板方法。为了便于理解,我使用了注释。
public abstract class Device
{
// The following method(step) will NOT vary
private void VisitShowroom()
{
Console.WriteLine("1.The customer visits a dealer showroom.");
}
// The following method(step) will NOT vary
private void GenerateBill()
{
Console.WriteLine("3.The bill is printed.");
}
private void DeliverProduct()
{
Console.WriteLine("4.The product is delivered.\n");
}
/*
The following method will vary. It will be
overridden by derived classes.
*/
protected abstract void SelectProduct();
// The template method
public void PurchaseProduct()
{
// Step-1
VisitShowroom();
// Step-2: Specialized action
SelectProduct();
// Step-3
GenerateBill();
// Step-4
DeliverProduct();
}
}
以下是重点:
-
这个父类是抽象的,因为它包含抽象方法
SelectProduct()
.
一个派生类重写这个方法来显示您购买的产品。 -
注意,在模板方法中有四个方法:
VisitShowroom(), SelectProduct(), GenerateBill(), DeliverProduct()
。这四种方法代表了算法的四个步骤。 -
SelectProduct()
是一个受保护的方法。它允许派生类重新定义/重写该方法。但是模板方法中的其他方法都标有私有关键字/访问修饰符。因此,客户端无法直接访问它们。 -
当您调用
PurchaseProduct()
方法时,派生类不能改变这些方法的执行顺序。此外,您不能在客户端代码中直接访问这些方法(例如,在Main()
方法中)。要完成购买,您需要调用模板方法。这就是我公开这个模板方法的原因。从客户的角度来看,他不知道模板方法是如何完成任务的,因为您没有向客户公开内部逻辑。这是一种更好的做法。
类图
下图(图 8-1 )显示了类图的重要部分。
图 8-1
PurchaseProduct()是这个例子中的模板方法
演示 1
下面是完整的演示:
using System;
namespace TemplateMethodDemo
{
/// <summary>
/// Basic skeleton of action steps
/// </summary>
public abstract class Device
{
// The following method(step) will NOT vary
private void VisitShowroom()
{
Console.WriteLine("1.The customer visits a dealer showroom.");
}
// The following method(step) will NOT vary
private void GenerateBill()
{
Console.WriteLine("3.The bill is printed.");
}
private void DeliverProduct()
{
Console.WriteLine("4.The product is delivered.\n");
}
/*
The following method will vary. It will be
overridden by derived classes.
*/
protected abstract void SelectProduct();
// The template method
public void PurchaseProduct()
{
// Step-1
VisitShowroom();
// Step-2: Specialized action
SelectProduct();
// Step-3
GenerateBill();
// Step-4
DeliverProduct();
}
}
// The concrete derived class-Television
public class Television : Device
{
protected override void SelectProduct()
{
Console.WriteLine("2.The customer purchases a television.");
}
}
// The concrete derived class-WashingMachine
public class WashingMachine : Device
{
protected override void SelectProduct()
{
Console.WriteLine("2.The customer purchases a washing machine.");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A demonstration of a template Method.***\n");
Console.WriteLine("---The customer wants a television.---");
Device device = new Television();
device.PurchaseProduct();
Console.WriteLine("---The customer wants a washing machine.---");
device = new WashingMachine();
device.PurchaseProduct();
Console.ReadLine();
}
}
}
输出
以下是输出:
***A demonstration of a template Method.***
---The customer wants a television.---
1.The customer visits a dealer showroom.
2.The customer purchases a television.
3.The bill is printed.
4.The product is delivered.
---The customer wants a washing machine.---
1.The customer visits a dealer showroom.
2.The customer purchases a washing machine.
3.The bill is printed.
4.The product is delivered.
分析
你可以看到,在未来,如果你需要考虑一个不同的产品,比如智能手机,你可以很容易地增强应用。在这种情况下,您可以创建一个继承自Device
的SmartPhone
类,并以同样的方式覆盖SelectProduct()
。这里有一个例子:
// The concrete derived class-SmartPhone
public class SmartPhone : Device
{
protected override void SelectProduct()
{
Console.WriteLine("2.The customer purchases a smartphone.");
}
}
Note
这种实现遵循 OCP 原理。但是这种实现违反 SRP 吗?答案在某种程度上似乎是肯定的。但是从一个销售者的角度来想:一个潜在的顾客参观陈列室并选择产品。然后,销售人员生成发票,并将产品交付给客户。从卖方的角度来看,所有这些活动都与“一次成功的销售”联系在一起。从程序员的角度来看,所有这些步骤都是为了完成一个任务:购买产品。这就是为什么这个例子中的客户只能访问模板方法,而其他方法对他是隐藏的。此外,您可能还记得我在前言中说过的话:有时根据问题的复杂性或本质来变通规则是可以的。
增强的需求
让我们为另一个真实场景增强应用。经销商可以决定向任何向他购买电视机的顾客提供特别优惠券。此优惠不适用于其他产品。如何修改这个应用来满足这个新的需求呢?
一种方法很简单。您可以使用一个方法(可以是public
或protected
)来反映这个提议,并将该方法放在父类Device
中。我们把这个方法命名为GenerateGiftCoupon()
。下面是一个示例代码:
/// <summary>
/// Basic skeleton of action steps
/// </summary>
public abstract class Device
{
// The following method(step) will NOT vary
private void VisitShowroom()
{
Console.WriteLine("1.The customer visits a dealer showroom.");
}
// The following method(step) will NOT vary
private void GenerateBill()
{
Console.WriteLine("3.The bill is printed.");
}
private void DeliverProduct()
{
Console.WriteLine("4.The product is delivered.\n");
}
/*
The following method will vary. It will be
overridden by derived classes.
*/
protected abstract void SelectProduct();
// The template method
public void PurchaseProduct()
{
// Step-1
VisitShowroom();
// Step-2: Specialized action
SelectProduct();
// Step-2.1: Elgible for a gift?
GenerateGiftCoupon();
// Step-3
GenerateBill();
// Step-4
DeliverProduct();
}
protected virtual void GenerateGiftCoupon()
{
Console.WriteLine("A gift coupon is generated.");
}
}
现在Device
的任何子类都可以拥有GenerateGiftCoupon()
方法。他们可以根据自己的需要重新定义它。所以现在,根据我们的新要求,如果你购买一台电视机,你可以从经销商那里得到一张特别的优惠券,但购买一台洗衣机则不行。因此,在WashingMachine
类中,你覆盖了这个方法,并以如下方式编写它:
protected override void GenerateGiftCoupon()
{
throw new NotImplementedException();
}
但是在某些情况下,在方法体中抛出异常是有风险的。当我在第四章讨论里斯科夫替代原理(LSP)时,你已经了解了这一点。
为了避免这个问题,您可以让这个方法为空,如下所示:
protected override void GenerateGiftCoupon()
{
// Empty body
}
现在想想:使用空方法是个好主意吗?我不这么认为。让我们看看另一种选择。您可能希望将GenerateGiftCoupon()
抽象化,并根据需要在其子类中覆盖它。是的,这个可以。但问题是,当你在父类中使用抽象方法时,派生类需要为该方法提供具体的实现(否则,它又是抽象的,你不能从中实例化)。所以,如果你有太多的专业课,而其中大部分都不能让你有资格获得礼券,你仍然不得不放弃它们。(还能记得 ISP 吗?)
有没有更好的解决办法?是的,我想是的。你可以用挂钩的方法。我将在演示 2 中展示这一点。 但是 编程中什么是挂钩?用非常简单的话来说,一个挂钩帮助你在现有代码之前或之后执行一些代码。它可以帮助你在运行时扩展程序的行为。挂钩方法可以提供一些默认的行为,如果需要的话,子类可以覆盖这些行为。通常,默认情况下他们什么都不做 。
让我向你展示一个简单的挂钩在这个程序中的用法。请注意以下代码段中的粗体行:
// The template method
public void PurchaseProduct()
{
// Step-1
VisitShowroom();
//Step-2: Specialized action
SelectProduct();
// Step-2.1: Elgible for a gift?
if(IsEligibleForGiftCoupon())
{
GenerateGiftCoupon();
}
// Step-3
GenerateBill();
// Step-4
DeliverProduct();
}
其中挂钩方法定义为:
// If a customer purchases a television
// he can get a gift. By default,
// there is no gift coupon.
protected virtual bool IsEligibleForGiftCoupon()
{
return false;
}
这两段代码告诉我们,当你调用模板方法时,默认情况下GenerateGiftCoupon()
不会被执行。这是因为IsEligibleForGiftCoupon()
返回false
,进而使得模板方法false
内部的if
条件。但是Television
类如下覆盖了这个方法:
protected override bool IsEligibleForGiftCoupon()
{
return true;
}
因此,当实例化一个Television
对象并调用模板方法时,GenerateGiftCoupon()
就在第 3 步之前被调用。
演示 2
下面是使用挂钩方法的完整演示。我保留这些评论是为了让你更容易理解。
using System;
namespace UsingHook
{
/// <summary>
/// Basic skeleton of action steps
/// </summary>
public abstract class Device
{
// The following method (step) will NOT vary
private void VisitShowroom()
{
Console.WriteLine("1.The customer visits a dealer showroom.");
}
// The following method (step) will NOT vary
private void GenerateBill()
{
Console.WriteLine("3.The bill is printed.");
}
private void DeliverProduct()
{
Console.WriteLine("4.The product is delivered.\n");
}
/*
The following method will vary. It will be
overridden by derived classes.
*/
protected abstract void SelectProduct();
// The template method
public void PurchaseProduct()
{
// Step-1
VisitShowroom();
// Step-2: Specialized action
SelectProduct();
// Step-2.1: Elgible for gift?
if(IsEligibleForGiftCoupon())
{
GenerateGiftCoupon();
}
// Step-3
GenerateBill();
// Step-4
DeliverProduct();
}
protected void GenerateGiftCoupon()
{
Console.WriteLine("A gift coupon is generated.");
}
// Hook
// If a customer purchases a television
// he can get a gift. By default,
// there is no gift coupon.
protected virtual bool IsEligibleForGiftCoupon()
{
return false;
}
}
// The concrete derived class-Television
public class Television : Device
{
protected override bool IsEligibleForGiftCoupon()
{
return true;
}
protected override void SelectProduct()
{
Console.WriteLine("2.The customer purchases a television.");
}
}
// The concrete derived class-WashingMachine
public class WashingMachine : Device
{
protected override void SelectProduct()
{
Console.WriteLine("2.The customer purchases a washing machine.");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A demonstration of a template Method.***\n");
Console.WriteLine("---The customer wants a television.---");
Device device = new Television();
device.PurchaseProduct();
Console.WriteLine("---The customer wants a washing machine.---");
device = new WashingMachine();
device.PurchaseProduct();
Console.ReadLine();
}
}
}
输出
以下是输出:
***A demonstration of a template Method.***
---The customer wants a television.---
1.The customer visits a dealer showroom.
2.The customer purchases a television.
A gift coupon is generated.
3.The bill is printed.
4.The product is delivered.
---The customer wants a washing machine.---
1.The customer visits a dealer showroom.
2.The customer purchases a washing machine.
3.The bill is printed.
4.The product is delivered.
摘要
本章向你展示了如何使用模板方法来创建一个高效的应用。稍后,它演示了如何使用挂钩来适应新的需求,而不改变算法的核心结构。
维基百科上说微软的 Windows 也允许你插入挂钩。您可以使用它们来插入、移除、处理或修改键盘和鼠标事件。不过也有不好的一面。如果不够小心,挂钩的使用会影响应用的整体性能。
但是在我们的例子中,使用挂钩方法是有益的。在类似的情况下,使用 hook 方法,您可以扩展应用以适应新的需求。