C--设计模式-二-

131 阅读34分钟

C# 设计模式(二)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

四、工厂方法模式

本章涵盖了工厂方法模式。

Note

为了更好地理解这种模式,我建议你首先阅读第二十四章,它涵盖了简单工厂模式。简单工厂模式没有直接落入四人组设计模式,所以它出现在本书的第二部分;然而,如果您首先理解简单工厂模式的优点和缺点,工厂方法模式会更有意义。

GoF 定义

定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法让一个类将实例化推迟到子类。

概念

这里,您从定义应用基本结构的抽象 creator 类开始,子类(从这个抽象类派生)负责执行实际的实例化过程。当你分析下面的例子时,这个概念就会对你有意义。

真实世界的例子

简单工厂模式的例子也适用于此。例如,在餐馆中,根据顾客的喜好,厨师可以在最终产品的准备过程中添加更多(或更少)的香料、油等。

让我们看另一个例子。假设一家汽车制造公司每年生产不同型号的汽车。根据他们的市场调查,他们决定一个模型,并开始生产。基于汽车的模型,不同的零件被制造和组装。一家公司应该随时准备好应对客户未来可能选择更好模式的变化。如果公司需要为只需要几个新功能的新模型创建一个全新的设置,这可能会极大地影响公司的利润率。因此,该公司应该以这样一种方式建立工厂,它可以很容易地为即将到来的模型生产零件。

计算机世界的例子

在数据库编程中,您可能需要支持不同的数据库用户。例如,一个用户可能使用 SQL Server,而另一个用户可能选择 Oracle。当你需要向你的数据库中插入数据时,你首先要创建一个连接对象,比如SqlConnection或者OracleConnection,然后才能继续。如果您将代码放入一个if-else块(或switch语句),您可能需要重复许多相似的代码,这不容易维护。此外,每当您决定开始支持一种新的连接类型时,您需要重新打开代码并进行一些修改。使用工厂方法模式可以解决这类问题。

履行

接下来的例子提供了一个名为AnimalFactory的抽象 creator 类来定义基本结构。根据定义,实例化过程是通过从这个抽象类派生的子类来执行的。这个例子里有很多小类。我可以为每个类创建单独的文件,这种方法经常被许多开发人员所鼓励。但是这些类非常简短、简单和直接。所以,我把它们放在一个文件里。对于本书中类似的例子,我遵循同样的原则。

类图

图 4-1 为类图。

img/463942_2_En_4_Fig1_HTML.jpg

图 4-1

类图

解决方案资源管理器视图

图 4-2 显示了程序的高层结构。

img/463942_2_En_4_Fig2_HTML.jpg

图 4-2

解决方案资源管理器视图

演示 1

下面是实现。类似于第二十四章中的简单工厂模式,我使用相同的继承层次结构;也就是说,这一次,您看到DogTiger类都实现了它们的父接口IAnimalAboutMe()方法。因此,您会在示例的开头看到下面的代码段。

public interface IAnimal
    {
        void AboutMe();
    }
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
        }
    }
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum. I prefer hunting.");
        }
    }

您可以看到另一个继承层次结构,其中两个具体的类——称为DogFactoryTigerFactory—创建了 dog 和 tiger 对象。它们中的每一个都继承自一个抽象类AnimalFactory。这两个具体的类推迟了实例化过程。我加入了支持性的评论来帮助你更好地理解。下面的代码段描述了它。

public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." The following method will create a tiger or a dog object, but at this point it does not know whether it will get a dog or a tiger. It will be decided by
        the subclasses i.e. DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }
    // DogFactory is used to create dog
    public class DogFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Dog
            return new Dog();
        }
    }
    // TigerFactory is used to create tigers
    public class TigerFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Tiger
            return new Tiger();
        }
    }

这是完整的演示。

using System;

namespace FactoryMethodPattern
{
    #region Animal Hierarchy
    /*
     * Both the Dog and Tiger classes will
     * implement the IAnimal interface method.
     */
    public interface IAnimal
    {
        void AboutMe();
    }
    // Dog class
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
        }
    }
    //Tiger class
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum. I prefer hunting.");
        }
    }
    #endregion

    #region Factory Hierarchy

    // Both DogFactory and TigerFactory will use this.
    public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." The following method will create a Tiger
        or a Dog, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e. DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }
    // DogFactory is used to create dog
    public class DogFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Dog
            return new Dog();
        }
    }
    // TigerFactory is used to create tigers
    public class TigerFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Tiger
            return new Tiger();
        }
    }
    #endregion
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Demo.***\n");
            // Creating a Tiger Factory
            AnimalFactory tigerFactory = new TigerFactory();
            // Creating a tiger using the Factory Method
            IAnimal tiger = tigerFactory.CreateAnimal();
            tiger.AboutMe();

            // Creating a DogFactory
            AnimalFactory dogFactory = new DogFactory();
            // Creating a dog using the Factory Method
            IAnimal dog = dogFactory.CreateAnimal();
            dog.AboutMe();

            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Factory Pattern Demo.***

The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.

修改后的实现 1

现在让我们来看看您可以对演示 1 进行的两个重要修改。

在第一个修改的实现中,更多的灵活性被添加到我们先前的实现中。注意,AnimalFactory类是一个抽象类,所以你可以利用它。假设您希望一个子类遵循一个可以从其父类(或基类)强加的规则。为简单起见,让我们通过控制台消息来实施该规则,如下面的演示所示。

部分演示 1

在修改后的实现中,我在AnimalFactory class中引入了一个叫做MakeAnimal()的新方法。

// Modifying the AnimalFactory class.
public abstract class AnimalFactory
    {
      public IAnimal MakeAnimal()
            {
                 Console.WriteLine("AnimalFactory.MakeAnimal()-You cannot ignore parent rules.");
                 IAnimal animal = CreateAnimal();
                 animal.AboutMe();
                 return animal;
            }
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." Following method will create a Tiger
        or a Dog class, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e.DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }

客户端代码采用了这些更改;也就是说,不是先调用CreateAnimal()再使用AboutMe()。我只是在下面的代码段中调用了MakeAnimal()。旧代码被注释以供参考,并与新代码进行比较。

class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Modified Demo.***\n");
            // Creating a Tiger Factory
            AnimalFactory tigerFactory = new TigerFactory();
            // Creating a tiger using the Factory Method
            //IAnimal tiger = tigerFactory.CreateAnimal();
            //tiger.AboutMe();
            IAnimal tiger = tigerFactory.MakeAnimal();

            // Creating a DogFactory
            AnimalFactory dogFactory = new DogFactory();
            // Creating a dog using the Factory Method
            //IAnimal dog = dogFactory.CreateAnimal();
            //dog.AboutMe();
            IAnimal dog = dogFactory.MakeAnimal();

            Console.ReadKey();
        }
    }

输出

下面是修改后的输出。

***Factory Pattern Modified Demo.***

AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The tiger says: Halum. I prefer hunting.

AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The dog says: Bow-Wow. I prefer barking.

分析

现在,在每种情况下,您都会看到以下警告:“…您不能忽略父规则。”这是对演示 1 的增强。

问答环节

4.1 为什么将 CreateAnimal() 方法 从客户端代码中分离出来?

我做这件事只有一个目的。我想让子类创建专门化的对象。如果你仔细观察,你会发现只有这个“创造性部分”在不同的产品中有所不同。我在第二十四章的“问答环节”中详细讨论了这一点。

使用这样的工厂有哪些 优势

以下是一些关键优势。

  • 您将变化的代码与不变化的代码分开(换句话说,使用简单工厂模式的优势仍然存在),这有助于您轻松地维护代码。

  • 代码不是紧密耦合的,所以您可以在系统中随时添加新的类,如LionBear等,而无需修改现有的架构。换句话说,我遵循了“修改时封闭,扩展时开放”的原则。

4.3 使用这样的工厂有哪些 挑战

如果您需要处理许多不同类型的对象,那么系统的整体性能会受到影响。

4.4 工厂方法模式支持两个平行的层次结构。这是正确的吗?

接得好。是的,从类图来看,很明显这种模式支持并行的类层次结构(见图 4-3 )。

img/463942_2_En_4_Fig3_HTML.jpg

