一.设计模式 , 并发 , 线程 , 大数据处理 .
类是 OOP 中的核心组成元素,通常都是使用类来 “ 封装 ” 对象(属性、行为)。在经典图书《代码大全》里定义: “ 创建高质量的类,第一步,可能也是最重要的一步,就是创建一个 好的接口 。这也包括了创建一个可以通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后
但要记住的是 “ 面向对象编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。 ” 在实际的项目中如果能够做到这样也就够了,类的划分还得根据实际的需求而定
习之以恒
Research shows that it takes “about ten years, or ten to twenty thousand hours of deliberate practice” to become an “expert.” That’s a lot of time. Furthermore, becoming an expert does not always mean doing the same task for 10 years; it often means doing a wide variety of tasks within a particular domain for 10 years. It will take a lot of time and energy to become an “expert”; working as a developer for a few years is not enough. Want to become a senior developer in your early 30s? Either start your education/training sooner or be willing to do a lot of work, reading, and practicing in your spare time. I started programming in high school, and I devoted a lot of off-hours to keeping up with the industry, learning new skills, and so on. As a result, I hit the intermediate and senior level developer positions significantly earlier in my career than most of my peers, which translates to an awful lot of money over time.
研究表明,要成为一名“专家”,需要花费 10 年,或者 10000 到 20000 小时的深入练习时间。真的很久。还有,成为专家并不一定代表 10 年内执行同样的任务;通常这意味着要在特定领域内执行广泛的任务 10 年 。要成为“专家”需要花费大量的时间和精力;做几年开发人员是不够的。想在 30 岁左右成为一名高级软件开发工程师?要么尽早开始接受教育 / 培训,要么你得愿意用业余时间进行大量的工作、阅读和练习 。我从高中开始编程,还牺牲了许多休息时间去跟踪行业发展、学习新技能等等 。结果,我获得中级和高级开发人员职位的时间就比我的大部分同事都要早得多,随着时间的推移,这些就转化成为非常多的收入。
见山只是山 见水只是水 —— 提升对继承的认识 |
作者:温 昱 本文发布于《 CSDN 开发高手》 |
|
接口的本质
1 )接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界 “ 如果你是 …… 则必须能 ……” 的理念。
2 )这里该选抽象类还是接口呢?还记得第一篇文章我对抽象类和接口选择的建议吗?看动机。这里, 我们的动机显然是实现多态性而不是为了代码复用,所以当然要用接口 。
3) 此乃面向接口思想一大作用: 使相互不认识的类进行交互 。这样做是很有好处的,首先它们之间的耦合度大大降低,其次双方都可以进行替换,只要实现了相同的接口,就没有问题。
课题 :
面向接口编程详解(二) —— 编程实例
通过上一篇文章的讨论,我想各位朋友对 “ 面接接口编程 ” 有了一个大致的了解。那么在这一篇里,我们用一个例子,让各位对这个重要的编程思想有个直观的印象。为充分考虑到初学者,所以这个例子非常简单,望各位高手见谅 。
问题的提出
定义: 现在我们要开发一个应用,模拟移动存储设备的读写,即计算机与 U 盘、 MP3 、移动硬盘等设备进行数据交换。
上下文(环境): 已知要实现 U 盘、 MP3 播放器、移动硬盘三种移动存储设备,要求计算机能同这三种设备进行数据交换,并且以后可能会有新的第三方的移动存储设备,所以计算机必须有扩展性,能与目前未知而以后可能会出现的存储设备进行数据交换。
各个存储设备间读、写的实现方法不同, U 盘和移动硬盘只有这两个方法, MP3Player 还有一个 PlayMusic 方法。
名词定义: 数据交换 ={ 读,写 }
看到上面的问题,我想各位脑子中一定有了不少想法,这是个很好解决的问题,很多方案都能达到效果。下面,我列举几个典型的方案。
解决方案列举
方案一: 分别定义 FlashDisk 、 MP3Player 、 MobileHardDisk 三个类,实现各自的 Read 和 Write 方法。然后在 Computer 类中实例化上述三个类,为每个类分别写读、写方法。例如,为 FlashDisk 写 ReadFromFlashDisk 、 WriteToFlashDisk 两个方法。总共六个方法。
方案二: 定义抽象类 MobileStorage ,在里面写虚方法 Read 和 Write ,三个存储设备继承此抽象类,并重写 Read 和 Write 方法。 Computer 类中包含一个类型为 MobileStorage 的成员变量,并为其编写 get/set 器,这样 Computer 中只需要两个方法: ReadData 和 WriteData ,并通过多态性实现不同移动设备的读写。
方案三: 与方案二基本相同,只是不定义抽象类,而是定义接口 IMobileStorage ,移动存储器类实现此接口。 Computer 中通过依赖接口 IMobileStorage 实现多态性。
方案四: 定义接口 IReadable 和 IWritable ,两个接口分别只包含 Read 和 Write ,然后定义接口 IMobileStorage 接口继承自 IReadable 和 IWritable ,剩下的实现与方案三相同。
下面,我们来分析一下以上四种方案:
首先,方案一最直白,实现起来最简单,但是它有一个致命的弱点:可扩展性差。或者说,不符合 “ 开放 - 关闭原则 ” (注:意为对扩展开放,对修改关闭)。当将来有了第三方扩展移动存储设备时,必须对 Computer 进行修改。这就如在一个真实的计算机上,为每一种移动存储设备实现一个不同的插口、并分别有各自的驱动程序。当有了一种新的移动存储设备后,我们就要将计算机大卸八块,然后增加一个新的插口,在编写一套针对此新设备的驱动程序。这种设计显然不可取。
此方案的另一个缺点在于,冗余代码多。如果有 100 种移动存储,那我们的 Computer 中岂不是要至少写 200 个方法,这是不能接受的!
我们再来看方案二和方案三,之所以将这两个方案放在一起讨论,是因为他们基本是一个方案(从思想层面上来说),只不过实现手段不同,一个是使用了抽象类,一个是使用了接口,而且最终达到的目的应该是一样的。
我们先来评价这种方案:首先它解决了代码冗余的问题,因为可以动态替换移动设备,并且都实现了共同的接口,所以不管有多少种移动设备,只要一个 Read 方法和一个 Write 方法,多态性就帮我们解决问题了。而对第一个问题,由于可以运行时动态替换,而不必将移动存储类硬编码在 Computer 中,所以有了新的第三方设备,完全可以替换进去运行。这就是所谓的 “ 依赖接口,而不是依赖与具体类 ” ,不信你看看, Computer 类只有一个 MobileStorage 类型或 IMobileStorage 类型的成员变量,至于这个变量具体是什么类型,它并不知道,这取决于我们在运行时给这个变量的赋值。如此一来, Computer 和移动存储器类的耦合度大大下降。
那么这里该选抽象类还是接口呢?还记得第一篇文章我对抽象类和接口选择的建议吗?看动机。这里,我们的动机显然是实现多态性而不是为了代码复用,所以当然要用接口。
最后我们再来看一看方案四,它和方案三很类似,只是将 “ 可读 ” 和 “ 可写 ” 两个规则分别抽象成了接口,然后让 IMobileStorage 再继承它们。这样做,显然进一步提高了灵活性,但是,这有没有设计过度的嫌疑呢?我的观点是:这要看具体情况。如果我们的应用中可能会出现一些类,这些类只实现读方法或只实现写方法,如只读光盘,那么这样做也是可以的。如果我们知道以后出现的东西都是能读又能写的,那这两个接口就没有必要了。其实如果将只读设备的 Write 方法留空或抛出异常,也可以不要这两个接口。总之一句话:理论是死的,人是活的,一切从现实需要来,防止设计不足,也要防止设计过度。
在这里,我们姑且认为以后的移动存储都是能读又能写的,所以我们选方案三。
面向对象关注什么? 关注的是对象的行为,面向对象是使用行为来对对象进行分类的 !在面向对象中派生类为什么能够替换基类(替换原则),不是因为派生类是一个基类, 而是因为派生类具有与基类一致的行为 ,在派生类与基类的行为不一致的情况下派生类仍然是一个基类(如果有人敢否认这个,大家说怎么办?旁边有人喊道:砍死他!), 但是这个时候派生类消减了基类的行为,违背了替换原则,这也是恶心设计的由来 。所以说,对于面向对象而言我们要关注 “Act As”, 用 “Act As” 的标准来对对象进行归类,至于什么 “Is A” 之类的伪标准统统扔到它姥姥家去
设计的目的是有以下的两点:
- 保证公司的利益
- 取悦客户
作为一个软件设计人员,如果能够同时 站在公司的立场和客户的立场, 做一个客户和公司都满意的解决方案就是一个非常合格的设计人员了,如果还能够高瞻远瞩的规划产品的远景目标,那么这个设计师绝对可以堪称是个高手
我们要的是行为!行为由什么决定的呢?由要用你这个类的地方期望要的行为来决定的
首先我们需要考虑的问题是:鱼自己能不能决定自己能不能吃,能不能决定自己好不好吃?应该是不能吧,决定鱼能不能吃,好不好吃的应该是吃鱼的对象对吧。也许从普通人的角度来看河豚是不能吃的,但是从高明的大厨或者资深的老饕的角度来看河豚就是无比的美味了,这也是我在序章的最后专门添加一幅图片重点解说对象行为的原因
话说回来,既然鱼不能决定能不能吃、好不好吃,也就是说明能不能吃的行为不是有鱼能够决定的,那么也许有人会问那为什么要实现 IEatable 接口呢,和直接做一个属性不是一样吗?这个问题问的非常的好,确实既然鱼不能够决定自己能不能吃、好不好吃,那么为什么鱼要实现 IEatable 接口呢。 其实,在 Fish 上实现 IEatable 接口完全是出于使用方便性和接口的层次 (后续的文章中会重点讨论这个问题)来考虑的,完全面向对象的搞法应该是有另外一个对象来鉴定这个鱼是否能吃、好不好吃的(这也是基于设计的平衡来考虑的,可以参看开放 - 封闭原则)。 在这个地方使用接口和属性本质上没有什么差别,但是一旦鉴别鱼能不能吃、好不好吃的鉴别方式(实现方法)发生变化的时候,使用属性的方式就难以扩展了,只能修改代码了,但是使用接口的好处是我可以使用其它的方式补救,例如做一个实现 IEatable 接口的装饰对象来装饰鱼对象
软件设计的时候没有一味的好,也没有一味的差,任何事情都有其两面性,这个是需要取舍的,我们能够做到的事情就是让设计可控,如果设计失控了,那就全部完蛋了。话说回来,如果能够确认当前的系统中评判鱼能不能吃的标准不会发生改变,把这个不会发生改变的东西集成到鱼对象中是完全可以的,在具体的实现上用属性来实现也是一个非常不错的搞法
If 语句不超过三个是可以的,要是多于三个,你就需要处理 .
只不过希望各位时刻牢记,其实是先有了子类,才从子类泛化出父类
现代办公几乎都要用到个人计算机,个人计算机本身是一个抽象概念,台式 PC 是其中一个子类。后来,发明了笔记本电脑,我们想把笔记本电脑归为个人计算机的子类,是否合理呢?根据 LSP ,我们将台式 PC 都替换成笔记本电脑,世界应该是照常运行的(当然,实际情况可能复杂些,有些地方不能用笔记本电脑替换,但这里我们忽略这种差别)。我们办公时依赖的类是 “ 个人计算机 ” ,而笔记本电脑完全可以替代这个类型而使得世界运行正常,所以,我们说将笔记本电脑归于个人计算机的子类是符合 LSP 的。
后来,又发明了转基因黄瓜,我们也想将它归到个人计算机的子类中去,行不行呢?好的,现在我们再运用 LSP ,将世界上每个依赖个人计算机的地方都替换成一根转基因黄瓜。好的,世界人民都疯了!明显这种替换会令世界运行错乱。所以,我们不能让转基因黄瓜继承个人计算机。
上面的例子是显而易见的,但有些却不那么明显。例如,现在问,兽医是医生的子类吗?这个问题,一下子还真不是很好回答,但我们可以 LSP 一下,现在,我们把医院里的医生都替换为兽医,你还敢去医院看病吗?嗯,这下子不用我多说了吧。
最后一定要说明的是, LSP 应用于程序世界和现实世界时有很大差别的,现实世界繁杂、不确定性因素多,而程序世界简单、确定。总之, LSP 就是让你记住一条,凡是系统中有继承关系的地方,子类型一定能代替父类型,而且替换后程序运行要正常。换言之,继承是一种严格的 “IS-A” 关系,也是 “ 一般和特殊 ” 的哲学原理在程序世界中的体现
图 5.1 展示了几种耦合的示例。其中汽车和交通工具属于泛化耦合,轮子和方向盘组合于汽车,汽车聚合成车队,而汽车和司机具有依赖关系。这幅图只是耦合的一个小片段,实际上,世界上各种对象形成了一张复杂的耦合网,正因为有耦合的存在,世界才能演进。正如马克思主义哲学所说:联系是普遍的、客观的。所以,耦合的存在,有其深刻的哲学意义。
认识到上面几点对于理解对象论的世界运行理论非常重要,时刻铭记,参与真正世界运行的, 只有对象,没有类!对象在世界中,类在我们心中 !
所谓低耦合,就是先剥夺对象的选择权,再剥夺对象的感觉。对象间谁也不认识谁,只知道对象能提供什么服务
这里附带说一个问题,产生这种疑惑的原因,大多是因为朋友们已经习惯了学习一个东西时,只看其什么样子?怎么用?而不习惯于弄清楚一个东西起源于哪?出现的动机是什么?其实,要想学好、用好任何一个东西,后两个问题更关键一些。
举个例子,有人发明了吹风机,我们如果只搞清楚其是什么样子 ——“ 有个把手,有个吹风筒 ” ,以及怎么用 ——“ 打开按钮能吹出热风,关闭按钮就停止了 ” 。如果我们只搞清楚这些,那么我们八成用不对这个东西,为什么?因为我们根本不知道这东西是怎么来的,它为什么要被发明出来。也许我们天天拿他吹脸取暖或吹衣服,还一派洋洋得意以为用的很好的样子。殊不知这东西其实是用来吹头发帮助头发快点干起来的。
不要笑,这种事经常发生在我们身上。因为在软件开发中,有太多的东西,我们只顾着学习其是什么样子,怎么个用法,也许就像吹风机一样,这些并不复杂,然后我们就把它用到不该用的地方,还以为自己用得很好。
用不用得好吹风机,不在于是否熟练掌握开开关关,而在于是不是用它吹头发。同理, 任何东西用得好不好,不在于是不是熟练掌握用法,而在于是不是用对了地方。而要想用对地方,就要弄清楚这个东西的 “ 怎么出来的 ” 和 “ 出来是做什么用的 ” 。
一个符合 OO 原则的、低耦合的程序世界的运作形式是这样的:首先参与运作的本质只有对象,对象不直接依赖,没有选择权,互相不知道,而只知道各个接口。客户类制定接口,对象间通过接口交互,形成运作。世界的统治者依赖注入容器决定选择哪个服务类给客户类使用
OO ,即面向对象技术,是一种旨在提高软件质量的综合性技术,其贯穿于软件系统的调研、分析、设计、开发、测试、维护、扩展、升级等整个生命周期,它包含一系列概念、思想、理论、目标、原则、实践、模式、工具、语言等要素,这些要素既相互区别又相互联系,同时从宏观和微观两个角度共同协作,指导和引导开发人员开发出高质量软件,并指导与开发有关的一切过程。
重点总结
1. 客户不会想到方方面面。
2. 有时客户并不明确自己想要什么东西,而仅仅是有个动机。
3. 不要和客户谈需求, 要谈特性 。
4. 开发人员 有义务引导和帮助客户挖掘系统的特性 。
5. 当客户描述不清某个特性时,可以采用找类似事物的方法,说 说这个特性像什么,不像什么
6. 在软件开发初期,我们需要首先整理出一张特性列表,而不是做需求分析。
重点总结
1. 高质量软件的第一要素是:软件做客户希望它做的事。
2. 在开发初期,我们要尽量站在客户角度。
3. 理解需求的最好方法是明白客户希望软件做什么。
4. 开发流程大约分为两个阶段:搞清用户想要系统做什么和迭代开发。
设计模式
1. 实践 - 总结 - 再实践 - 再总结
2. 好脑子不如烂笔头,这既是自己对之前的总结,也是将来可以看到的曾经的历程,总归是一件好事
3. 策略模式关注行为的变化 , 状态模式关注对象状态的变化如何导致对象行为的变化
4. 状态模式一个形象的比喻是” 饱了睡, 饿了吃”. 在这里” 饱和饿” 是两种状态,” 睡和吃” 是两种行为. 另外一个典型的例子是银行账户. 根据客户账户中余额的不同用户可以有不同的操作行为. 这里要注意到, 状态模式中状态与行为的对应关系. 虽然不是一一对应, 但潜藏了一些信息, 那就是实际例子中行为与状态的有限和稳定, 行为的唯一性. 有限和稳定是指对象的行为一般就那几种, 除非业务需求变动, 否则不会发生改变. 唯一性只是对象只有一个吃的行为, 二不会有第二个吃的行为.
5. 策略模式关注行为的变化 , 但归根结底只有一个行为 , 变化的只是行为的实现 . 客户不关注这些 . 当新增变化时对客户可以没有任何影响 .
状态模式同样关注行为的变化 , 但这个变化是由状态来驱动 , 一般来说每个状态和行为都不同 . 新增的状态或行为一般与已有的不同 , 客户需要关注这些变化 .
6 计算机科学是一门相信所有问题都可以通过多一个间接层( indirection )来解决的科学
7. 间接层应用如此广泛,得益于它能带来如下好处:共享逻辑(重用)、分离意图和实现(提高灵活性)、 隔离变化(封装)、解耦等等。既然我们知道了间接层这么一回事,似乎我们可以不用知道设计模式也能做出像设计模式那样好的设计来。但是要记住,间接层应用过于泛滥,则会过犹不及,它会导致简单问题复杂化、跳跃阅读难以理解等问题
8. 做到设计模式的活学活用,我认为还要做到以解决问题为中心,将设计模式融合使用,避免为了设计而模式。当然这是建立在对各种设计模式了如指掌的情况下。比如,有一段解析字符串的对象,而在使用它之前,还要做一些参数的判断等其他非解析操作,这时,你很快就会想起使用代理模式;但是这个代理提供的接口可能有两三个(满足不同情形),而解析对象仅有一个接口,命名还不相同,这时你又想到了适配器模式。这时也许将两者融合成你独有的解决方案比笨拙的套用两个模式好的多得多。
9. 对象是什么?
– 从概念层面讲, 对象是某种拥有责任的抽象 。
– 从规格层面讲, 对象是一系列可以被其他对象使用的公共接口 。
– 从语言实现层面来看, 对象封装了代码和数据 。
10. 三大基本面向对象设计原则
– 针对接口编程,而不是针对实现编程
– 优先使用对象组合,而不是类继承
– 封装变化点
11. 理解了这些原则,再看设计模式,只是在具体问题上怎么实现这些原则而已。张无忌学太极拳,忘记了所有招式,打倒了 " 玄幂二老 " ,所谓 " 心中无招 " 。设计模式可谓招数,如果先学通了各种模式,又忘掉了所有模式而随心所欲,达到 " 心中无模式 ", 可谓 OO 之最高境界。
12. 我认为学习模式时,一定要理解一个模式的意图,结构,使用性。只有这样你才可能在实际的项目中使用,具体的实现时的细节可以翻阅资料,只有你非常了解模式时,才会很自然的应用模式
模式的意图,结构,优点和缺点 ( 使用性 )
13.Swing 中的 MVC
(JButton 举例子 )
Model:
一个按钮的 model 所应该具备的行为由一个接口 ButtonModel 来完成。一个按钮 model 实例封装了其内部的状态,并且定义了按钮的行为。它的所有方法可以分为四类:
1 、查询内部状态
2 、操作内部状态
3 、添加和删除事件监听器
4 、发生事件
View & Controller
上面的图中讲述一个按钮的 view/controller 由一个接口 ButtonUI 完成。如果一个类实现了这个接口,那么它将会负责创建一个用户界面,处理用户的操作。它的所有方法可以被分为三大类:
1 、绘制 Paint
2 、返回几何类型的信息
3 、处理 AWT 事件
程序员通常并不会直接和 model 以及 view/controller 打交道,他们通常隐藏于那些继承自 java.awt.Component 的组件里面了,这些组件就像胶水一样把 MVC 三者合三为一。也正是由于这些继承的组件对象,一个程序员可以很方便的混合使用 Swing 组件和 AWT 组件,然后,我们知道, Swing 组件有很多都是直接继承自相应的 AWT 组件,它能提供比 AWT 组件更加方便易用的功能,所以通常情况下,我们没有必要混合使用两者
示例 :JButton
顺序 JButton 源码 -----ButtonModel--------ButtonUI
14.OO 中划分责任,并将责任委派给不同的对象 . 可以加入中间层 .
Layers 架构模式
在收集到用户对软件的要求之后,架构设计就开始了。架构设计一个主要的目的,就是把系统划分成为很多 " 板块 " 。划分的方式通常有两种,一种是 横向 的划分,一种是 纵向 划分。
横向划分将系统按照商业目的划分。比如一个书店的管理系统可以划分成为进货、销售、库存管理、员工管理等等 。
纵向划分则不同,它按照抽象层次的高低,将系统划分成 " 层 " ,或叫 Layer 。比如一个公司的内网管理系统通常可以划分成为下面的几个 Layer:
一、网页,也就是用户界面,负责显示数据、接受用户输入;
二、领域层,包括 JavaBean 或者 COM 对象、 B2B 服务等,封装了必要的商业逻辑,负责根据商业逻辑决定显示什么数据、以及如何根据用户输入的数据进行计算;
三、数据库,负责存储数据,按照查询要求提供所存储的数据。
四、操作系统层,比如 Windows NT 或者 Solaris 等
五、硬件层,比如 SUN E450 服务器等
有人把这种 Layer 叫做 Tier ,但是 Tier 多带有物理含义,不同的 Tier 往往位于不同的计算机上,由网络连接起来,而 Layer 是纯粹逻辑的概念,与物理划分无关。
Layers 架构模式的好处是:
第一、任何一层的变化都可以很好地局限于这一层,而不会影响到其他各层。
第二、更容易容纳新的技术和变化。 Layers 架构模式容许任何一层变更所使用的技术
创建模式是对类实例化过程的抽象。
一些系统在创建对象的时候需要动态的决定怎样创建对象、创建哪些对象、以及如何组合,表示这些对象。创建模式描述了怎样构造和封装这些动态的决定