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

深入剖析:策略设计模式、依赖注入(DI)、开闭原则(OCP)和松耦合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (15投票s)

2015年3月22日

CPOL

7分钟阅读

viewsIcon

33622

downloadIcon

328

深入剖析:策略设计模式、依赖注入(DI)、开闭原则(OCP)和紧耦合与松耦合

引言

我们大多数参与核心架构设计的人都曾在网上查找和理解核心设计原则。我最近一直在阅读各种关于讨论各种原则、良好可扩展架构模式的在线论文,以解决各种设计挑战。大多数论文都写得很好,并附有阐述各种原则的示例,但要真正理解其深度需要一些时间。我理解某些概念时遇到的挑战,并非在于“如何实现?”,而在于“为什么这样的实现更好?”。这就是迫使我写下这篇文章的原因之一。

为什么我们要再次讨论?

正如本文标题所示,我们将讨论策略设计模式依赖注入开闭原则松耦合。但我的目的不仅仅是向您展示如何实现,而是解释为什么这样的实现是有意义的。而最好的方法是

  • 采用一个现实生活中的场景
  • 编写一个程序来实现并覆盖现实生活中的场景
  • 作为 SDLC 的因果关系,我们将通过扩展一些功能来引入一个变化,然后看看我们在初始阶段所做的实现是否在 OCP(开闭原则)方面表现良好。
  • 然后我们将修改设计并检查可扩展性
  • 并做出最终的结论。

重要提示:这里的想法是让概念正确,而不是让所谓的“现实生活场景”真正存在或不存在。此外,在整个讨论中,某些实现可能不符合其他原则,因为我不想通过引入不适用于此讨论的内容来使实际讨论过于复杂。为了更好地理解,我还鼓励您下载可工作的解决方案以更好地理解代码。

问题陈述

根据陈述

“一家知名企业集团计划在其某个组织中晋升员工。在该组织中,员工的 ID、姓名、自上次晋升以来的年限、技能证书数量和目标达成百分比都已预先填充。管理层已要求人力资源部根据自上次晋升以来的年限大于 4 的员工列出名单。”

我们已设法使场景尽可能简单。

实现 1

当 IT 部门收到请求时,我们构建了一个小型程序来列出项目。我们来看一下。

在简单的实现中,我们有一个 `Employee` 类,一个 `HumanResource` 类负责保存 `Employee` 列表,并且有一个名为 `GetPromotionEligibleEmployees()` 的方法,该方法将内部创建一个 `BasedOnYearsSinceLastPromotion` 类的对象来识别有资格晋升的员工。我们来看一下实际代码。

    /// <summary>
    /// Employee Class
    /// </summary>
    public class Employee
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public int YearsSinceLastPromotion { get; set; }
        public int NoOfSkillCertificate { get; set; }
        public int PercentageOfGoalAchieved { get; set; }
    }

    /// <summary>
    /// Human Resource Class
    /// </summary>
    public class HumanResource
    {
        private List<Employee> _employees;
        private BasedOnYearsSinceLastPromotion _promotionStategy;

        public HumanResource()
        {
            _employees = new List<Employee>();
            _promotionStategy = new BasedOnYearsSinceLastPromotion();
        }

        public void AddEmployee(Employee employee)
        {
            _employees.Add(employee);
        }

        /// <summary>
        /// Uses Promotion Strategy classes to find out list of Employee Eligible
        /// </summary>
        /// <returns>List of Eligible Employee</returns>
        public List<Employee> GetPromotionEligibleEmployees()
        {
            return _promotionStategy.IdentifyPromotionEligibleEmployees(_employees);
        }
    }

我们的晋升策略类

    class BasedOnYearsSinceLastPromotion
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

同时,`Main()` 程序

        /// <summary>
        /// Client Program
        /// </summary>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();

            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved = 75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client default strategy BasedOnYearsSinceLastPromotion
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
            Print(employeeYOS, "Years Since Last Promotion");

            Console.ReadLine();
        }

以及用于显示员工的实用函数 `Print()`

        /// <summary>
        /// Utility function to Print Employees
        /// </summary>
        /// <param name="employees">List of Employees</param>
        /// <param name="criterion">Criterion</param>
        public static void Print(List<Employee> employees, string criterion)
        {
            Console.WriteLine(" Based On '{0}'", criterion);
            Console.WriteLine(" ID\tName");
            foreach (Employee e in employees)
            {
                Console.WriteLine(" {0}\t{1}", e.ID, e.Name);
            }
            Console.WriteLine();
        }

 

最后我们运行代码。Bingo!

我们获得了输出,代码运行良好。我们通过了审批流程,在没有太多障碍的情况下将代码推向了生产环境。

与此同时,在一次高层管理会议上,

“我们决定,我们仍然希望根据自上次晋升以来的年限来识别员工,但我们可能会重新考虑基于他们拥有的技能证书数量来晋升员工的决定,因为这将有助于提升我们公司的市场形象,我们希望鼓励这一点。”

简而言之,IT 部门(我们)必须修改程序,以支持当前策略(即基于自上次晋升以来的年限),同时也开放接受新策略(即基于技能证书数量)。因此,我们将修改我们的程序。其结果是实现 2。