图 4-3

本例中的两个类层次结构

在这个例子中,AnimalFactoryDogFactoryTigerFactory被放置在一个层次中,而IAnimalDogTiger被放置在另一个层次中。因此,你可以看到创作者和他们的创作/产品是并行运行的两个层次。

你应该总是用一个抽象的关键字来标记工厂方法,这样子类就可以完成它们。这是正确的吗?

不。当创建者没有子类时,您可能会对默认的工厂方法感兴趣。在这种情况下,不能用关键字abstract标记工厂方法。

然而,要看到工厂方法模式的真正威力,您可能需要遵循这里在大多数情况下实现的设计。

看来工厂方法模式与简单工厂模式没有太大的不同。这是正确的吗?

如果你看看这两章的例子中的子类,你可能会发现一些相似之处。但是你不应该忘记工厂方法模式的主要目的;它为你提供了一个框架,通过这个框架,不同的子类可以生产不同的产品。在简单工厂模式中,您不能以类似的方式改变产品。您可以将简单的工厂模式视为一次性交易,但最重要的是,您的创造性部分不会因为修改而关闭。每当您想要添加新的东西时,您需要在简单工厂模式的工厂类中添加一个if-else块或一个switch语句。

在这种情况下,永远记住 GoF 定义,它说,“工厂方法模式让一个类延迟实例化到子类。”仔细看看修改后的实现。你可以看到CreateAnimal()通过AnimalFactory的适当子类创建了一只狗或一只老虎。所以,CreateAnimal()是这个设计中抽象的工厂方法。当MakeAnimal()在其体内使用CreateAnimal()时,它不知道是对狗还是对老虎有效。AnimalFactory的子类只知道为这个应用创建具体的实现(一只狗或一只老虎)。

Note

System.Web.WebRequest类中,你可以看到Create方法,它有两个重载。在这个方法中,您可以传递一个统一资源标识符(URI)。此方法为请求确定适当的协议并返回适当的子类,例如,HttpWebRequest(如果 URI 以 http://或 https://开头)、FtpWebRequest(如果 URI 以 ftp://开头)等等。如果 URI 从 HTTP 更改为 FTP,底层代码不需要更改,调用者也不需要担心协议的细节。这种架构促进了工厂模式的使用,但是对于新的开发不推荐使用 HttpWebRequest。微软建议你使用系统。请改用. Net.Http.HttpClient 类。

修改后的实现 2

本章以对我们的初始实现的额外更新结束。现在让我们通过使用方法参数来更新演示 1。我们继续吧。当您从 Apress 网站下载代码时,您可以获得完整的实现。为了简洁起见,这里只展示了部分演示。

部分演示 2

这段代码表明,如果在CreateAnimal() .中使用方法参数,可以使原来的实现变得更好,这种方法提供了一个好处。您可以只创建一个具体的工厂类,而不是创建DogFactory, TigerFactory等等,如下所示。

#region Factory Hierarchy

    // Both DogFactory and TigerFactory will use this.
    public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." Following method will create a Tiger
        or a Dog, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e.DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal(string animalType);
    }
    /*
     * ConcreteAnimalFactory is used to create dogs or tigers
     * based on method parameter of CreateAnimal() method.
     */
    public class ConcreteAnimalFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal(string animalType)
        {
            if (animalType.Contains("dog"))
            {
                // Creating a Dog
                return new Dog();
            }
            else
            if (animalType.Contains("tiger"))
            {
                // Creating a Dog
                return new Tiger();
            }
            else
            {
                throw new ArgumentException("You need to pass either a dog or a tiger as an argument.");
            }
        }
    }

    #endregion

现在你可以在CreateAnimal(...)方法中传递一个“狗”字符串或一个“老虎”字符串来创建一个Dog或一个Tiger实例。为了适应这些变化,您可以按如下方式更新客户端代码。(这一次,animalFactory创建了DogTiger实例。每个人都知道“编程到接口”有这种好处。)

class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Demo.***");
            Console.WriteLine("***It's a modified version using method parameter(s).***\n");
            // Creating a factory that can produce animals
            AnimalFactory animalFactory = new ConcreteAnimalFactory();
            // Creating a tiger using the Factory Method
            IAnimal tiger = animalFactory.CreateAnimal("tiger");
            tiger.AboutMe();
            // Now creating a dog.
            IAnimal dog = animalFactory.CreateAnimal("dog");
            dog.AboutMe();

            Console.ReadKey();
        }
    }

输出

现在,如果您执行这个程序,您可以得到以下输出。

***Factory Pattern Demo.***
***It's a modified version using method parameter(s).***

The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.

我希望您现在对如何实现工厂方法模式有了更好的理解。提供两个修改的实现作为参考。(Apress 网站上提供了完整的实现。).您可以决定是否要在您的程序中使用这些修改中的一个(或两个)。但是您应该记住,工厂方法应该在幕后为客户机创建适当的对象,这是该模式的终极座右铭。

五、抽象工厂模式

本章涵盖了抽象工厂模式。

GoF 定义

提供创建相关或依赖对象系列的接口,而无需指定它们的具体类。

Note

如果你理解简单工厂模式(第章第二十四部分)和工厂方法模式(第章第四部分),抽象工厂模式对你来说会更有意义。简单工厂模式并不直接属于四人组设计模式,所以对该模式的讨论出现在本书的第二部分。我建议你先阅读第四章和第二十四章,然后再开始阅读这一章。

概念

抽象工厂通常被称为工厂中的工厂。这种模式提供了一种封装一组具有共同主题的单个工厂的方法。在这个过程中,你不直接实例化一个类;相反,您实例化一个具体的工厂,然后使用该工厂创建产品。

在我们接下来的例子中,实例化了一个工厂实例(animalFactory)。通过使用这个工厂实例,我创建了狗和老虎实例(狗和老虎是最终产品),这就是为什么您会在客户端代码中看到下面的代码段。

// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

当产品相似,但产品系列不同时,这种模式最适合(例如,家养的狗和野生的狗很不一样)。这种模式有助于您交换特定的实现,而无需更改使用它们的代码,甚至在运行时也是如此。但是,这可能会导致不必要的复杂性和额外的工作。在某些情况下,甚至调试也变得很困难。

真实世界的例子

假设你用两种不同类型的桌子装饰你的房间;一个是木制的,另一个是钢制的。对于木制的,你需要拜访一个木匠,对于其他类型的,你可能需要去一个金属商店。所有这些都是桌子工厂。所以,基于需求,你决定你需要什么样的工厂。

计算机世界的例子

ADO.NET 实现了类似的概念来建立到数据库的连接。

履行

