1. 面试受挫
小菜今年计算机专业大四了,学了不少软件开发方面的东西,也学着编了些小程序,踌躇满志,一心要找到一个好单位。
当投递了无数份简历后,终于收到了一个单位的面试通知,小菜欣喜若狂。
到了人家单位,前台给了他一份题目,上面写着:
“请用C++、Java、C#或VB.NET任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”
小菜一看,这个还不简单,三下五除二,10分钟不到,小菜写完了,感觉也没错误。
交卷后,单位说一周内等通知吧。
于是小菜只得耐心等待。
可是半个月过去了,什么消息也没有,小菜很纳闷,我的代码实现了呀,为什么不给我机会呢。
小菜找到从事软件开发工作七年的表哥小李,请教原因,小李问了题目和了解了小菜代码的细节以后,哈哈大笑说道:“小菜啊小菜,你上当了,人家单位出题的意思,你完全都没明白,当然不会再联系你了。”
小菜说:“我的代码有错吗?单位题目不就是要我实现一个计算器的代码吗,我这样写有什么问题。”
public class computer
{
static void Main(string[] args)
{
Console.WriteLine("请输入数字A");
string A = Console.ReadLine();
Console.WriteLine("请输入+、-、*、/");
string B = Console.ReadLine();
Console.WriteLine("请输入数字B");
string C = Console.ReadLine();
string D = "";
if (B == "+")
{
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
}
if (B == "-")
{
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
}
if (B == "*")
{
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
}
if (B == "/")
{
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
}
Console.WriteLine("结果是:" + D);
}
}
2. 初学者代码毛病
小李说:“且先不说出题人的意思,单就你现在的代码,就有很多不足的地方需要改进。”
![](https://ichistudio.cn/wp-content/uploads/2023/09/图片-14-1024x743.png)
3. 代码规范
“哦,说得没错,这个我以前听老师说过,可是从来没有在意过,我马上改,改完给你看看。”
public class computer
{
static void Main(string[] args)
{
try
{
Console.WriteLine("请输入数字A");
string strNumberA = Console.ReadLine();
Console.WriteLine("请输入+、-、*、/");
string strOperate = Console.ReadLine();
Console.WriteLine("请输入数字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 (strOperate!="0")
{
Convert.ToString(Convert.ToDouble(strNumberA) / Convert.ToDouble(strNumberB));
}
else
{
Console.WriteLine("除数不能为0");
}
break;
}
Console.WriteLine("结果是:"+strResult);
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("您的输入有误"+ex.Message);
}
}
}
4. 面向对象编程
小菜:”我明白了,他说用任意一种面向对象语言实现,那意思就是要用面向对象的编程方法去实现,对吗?OK,这个我学过,只不过我当时没想到而已。”
小李:”所有编程初学者都会有这样的问题,就是碰到问题就直觉地用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程。这其实就是用计算机的方式去思考,比如计算器这个程序,先要求输入两个数和运算符号,然后根据运算符号判断选择如何运算,得到结果,这本身没有错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。”
5. 活字印刷,面向对象
小李:“这样吧,我给你讲个故事。你就明白了。”
“话说三国时期,曹操带领百万大军攻打东吴,大军在长江赤壁驻扎,军船连成一片,眼看就要灭掉东吴,统一天下,曹操大悦,于是大宴众文武,在酒席间,曹操诗兴大发,不觉吟道:”喝酒唱歌,人生真爽。…….”。众文武齐呼:“丞相好诗!”于是一臣子速命印刷工匠刻版印刷,以便流传天下。
![](https://ichistudio.cn/wp-content/uploads/2023/11/图片-12.png)
样张出来给曹操一看,曹操感觉不妥,说道:”喝与唱,此话过俗,应改为‘对酒当歌’ 较好!”,于是此臣就命工匠重新来过。
工匠眼看连夜刻版之工,彻底白费,心中叫苦不迭。只得照办。
![](https://ichistudio.cn/wp-content/uploads/2023/11/图片-13.png)
样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:”人生真爽太过直接,应改问语才够意境,因此应改为’对酒当歌,人生几何?……”,当臣转告工匠之时,工匠晕倒……!
![](https://ichistudio.cn/wp-content/uploads/2023/11/图片-15.png)
“小菜你说,这里面问题出在哪里?” 小李问道。
小菜说:”是不是因为三国时期活字印刷还未发明,所以要改字的时候,就必须要整个刻板全部重新刻”。
小李:”说得好!如果是有了活字印刷,则只需要更改四个字就可,其余工作都未白做。岂不妙哉。”
![](https://ichistudio.cn/wp-content/uploads/2023/11/图片-16-1024x295.png)
第一,要改,只需更改要改的字,此为可维护;
第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;
第三,此诗若要加字,只需另刻字加入即可,这是可扩展;
第四,字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好。
而在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可再利用价值。
小菜:”是的,小时候,我一直奇怪,为何火药、指南针、造纸术都是从无到有,从未知到发现的伟大发明,而活字印刷仅仅是从刻版印刷到活字印刷的一次技术上的进步,为何不是评印刷术为四大发明之一呢?原来活字印刷的成功是这个原因。”
6. 面向对象的好处
小李:”哈,这下你明白了?我以前也不懂,不过做了软件开发几年后,经历了太多的类似曹操这样的客户要更改需求,更改最初想法的事件,才逐渐明白当中的道理。其实客观来说,客户的要求也并不过分,不就是改几个字吗,但面对已完成的程序代码,却是需要几乎重头来过的尴尬,这实在是痛苦不堪。说白了,原因就是因为我们原先所写的程序,不容易维护,灵活性差,不容易扩展,更谈不上复用,因此面对需求变化,加班加点,对程序动大手术的那种无奈也就成了非常正常的事了。之后当我学习了面向对象的分析设计编程思想,开始考虑通过封装、继承、多态把程序的耦合度降低,传统印刷术的问题就在于所有的字都刻在同一版面上造成耦合度太高所致,开始用设计模式使得程序更加的灵活,容易修改,并且易于复用。”
“是呀是呀,你说得没错,中国古代的四大发明,另三种应该都是科技的进步,伟大的创造或发现。而唯有活字印刷,实在是思想的成功,面向对象的胜利。
小菜也兴奋起来:”你的意思是,面试公司出题的目的是要我写出容易维护,容易扩展,又容易复用的计算器程序?那该怎么做呀””
7. 复制VS复用
小李说:”比如说,我现在要求你再写一个Windows的计算器,你现在的代码能不能复用呢?”
小菜:”这还不简单,把代码复制过去不就行了吗?改动又不大,不算麻烦。”
小李:”小菜看来还是小菜啊,有人说初级程序员的工作就是Ctrl+C和Ctrl+V,这其实是非常不好的编码习惯,因为当你的代码中重复的代码多到一定程度,维护的时候,可能就是一场灾难。越大的系统,这种方式带来的问题越严重,编程有一原则,就是用尽可能的办法去避免重复。想想看,你写的这段代码,有哪些是和控制台无关的,而只是和计算器有关的?”
小菜:”你的意思是分一个类出来?哦,对的,让计算和显示分开。”
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()
{
try
{
Console.WriteLine("请输入数字A:");
string strNumberA = Console.ReadLine();
Console.WriteLine("请选择运算符号(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.WriteLine("请输入数字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、手机等需要移动系统的软件需要运算也可以用它呀。”
小菜:”哈,面向对象不过如此。下回写类似代码不怕了。”
小李:”别急,仅此而已,实在谈不上完全面向对象,你只用了面向对象三大特性中的一个,还有两个没用呢?”
小菜:”面向对象三大特性不就是封装、继承和多态吗,这里我用到的应该是封装。这还不够吗?我实在看不出,这么小的程序如何用到继承。至于多态,其实我一直也不太了解它到底有什么好处,如何使用它。”
小李:”慢慢来,要学的东西多着呢,你好好想想该如何应用面向对象的继承和多态。”
9. 紧耦合vs松耦合
第二天。
小菜问道:”你说计算器这样的小程序还可以用到面向对象三大特性?继承和多态怎么可能用得上,我实在不能理解。”
小李:”小菜很有钻研精神嘛,好,今天我让你功力加深一级。你先要考虑一下,你昨天写的这个代码,能否做到很灵活的可修改和扩展呢?”
小菜:”我已经把业务和界面分离了呀,这不是很灵活了吗?”
小李:”那我问你,现在如果我希望增加一个开根(sqrt)运算,你如何改?”
小菜:”那只需要改Operation类就行了,在switch中加一个分支就行了。”
小李:”问题是你要加一个平方根运算,却需要让加减乘除的运算都得来参与编译,如果你一不小心,把加法运算改成了减法,这岂不是大大的糟糕。
打个比方,如果现在公司要求你为公司的薪资管理系统做维护,原来只有技术人员(月薪),市场销售人员(底薪+提出),经理(年薪+股份)三种运算算法,现在要增加兼职工作人员的算法,但按照你昨天的程序写法,公司就必须要把包含原三种算法的运算类给你,让你修改,你如果心中小算盘一打,”主动调薪的机会到咯!”于是你除了增加了兼职算法以外,在技术人员(月薪)算法中写了一句
if(员工是小菜)
{
salary = salary*1.1;
}
那就意味着,你的月薪每月都会增加10%(小心被抓去坐牢),本来是让你加一个功能,却使得原有的运行良好的功能代码产生了变化,这个风险太大了。你明白了吗?”
小菜:”哦,你的意思是,我应该把加减乘除等运算分离,修改其中一个不影响另外的几个,增加运算算法也不影响其他代码,是这样吗?”
小李:”自己想去吧,如何用继承和多态,你应该有感觉了。”
Operation运算类
public class Operation
{
private double _numberA = 0;
private double _numberB = 0;
public double NumberA
{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB
{
get => _numberB;
set => _numberB = value;
}
public virtual double GetResult()
{
double result = 0;
return result;
}
}
加减乘除类
public class OperationAdd:Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA + NumberB;
return result;
}
}
public class OperationSub:Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA - NumberB;
return result;
}
}
public class OperationMul:Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA * NumberB;
return result;
}
}
public class OperationDiv : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA / NumberB;
return result;
}
}
小菜:”李哥,我按照你说的方法写出来了一部分,首先是一个运算类,它有两个Number属性,主要用于计算器的前后数,然后有一个虚方法GetResult(),用于得到结果,然后我把加减乘除都写成了运算类的子类,继承它后,重写了GetResult()方法,这样如果要修改任何一个算法,就不需要提供其他算法代码了。但问题来了,我如何让计算器知道我是希望用哪一个算法呢?”
10. 简单工厂模式
小李:”写得不错嘛,大大超出我的想象了,你现在的问题其实就是如何去实例化对象的问题,哈,今天心情不错,再教你一招’简单工厂模式’,也就是说,到底要实例化谁,将来会不会增加实例化的对象,比如增加开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂,来,我们看看这个类如何写。”
简单运算工厂类
public static Operation createOperate(string operatea)
{
Operation op = null;
switch (operatea)
{
case "+":
op = new OperationAdd();
break;
case "-":
op = new OperationMul();
break;
case "*":
op = new OperationSub();
break;
case "/":
op = new OperationDiv();
break;
}
return op;
}
小李:”哈,看到了吧,这样子,你只需要输入运算符号,工厂就实例化出合适的对象,通过多态,返回父类的方法实现了计算器的结果。”
客户端代码
static void Main()
{
Operation oper;
oper = OperationFactory.createOperate(Console.ReadLine());
oper.NumberA = 33.1;
oper.NumberB = 1.5;
double result = oper.GetResult();
Console.WriteLine(result);
}
小李:”界面的实现就是这样的代码,不管你是控制台程序,Windows程序,Web程序,PDA或手机程序,都可以用这段代码来实现计算器的功能,如果有一天我们需要更改加法运算,我们只需要改哪里?”
小菜:”改OperationAdd就可以了。”
小李:”那么我们需要增加各种复杂运算,比如平方根,立方根,自然对数,正弦余弦等,如何做?”
小菜:”只要增加相应的运算子类、修改运算类工厂、在switch中增加分支就可以了呀。”
小李:”那如果要修改界面呢?”
小菜:”那就去改界面,关运算什么事呀?”
小李:”好了,最后,我们来看看这几个类的结构图。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-13-1024x660.png)
11. UML类图
小菜:”对了,我时常在一些技术书中看到这些类图表示,简单的还看得懂,有些标记我很容易混淆。要不你给我讲讲吧。”
小李:”这个其实多看多用就熟悉了。我给你举一个例子,来看这样一副图,其中就包括了UML类图中的基本图示法。”
UML类图图示样例
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-1.png)
小李:”首先你看那个’动物’矩形框,它就代表一个类(class)。类图分三层,第一层显示类的名称,如果是抽象类,则用斜体显示。第二层是类的特性,通常是字段和属性。第三层是类的操作,通常是方法或行为。注意前面的符号,’+’表示private,’#’表示protected。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-2.png)
小李:”然后注意左下角的’飞翔’,它表示一个接口图,与类图的区别主要是顶端有<interface>显示。第一行是接口图,与类图的区别主要是顶端有<interface>显示。第一行是接口名称,第二行是接口方法。接口还有另一种表示方式,俗称棒棒糖表示法,比如图中的唐老鸭类就是实现了’讲人话’的接口。”
小菜:”为什么要是’讲人话’?”
小李:”鸭子本来也有语言,只不过只有唐老鸭是能讲人话的鸭子。”
小菜:”有道理。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-3.png)
interface IFly
{
void Fly();
}
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-4.png)
interface ILanguage
{
void Speak();
}
小李:”接下来就是可讲类与类,类与接口之间的关系了。你可首先注意动物、鸟、鸭、唐老鸭之间关系符号。”
小菜:”明白了,它们都是继承的关系,继承关系用空心三角形+实线来表示。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-5.png)
小李:”我举的几种鸟中,大雁是最能飞的,我让它实现了飞翔接口。实现接口用空心三角形+虚线来表示。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-6.png)
class Bird:Animal(继承动物类
{
}
class WideGoose:IFly(实现飞翔接口
{
}
小李:”你看企鹅和气候两个类,企鹅是很特别的鸟,会游不会飞。更重要的是,它与气候有很大的关联。我们不去讨论为什么北极没有企鹅,为什么它们要每年长途跋涉。总之,企鹅需要’知道’气候的变化,需要’了解’气候规律。当一个类’知道’另一个类时,可以用关联(association)。关联关系用实线箭头来表示。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-7.png)
class Penguin:Bird
{
//在企鹅Penguim中,引用到气候Climate对象
private Climate climate;
}
小李:”我们再来看大雁与雁群这两个类,大雁是群居动物,每只大雁都是属于一个雁群,一个雁群可以有多只大雁。所以它们之间就满足聚合(Aggregation)关系。聚合表示一种弱的’拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分[DPE](DPE(Design Pattern Explorer)是一个设计模式浏览器或设计模式目录)。聚合关系用空心的菱形+实线箭头来表示。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-8.png)
class WideGooseAggregate
{
private WideGoose [] arrayWideGoose;
}
小李:”合成(Composition,也有翻译成’组合’的)是一种强的’拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样[DPE]。在这里鸟和其他翅膀就是合成(组成)关系,因为它们是部分和整体的关系,并且翅膀和鸟的生命周期是相同的。合成关系用实心的菱形+实现箭头来表示。另外,你会注意到合成关系的连线两端还有数字’1’和数字’2’,这被称为基数。表明这一端的类可以有几个实例,很显然,一个鸟应该有两只翅膀。如果一个类可能有无数个实例,则就用’n’来表示。关联关系、聚合关系也可以有基数的。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-9.png)
class Bird
{
private Wing wing;
public Bird()
{
wing = new Wing();
}
}
小李:”动物几大特征,比如有新陈代谢,能繁殖。而动物要有生命力,需要氧气、水以及食物等。也就是说,动物依赖于氧气和水。它们之间是依赖关系(Dependency),用虚线箭头来表示。”
![](https://ichistudio.cn/wp-content/uploads/2023/12/图片-10.png)
abstract class Animal
{
public Metabolism (Oxygen oxygen, Water water)
}
小菜:”啊,看来UML类图也不算难呀。回想那天我面试题写的代码,我终于明白我为什么写得不成功了,原来一个小小的计算器也可以写出这么精彩的代码,谢谢李哥”
小李:”吼吼,记住哦,编程是一门技术,更加是一门艺术,不能只满足于写完代码运行结果正确就完事,时常考虑如何让代码更加简练,更加容易维护,容易扩展和复用,只有这样才可以真正得到提高。写出优雅的代码真的是一种很爽的事情。UM类图也不是一学就会的。需要有一个慢慢熟练的过程。所谓学无止境,其实这才是理解面向对象的开始呢。”