zoukankan      html  css  js  c++  java
  • 写给自己看的小设计3

      由于对象设计的核心是类,所以下面的原则也都基本都是讨论类的设计问题,其它类型的元素都比较简单,基本上也符合大多数这里列出的原则。
      前面我们分析完了对象设计的基本原则,这里我将重新温习一下对象设计的核心原则 - SOLID原则。几乎所有的设计模式都可以看到这些原则的影子。
     
    单一职责原则(SRP):做一个专一的人
      单一职责原则的全称是Single Responsibility Principle,简称是SRP。SRP原则的定义很简单:
    即不能存在多于一个导致类变更的原因。简单的说就是一个类只负责一项职责。

      让一个类仅负责一项职责,如果一个类有多于一项的职责,这是比较脆弱的设计。因为一旦某一项职责发生了改变,需要去更改代码,那么有可能会引起其他职责改变。所谓牵一发而动全身,这显然是我们所不愿意看到的,所以我们会把这个类分拆开来,由两个类来分别维护这两个职责,这样当一个职责发生改变,需要修改时,不会影响到另一个职责。

      做且只做好一件事,这一条原则其实不仅仅适用于对象,也同样适用于函数,变量等一切编程元素。当然在商业模式中,将一件事做到极致就是成功,我个人觉得这一条也还是成立的。
      随便举个例子,如果大家有深入研究过迭代器的思想的话,那其实就是把存储数据和遍历数据的职责分开了,集合只负责实现存储数据的功能,而迭代器完成遍历数据的功能。
      再说我看到过的一个例子:说有一个辅助类CommonUtil,在这里面提供了所有不能归入其他模块的辅助方法,它的结构如下:
    public class CommonUtil
    {
     #region Canvas Helpers
     public void M1() { }
     //...
     #endregion
     
     #region Screen Helpers
     public void M2() { }
     //...
     #endregion
     
     #region Size Helpers
     public void M3() { }
     //...
     #endregion
     
     #region Data Helpers
     public void M4() { }
     //...
     #endregion
    }

    各位,你觉得这个类写的怎么样?

      这里面放进了各种不同类型的辅助方法,每个模块有辅助方法需要找地方放的时候,人们都是自觉的找到了这个类,于是这个类在每个Release中都不断有新成员加入,于是最终变成了一个庞然大物,使用的时候,光看函数列表都够大家喝一壶了。
      我的想法是,为什么不拆分成4个小类,每个类专门负责某一类型辅助功能呢?
     
     
    开放封闭原则(OCP):改造世界大部分不是破坏原来的秩序
      开放封闭原则全称是Open Closed Principle, 简称OCP, 该原则的定义是:
    软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

    这条原则是所有面向对象原则的核心。

      软件设计所追求的第一个目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其它的原则或多或少都是为了这个目标而努力的,例如以Liskov替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。
      软件设计所最求的第二个目标就是重用。这个是继承机制的核心动力。由于通常来说抽象的东西最稳定,最不容易变化,所以抽象与继承是实现开闭原则强大的工具,但不是唯一工具,后面我们会说到实现开闭原则的另一个更加强大,更加灵活的工具:组合。
      一言以蔽之,继承与组合是封装变化,降低耦合的不二法门。能否合理的使用继承和组合是体现一名码农水平高低的又一标准。
      在实际的代码中,添加新的功能一般意味着新的对象,一个好的设计也意味着这个新的修改不要大幅度波及现有的对象。这一条理解起来最简单,实施起来却是最困难。无数的模式和解耦方法都是为了达到这个目的而诞生的。
      看一个经典的例子:
    public class Component
    {
     public enum Status
     {
      None,
      Installed,
      Uninstalled
     }
     
     Status m_status = Status.None;
     
     void Do()
     {
      switch (m_status)
      {
       case Status.None:
        Console.WriteLine("Error...");
        break;
       case Status.Installed:
        Console.WriteLine("Hello!");
        break;
       case Status.Uninstalled:
        Console.WriteLine("Error...");
        break;
       default:
        break;
      }
     }
    }

      我们这里定义了一个组件,用户动态加载,加载完了以后程序就可以用了,为了处理方便,我们给组件定义了一些状态,在不同的状态下,这个组件有不同的行为,于是就有了上面的代码:enum定义状态,函数中使用switch实现路由。

      使用switch分支是一种经典的做法,当组件的状态类型不存在变化的可能时,该段代码无可挑剔,堪称完美。
      可是在实际项目中,过了一阶段,我们发现组件的状态不够,比如说我们需要处理组件还未配置时的行为,于是我们在枚举中加了一个状态:Configured,然后在switch中加了一个分支。 
      又过了一阶段,我们又发现还需要处理组件还未初始化时的行为,于是我们在枚举中又加了一个状态:Initialized,然后在switch中加了一个分支。
      至于以后是否还需要别的状态,我们目前不得而知,应该说还是有可能的。
      上面这个行为是严重违反开闭原则的,这个不用多讲了吧。那么如何改进呢?使用我们最强大的工具吧:使用继承或/和组合封装变化点
      这里我们分析一下,该组件存在变化的地方就是组件的状态,这是一个变化点,对于变化点,对于变化点不要手软,封印它。
    public class ComponentStaus
    {
     public virtual void Do() { }
    }
    public class ComponentNone : ComponentStaus
    {
     public override void Do() { Console.WriteLine("Error..."); }
    }
    public class ComponentInitialized : ComponentStaus
    {
     public virtual void Do() { Console.WriteLine("Hello!"); }
    }
     
    public class Component
    {
     ComponentStaus m_status = new ComponentNone();
     
     public void ChangeStatus(ComponentStaus newStatus)
     {
      m_status = newStatus;
     }
     
     public void Do()
     {
      m_status.Do();
     }
    }

      在上面的例子中,我们发现了变化点,然后抽象出一个基类放在那,然后使用继承机制,让子类去演绎变化。当我们需要添加新的状态Configured的时候,我们只要添加一个新的子类ComponentConfigured,让它从ComponentStaus继承,并重写Do方法即可。使用的时候,在合适的时机(如事件处理中)把该子类的实例传给Component就可以了,当然也有可能是Component自己处理事件或方法时自己修改该状态实例。

      能看到开闭原则的影子吗?(当然,不要妄想对修改完全封闭,这个是不可能的,就像组件之间零依赖是不可能的一样)
     
    里氏替换原则(LSP):长大后,我就成了你
      里氏替换原则全称Liskov Substitution Principle,简称 LSP,它的定义是:
    任何基类可以出现的地方,子类一定可以出现。

      LSP原则是继承复用的基石,只有当派生类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。

      LSP原则保证了继承的正确实现。它希望子类不要破坏父类的接口成员。一旦破坏了,就如果人与人之间破坏合同一样,有时候会很糟糕。
      这个原则看起来也很容易,但是却也很容易和现实中的概念混淆,看个经典的小例子:长方形与正方形问题。
      在我们小学学数学的时候,就知道正方形是特殊的长方形,于是写代码的时候,自然的正方形类就继承自长方形了,代码如下:
    public class Program
    {
     static void Main(string[] args)
     {
      Rectangle rect = new Rectangle();
      rect.setWidth(100);
      rect.setHeight(20);
      Console.WriteLine(rect.Area == 100 * 20);
     
      Rectangle squ = new Square();
      rect.setWidth(100);
      rect.setHeight(20);
      Console.WriteLine(squ.Area == 100 * 20);
     }
    }
     
    class Rectangle
    {
     public double m_width;
     public double m_height;
     
     public virtual void setWidth(double width) { m_width = width; }
     public virtual void setHeight(double height) { m_height = height; }
     
     public double Area
     {
      get { return m_width * m_height; }
     }
    }
     
    class Square : Rectangle
    {
     public override void setWidth(double width)
     {
      m_width = width;
      m_height = width;
     }
     
     public override void setHeight(double height)
     {
      m_width = height;
      m_height = height;
     }
    }

      很显然输入的不是两个True,根本原因就在于正方形只有长的概念,而没有长方形所期望的宽的概念,所以长方形中定义了正方形根本没有的东西,也就是说长方形不应该是正方形的基类。

      当一个基类出现了其子类不想要的接口成员时,继承关系必然是欠缺考虑的继承,也必然是违反LSP原则的。这个时候要么把想办法把基类的那个成员抽象出去,要么子类再选择从合适的基类继承。记住这个思路,在下一个原则我们还会再相见。
      此外,当我在小孩玩橡皮鸭子的时候,常常在想:橡皮鸭子能从鸭子继承吗?你觉得呢?
     
    接口分离原则(ISP):不要一口吃成胖子
      接口分离原则全称interface segregation principle,简称ISP,它的定义是:
    不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

      这一原则与单一职责原则息息相关,它们对于高内聚的追求是一致的,但是它更加强调了接口的高内聚性。

      看个例子,我们有一个服务接口,是这么定义的:
    interface IService
    {
     void GetUser();
     void RegisterUser();
     void LoadProducts();
     void AddProduct();
     void AcceptRequest();
     void SendResponse();
    }

      因为是面向所有Client的,所以这个接口提供了所有Client需要的方法,比如用户的操纵,产品的操作,数据传输的一些操作,每个Client都可能用到其中的一部分服务。

      这个设计运行很好,服务端提供一个类Service实现这个接口,而Client,它通过某些网络服务方式获取到这个接口IService就可以了,然后直接调用相关方法就可以了。
      先说第一点,这个接口违反了单一职责原则,一个字,""。
      再说第二点,每个类型的Client只处理一种对象,比如有的Client,如工资系统只处理User,而仓库系统只处理Product,接口的其它方法对它们没用,还是一个字,""。
      于是得到下列接口:
    interface IUser
    {
     void GetUser();
     void RegisterUser();
    }
    interface IProduct
    {
     void LoadProducts();
     void AddProduct();
    }
    interface IPeer
    {
     void AcceptRequest();
     void SendResponse();
    }
    class Service : IUser, IProduct, IPeer {}

      这样拿到代理对象后,想处理用户的Client,将该对象转换成IUser即可,想处理产品的转换成IProduct即可。

      同样的,试想一下,如果某一天某Service只提供有关用户的服务,在原先的设计中会怎么样?
     
    依赖倒置原则(DIP):抽象的艺术才有生命力
      依赖倒置原则全称Dependence Inversion Principle,简称DIP,它的定义有3点含义:
    1、高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)
    2、抽象(抽象类或接口)不应该依赖于细节(具体实现类)
    3、细节(具体实现类)应该依赖抽象

      总结起来,这个原则说的就是每个类与别的类交互时,尽量只使用满足接口规范的抽象类。为啥?因为抽象类实现细节几乎没有,没什么需要变化的。这一条深刻揭示了抽象的生命力,抽象的对象才是最有表达能力的对象,因为它通常是“无形”的,可以随时填充相关的细节。

      直接看一个例子:
    public class Program
    {
     static void Main(string[] args)
     {
      UI layer = new UI();
      layer.SetDataAccessor(new XmlDataAccessor());
      layer.Do();
     }
    }
     
    class UI
    {
     DataAccessor m_accessor;
     
     public void SetDataAccessor(DataAccessor accessor) { m_accessor = accessor; }
     public void Do()
     {
      m_accessor.GetUser();
     }
    }
     
    interface DataAccessor
    {
     void GetUser();
     void RegisterUser();
    }
     
    class XmlDataAccessor : DataAccessor
    {
     public void GetUser() { }
     public void RegisterUser() { }
    }

      这里上游的组件UI依赖的是DataAccessor这样的接口,而不是依赖各种具体的子类,如XmlDataAccessor,这样当想使用其他的数据库存储数据的时候,只要增加新的DatabaseDataAccessor之类的新类,然后在设置的时候设置一下就可以了。这种手段,很多人也称为"依赖注入"。

     
      好了,核心原则说完了,总结一下,似乎就是一句话:"类要单纯,继承要谨慎,变化要封装,抽象类型要多用"。
  • 相关阅读:
    初认识AngularJS
    (imcomplete) UVa 10127 Ones
    UVa 10061 How many zero's and how many digits?
    UVa 11728 Alternate Task
    UVa 11490 Just Another Problem
    UVa 10673 Play with Floor and Ceil
    JSON对象和字符串的收发(JS客户端用typeof()进行判断非常重要)
    HTML.ActionLink 和 Url.Action 的区别
    EASYUI TREE得到当前节点数据的GETDATA方法
    jqueery easyui tree把已选中的节点数据拼成json或者数组(非常重要)
  • 原文地址:https://www.cnblogs.com/dxy1982/p/4274521.html
Copyright © 2011-2022 走看看