维基百科描述了这种模式的典型结构,类似于图 5-1 (见 https://en.wikipedia.org/wiki/Abstract_factory_pattern )所示。

img/463942_2_En_5_Fig1_HTML.jpg

图 5-1

抽象工厂模式

在本章的实现中,我遵循类似的结构。在这个例子中,有两种动物:宠物和野生动物。Program.cs客户正在寻找一些动物(在本例中是野狗、宠物狗、野生老虎和宠物老虎)。在这个实现中,您将探索宠物和野生动物的构造过程。

IAnimalFactory是一个抽象工厂。名为WildAnimalFactoryPetAnimalFactory的两个具体工厂继承自这个抽象工厂。你可以看到这些混凝土工厂负责创造狗和老虎的混凝土产品。顾名思义,WildAnimalFactory创造野生动物(野狗、野虎),而PetAnimalFactory创造宠物(宠物狗、宠物虎)。下面总结了参与者及其角色。

  • IAnimalFactory:抽象工厂

  • WildAnimalFactory:实现IAnimalFactory的混凝土工厂;它创造了野狗和野生老虎

  • PetAnimalFactory:实现IAnimalFactory,的混凝土工厂,但是这个工厂制造宠物狗和宠物老虎

  • ITigerIDog:抽象产品

  • PetTigerPetDogWildTigerWildDog:混凝土制品。PetTigerWildTiger实现ITiger接口。PetDogWildDog实现IDog接口。IDogITiger接口只有一个方法AboutMe(),在简单工厂模式和工厂方法模式中都使用。

  • 客户端代码中使用了一个名为FactoryProvider的静态类,如下所示:

    // Making a wild dog and wild tiger through
    // WildAnimalFactory
    IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
    IDog dog = animalFactory.GetDog();
    ITiger tiger = animalFactory.GetTiger();
    dog.AboutMe();
    tiger.AboutMe();
    
    
  • 从前面代码段中的粗线可以看出,我是而不是直接实例化工厂实例;相反,我使用FactoryProvider静态类来获取工厂实例。(这个类的结构类似于在工厂方法模式中使用具体工厂时的结构。)FactoryProvider根据在GetAnimalFactory(...)方法中传递的参数提供合适的工厂。

类图

图 5-2 显示了类图。

img/463942_2_En_5_Fig2_HTML.jpg

图 5-2

类图

解决方案资源管理器视图

图 5-3 显示了程序的高层结构。

img/463942_2_En_5_Fig3_HTML.jpg

图 5-3

解决方案资源管理器视图

演示 1

这是完整的程序。

using System;

namespace AbstractFactoryPattern
{
    // Abstract Factory
    public interface IAnimalFactory
    {
        IDog GetDog();
        ITiger GetTiger();
    }

    // Abstract Product-1
    public interface ITiger
    {
        void AboutMe();
    }
    // Abstract Product-2
    public interface IDog
    {
        void AboutMe();
    }

    // Concrete product-A1(WildTiger)
    class WildTiger : ITiger
    {
        public void AboutMe()
        {
            Console.WriteLine("Wild tiger says: I prefer hunting in jungles. Halum.");
        }
    }
    // Concrete product-B1(WildDog)
    class WildDog : IDog
    {
        public void AboutMe()
        {
            Console.WriteLine("Wild dog says: I prefer to roam freely in jungles. Bow-Wow.");
        }
    }

    // Concrete product-A2(PetTiger)
    class PetTiger : ITiger
    {
        public void AboutMe()
        {
            Console.WriteLine("Pet tiger says: Halum. I play in an animal circus.");
        }
    }

    // Concrete product-B2(PetDog)
    class PetDog : IDog
    {
        public void AboutMe()
        {
            Console.WriteLine("Pet dog says: Bow-Wow. I prefer to stay at home.");
        }
    }
    // Concrete Factory 1-Wild Animal Factory
    public class WildAnimalFactory : IAnimalFactory
    {

        public ITiger GetTiger()
        {
            return new WildTiger();
        }
        public IDog GetDog()
        {
            return new WildDog();
        }
    }
    // Concrete Factory 2-Pet Animal Factory
    public class PetAnimalFactory : IAnimalFactory
    {
        public IDog GetDog()
        {
            return new PetDog();
        }

        public ITiger GetTiger()
        {
            return new PetTiger();
        }
    }
    // Factory provider
    class FactoryProvider
    {
        public static IAnimalFactory GetAnimalFactory(string factoryType)
        {
            if (factoryType.Contains("wild"))
            {
                // Returning a WildAnimalFactory
                return new WildAnimalFactory();
            }
            else
           if (factoryType.Contains("pet"))
            {
                // Returning a PetAnimalFactory
                return new PetAnimalFactory();
            }
            else
            {
                throw new ArgumentException("You need to pass either wild or pet as argument.");
            }
        }
    }

    // Client
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Abstract Factory Pattern Demo.***\n");            // Making a wild dog and wild tiger through WildAnimalFactory
            IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
            IDog dog = animalFactory.GetDog();
            ITiger tiger = animalFactory.GetTiger();
            dog.AboutMe();
            tiger.AboutMe();

            Console.WriteLine("******************");

            // Making a pet dog and pet tiger through PetAnimalFactory now.
            animalFactory = FactoryProvider.GetAnimalFactory("pet");
            dog = animalFactory.GetDog();
            tiger = animalFactory.GetTiger();
            dog.AboutMe();
            tiger.AboutMe();

            Console.ReadLine();
        }
    }
}

输出

这是输出。

***Abstract Factory Pattern Demo.***

Wild dog says: I prefer to roam freely in jungles. Bow-Wow.
Wild tiger says: I prefer hunting in jungles. Halum.
******************
Pet dog says: Bow-Wow.I prefer to stay at home.
Pet tiger says: Halum.I play in an animal circus.

问答环节

5.1IDog ITiger 接口 都包含同名的方法。例如,两个接口都包含 AboutMe() 方法 。这是强制性的吗?

不,你可以为你的方法使用不同的名字。此外,这些接口中方法的数量可以不同。然而,在第二十四章中,我介绍了简单工厂模式,在第四章中,我介绍了工厂方法模式。在这一章中,我继续例子,这就是为什么我保持同样的方法。

使用这样一个抽象工厂有哪些挑战****?****

**抽象工厂中的任何变化都会迫使您将修改传播到具体工厂。标准设计理念建议你对接口编程,而不是对实现编程。这是开发人员应该始终牢记的关键原则之一。在大多数情况下,开发人员不想改变他们的抽象工厂。

此外,整体架构很复杂,这就是为什么在某些情况下调试非常具有挑战性。

如何区分简单工厂模式 和工厂方法模式或者抽象工厂模式?

我将在第四章的“问答”部分讨论简单工厂模式和工厂方法模式的区别。

让我们修改一下客户端代码如何使用这些工厂,如下图所示。下面是简单工厂模式的代码片段。

IAnimal preferredType = null;
SimpleFactory simpleFactory = new SimpleFactory();
#region The code region that can vary based on users preference
/*
* Since this part may vary, we're moving the
* part to CreateAnimal() in SimpleFactory class.
*/
preferredType = simpleFactory.CreateAnimal();
#endregion

#region The codes that do not change frequently.
preferredType.AboutMe();
#endregion

图 5-4 显示了简单的工厂模式。

img/463942_2_En_5_Fig4_HTML.jpg

图 5-4

简单工厂模式

下面是工厂方法模式的代码片段。

// Creating a Tiger Factory
AnimalFactory tigerFactory = new TigerFactory();
// Creating a tiger using the Factory Method
IAnimal tiger = tigerFactory.CreateAnimal();
tiger.AboutMe();

// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
IAnimal dog = dogFactory.CreateAnimal();
dog.AboutMe();

图 5-5 显示了工厂方法模式。

img/463942_2_En_5_Fig5_HTML.jpg

图 5-5

工厂方法模式

下面是抽象工厂模式的代码片段。

// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

Console.WriteLine("******************");

// Making a pet dog and pet tiger through PetAnimalFactory now.
animalFactory = FactoryProvider.GetAnimalFactory("pet");
dog = animalFactory.GetDog();
tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

图 5-6 显示了抽象工厂模式。

img/463942_2_En_5_Fig6_HTML.jpg

图 5-6

抽象工厂模式

简而言之,使用简单工厂模式,您可以将不同于其他代码的代码分离出来(基本上,您可以将客户端代码解耦)。这种方法有助于您更轻松地管理代码。这种方法的另一个主要优点是客户端不知道对象是如何创建的。因此,它同时促进了安全性和抽象性。

但是,这种方法会违反开闭原则。使用工厂方法模式可以克服这个缺点,工厂方法模式允许子类决定如何完成实例化过程。简而言之,您将对象创建委托给实现工厂方法来创建对象的子类。

抽象工厂基本上是工厂的工厂。它创建了一系列相关的对象,但是它不依赖于具体的类。在这个模式中,您封装了一组具有共同主题的独立工厂。在这个过程中,你不直接实例化一个类;取而代之的是,您得到一个具体的工厂(我为此使用了一个提供者),然后,使用该工厂创建产品。

最后,我试图保持例子简单。工厂方法促进继承,它的子类需要实现工厂方法来创建对象。抽象工厂模式可以通过使用工厂接口中公开的方法创建相关对象来促进对象组合。最后,所有的工厂都通过减少对具体类的依赖来促进松散耦合。**

六、代理模式

本章介绍代理模式。

GoF 定义

为另一个对象提供代理或占位符,以控制对它的访问。

概念

