代码无错就是优? – 读《大话设计模式》笔记

读《大话设计模式》前言和第一章,阅读笔记和感悟,建议购买原书阅读,文中作者即原书《大话设计模式》一书作者。

1 前言

书中章节阅读之前,作者提出的两个观点,大家可以领会一下:

1.1 设计模式是否有必要全部学一遍?

答案是,Yes!别被那些说什么设计模式大多用不上,根本不用全学的舆论所左右。尽管现在设计模式远远不止23种,对所有都有研究是不太容易的,但就像作者本人一样,在学习GoF总结的23个设计模式过程中,你会被那些编程大师们进行伟大的技术思想洗礼,不断增加自己对面向对象的深入理解,从而更好的把这种思想发扬光大。这就如同高中时学立体几何感觉没用,但当你装修好房子购买家具时才知道,有空间感,懂得空间计算是如何的重要,你完全可能遇到买了一个大号的冰箱却放不进厨房,或买了开关门的衣橱(移门不占空间)却因床在旁边堵住了门而打不开的尴尬。

重要的不是你将来会不会用到这些模式,而是通过这些模式让你找到“封装变化”、“对象间松散耦合”、“针对接口编程”的感觉,从而设计出易维护、易扩展、易复用、灵活性好的程序。成为诗人后可能不需要刻意地按照某种模式去创作,但成为诗人前他们一定是认真地研究过成百上千的唐诗宋词、古今名句。

如果说,数学是思维的体操,那设计模式,就是面向对象编程思维的体操

1.2 我学了设计模式后时常会过度设计,如何办?

作者建议,暂时现象,继续努力。

设计模式有四境界:

  1. 没学前是一点不懂,根据想不到用设计模式,设计的代码很糟糕;
  2. 学了几个模式后,很开心,于是到处想着要用自己学过的模式,于是时常造成误用模式而不自知;
  3. 学完全部模式时,感觉诸多模式极其相似,无法分清模式之间的差异,有困惑,但深知误用之害,应用之时有所犹豫;
  4. 灵活应用模式,甚至不应用具体的某种模式也能设计出非常优秀的代码,以达到无剑胜有剑的境界。

从作者本人的观点来说,不会用设计模式的人要远远超过过度使用设计模式的人,从这个角度讲,因为怕过度设计而不用设计模式显示是因噎废食。当你认识到自己有过度使用模式的时候,那就证明你已意识到问题的存在,只有通过不断的钻研和努力,你才能突破“不识庐山真面目,只缘身在此山中”的瓶颈,达到“会当凌绝顶,一览众山小”的境界。

2 第1章 代码无错就是优? – 简单工厂模式

2.1 面试题

一道面试题目:“请用C++、Java、C#或VB.NET任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”

不太合理的代码:

class Program
{
  static void Main(string[] args)
  {
    Console.Write("请输入数字A:");
    string A = Console.ReadLine();
    Console.WriteLine("请选择运算符号(+、-、*、/):");
    string B = Console.ReadLine();
    Console.Write("请输入数字B:");
    string C = Console.ReadLine();
    string D = string.Empty;

    if ("+" == B)
      D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
    else if ("-" == B)
      D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
    else if ("*" == B)
      D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
    else if ("/" == B)
      D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));

    Console.WriteLine("结果是:" + D);
  }
}

2.2 初学者代码毛病

  • 命名不规范:变量命名A、B、C、D
  • 判断分支:多条件不加else,意味着每个条件都要做判断,等于计算机做了三次无用功
  • “/”运算:如果除数为0怎么办?如果用户输入的字符符号不是数字怎么办?

2.3 代码规范

小改代码:

class Program
{
  static void Main(string[] args)
  {
    try
    {
      Console.Write("请输入数字A:");
      string strNumberA = Console.ReadLine();
      Console.WriteLine("请选择运算符号(+、-、*、/):");
      string strOperate = Console.ReadLine();
      Console.Write("请输入数字B:");
      string strNumberB = Console.ReadLine();
      string strResult = "";

      switch (strOperate)
      {
        case "+":
          strResult = Convert.ToString(Convert.ToDouble(strNumberA) + Convert.ToDouble(strNumberB));
          break;
        case "-":
          strResult = Convert.ToString(Convert.ToDouble(strNumberA) - Convert.ToDouble(strNumberB));
          break;
        case "*":
          strResult = Convert.ToString(Convert.ToDouble(strNumberA) * Convert.ToDouble(strNumberB));
          break;
        case "/":
          if (strNumberB != "0")
            strResult = Convert.ToString(Convert.ToDouble(strNumberA) / Convert.ToDouble(strNumberB));
          else
            strResult = "除数不能为0";
          break;

      }

      Console.WriteLine("结果是:" + strResult);
      Console.ReadLine();
    }
    catch (Exception ex)
    {
      Console.WriteLine("您的输入有错:" + ex.Message);
    }
  }
}

