作者已经搬迁去隔壁网站,也欢迎大家关注我们的写作团队:天星技术团队。
前言
不知道是否有许多萌新跟我一样,在看java源码的时候,脑袋容易晕。通常查一个方法,要跳几个类出来,有些类动不动就上千行。像我这样血气方刚的少年,哪静得下心来理解这么多结构复杂的代码!还不如看点番剧,喝点快乐肥宅水!
看吧,不知道该如何下手去啃源码。不看吧,心里也急得很。总不能一直这样放着吧,既然选择了做这行, 还是得好好学习,毕竟面向工资编程。于是在某位大神的指导下,推荐我先学习设计模式。okk!那接下来我们就一起来学习设计模式!
什么是设计模式
- 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
- 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
- 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结
为何要学习设计模式
- 别人都学你不学,是想当咸鱼吗?大佬都懂你不懂,不想混进大佬圈装逼了?
- 当你用模式描述的时候,其他开发人员很容易知道你对设计的想法。
- 使用模式谈论软件系统,可以让你保持在设计层次上,而不会压低到对象与类这种琐碎的事情上。
- 当用模式名称交流时,你们之间交流的不只是模式名称,而是一整套模式背后所象征的质量,特性,约束。
设计原则
提倡使用设计模式的根本原因是为了代码复用,增加可维护性。现在被命名的23种设计模式就是遵守了以下六大设计原则,才达到了代码复用,增加可维护性的目的。
-
单一职责原则:
There should never be more than one reason for a class to change.
不要存在多于一个导致类变更的原因,一个类只承担一个职责 -
开闭原则:
Software entities like classes,modules and functions should be open for extension but closed for modifications.
类、模块、函数,对扩展是开放的,对修改是封闭的。 -
里式替换原则:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
子类可以扩展父类的功能,但不能改变父类原有的功能 -
迪米特原则(最少知识原则):
Only talk to you immediate friends.
尽量减少对象之间的交互,从而减小类之间的耦合。 -
接口隔离原则:
The dependency of one class to another one should depend on the smallest possible interface.
不要对外暴露没有实际意义的接口。 -
依赖倒置原则:
High level modules should not depends upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
核心思想:面向接口编程
走进设计原则
看了这么多理论东西了,再不来点代码刺激刺激神经,我都要点右上角了。接下来,我们就通过一个例子,小写一点代码,让我们更好的理解设计模式!
现在我们来设计一个游戏人物。(为了突出重点,更好的理解,只讲部分功能,理解要表达的意思即可,并不会真正的把所有功能实现。)
class Character {
//使用武器
fun useWeapon() {
Log.i("TAG", "fist")
}
}
现在有了一个初步人物模型的设计,实现了攻击的方法。当人物到了一定等级,可以转职为魔法师,道士,战士的话,我们可以这样写:
class Character {
//使用武器
fun useWeapon(profession : String) {
if(profession.equals("magician")){
Log.i("TAG", "火墙风咆哮")
}
if(profession.equals("taoist")){
Log.i("TAG", "召唤4级宝宝")
}
if(profession.equals("warrior")){
Log.i("TAG", "刀刀烈火")
}
}
}
如果每个方法都这样写,当方法数量增多的时候,这样的写法就变得很杂乱无章。还导致了影响类变化原因不止一个,也就违反了“单一职责原则”。那我们现在换一种方式来写:创建Magician,Taoist,Warrior三个类。在父类中采用重载的方式来实现useWeapon()。
//魔法师使用武器
fun useWeapon(magician: Magician) {
Log.i("TAG", "火墙风咆哮")
}
//道士使用武器
fun useWeapon(taoist: Taoist) {
Log.i("TAG", "召唤4级宝宝")
}
//战士使用武器
fun useWeapon(warrior: Warrior) {
Log.i("TAG", "刀刀烈火")
}
这样做更傻,不仅没有遵守单一职责原则,还违反了迪米特法则。
实际上,当我们一看到这种需求,有点经验的都知道,应该写三个类,分别对应魔法师,道士,战士,而不是把所有东西写进一个类里面来。useWeapon方法写在父类中的缺点已经暴露出来了,直觉告诉我们,这个方法是应该写在子类中的。那父类中的这个方法留不留呢?里式替换原则告诉我们,子类可以扩展父类的功能,但不能改变父类原有的功能。于是乎……
class Magician : Character(){
fun useWeapon(){
Log.i("TAG", "火墙风咆哮")
}
}
class Taoist : Character(){
fun useWeapon(){
Log.i("TAG", "召唤4级宝宝")
}
}
class Warrior : Character() {
fun useWeapon(){
Log.i("TAG", "刀刀烈火")
}
}
现在来看,好像没什么问题哦。但是却没有遵守依赖倒置原则。我们应该尽量的针对接口编程,而不是针对实现编程。而现在我们把实际的行为都写在子类当中!这样的坏处的是你必须去在每一个子类中手写useWeapon方法,修改起来相当麻烦!而且在代码执行时没办法更改具体行为(除非写更多代码,但那样并划不来)。
现在这样写还有点怪怪的,明明每一个类有useWeapon(),却不能写进父类中。很气有没有!
那我们把character写成一个接口?把方法写进去?
牛逼!真是太聪明了!既不违反单一职责原则,也不违反迪米特法则,还遵守了里式替换原则。
牛逼个鸡儿!
现在我们只考虑了玩家的角色,游戏里的NPC怎么办? 你打得过NPC?
假如我们已经把父类写成了接口,再创建一个npc类,看看吧。
interface Character {
//使用武器
fun useWeapon()
}
class NPC :Character {
override fun useWeapon() {
//空方法
}
}
这样造成了npc类里面有一个空方法。你可能永远都不会去用它。那放这儿有什么意思?这样写还违反了接口隔离原则:不要对外暴露没有实际意义的接口!不要对外暴露没有实际意义的接口!不要对外暴露没有实际意义的接口!
问题不大!只需要打一个响指!我们重新理一理思绪!
现在问题在于我们要让某些子类实现useWeapon(),而不是全部子类都要去实现useWeapon()。
okk的!useWeapon()既然不能放进接口里面,也不能放进父类里面,那我们就把这个方法单独提出来,新写一个useWeapon接口!
interface IUseWeapon {
fun useWeapon()
}
然后让有攻击功能的子类来实现IUseWeapon接口。
emmmmmmmmm……
这样使用接口还是得一个个去写子类实现的具体方法,而且也没有遵守依赖倒置原则,在上面我已经写过了,依赖倒置原则的核心思想就在于面向接口编程,那面向接口编程是个啥意思呢?
“针对接口编程”真正的意思是“针对超类型编程”。
- “针对接口编程”,关键就在于多态!利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死咋超类型的行为上。
- “针对超类型编程”这句话,可以更明确地说成变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口。
- 只要是具体实现此超类型的类,所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后会执行时的真正对象!
//针对实现编程
var magician : Magician = Magician()
//针对接口\超类型编程
var character : Character = Magician()
此时我们已经有了一个IUseWeapon接口,里面只有一个useWeapon()方法。我们不能用子类直接实现IUseWeapon,也不能用父类直接实现IUseWeapon,那我们就专门创建一个“行为类”来实现行为接口!
class UseFireWall : IUseWeapon {
override fun useWeapon() {
Log.i("TAG", "火墙风咆哮")
}
}
class UseDogBaby : IUseWeapon{
override fun useWeapon() {
Log.i("TAG", "召唤4级宝宝")
}
}
class UseFireKnife : IUseWeapon{
override fun useWeapon() {
Log.i("TAG", "刀刀烈火")
}
}
这样的设计,就让使用武器这个行为跟character类无关了,还可以被其他对象服复用。而新增一些使用武器行为时候,不会影响到既有的行为类,也不会影响使用到行为类的character类。
现在我们在整合一下整个人物的设计:
1. 拥有一个父类Character
2. 拥有四个子类,Magician, Taoist, Warrior, NPC
3. 有一个行为接口IUseWeapon
4. 有三个行为类实现了行为接口
目标:遵守六大设计原则的条件下,使Magician, Taoist, Warrior 才有useWeapon()
要实现攻击的功能,那父类肯定得有调用useWeapon()的方法,也必须得拥有行为接口。所以……
open class Character {
lateinit var iUseWeapon :IUseWeapon
fun coverUseWeapon(){
iUseWeapon.useWeapon()
}
}
在编译时,已经能通过charater.coverUseWeapon()调用使用武器的方法。在代码真正执行时,子类还没对iUseWeapon进行声明,所以在子类中要做的只是声明iUseWeapon而已。这个时候,行为类实现行为接口的好处就体现出来了,我们希望子类做什么样的攻击,就可以声明为什么样的行为类,要想有新的新的攻击动作,再创建一个行为类去实现IUseWeapon就可以了。反正实现的代码没有写在子类中,而是在行为类中,不用更改之前写的所有代码。
class Magician : Character(){
init {
iUseWeapon = UseFireWall()
}
}
美滋滋!就这么简单的一行代码!但这样还是不够灵活,我们还是在子类中做了一小部分的具体实现(创建iUseWeapon的实例),也就是没有完全的做到针对接口编程,所以我们需要有一个可以更改iUseWeapon实例的方法。
java代码中,我们可以在父类中加入set方法。
public void setIUseWeapon(iUseWeapon : IUseWeapon) {
this.iUseWeapon = iUseWeapon ;
}
kotlin代码中,直接在声明character对象处
//让魔法师召唤4级宝宝
var character : Character = Magician()
character.iUseWeapon = UseDogBaby()
子类里面的init方法,就可以完全不写了。
最后
相信各位看到这里对六大原则其中五个都有了一定的理解,剩下一个开闭原则没有提到,是因为,这玩意儿不好讲!
设计模式就是个经验性的东西,你完全可以不照着这样去写你的代码,只要遵守六大设计原则,都是好的设计。
以下是我“设计模式系列”文章,欢迎大家关注留言投币丢香蕉。
也可以进群跟大神们讨论。qq群:557247785
设计模式入门
Java与Kotlin的单例模式
Kotlin的装饰者模式与源码扩展
由浅到深了解工厂模式
为了学习Rxjava,年轻小伙竟作出这种事!
网友评论