您需要支持这种设计,因为在许多情况下,与原始对象的直接通信并不总是可能的。这是由许多因素造成的,包括安全性和性能问题、资源限制、最终产品处于开发阶段等等。代理可以是不同的类型,但从根本上说,它是原始对象的替代物(或占位符)。因此,当客户端与代理对象交互时,看起来它是在直接与实际对象对话。因此,使用这种模式,您可能希望使用一个可以作为原始类的接口的类。

真实世界的例子

在教室里,当一个学生缺席时,他最好的朋友可能会在点名时试图模仿他的声音,让老师认为他的朋友在那里。除了这个例子,您还可以考虑另一个领域的例子,例如,ATM。ATM 实现可以保存远程服务器上的银行信息的代理对象。

计算机世界的例子

在真实的编程世界中,创建一个复杂对象的多个实例可能成本很高,因为您可能需要不容易获得或分配的资源。在这种情况下,您可以创建多个可以指向原始对象的代理对象。这种机制可以帮助您节省计算机/系统内存并提高应用的性能。

代理的另一个常见用途是当用户不想公开他/她的机器的真实 IP 地址并使其匿名时。

在 WCF 应用中,您可能会注意到 WCF 客户端代理,客户端应用使用它与服务进行通信。您还可以配置一个 REST API 在代理服务器后面工作,以促进授权的通信。

履行

在这个程序中,Subject是一个抽象类,它有一个名为DoSomeWork() .的抽象方法,如下所示。

public abstract class Subject
    {
        public abstract void DoSomeWork();
    }

ConcreteSubject是一个继承自Subject的具体类,完成了DoSomeWork()方法。所以,看起来是这样的。

public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.");
        }
    }

让我们假设您想要限制客户端直接调用ConcreteSubject中的方法。(考虑一下计算机世界例子中讨论的案例,这背后有一些原因。)所以,你做了一个名为Proxy的代理类。在我们的实现中,Proxy类还包含一个名为DoSomeWork()的方法,客户端可以通过一个Proxy实例来使用这个方法。当客户端调用代理对象的DoSomeWork()方法时,这个调用又被传播到ConcreteSubject对象中的DoSomeWork()方法。这让客户感觉好像他们直接调用了来自ConcreteSubject的方法,这就是为什么Proxy类看起来像下面这样。

public class Proxy : Subject
    {
        Subject subject;

        public override void DoSomeWork()
        {
            Console.WriteLine("Welcome, my client.");
            /*
            Lazy initialization:We'll not instantiate until
            the method is called.
            */
            if (subject == null)
            {
                subject = new ConcreteSubject();
            }
            subject.DoSomeWork();
        }
    }

类图

图 6-1 为类图。

img/463942_2_En_6_Fig1_HTML.jpg

图 6-1

类图

解决方案资源管理器视图

图 6-2 显示了程序的高层结构。(注意,您可以将代理类分离到一个不同的文件中,但是由于本例中的各个部分都很小,所以我将所有内容都放在一个文件中。同样的评论也适用于本书中的其他程序。)

img/463942_2_En_6_Fig2_HTML.jpg

图 6-2

解决方案资源管理器视图

演示 1

下面是完整的实现。

using System;

namespace ProxyPatternDemo
{
    /// <summary>
    /// Abstract class Subject
    /// </summary>
    public abstract class Subject
    {
        public abstract void DoSomeWork();
    }
    /// <summary>
    /// ConcreteSubject class
    /// </summary>
    public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.");
        }
    }
    /// <summary>
    /// Proxy class
    /// </summary>
    public class Proxy : Subject
    {
        Subject subject;

        public override void DoSomeWork()
        {
            Console.WriteLine("Welcome, my client.");
            /*
             Lazy initialization:We'll not instantiate the object until the method is called.
            */
            if (subject == null)
            {
                subject = new ConcreteSubject();
            }
            subject.DoSomeWork();
        }
    }
    /// <summary>
    /// Client class
    /// </summary>

    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Proxy Pattern Demo.***\n");
            Subject proxy = new Proxy();
            proxy.DoSomeWork();
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Proxy Pattern Demo.***

Welcome, my client.
I've processed your request.

问答环节

6.1 有哪些不同类型的代理人

这些是代理的常见类型。

  • 远程代理:这些代理可以隐藏位于不同地址空间的对象。

  • 虚拟代理:这些代理执行优化技术,比如按需创建一个重对象。

  • 保护代理:这些代理一般处理不同的访问权限。

  • 智能引用:当客户端访问对象时执行额外的内务处理。典型的操作可以包括计算在某一时刻对对象的引用次数。

6.2 您可以在代理类构造函数中创建 ConcreteSubject 实例 ,如下所示。

/// <summary>
/// Proxy class
/// </summary>
public class Proxy : Subject
        {
        Subject subject;
        public Proxy()
        {
                // Instantiating inside the constructor
                subject = new ConcreteSubject();
        }
        public override void DoSomeWork()
        {
                Console.WriteLine("Proxy call happening now..");
                cs.DoSomeWork();
        }
}

这是正确的吗?

是的,你可以这样做。但是不要忘记代理类可以有不依赖于ConcreteSubject的额外方法。因此,如果您需要来自代理类的这些方法,并且遵循您提出的设计,无论何时您实例化一个代理对象,您也实例化了一个ConcreteSubject类的对象。因此,这最终可能会创建不必要的对象。

6.3 使用这个 惰性实例化过程 ,你可能会在多线程应用中创建不必要的对象。这是正确的吗?

是的。这是一个简单的插图,让您了解实际模式背后的核心思想。在第一章对单例模式的讨论中,我们分析了一些告诉你如何在多线程环境中工作的替代方法。在这种情况下,你可以参考这些讨论。(例如,在这种情况下,您可以实现一个智能代理,以确保在授予对象访问权限之前锁定该对象)。

6.4 能否举一个 远程代理 的例子?

假设您想要调用一个对象的方法,但是该对象正在不同的地址空间中运行(例如,在不同的位置或不同的计算机上)。你是如何进行的?在远程代理的帮助下,您可以调用代理对象上的方法,该方法又将调用转发给远程计算机上运行的实际对象。(如果实际的方法存在于不同的计算机上,并且您通过网络上的代理对象连接到它,那么演示 1 就是这种情况下的一个例子。)这种类型的需求可以通过不同的公知机制来实现,例如 ASP.NET、CORBA、COM/DCOM 或 Java 的 RMI。在 C# 应用中,您可以使用 WCF(.NET Framework 版及更高版本)或。NET web 服务/remoting(主要用于早期版本)。值得注意的是。NET remoting 不被支持。NET Core,微软未来也不打算增加这种支持(见 https://docs.microsoft.com/en-us/dotnet/core/porting/net-framework-tech-unavailable#:~:text=NET%20Remoting%20isn't%20supported 、IO)。

图 6-3 显示了一个简单的远程代理结构。

img/463942_2_En_6_Fig3_HTML.jpg

图 6-3

一个简单的远程代理图

6.5 你什么时候使用 虚拟代理

虚拟代理保护内存不被分配给对象。如果实际的对象创建是一个昂贵的操作,您可以创建一个包含最重要细节的目标对象的简单副本,并将其提供给用户。昂贵的对象只有在真正需要的时候才会被创建。例如,您可以使用这个概念来避免加载不必要的超大图像,以获得更好的应用性能。

6.6 什么时候使用 保护代理

在组织中,安全团队可以实施保护代理来阻止对特定网站的 Internet 访问。

考虑下面的例子,它是前面描述的代理模式实现的修改版本。为了简单起见,让我们假设只有三个注册用户可以使用DoSomeWork()代理方法。如果一个不需要的用户(名为 Robin)试图调用该方法,系统会拒绝他的访问请求。当系统拒绝这种不需要的访问时,创建代理对象就没有意义了。在接下来的例子中,这些注册用户在代理类构造函数中初始化,但是我避免了在其中实例化一个ConcreteSubject对象。它帮助我避免为未授权用户创建不必要的对象。

现在让我们来看一下修改后的实现。

演示 2

下面是修改后的实现。

using System;
using System.Linq; // For Contains() method below