实现 2

我们仍然拥有实现管理层决定在新程序中实现的新功能所需的所有属性(特别是 `NoOfSkillCertificate`)。因此,我们不需要更改 `Employee` 类。

正如您所见,我们引入了两个新项目。

  1. 新的 `BasedOnNoOfSkillCertificate` 策略类,它将使用 `Employee` 对象的 `NoOfSkillCertificate` 属性来确定员工的晋升资格。
  2. 枚举,它将帮助客户端函数在不同的晋升策略之间进行选择,例如 `BasedOnYearsSinceLastPromotion` 或 `BasedOnNoOfSkillCertificate`。
    public enum PromotionStrategy
    {
        BasedOnYearSinceLastPromotion = 0,
        BasedOnNoOfSkillCertificate = 1
    }

    public class BasedOnYearsSinceLastPromotion
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    public class BasedOnNoOfSkillCertificate
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

我们正在修改 2 个现有项目:`HumanResource` 类和 `Main()` 函数,以支持基于晋升策略枚举的新策略。

    /// <summary>
    /// Human Resource Class
    /// </summary>
    public class HumanResource
    {
        private List<Employee> _employees;
        private BasedOnYearsSinceLastPromotion _promotionStategyOne;
        private BasedOnNoOfSkillCertificate _promotionStategyTwo;

        public HumanResource()
        {
            _employees = new List<Employee>();
            _promotionStategyOne = new BasedOnYearsSinceLastPromotion();
            _promotionStategyTwo = new BasedOnNoOfSkillCertificate();
        }

        public void AddEmployee(Employee employee)
        {
            _employees.Add(employee);
        }

        /// <summary>
        /// Uses Promotion Strategy classes to find out list of Employee Eligible
        /// </summary>
        /// <returns>List of Eligible Employee</returns>
        public List<Employee> GetPromotionEligibleEmployees(PromotionStrategy strategy)
        {
            if (strategy == PromotionStrategy.BasedOnYearSinceLastPromotion)
                return _promotionStategyOne.IdentifyPromotionEligibleEmployees(_employees);
            else if (strategy == PromotionStrategy.BasedOnNoOfSkillCertificate)
                return _promotionStategyTwo.IdentifyPromotionEligibleEmployees(_employees);
            else
                throw new ApplicationException("Unknown Strategy!");
        }
    }

        /// <summary>
        /// Client Program
        /// </summary>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();


            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved = 75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client default strategy BasedOnYearsSinceLastPromotion
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees(PromotionStrategy.BasedOnYearSinceLastPromotion);
            Print(employeeYOS, "Years Since Last Promotion");

            //Client default strategy BasedOnNoOfSkillCertificate
            List<Employee> employeeNOS = hr.GetPromotionEligibleEmployees(PromotionStrategy.BasedOnNoOfSkillCertificate);
            Print(employeeNOS, "Based on No of Skill Certificate");

            Console.ReadLine();
        }

通过这些更改(2 个新增项和 2 个修改项),我们运行程序,代码运行正常。所以输出是

我们创建的代码可以编译、执行并且产生相对不错的输出。但对于我们的讨论来说,仅输出不是我们的意图。如前所述,我们希望重新评估我们程序的扩展性。在实现 2 中,存在几个问题。

真的吗?实现 2 有什么问题?

  • 为了实现更改,我们添加了新的策略类,并从客户端程序调用了策略,这完全没问题。但为什么我们需要更改 `HumanResource` 类?更改 `HumanResource` 类的原因是它与策略类之间存在紧耦合。也就是说,它利用了策略的具体实现。
  • 其次,如果我们将来想添加新的策略,我们将再次修改 `HumanResource` 类,就像我们在第一点中讨论的那样,但这会导致 `HumanResource` 类不必要地扩展,而没有任何新功能的添加。请注意,无论我们将多少晋升策略插入 `HumanResource` 类,功能仍然是单一的。那就是确定晋升资格。所以理想情况下,该类不应该增长。但以我们目前的实现,它会随着每个新策略而增长。

我们希望用一些核心软件开发原则来解决实现 2 中的问题。但在此之前,让我们先看一下这些原则。

开闭原则(OCP)

根据维基百科的定义

“软件实体(类、模块、函数等)应该是对扩展开放的,对修改关闭的。”
 
在我们的示例中,`HumanResource` 类应该能够支持额外的晋升策略,而无需修改。

依赖注入 (DI)

根据维基百科

“依赖注入是一种软件设计模式,其中一个或多个依赖项(或服务)被注入,或通过引用传递,到依赖对象(或客户端)中,并成为客户端状态的一部分。”

简而言之,我们必须从外部提供依赖项的引用,而不是在内部实际创建依赖项。在我们的例子中,我们必须从客户端程序提供策略的引用,并将 `HumanResource` 类的责任隔离出来。

如何解决问题?