至少就目前代码来说,实现计算器是没有问题了,但这样写出的代码是否符合出题人的意思呢?看下一小节。

2.4 面向对象编程

所有编程初学者都会有这样的问题,就是碰到问题就直觉地用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程。这其实是用计算机的方式去思考,比如计算器这个程序,先要求输入两个数和运算符号,然后根据运算符号判断选择如何运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。

如何才能容易维护,容易扩展,又容易复用呢?

2.5 活字印刷,面向对象

前面省略,只书关键段落,对比刻版印刷到活字印刷:

第一,要改,只需更改要改之字,此为可维护;第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;第三,此诗若要加字,只需另刻字加入即可,这是可扩展;第四,字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好

而在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可利用价值。

2.6 业务的封装

中间跳过1.6~1.8的故事,来到1.8(本文对应2.6)。

让业务逻辑与界面逻辑分开,让它们之间的耦合度下降。只有分离开,才可以达到容易维护或扩展。

Operation运行类

public class Operation
{
  public static double GetResult(double numberA, double numberB, string operate)
  {
    double result = default(double);

    switch (operate)
    {
      case "+":
        result = numberA + numberB;
        break;
      case "-":
        result = numberA -numberB;
        break;
      case "*":
        result = numberA * numberB;
        break;
      case "/":
        result = numberA / numberB;
        break;

    }

    return result;
  }
}

客户端代码

static void Main(string[] args)
{
  try
  {
    Console.Write("请输入数字A:");
    string strNumberA = Console.ReadLine();
    Console.WriteLine("请选择运算符号(+、-、*、/):");
    string strOperate = Console.ReadLine();
    Console.Write("请输入数字B:");
    string strNumberB = Console.ReadLine();
    string strResult = "";
    strResult = Convert.ToString(Operation.GetResult(Convert.ToDouble(strNumberA), Convert.ToDouble(strNumberB), strOperate));
    Console.WriteLine("结果是:" + strResult);
    Console.ReadLine();
  }
  catch (Exception ex)
  {
    Console.WriteLine("您的输入有错:" + ex.Message);
  }
}

小菜:“如果你现在要我写一个Windows应用程序的计算器,我就可以复用这个运算类(Operation)了。“

大鸟:“其实不单是Windows程序,Web版程序需要计算可以用它,PDA、手机等需要移动系统的软件需要运算也可以用它。”

小菜:“面向对象不过如此?”

大鸟:“别急,仅此而已,实在谈不上完全面向对象,现在只用了面向对象三大特性中的一个,还有两个没用呢?”

小菜:“面向对象三大特性不就是封装、继承和多态吗,这里用到的是封装,这还不够吗?我实在看不出,这么小的程序如何用到继承。至于多态,其实我一直也不太了解它到底有什么好处,如何使用它。”

大鸟:“慢慢来,要学的东西多着呢,你好好想想该如何应用面向对象的继承和多态。”

2.7 紧耦合 vs. 松耦合

号主觉得原书模拟对话挺有意思的,本小节照搬。

第二天。

小菜问道:“你说计算器这样的小程序还可以用到面向对象三大特性?继承和多态怎么可能用得上,我实在不能理解。”

大鸟:”小菜很有钻研精神嘛,好,今天我让你功力加深一级。你先考虑一下,你昨天写的这个代码,能否做到很灵活的可修改和扩展呢?“

小菜:“我已经把业务和界面分离了呀,这不是很灵活了吗?”

大鸟:“那我问你,现在如果我希望增加一个开根(sqrt)运算,你如何改?”

小菜:“那只需要改Operation类就行了,在switch中加一个分支就行了。”

大鸟:“问题是你要加一个平方根运行,却需要让加减乘除的运算都得来参与编译,如果你一不小心,把加法运算改成了减法,这岂不是大大的糟糕。打个比方,如果现在公司要求你为公司的薪资管理系统做维护,原来只有技术人员(月薪),市场销售人员(底薪+提成),经理(年薪+股份)三种运算算法,现在要增加兼职人员(时薪)的算法,但按照你昨天的程序写法,公司就必须要把包含原三种算法的运算类给你,让你修改,你如果心中小算盘一打,‘TMD,公司给我的工资这么低,我真是郁闷,这下有机会了’,于是你除了增加 了兼职算法外,在技术人员(月薪)算法中写了一句

if (员工是小菜)
{
  salary = salary * 1.1;
}