namespace ProxyPatternQAs
{
    /// <summary>
    /// Abstract class Subject
    /// </summary>
    public abstract class Subject
    {
        public abstract void DoSomeWork();
    }
    /// <summary>
    /// ConcreteSubject class
    /// </summary>
    public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.\n");
        }
    }
    /// <summary>
    /// Proxy class
    /// </summary>
    public class Proxy : Subject
    {
        Subject subject;
        string[] registeredUsers;
        string currentUser;
        public Proxy(string currentUser)
        {
            /*
             * Avoiding to instantiate ConcreteSubject
             * inside the Proxy class constructor.
             */
            //subject = new ConcreteSubject();

            // Registered users
            registeredUsers = new string[] { "Admin", "Rohit", "Sam" };
            this.currentUser = currentUser;
        }
        public override void DoSomeWork()
        {
            Console.WriteLine($"{currentUser} wants to access into the system.");
            if (registeredUsers.Contains(currentUser))
            {
                Console.WriteLine($"Welcome, {currentUser}.");
                /* Lazy initialization: We'll not instantiate until the method is called through an authorized user. *.
                if (subject == null)

                {
                    subject = new ConcreteSubject();
                }
                subject.DoSomeWork();
            }
            else
            {
                Console.WriteLine($"Sorry {currentUser}, you do not have access into the system.");
            }
        }
    }
    /// <summary>
    /// Client
    /// </summary>

    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Proxy Pattern Demo2.***\n");
            // Authorized user-Admin
            Subject proxy = new Proxy("Admin");
            proxy.DoSomeWork();
            // Authorized user-Sam
            proxy = new Proxy("Sam");
            proxy.DoSomeWork();
            // Unauthorized User-Robin
            proxy = new Proxy("Robin");
            proxy.DoSomeWork();
            Console.ReadKey();
        }
    }
}

输出

这是修改后的输出。

***Proxy Pattern Demo2.***

Admin wants to access into the system.
Welcome, Admin.
I've processed your request.

Sam wants to access into the system.
Welcome, Sam.
I've processed your request.

Robin wants to access into the system.
Sorry Robin, you do not have access into the system.

看起来代理人就像装饰者一样(见第七章**)。这是正确的吗?**

有时候代理实现和装饰器有一些相似之处,但是你不应该忘记代理的真正意图。装饰者专注于增加职责,而代理者专注于控制对对象的访问。所以,如果你记得它们的用途,在大多数情况下,你可以区分代理和装饰者。

什么时候我应该考虑设计一个代理?

下面是代理可以帮助您的一些重要用例。

  • 您正在为一个仍处于开发阶段或很难重现的场景编写测试用例。例如,当您想要评估应用中只能在客户环境中看到的行为,但您也认识到当应用正在运行时,获得该行为的概率非常低。在这种情况下,您可以在您的代理对象中模拟客户环境行为,并执行您的测试用例来评估该行为的正确性。您不希望您的客户端直接与目标对象对话。

  • 你想隐藏复杂性,增强系统的安全性。

6.9 与代理相关的 缺点有哪些?

使用这种模式时,您应该记住以下一些因素。

  • 总的响应时间可能是一个问题,因为您没有直接与实际的对象对话。

  • 您需要为代理维护额外的层。

  • 代理可以隐藏对象的实际响应,这在某些情况下可能会造成混乱。

七、装饰模式

本章涵盖了装饰模式。

GoF 定义

动态地将附加责任附加到对象上。Decorators 为扩展功能提供了子类化的灵活替代方案。

概念

从 GoF 定义来看,很明显这种模式使用了子类化的替代方法(即继承)。如果不允许继承,怎么进行?是的,你猜对了。它规定你用合成代替继承。

通过遵循 SOLID 原则,这种模式推广了这样一个概念,即类对修改是封闭的,但对扩展是开放的。(如果你想了解更多坚实的原理,去 https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) 。)使用这种模式,您可以在不改变底层类的情况下向特定对象添加特殊功能。

装饰器就像一个包装器(或顶层),包裹着原始对象,并为其添加额外的功能。这就是装饰模式也被称为包装模式的原因。当您动态添加装饰者时,这种模式是最有效的。由于 decorators 通常是动态添加的,所以如果您不希望在开发的后期阶段使用它们,这完全没问题,因为原始对象仍然可以工作。

真实世界的例子

假设你有一栋单层的房子,你决定在上面加建一层。您可能不希望更改底层的架构,但是您可能希望为新添加的楼层采用一种新的设计,以适合现有架构的顶部。

图 7-1 、 7-2 和 7-3 说明了这个概念。

img/463942_2_En_7_Fig3_HTML.jpg

图 7-3

在已有装饰者的基础上再加一个装饰者,并修改房子(现在粉刷房子)

img/463942_2_En_7_Fig2_HTML.jpg

图 7-2

有装修工的原始房屋(附加楼层建在原始结构的顶部)

img/463942_2_En_7_Fig1_HTML.jpg

图 7-1

原始房屋

Note

图 7-3 所示的情况是可选的。您可以使用现有的 decorator 对象来增强行为,或者您可以创建一个新的 decorator 对象并将新的行为添加到其中。在第二步中,你也可以直接油漆原来的房子。一旦添加了新地板,您就不需要开始粉刷了。

计算机世界的例子

假设您想在基于 GUI 的工具包中添加边框属性。您可以使用继承来做到这一点,但这不能被视为最终的解决方案,因为您可能无法从一开始就对所有事情拥有绝对的控制权。因此,这种技术本质上是静态的。

在这种情况下,装饰者可以为你提供一种灵活的方法。他们推广动态选择的概念。例如,您可以将组件包装在另一个对象中(类似于图 7-2 和 7-3 )。封闭对象被称为装饰器,它必须符合它所装饰的组件的接口。它将请求转发给原始组件,并可以在这些请求之前或之后执行附加操作。事实上,这个概念允许您添加无限数量的责任。

履行

在这个例子中,涉及五个玩家:AbstractHome, ConcreteHome, AbstractDecorator, FloorDecoratorPaintDecorator

AbstractHome定义如下。

    abstract class AbstractHome
    {
        public double AdditionalPrice { get; set; }
        public abstract void MakeHome();
    }

AbstractHome的具体实现者必须实现MakeHome()方法。除此之外,您可以通过使用AdditionalPrice属性来设置价格。这就是为什么从AbstractHome,继承的一个叫ConcreteHome的具体类完成了原来的结构,看起来像下面这样(我假设家一旦建好,不需要立即修改;所以,AdditionalPrice初始设置为 0)。

    class ConcreteHome : AbstractHome
    {
        public ConcreteHome()
        {
            AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            Console.WriteLine($"Original House is constructed.Price for this 10000$");
        }
    }

此时,你可以选择在现有的房屋上加建一层,或者你可以粉刷房屋,或者两者兼而有之。于是,FloorDecoratorPaintDecorator both出现了。虽然并不严格要求共享公共代码,但是两个装饰器都继承了AbstractDecorator,它具有以下结构。

abstract class AbstractDecorator : AbstractHome
    {
        protected AbstractHome home;
        public AbstractDecorator(AbstractHome home)
        {
            this.home = home;
            this.AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            home.MakeHome();
        }
    }

请注意,AbstractDecorator保存了对AbstractHome的引用。因此,具体的装饰者(本例中的FloorDecoratorPaintDecorator)正在装饰AbstractHome的一个实例。

现在我们来看一个混凝土装饰工的结构,FloorDecorator,如下。

    // Floor Decorator used to add a floor
    class FloorDecorator : AbstractDecorator
    {
        public FloorDecorator(AbstractHome home) : base(home)
        {

            this.AdditionalPrice = 2500;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Adding a floor on top of original house.
            AddFloor();
        }
        private void AddFloor()
        {
            Console.WriteLine($"-Additional Floor added.Pay additional {AdditionalPrice}$ for it .");
        }
    }

你可以看到FloorDecorator可以加一层楼(使用AddFloor()的方法),使用的时候必须额外支付 2500 美元的额外建设费用。更重要的是,在添加楼层之前,它调用了AbstractHome类的MakeHome()方法,后者又从AbstractHome(即ConcreteHome)的一个具体实现中调用了MakeHome()方法。

类似的行为,但你要为此付出更多。(是的,我假设你正在为你的家使用奢华的油漆。)

类图

图 7-4 显示了类图中最重要的部分。

img/463942_2_En_7_Fig4_HTML.jpg

图 7-4

类图。这里没有显示客户端类。

解决方案资源管理器视图

图 7-5 显示了程序的高层结构。

img/463942_2_En_7_Fig5_HTML.jpg

图 7-5

解决方案资源管理器视图

示范

下面是完整的实现,它测试了两个场景(用#region 标记)。在场景 1 中,我在现有房屋上添加了一层,然后对其进行了粉刷。在场景 2 中,我粉刷了原来的家,然后在现有建筑的顶部添加了两层。

using System;

namespace DecoratorPatternDemo
{
    abstract class AbstractHome
    {
        public double AdditionalPrice { get; set; }
        public abstract void MakeHome();
    }
    class ConcreteHome : AbstractHome
    {
        public ConcreteHome()
        {
            AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            Console.WriteLine($"Original House is constructed.Price for this $10000");
        }
    }
    abstract class AbstractDecorator : AbstractHome
    {
        protected AbstractHome home;
        public AbstractDecorator(AbstractHome home)
        {
            this.home = home;
            this.AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            home.MakeHome();//Delegating task
        }
    }

    // Floor Decorator is used to add a floor

    class FloorDecorator : AbstractDecorator
    {

        public FloorDecorator(AbstractHome home) : base(home)
        {
            //this.home = home;
            this.AdditionalPrice = 2500;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Adding a floor on top of original house.
            AddFloor();
        }
        private void AddFloor()
        {
            Console.WriteLine($"-Additional Floor added.Pay additional ${AdditionalPrice} for it .");
        }
    }

    // Paint Decorator used to paint the home.

    class PaintDecorator : AbstractDecorator
    {

        public PaintDecorator(AbstractHome home):base(home)
        {
            //this.home = home;
            this.AdditionalPrice = 5000;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Painting home.
            PaintHome();
        }
        private void PaintHome()
        {
            Console.WriteLine($"--Painting done.Pay additional ${AdditionalPrice} for it .");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Decorator pattern Demo***\n");

            #region Scenario-1
            Console.WriteLine("\n**Scenario-1:");
            Console.WriteLine("**Building home.Adding floor and then painting it.**");

            AbstractHome home = new ConcreteHome();
            Console.WriteLine("Current bill breakups are as follows:");
            home.MakeHome();

            // Applying a decorator
            // Adding a floor
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();

            // Working on top of the previous decorator.
            // Painting the home
            home = new PaintDecorator(home);
            Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
            home.MakeHome();
            #endregion

            #region Scenario-2
            Console.WriteLine("\n**Scenario-2:");
            Console.WriteLine("**Building home,painting it and then adding two additional floors on top of it.**");
            // Fresh start once again.
            home = new ConcreteHome();
            Console.WriteLine("\nGoing back to original home.Current bill breakups are as follows:");
            home.MakeHome();

            // Applying paint on original home.
            home = new PaintDecorator(home);
            Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
            home.MakeHome();

            // Adding a floor on the painted home.
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();

            // Adding another floor on the current home.
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();
            #endregion

            Console.ReadKey();
        }
    }
}

输出

***Decorator pattern Demo***

**Scenario-1:
**Building home. Adding floor and then painting it.**
Current bill breakups are as follows:
Original House is constructed. Price for this $10000

Floor added. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added.Pay additional $2500 for it.

Paint applied. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added. Pay additional $2500 for it.
--Painting done. Pay additional $5000 for it.

**Scenario-2:
**Building home, painting it and then adding two additional floors on top of it.**

Going back to original home. Current bill breakups are as follows:
Original House is constructed. Price for this $10000

Paint applied. Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done. Pay additional $5000 for it.

Floor added.Current bill breakups are as follows:
Original House is constructed.Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added. Pay additional $2500 for it.

Floor added.Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added.Pay additional $2500 for it.
-Additional Floor added.Pay additional $2500 for it.

问答环节

7.1 你能解释一下合成是如何促进一种继承所不能的动态行为的吗?

当派生类从基类继承时,它只继承基类当时的行为。尽管不同的子类可以以不同的方式扩展基类或父类,但这种类型的绑定在编译时是已知的。所以这个方法是静态的。但是通过使用组合的概念,就像前面的例子一样,您可以获得动态行为。

当你设计一个父类时,你可能没有足够的洞察力去了解你的客户在以后的某个阶段可能需要什么样的额外职责。由于约束是您不能修改现有的代码,在这种情况下,对象组合不仅远远超过继承,而且还确保您不会在旧的架构中引入错误。

最后,在这种情况下,你必须记住一个关键的设计原则,即类应该对扩展开放,但对修改关闭。

使用装潢师的关键 优势 是什么?

以下是一些关键优势。

  • 现有的结构是原封不动的,所以你不能在那里引入错误。

  • 新的功能可以很容易地添加到现有的对象。

  • 你不仅可以在界面上添加行为,还可以改变行为。

  • 您不需要立即预测/实现所有支持的功能(例如,在初始设计阶段)。你可以增量开发。例如,您可以逐个添加装饰器对象来支持您的需求。你必须承认,如果你先创建一个复杂的类,然后想扩展它的功能,这将是一个乏味的过程。

7.3 整体设计模式与 传承 有何不同?

您可以通过简单地附加或分离 decorators 来添加、改变或删除职责。但是使用简单的继承技术,您需要为新的职责创建新的类。所以,你最终可能会得到一个复杂的系统。

再次考虑这个例子。假设你想加一层新地板,粉刷房子,做一些额外的工作。为了满足这一需求,您可以从FloorDecorator开始,因为它已经提供了添加地板的支持,然后使用PaintDecorator来粉刷房子。然后,您需要添加一个简单的包装器来完成这些额外的职责。

但是如果你从继承开始,然后你可能有多个子类;比如一个加层,一个粉刷房子,如图 7-6 (一个层次继承)。

img/463942_2_En_7_Fig6_HTML.jpg

图 7-6

等级继承

因此,如果你需要一个带有额外功能的额外油漆地板,你可能需要最终得到如图 7-7 所示的设计。

img/463942_2_En_7_Fig7_HTML.jpg

图 7-7

一个类(额外特性)需要从多个基类继承

现在你感受到了“钻石效应”的热度,因为在许多编程语言中,包括 C#,多个基类是不允许的。

您还会发现,与装饰模式相比,继承机制不仅更具挑战性和耗时,而且可能会在应用中产生重复代码。最后,不要忘记继承只促进编译时绑定(不是动态绑定)。

7.4 你为什么要创建一个职责单一的类?你可以创建一个子类,简单地添加一个地板,然后进行绘画。在这种情况下,您可能会得到更少的子类。这是正确的吗?

如果你熟悉固体原理,你就知道有一个原理叫单责。这个原则背后的思想是,每个类应该对软件中提供的功能的一个部分负责。当您使用单一责任原则时,装饰模式是有效的,因为您可以简单地动态添加或删除责任。

7.5 与此模式相关的 缺点 有哪些?

我相信如果你小心的话,没有显著的缺点。但是如果你在系统中创建了太多的装饰器,那么维护和调试将会很困难。所以,在这种情况下,他们会制造不必要的混乱。

7.6 例子中的 AbstractDecorator 类 是抽象的,但是里面没有抽象方法。这怎么可能?

在 C# 中,一个类可以是抽象的,而不包含抽象方法,但反之则不然。换句话说,如果一个类包含至少一个抽象方法,就意味着这个类是不完整的,你被迫用abstract关键字来标记它。

同样,如果你阅读了图 7-8 中的注释,你正在将任务委托给一个具体的装饰者,在这种情况下,因为你只想使用和实例化具体的装饰者。

img/463942_2_En_7_Fig8_HTML.jpg

图 7-8

抽象类:AbstractDecorator

因此,在这个例子中,您不能简单地实例化一个AbstractDecorator实例,因为它用abstract关键字标记。

下面一行创建了一个编译错误。

AbstractDecorator abstractDecorator = new AbstractDecorator();
saying “CS0144    Cannot create an instance of the abstract class or interface 'AbstractDecorator'”

7.7 装饰器是否只用于 动态绑定

不。您可以将这个概念用于静态和动态绑定。但是动态绑定是它的强项,所以我在这里集中讨论这一点。GoF 定义也只关注动态绑定。

Note

中的 I/O 流实现。NET 框架,。NET 核心,Java 使用装饰模式。例如,BufferedStream类继承自Stream类。注意这个类中存在两个重载的构造函数;它们每个都以一个Stream(父类)作为参数(就像演示 1 一样)。当您看到这种结构时,您可能会看到装饰模式的一个例子。BufferedStream在. NET 中表现得像个装潢师。

八、适配器模式

本章介绍适配器模式。

GoF 定义

将一个类的接口转换成另一个客户期望的接口。适配器允许类一起工作,否则由于不兼容的接口而不能。

概念

从 GoF 定义中,您可以猜测这种模式处理至少两个不兼容的继承层次结构。在特定领域的系统中,客户习惯于如何调用软件中的方法。这些方法可以遵循继承层次结构。现在假设您需要升级系统,并且需要实现一个新的继承层次结构。当你这样做的时候,你不想强迫你的客户学习访问软件的新方法。那么,你能做什么?解决方案很简单:编写一个适配器,接受客户机请求,并以新层次结构中的方法可以理解的形式翻译这些请求。因此,客户可以享受更新的软件,没有任何麻烦。

下面的例子也可以帮助你更好地理解这些模式。

真实世界的例子

这种模式的一个常见应用是在国际旅行中使用电源插座适配器/交流电源适配器。这些适配器可以充当中间人,以便电子设备(如接受美国电源的笔记本电脑)可以插入欧洲电源插座。

考虑另一个例子。假设你需要给手机充电。但是你看到电源插座和你的充电器不兼容。在这种情况下,您可能需要使用适配器。即使是一个将一种语言转换成另一种语言的译者在现实生活中也遵循这种模式。

让我们考虑这样一种情况,您有两个不同的形状(例如,形状 1 和形状 2),它们都不是矩形,它们看起来像图 8-1 。

img/463942_2_En_8_Fig1_HTML.jpg

图 8-1

在使用适配器之前

让我们进一步假设,将这两种不同的形状结合起来,你需要形成一个矩形。你是如何进行的?一个简单的解决方法是再带一个有界的 X 形图形(填充颜色),如图 8-2 所示。

img/463942_2_En_8_Fig2_HTML.jpg

图 8-2

电源适配器

然后贴上三个形状,如图 8-3 所示。

img/463942_2_En_8_Fig3_HTML.jpg

图 8-3

使用适配器后

在编程中,你可以把 Shape1 和 Shape2 想象成两个不同的接口,除非你用这个 X 形的图形把它们组合起来形成一个矩形,否则它们是不能一起工作的。在这个场景中,X 形图形扮演着适配器的角色。

计算机世界的例子

假设您有一个应用,可以大致分为两部分:用户界面(UI 或前端)和数据库(后端)。通过用户界面,客户端可以传递一些特定类型的数据或对象。您的数据库与那些对象兼容,可以顺利地存储它们。随着时间的推移,你可能会意识到你需要升级你的软件来让你的客户满意。因此,您可能希望允许一些其他类型的对象也通过 UI。但是在这种情况下,第一个问题来自您的数据库,因为它不能存储这些新类型的对象。在这种情况下,可以使用适配器将这些新对象转换成现有数据库可以接受和存储的兼容形式。

履行

在接下来的例子中,有两个层次结构:一个用于Rectangle,一个用于TriangleIRectangle接口有两个方法叫做CalculateArea()AboutMe()Rectangle类实现了IRectangle接口,并形成了第一个层次,如下所示。

class Rectangle : IRectangle
    {
        double length;
        public double width;
        public Rectangle(double length, double width)
        {
            this.length = length;
            this.width = width;
        }

        public double CalculateArea()
        {
            return length * width;
        }

        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

ITriangle接口有两个方法:CalculateAreaOfTriangle()AboutTriangle()Triangle类实现了ITriangle接口并形成了另一个层次结构,如下所示。

class Triangle : ITriangle
    {
        double baseLength; // base
        double height; // height
        public Triangle(double length, double height)
        {
            this.baseLength = length;
            this.height = height;
        }
        public double CalculateAreaOfTriangle()
        {
            return 0.5 * baseLength * height;
        }
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle.");
        }
    }

这两个层次很容易理解。现在,让我们来看一个问题,在这个问题中,您需要使用矩形层次结构来计算三角形的面积。

你是如何进行的?您可以使用适配器来解决这个问题,如下例所示。

/*
 * RectangleAdapter is implementing IRectangle.
 * So, it needs to implement all the methods
 * defined in the target interface.
 */
class RectangleAdapter : IRectangle
{
        ITriangle triangle;
        public RectangleAdapter(ITriangle triangle)
        {
                this.triangle = triangle;
        }

        public void AboutMe()
        {
                triangle.AboutTriangle();
        }

        public double CalculateArea()
        {
                return triangle.CalculateAreaOfTriangle();
        }
}

注意使用适配器的好处。您没有对任何层次结构进行任何更改,从高层次上看,似乎通过使用IRectangle方法,您可以计算一个三角形的面积。这是因为您在高层使用了IRectangle接口的AboutMe()CalculateArea()方法,但是在这些方法内部,您调用了ITriangle方法。

除了这个优点,您还可以扩展使用适配器的好处。例如,假设您需要在一个应用中有大量的矩形,但是对您创建的矩形的数量有一个限制。(为了简单起见,让我们假设在一个应用中,您最多可以创建五个矩形和十个三角形,但是当应用运行时,在某些情况下,您可能需要提供十个矩形。)在这些情况下,使用这种模式,您可以使用一些行为类似矩形对象的三角形对象。怎么会?嗯,当使用适配器时,你调用的是CalculateArea(),,但它调用的是CalculateAreaOfTriangle() .,所以你可以根据需要修改方法体。例如,在您的应用中,假设每个矩形对象的长度为 20 个单位,宽度为 10 个单位,而每个三角形对象的底边为 20 个单位,高度为 10 个单位。因此,每个矩形对象的面积为 2010=200 平方单位,每个三角形对象的面积为 0.520*10=100 平方单位。因此,您可以简单地将每个三角形面积乘以 2,以获得一个等效的矩形面积,并在需要矩形面积的地方替换(或使用)它。我希望这对你有意义。

最后,您需要记住,当您处理不完全相同但非常相似的对象时,这种技术最适合。

Note

在前一点的上下文中,您不应该尝试将圆形区域转换为矩形区域(或进行类似类型的转换),因为它们是不同的形状。在这个例子中,我谈论三角形和矩形是因为它们有相似之处。

类图

图 8-4 显示了程序重要部分的类图。

img/463942_2_En_8_Fig4_HTML.jpg

图 8-4

类图。这里没有显示客户端类。

解决方案资源管理器视图

图 8-5 显示了程序的高层结构。

img/463942_2_En_8_Fig5_HTML.jpg

图 8-5

解决方案资源管理器视图

演示 1

下面是实现。

using System;

namespace AdapterPatternDemonstration
{
    interface IRectangle
    {
        void AboutMe();
        double CalculateArea();
    }
    class Rectangle : IRectangle
    {
        double length;
        public double width;
        public Rectangle(double length, double width)
        {
            this.length = length;
            this.width = width;
        }

        public double CalculateArea()
        {
            return length * width;
        }

        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

    interface ITriangle
    {
        void AboutTriangle();
        double CalculateAreaOfTriangle();
    }
    class Triangle : ITriangle
    {
        double baseLength; // base
        double height; // height
        public Triangle(double length, double height)
        {
            this.baseLength = length;
            this.height = height;
        }
        public double CalculateAreaOfTriangle()
        {
            return 0.5 * baseLength * height;
        }
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle.");
        }
    }

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : IRectangle
    {
        ITriangle triangle;
        public RectangleAdapter(ITriangle triangle)
        {
            this.triangle = triangle;
        }

        public void AboutMe()
        {
            triangle.AboutTriangle();
        }

        public double CalculateArea()
        {
            return triangle.CalculateAreaOfTriangle();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Adapter Pattern  Demo***\n");
            IRectangle rectangle = new Rectangle(20, 10);
            Console.WriteLine("For initial verification purposes, printing the areas of both shapes.");
            Console.WriteLine("Rectangle area is:{0} Square unit", rectangle.CalculateArea());
            ITriangle triangle = new Triangle(20, 10);
            Console.WriteLine("Triangle area is:{0} Square unit", triangle.CalculateAreaOfTriangle());

            Console.WriteLine("\nNow using the adapter.");
            IRectangle adapter = new RectangleAdapter(triangle);
            Console.Write("True fact : ");
            adapter.AboutMe();
            Console.WriteLine($" and my area is : {adapter.CalculateArea()} square unit.");

            // Alternative way:
            Console.WriteLine("\nUsing the adapter in a different way now.");
            // Passing a Triangle instead of a Rectangle
            Console.WriteLine($"Area of the triangle using the adapter is :{GetDetails(adapter)} square unit.");
            Console.ReadKey();
        }
        /*
         * The following method does not know
         * that through the adapter, it can
         * actually process a
         * Triangle instead of a Rectangle.
         */
        static double GetDetails(IRectangle rectangle)
        {
            rectangle.AboutMe();
            return rectangle.CalculateArea();
        }
    }
}

输出

这是输出。

***Adapter Pattern  Demo***

For initial verification purposes, printing the areas of both shapes.
Rectangle area is:200 Square unit
Triangle area is:100 Square unit

Now using the adapter.
True fact : Actually, I am a Triangle.
 and my area is : 100 square unit.

Using the adapter in a different way now.
Actually, I am a Triangle.
Area of the triangle using the adapter is :100 square unit.

分析

注意下面的代码段,在Main()方法中有注释,如下所示。

/*
 * The following method does not know
 * that through the adapter, it can
 * actually process a
 * Triangle instead of a Rectangle.
 */
static double GetDetails(IRectangle rectangle)
{
        rectangle.AboutMe();
        return rectangle.CalculateArea();
}

此部分是可选的。我保留它是为了向您展示在哪里可以在一次调用中调用这两个 adaptee 方法。

适配器的类型

GoF 描述了两种适配器:类适配器和对象适配器。

对象适配器

对象适配器通过对象组合进行适配,如图 8-6 所示。因此,到目前为止讨论的适配器是对象适配器的一个例子。

img/463942_2_En_8_Fig6_HTML.jpg

图 8-6

对象适配器

在我们的例子中,RectangleAdapter是实现IRectangle (Target interface). ITriangle的适配器,它是Adaptee接口。适配器保存被适配器实例。

类别适配器

类适配器通过子类化来适应,并支持多重继承。但是你知道在 C# 中,不支持通过类的多重继承。(你需要接口来实现多重继承的概念。)

图 8-7 显示了支持多重继承的类适配器的典型类图。

img/463942_2_En_8_Fig7_HTML.jpg

图 8-7

类别适配器

问答环节

8.1 如何在 C# 中实现一个 类适配器设计模式

您可以子类化现有的类并实现所需的接口。演示 2 向您展示了一个完整的输出示例。

演示 2

这个演示展示了一个类适配器。为了使这个例子简单明了,我只用一种方法制作了IRectangleITriangle接口。IRectangle只有AboutMe()方法, and the Rectangle类实现了IRectangle接口,这样就形成了下面的层次结构。

interface IRectangle
    {
        void AboutMe();
    }
    class Rectangle : IRectangle
    {
        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

ITriangle有了AboutTriangle()方法.``Triangle类实现了这个接口,下面的层次结构就形成了。

interface ITriangle
    {
        void AboutTriangle();
    }
    class Triangle : ITriangle
    {
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle");
        }
    }

现在是我们的类适配器,它使用了多重继承的概念,使用了一个具体的类和一个接口。附加的注释有助于您更好地理解代码。

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : Triangle, IRectangle
    {
        public void AboutMe()
        {
            // Invoking the adaptee method
            AboutTriangle();
        }
    }

现在您可以进行完整的演示,如下所示。

using System;

namespace AdapterPatternAlternativeImplementationDemo
{
    interface IRectangle
    {
        void AboutMe();
    }
    class Rectangle : IRectangle
    {
        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

    interface ITriangle
    {
        void AboutTriangle();
    }
    class Triangle : ITriangle
    {
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle");
        }
    }

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : Triangle, IRectangle
    {
        public void AboutMe()
        {
            // Invoking the adaptee method
            AboutTriangle();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Adapter Pattern Alternative Implementation Technique Demo.***\n");
            IRectangle rectangle = new Rectangle();
            Console.WriteLine("For initial verification purposes, printing the details from of both shapes.");
            Console.WriteLine("The rectangle.AboutMe() says:");
            rectangle.AboutMe();
            ITriangle triangle = new Triangle();
            Console.WriteLine("The triangle.AboutTriangle() says:");
            triangle.AboutTriangle();

            Console.WriteLine("\nNow using the adapter.");
            IRectangle adapter = new RectangleAdapter();
            Console.Write("True fact : ");
            adapter.AboutMe();
        }
    }
}

输出

这是输出。

***Adapter Pattern Alternative Implementation Technique Demo.***

For initial verification purposes, printing the details from of both shapes.
The rectangle.AboutTriangle() says:
Actually, I am a Rectangle.
The triangle.AboutTriangle() says:
Actually, I am a Triangle.

Now using the adapter.
True fact : Actually, I am a Triangle.

分析

这种方法可能不适用于所有情况。例如,您可能需要采用 C# 接口中没有指定的方法。在这些情况下,对象适配器更好。

问答环节

**8.2 你更喜欢哪个——**类适配器 还是 对象适配器

在大多数情况下,我更喜欢组合而不是继承。对象适配器使用组合,更加灵活。在许多情况下,当您需要从 adaptee 接口修改特定方法时,实现真正的类适配器是一项挑战,但是在目标接口中没有与之非常匹配的方法。除此之外,如果 adaptee 类(在我们的例子中是 Triangle)是密封的,那么您不能从它继承。

8.3 你说过,“……当你需要从一个被适配者接口适配一个特定的方法时,实现一个真正的类适配器是具有挑战性的,但是在目标接口中没有与之非常匹配的方法。”你能详细说明一下吗?

在我的例子中,目标接口方法和适配器接口方法是相似的。比如在IRectangle里有AboutMe()法,在ITriangle里有AboutTriangle()法。他们是做什么的?他们指出它是矩形还是三角形。

现在假设在IRectangle,中没有这个叫做AboutMe()的方法,但是在ITriangle中仍然存在AboutTriangle()。所以,在这种情况下,如果你需要采用AboutTriangle()方法,你需要分析如何进行。在我们的例子中,AboutTriangle()是一个简单的方法,但是在现实世界的编程中,这个方法要复杂得多,并且可能存在与之相关的依赖关系。因此,当您没有相应的目标方法时,您可能会发现从一个被适应者那里适应该方法是一个挑战。

我明白客户不应该知道他们正在使用适配器。这是正确的吗?

没错。我做这个实现是为了向您展示,客户机不需要知道它们的请求通过适配器被转换到被适配器。如果您希望它们显示任何消息,您只需在演示 2 中的适配器中添加一个控制台消息,如下所示。

class RectangleAdapter : Triangle, IRectangle
{
        public void AboutMe()
        {
                // Invoking the adaptee method
                // For Q&A
                Console.WriteLine("You are using an adapter now.");
                AboutTriangle();
        }
}

8.5 如果 目标接口 adaptee 接口方法 签名不同会怎样?

一点问题都没有。如果一个适配器方法有几个参数,您可以用一些额外的伪参数调用 adaptee 方法。在构建器模式中(第三章中的演示 2),您看到了可选参数。您可以在这里使用相同的概念。

在相反的情况下(如果适配器方法比 adaptee 方法有更多的参数),通过使用这些额外的参数,您可以在将调用转移到 adaptee 方法之前添加功能。

最后,如果方法参数不兼容,您可能需要进行转换(如果可能的话)。

8.6 与此模式相关的 弊端 有哪些?

我看不出有什么重大挑战。我认为适配器的工作简单明了,但是您需要编写一些额外的代码。然而,回报是巨大的,特别是对于那些不能改变的遗留系统,但是您仍然希望使用它们的稳定性和简单性。