我们需要按照以下方式重写我们的代码组件

  • 通过实现共同接口来统一策略类(`BasedOnYearsSinceLastPromotion` 和 `BasedOnNoOfSkillCertificate`),将它们归为一类。
  • 并将策略类(`BasedOnYearsSinceLastPromotion` 和 `BasedOnNoOfSkillCertificate`)的对象创建逻辑与 `HumanResource` 类分离。为了实现这一点,我们需要引入依赖注入(DI)。更准确地说,我们需要通过提供接口引用来实现 `HumanResource` 类的松耦合,而不是像早期实现那样进行紧耦合

其结果是策略模式。

使用策略模式的最终实现

让我们看一下代码以获得清晰的理解。

    /// <summary>
    /// Promotion Strategy Template
    /// </summary>
    public interface IPromotionStrategy
    {
        List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Years Since Last Promoted'
    /// </summary>
    public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'No Of Skill Certificate Completed'
    /// </summary>
    public class BasedOnNoOfSkillCertificate : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

正如您所见,`BasedOnYearsSinceLastPromotion` 和 `BasedOnNoOfSkillCertificate` 都实现了 `IPromotionStrategy`。因此,它们将与 `HumanResource` 松耦合。

    /// <summary>
    /// Human Resource Class
    /// </summary>
    public class HumanResource
    {
        private List<Employee> _employees;
        private IPromotionStrategy _promotionStategy;

        public HumanResource()
        {
            _employees = new List<Employee>();
        }

        public void AddEmployee(Employee employee)
        {
            _employees.Add(employee);
        }

        public void AddPromotionStrategy(IPromotionStrategy NewPromotionStrategy)
        {
            _promotionStategy = NewPromotionStrategy;
        }

        public List<Employee> GetPromotionEligibleEmployees()
        {
            if (_promotionStategy == null)
                throw new ApplicationException("Promotion Strategy is not Provided.");

            return _promotionStategy.IdentifyPromotionEligibleEmployees(_employees);
        }
    }

我最喜欢的部分,我们从 `GetPromotionEligibleEmployees()` 函数中清理掉了多少混乱的代码。

并且 `Main()` 客户端函数也更加成熟。

        /// <summary>
        /// Client Program
        /// </summary>
        /// <param name="args"></param>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();

            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved =75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client invocation of BasedOnYearsSinceLastPromotion
            hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
            Print(employeeYOS, "Years Since Last Promotion");

            //Client invocation of BasedOnNoOfSkillCertificate
            hr.AddPromotionStrategy(new BasedOnNoOfSkillCertificate());
            List<Employee> employeeSK = hr.GetPromotionEligibleEmployees();
            Print(employeeSK, "No Of Skill Certificate");

            Console.ReadLine();
        }

还有输出,为什么不呢!

一个重要的问题

新的策略设计模式方法解决了我们之前讨论中提出的问题吗?

简单回答:“让我们试试”。我们将引入另一个晋升策略。这不是很疯狂吗!

“我们还希望根据目标达成百分比来确定员工的资格。”

请记住,我们已经有了 `PercentageOfGoalAchieved` 字段。所以新的策略类

    /// <summary>
    /// Promotion Strategy Template
    /// </summary>
    public interface IPromotionStrategy
    {
        List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Years Since Last Promoted'
    /// </summary>
    public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'No Of Skill Certificate Completed'
    /// </summary>
    public class BasedOnNoOfSkillCertificate : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Percentage of Goal Achieved'
    /// </summary>
    public class BasedOnPercentageOfGoalAchieved : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.PercentageOfGoalAchieved >= 80).ToList();
        }
    }

并在 `Main()` 中添加对新晋升策略的调用。

        /// <summary>
        /// Client Program
        /// </summary>
        /// <param name="args"></param>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();

            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved =75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client invocation of BasedOnYearsSinceLastPromotion
            hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
            Print(employeeYOS, "Years Since Last Promotion");

            //Client invocation of BasedOnNoOfSkillCertificate
            hr.AddPromotionStrategy(new BasedOnNoOfSkillCertificate());
            List<Employee> employeeSK = hr.GetPromotionEligibleEmployees();
            Print(employeeSK, "No Of Skill Certificate");

            //Client invocation of BasedOnPercentageOfGoalAchieved
            hr.AddPromotionStrategy(new BasedOnPercentageOfGoalAchieved());
            List<Employee> employeeGA = hr.GetPromotionEligibleEmployees();
            Print(employeeGA, "Percentage of Goal Achieved");

            Console.ReadLine();
        }

这就是我们所需要的!只需两个更改。通过这次添加(现在没有修改),我们编译、运行并得到输出。

结论

在整个练习中,我们经历了各种变更周期,以了解软件组件的行为方式。我们为现实生活中的问题实现了策略设计模式,并以简洁的方式研究了各种概念:依赖注入(DI)开闭原则(OCP)松耦合与紧耦合。最重要的是,我们分析了一些不遵循这些原则的方法的缺点,然后最终实现了策略设计模式。我们不仅学习了“如何实现策略设计模式?”,而且还探讨了“为什么这种实现方式更好?”。专家们,如果您有任何有助于我改进本文的评论,请告诉我。您的评论非常受欢迎。

学习愉快!

作者的其他文章

 

© . All rights reserved.