北川广海の梦

北川广海の梦

设计模式:工厂

2021-10-31

简单工厂模式

简单工厂模式可以有效的封装根据输入参数创建对应对象的行为。

通过简单工厂模式,可以使需要这个对象的地方,无需关心这个对象的创建细节,将创建过程完全委托于工厂,自己仅需要接收工厂产生的对象,并拿着使用即可。

这样可以有效的避免重复写出大量的冗余代码。试想项目中存在大量的依赖某个对象,此对象的创建过程较为麻烦。通过暴力创建的方式,万一哪天对象的创建细节发生变化,修改的地方会发生几十处,简直是一场灾难。

下面用一些代码例子实现一个简单工厂模式。

class Program
    {
        static void Main(string[] args)
        {
            SimpleFactory.CreateProduct(nameof(Phone));
        }
    }

    static class SimpleFactory
    {
        public static IProduct CreateProduct(string t)
        {
            return t switch
            {
                nameof(Phone) => new Phone(),
                nameof(Computer) => new Computer(),
                _ => throw new ArgumentException()
            };
        }
    }

    #region Products

    interface IProduct
    {
        void Use();
    }

    class Phone : IProduct
    {
        public void Use()
        {
        }
    }

    class Computer : IProduct
    {
        public void Use()
        {
        }
    }

    #endregion

通过静态工厂方法,SimpleFactory.CreateProduct作为访问产品的唯一入口,可以很好的封装产品的创建细节。调用者只需要告诉工厂,自己需要什么样的对象就可以了。

缺点
简单工厂模式违背了开闭原则。如果我们需要加入更多类型的产品,就需要修改SimpleFactory类的CreateProduct方法。

工厂模式

工厂模式的提出,有效的解决了简单工厂模式违背了开闭原则的问题。

工厂模式通过抽象出一个工厂的接口,解除了产品与工厂的耦合。不同的工厂只需要实现这个接口,就能生产对应的产品。

下面通过代码简单实现:

class Program
    {
        static void Main(string[] args)
        {
            IFactory phoneFactory = new PhoneFactory();
            var phone = phoneFactory.CreateProduct();
            IFactory computerFactory = new ComputerFactory();
            var computer = computerFactory.CreateProduct();
        }
    }

    interface IFactory
    {
        IProduct CreateProduct();
    }

    class PhoneFactory : IFactory
    {
        public IProduct CreateProduct() => new Phone();
    }

    class ComputerFactory : IFactory
    {
        public IProduct CreateProduct() => new Computer();
    }

    #region Products

    interface IProduct
    {
        void Use();
    }

    class Phone : IProduct
    {
        public void Use()
        {
        }
    }

    class Computer : IProduct
    {
        public void Use()
        {
        }
    }

    #endregion

这样在需要增加新的产品的时候,我们只需要一个新的实现了工厂接口的工厂类。然后用它就能继续完成新的产品的生产。

但是工厂模式也带来了新的问题,客户端必须知道自己产品对应的工厂,才能拿到想要的对象,就好比我要买一个手机,还得必须知道是成都生产的还是广州生产的,才能拿到手机。

改进
使用反射同时解决扩展和屏蔽具体工厂的问题。

 class Program
    {
        static void Main(string[] args)
        {
            var phone = ProductFactory.CreateProduct(nameof(Phone));

            var computer = ProductFactory.CreateProduct(nameof(Computer));
        }
    }

    static class ProductFactory
    {
        private static readonly Dictionary<string, ConstructorInfo> FactoryDict;

        static ProductFactory()
        {
            var types = typeof(ProductFactory).Assembly.GetTypes().Where(t =>
                t.BaseType == typeof(IProduct) && t.IsDefined(typeof(ProductAttribute), false)).ToList();

            FactoryDict = types
                .ToDictionary(t => t.GetCustomAttribute<ProductAttribute>()?.Name ?? "",
                    p => p.GetConstructors().FirstOrDefault(t => t.GetParameters().Length == 0));
        }

        public static IProduct CreateProduct(string name)
        {
            if (FactoryDict.TryGetValue(name, out var constructor))
            {
                constructor.Invoke(new object[0]);
            }

            throw new ArgumentException();
        }
    }

    #region Products

    interface IProduct
    {
        void Use();
    }

    [Product(Name = nameof(Phone))]
    class Phone : IProduct
    {
        public void Use()
        {
        }
    }

    [Product(Name = nameof(Computer))]
    class Computer : IProduct
    {
        public void Use()
        {
        }
    }

    #endregion

    class ProductAttribute : Attribute
    {
        public string Name { get; set; }
    }

以上代码通过注解加反射,在静态构造函数执行时,就遍历了程序集下的所有产品,并且根据产品的注解上的name,将产品的name和其构造函数组合成了一个字典,客户端在调用工厂的CreateProduct的时候,传入一个名称,工厂就能以此查询字典,找到合适的产品构建方法。字典中不仅可以存储构造函数,也可以保存其他能够产生产品的方法。

我们通过反射Attribute,来确定需要哪一个产品。这里虽然遍历的是产品类,但是我们拿到的却是构造函数,这构造函数不就可以看作一个工厂的创建方法吗?对于需要扩展的时候,我们只需再写一个新的产品类,并且给它一个合适的attribute即可。在程序运行时,我们就能很方便的拿到产品。

抽象工厂模式

现在我们已经能够创建各种各样的产品,他们分别是电脑和手机,但是现在他们的分类更加细化,分成了苹果的电脑和手机,华为的电脑和手机。一般来说,同一品牌的两款设备才能有最佳的用户体验。那么我们的工厂应该如何改进才能适应呢?

 public interface IFactory
    {
        IPhone CreatePhone();
        IComputer CreateComputer();
    }

    public class HuaweiFactory:IFactory
    {
        public IPhone CreatePhone()
        {
            return new HuaweiPhone();
        }

        public IComputer CreateComputer()
        {
            return new HuaweiComputer();
        }
    }

    public class AppleFactory : IFactory
    {
        public IPhone CreatePhone()
        {
            return new ApplePhone();
        }

        public IComputer CreateComputer()
        {
            return new AppleComputer();
        }
    }
    
    public interface IPhone
    {
    }

    public interface IComputer
    {
    }

    public class HuaweiPhone : IPhone
    {
    }

    public class ApplePhone : IPhone
    {
    }

    public class HuaweiComputer : IComputer
    {
    }

    public class AppleComputer : IComputer
    {
    }

以上代码实现了华为和苹果的两种设备的工厂创建。
可以看到,客户端无需区分手动去拿到合适的品牌的产品,他们只需每次使用相同的工厂来创建对象,就能使产品的“风格”保持一致。

开闭原则

当我们需要增加更多品牌的时候,例如下次来了一个小米的手机和电脑,我们无需改动任何旧代码。只需实现一个新的小米工厂即可。

但是如果我们需要增加更多的产品类的时候,就不得不去修改其他所有的工厂。这就违反了开闭原则

总结

在设计初期,我们往往会通过工厂模式,去构建不同的产品的创建,这个时候,我们应对的变化往往是产品本身的修改,例如产品的行为,产品的创建规则,亦或者是需要引入其他的产品。这个时候工厂模式能够较好的满足扩展性。

而在后期,各个产品之间逐渐产生关联,而此时我们应对的主要变化,也从产品本身变成了产品间的关联关系。此时我们可以引入抽象工厂方法,针对产品的关联关系变化,抽象工厂能够很好的进行处理,且符合开闭原则。