第一章 代码无错就是忧?——简单工厂模式
1.1 面试受挫
小菜今年计算机专业大四了,学了不少软件开发方面的知识,也学着编了些小程序,踌躇满志,一心要找个好单位。投递无数份简历后,终于收到了一个单位的面试通知,小菜欣喜若狂。到了人家单位,前台小姐给了他一份题目,上面写着:“ 请用 C++ \ Java \ C \ VB.NET 任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符,得到结果。”
小菜一看这还不简单,三下五除二,10分钟不到小菜就写完了,感觉没有错。交卷后,单位说一周内等通知吧。于是小菜只得耐心等待。可是半个月过去了,什么消息也没有,小菜很纳闷,我的代码实现了呀,为什么不给我机会呢?
小菜的代码:
class Program{
static void Main(String[] args){
Console.write("请输入数字A:");
String A = Console.ReadLine();
Console.write("请输入数字B:");
String B = Console.ReadLine();
Console.write("请选择运算符号(+、-、*、/):");
String C = Console.ReadLine();
String D = "";
if(C == "+"){
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
}
if(C == "-"){
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
}
if(C == "*"){
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
}
if(C == "/"){
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
}
Console.WriteLine("结果是:" + D);
}
}
1.2 初学者代码的毛病
小菜找到了从事软件开发工作七年的表格大鸟,请教原因,大鸟问了题目和了解小菜代码的细节以后,哈哈大笑,说道:“ 小菜啊小菜,你上当了,人家单位出题的意思,你完全没明白,当然不会再联系你了。 ”,那小菜这样的写法有什么问题。
小菜的代码,批注:
class Program{
static void Main(String[] args){
Console.write("请输入数字A:");
String A = Console.ReadLine();
--这样的命名是非常不规范的
Console.write("请输入数字B:");
String B = Console.ReadLine();
Console.write("请选择运算符号(+、-、*、/):");
String C = Console.ReadLine();
String D = "";
if(C == "+"){ 这样的判断分支,你这样的写法,意味着每个条件都要做判断,等于计算机做了三次无用功
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
}
if(C == "-"){
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
}
if(C == "*"){
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
}
if(C == "/"){
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
如果除数时,客户输入了0怎么办,如果用户输入的是字符符号,而不是数字怎么办?
}
Console.WriteLine("结果是:" + D);
}
}
1.3 代码规范
小菜听后,赶快修改代码,代码如下:
class Program{
static void Main(String[] args){
try{
Console.write("请输入数字A:");
String strNumberA = Console.ReadLine();
Console.write("请输入数字B:");
String strNumberB = Console.ReadLine();
Console.write("请选择运算符号(+、-、*、/):");
String strOperator = Console.ReadLine();
String strResult = "";
switch (strOperator) {
case "+" :
strResult = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
break;
case "-" :
strResult = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
break;
case "*" :
strResult = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
break;
case "/" :
if (strNumberB != "0") {
strResult = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
} else {
strResult = "除数不能为0";
}
break;
}
Console.WriteLine("结果是:" + strResult);
Console.ReadLine();
} catch (Exception ex) {
Colsole.WriteLine("您的输入有误:" + ex.Message);
}
}
}
大鸟: “ 吼吼,不错,不错,改的很快嘛?至少就目前代码来说,实现计算器是没有问题了,但这样写是否符合出题人的意思呢? ”
小菜:“ 你的意思是面向对象? ”
大鸟: “ 小菜非菜也! ”
1.4 面向对象编程
大鸟: “ 所有编程初学者都会遇到这样的问题,就是遇到问题就直觉地用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程。这其实是用计算机的方式去思考,比如计算器这个程序,先要求输入这两个数的运算符号,然后根据运算符号判断该如何选择运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用,从而达不到高质量代码的要求 。”
小菜: “ 鸟哥呀,我有点糊涂了,如何才能容易维护,容易扩展,又容易复用呢,能不能具体点? ”
1.5 活字印刷,面向对象
大鸟: “ 这样吧,给你讲个故事,你就明白了。 ”
“ 话说三国时期,曹操带领百万大军攻打东吴,大军在长江赤壁驻扎,军船连成一片,眼看就要灭掉东吴,统一天下,曹操大悦,于是大雁众文武,在酒席间,曹操诗兴大发,不觉吟道:‘ 喝酒唱歌,人生真爽。······ ’ 众文武齐呼:‘ 丞相好诗! ’ 于是一臣子速命印刷工匠刻板印刷,以便流传天下。”
样张出来给曹操一看,曹操感觉不妥,说道:“ 喝与唱,此话过俗,应改为 ‘ 对酒当歌 ’ 较好!,于是此臣就命工匠重新来过。工匠眼看连夜刻板之工,彻底白费,心中叫苦不迭。只得照办。”
样张出来再次请曹操过目,曹操细细一品,觉得还是不好,说:“ 人生真爽太过直接,应改问句才够意境,因此应该为 ‘ 对酒当歌,人生几何?······ ’ 当臣转告工匠之时,工匠晕倒······!”
“ 小菜你说,这里面的问题出在哪里? ” 大鸟问道。
大鸟:“ 说得好!如果是有了活字印刷,则只需更改四字即可,其余的工作都为白做,岂不妙哉。”
“ 第一,要改,只需更改要改之字,此为可维护 ;第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用 ;第三,此诗若要加字,只需另刻字加入即可,这是可扩展;第四,字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此时灵活性好。”
1.6 面向对象的好处
大鸟:“ 哈!这下你明白了?我以前也不懂,不过做了软件开发几年后,经历了太多类似曹操这样的客户要改变需求,更改最初的想法事件,才逐渐明白当中的道理。其客观地说,客户的要求也并不过分,不就是修改几个字吗,但面对已完成的程序代码,不容易维护,灵活性差,不容易扩展,更谈不上复用,因此面对需求变化,加班加点,对程序动大手术的那种无奈也就成了非常正常的事了。之后当我学习了面向对象分析设计编程思想,开始考虑通过封装、继承、多态把程序的耦合度降低,传统印刷术的问题就在于所有的字都刻在同一版面上造成耦合度太高所致,开始用设计模式使得程序更加的灵活,容易修改,并且易于复用。 ” 体会到面向对象带来的好处,那种感觉应该就如同是一中国酒鬼第一次喝到了茅台,西洋酒鬼第一次喝到了XO一样,怎个爽字形容。”
中国古代的四大发明:火药、指南针、造纸术、活字印刷术,前三种都是从无到有,从未知到发现的伟大发明,而活字印刷仅仅是从刻板印刷到活字印刷的一次技术进步,实在是思想上的成功,面向对象的胜利。
那么,面试公司给出的题目的目的是要我写出容易维护,容易扩展,又容易复用的计算机程序?那该如何做?
1.7 复制 vs. 复用
大鸟说:“ 比如说,我现在要求你写个Windows计算器,你现在的代码能不能复用呢?”
小菜:“ 那还不简单,把代码复制过去不久可以了吗?改动又不大,不算麻烦。”
大鸟:“ 小菜看来你还是小菜啊,有人说初级程序员的工作是Ctrl+C和Ctrl+V,这其实是非常不好的编码习惯,因为当你的代码中重复的代码多到一种程度,维护的时候可能就是一种灾难。越大的系统,这种方式带来的问题越严重,编程有一原则,就是用尽可能的办法去避免重复。想想看,你写的这段代码,有哪些是和控制台无关的,而只是和计算器有关的?”
小菜:“ 你的意思是分一个类出来?哦,对的,让计算和显示分开。”
1.8 业务的封装
大鸟说:“ 准确的说,就是让业务逻辑与界面逻辑分开,让它们之间的耦合度降下来。只有分离开,才可以达到容易维护或扩展。”
小菜:“ 让我来试试看。”
Operation运算类
public class Operation {
public static double GetResult(double numberA, double numberB, String operate) {
double result = 0d;
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.Write("请选择运算符号(+、-、*、/):");
String strOperator = 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、手机等需要移动系统的软件需要运算也可以用它呀。”
小菜说:“ 面向对象三大特性不就是封装、继承和多态吗,这里我用到的应该是封装。这还不够吗?我实在看不出,这么小的程序如何用到继承。至于多态,其实我一直也不太了解它到底有什么好处,如何使用它。”
1.9 紧耦合 vs.松耦合
这样的小程序可以用到面向对象的三大特性?继承和多态怎么可能用得上,我实在不能理解。
如果我要加一个开跟运算(sqrt),你如何改?
public class Operation {
private double _numberA = 0;
private double _numberB = 0;
public double NumberA{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB{
get { return _numberB; }
set { _numberB = value; }
}
public virtual double GetResult(){
double result = 0;
return result;
}
}
加减乘除类
class OperationAdd : Operation { // 加法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA + NumberB;
return result;
}
}
class OperationSub : Operation { // 减法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA - NumberB;
return result;
}
}
class OperationMul : Operation { // 乘法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA * NumberB;
return result;
}
}
class OperationDiv : Operation { // 除法类,继承运算类
public override double GetResult(){
double result = 0;
if (NumberB == 0) {
throw new Exception("除数不能为0。");
}
result = NumberA / NumberB;
return result;
}
}
1.10 简单工厂模式
简单工厂模式:去实例化对象,到底要实例化谁,将来会不会增加实例化的对象,比如增加开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂。
简单工厂类
public class OperationFactory{
public static Operation createOperate(String operate){
Operation oper = null;
switch (operate) {
case "+" :
oper = new OperationAdd();
break;
case "-" :
oper = new OperationAdd();
break;
case "*" :
oper = new OperationAdd();
break;
case "/" :
oper = new OperationAdd();
break;
}
return oper;
}
}
客户端代码(main函数里的代码)
Operation oper;
oper = OperationFactory.createOperate("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
public class Operation {
private double _numberA = 0;
private double _numberB = 0;
public double NumberA{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB{
get { return _numberB; }
set { _numberB = value; }
}
public virtual double GetResult(){
double result = 0;
return result;
}
}
加减乘除类
class OperationAdd : Operation { // 加法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA + NumberB;
return result;
}
}
class OperationSub : Operation { // 减法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA - NumberB;
return result;
}
}
class OperationMul : Operation { // 乘法类,继承运算类
public override double GetResult(){
double result = 0;
result = NumberA * NumberB;
return result;
}
}
class OperationDiv : Operation { // 除法类,继承运算类
public override double GetResult(){
double result = 0;
if (NumberB == 0) {
throw new Exception("除数不能为0。");
}
result = NumberA / NumberB;
return result;
}
}
界面就是这样实现代码的,不管你是控制台程序,Windows程序,Web程序,PDA或手机程序,都可以通过这段代码来实现计算器的功能,如果有一天我们需要更改加法运算,我们只需修改OperationAdd就可以了;如果我们需要增加各种复杂运算:立方根、自然对数、正弦余弦等,我们需要增加运算的子类,和在createOperate添加switch分支。
如果我们需要去修改界面呢?
————让我们看几个类的结构图。
- 首先看看 ‘ 动物 ’ 的矩形框,它就代表一个类(Class)。类图分三层,第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法和行为。注意前面的符号:“+”:表示public;“-”:表示private;“#”:protected。
- 大鸟:“然后注意左下角的 ‘ 飞翔 ’ 的接口图,与类图的区别主要是顶端有
<<interface>>
显示。第一行是接口名称,第二行是接口方法。
interface IFly{
void Fly();
}
- 接口还有另一种表示方法,俗称棒棒糖表示法,就是唐老鸭实现了 ‘ 讲人话 ’ 的接口。”
interface ILanguage{
void Speak();
}
- 继承用空心三角形+实现来表示
class Bird : Animal{
Bird 继承动物类
}
class WideGoose : IFly{
实现飞翔接口
}
- 企鹅和气候两个类,企鹅是很特别的鸟,会游不会飞。更重要的是,它与气候有很大关联。我们不去讨论为什么北极没有企鹅,为什么它们要每年长途跋涉。总之,企鹅需要知道气候的变化,需要了解气候规律。当一个类知道另一个类时,可以用关联association。关联关系用实线箭头来表示。
class Penguin : Bird {
private Climate climate;
} //在企鹅Penguin中,引用到气候Climate对象
- 聚合表示一种弱的 ‘ 拥有 ’ 关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分[DPE]。聚合关系用空心的菱形+实线箭头来表示。
class WideGooseAggregate {
private WideGoose[] arrayWideGoose;
}//在雁群WideGooseAggregate类中,有大雁数组对象arrayWideGoose
- 合成(Composition,也有翻译成 ‘ 组合 ’ 的)是一种强的 ‘ 拥有 ’ 关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样[DPE]。
class Bird {
private Wing wing;
public Bird(){
wing = new Wing();
}
} 在鸟Bird类中,初始化时,实例化翅膀Wing,它们之间同时生成
- 动物有几大特征,比如有新陈代谢,能繁殖。而动物要有生命力,需要氧气、水以及食物等。也就是说,动物依赖氧气和水。他们之间是依赖关系(Dependency),用虚线箭头来表示。
abstract class Animal{
public Metabolism (Oxygen oxygen,Water water){
}
}