那就意味着,你的月薪每月都会增加10%(小心被抓去坐牢),本来是让你加一个功能,却使得原有的运行良好的功能代码产生了变化,这个风险太大了。你明白了吗?“

小菜:”哦,你的意思是,我应该把加减乘除等运算分离,修改其中一个不影响另外的几个,增加运算算法也不影响其他代码,是这样吗?“

大鸟:”自己想去吧,如何用继承和多态,你应该有感觉了。“

小菜:”OK,我马上去写。”

Operation运算类

public class Operation
{
  public double NumberA { get; set; }
  public double NumberB { get; set; }
  public virtual double GetResult()
  {
    double result = 0;
    return result;
  }
}

加减乘除类

/// <summary>
/// 加法类,继承运算类
/// </summary>
public class OperationAdd : Operation
{
  public override double GetResult()
  {
    double result = 0;
    result = NumberA + NumberB;
    return result;
  }
}

/// <summary>
/// 减法类,继承运算类
/// </summary>
public class OperationSub : Operation
{
  public override double GetResult()
  {
    double result = 0;
    result = NumberA - NumberB;
    return result;
  }
}

/// <summary>
/// 乘法类,继承运算类
/// </summary>
public class OperationMul : Operation
{
  public override double GetResult()
  {
    double result = 0;
    result = NumberA * NumberB;
    return result;
  }
}

/// <summary>
/// 除法类,继承运算类
/// </summary>
public class OperationDIv : Operation
{
  public override double GetResult()
  {
    double result = 0;
    if (NumberB == 0)
      throw new Exception("除数不能为0。");
    result = NumberA / NumberB;
    return result;
  }
}

小菜:“大鸟哥,我按照你说的方法写出来了一部分,首先是一个运算类,它有两个Number属性,主要用于计算器的前后数,然后有一个虚方法GetResult(),用于得到结果,然后我把加减乘除都写成了运算类的子类,继承它后,重写了GetResult()方法,这样如果要修改任何一个算法,就不需要提供其他算法的代码了。但问题来了,我如何让计算器知道我是希望用哪一个算法呢?”

2.8 简单工厂模式

大鸟:“写得很不错嘛,大大超出我的想象了,你现在的问题其实就是如何去实例化对象的问题,哈,今天心情不错,再教你一招’简单工厂模式’,也就是说,到底要实例化谁,将来会不会增加实例化的对象,如果增加开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂,来,我们看看这个类如何写。“

简单运算工厂类

public class OperationFactory
{
  public static Operation CreateOpearte(string operate)
  {
    Operation oper = null;
    switch(operate)
    {
      case "+":
        oper = new OperationAdd();
        break;
      case "-":
        oper = new OperationSub();
        break;
      case "*":
        oper = new OperationMul();
        break;
      case "/":
        oper = new OperationDIv();
        break;
    }

    return oper;
  }
}

这样子,呆需要输入运算符,工厂就实例化出合适的对象,通过多态,返回返回父类的方式实现了计算器的结果。

客户端代码

Operation oper;
oper = OperationFactory.CreateOpearte("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();

原书有趣又有涨姿势的对话来啦:

大鸟:“哈,界面的实现就是这样的代码,不管你是控制台程序,Windows 程序,Web 程序,PDA 或手机程序,都可以用这段代码来实现计算器的功能,如果有一天我们需要更改加法运算,我们只需要改哪里?”

小菜:“改OpeartionAdd 就可以了。”

大鸟:“那么我们需要增加各种复杂运算,比如平方根,立方根,自然对数,正弦余弦等,如何做?”

小菜:“只要增加相应的运算子类就可以了呀。”

大鸟:“嗯?够了吗?”

小菜:“对了,还需要去修改运算类工厂,在switch中增加分支。”

大鸟:“哈,那才对,那如果要修改界面呢?”

小菜:“那就去修改界面呀,关运算什么事呀。”

大鸟:“我们来看看这几个类的结构图。”

结构图
结构图

2.9 UML类图

详细的就不说了,本小节关键总结值得分享:“编程是一门技术,更加是一门艺术,不能只满足于写完代码运行结果正确就完事,时常考虑如何让代码更加简练,更加容易维护,容易扩展和复用,只有这样才可以真正得到提高。写出优雅的代码真的是一种很爽的事情。UML类图也不是一学就会的,需要有一个慢慢熟练的过程。所谓学无止境,其实这才是理解面向对象的开始呢。”

下次读《大话设计模式》的第2章-策略模式

除非注明,文章均由 Dotnet9 整理发布,欢迎转载。

转载请注明:
作者:乐趣课堂
链接:https://dotnet9.com/17155.html
来源:Dotnet9
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

登录后才能评论