Java中
一个软件设计的好坏,我想很大程度上取决于它的整体架构,而这个整体架构其实就是你对整个宏观商业业务的抽象框架,当代表业务逻辑的高层抽象层结构 合理时,你底层的具体实现需要考虑的就仅仅是一些算法和一些具体的业务实现了。当你需要再开发另一个相近的项目时,你以前的抽象层说不定还可以再次利用 。
面对对象的设计,复用的重点其实应该是抽象层的复用,而不是具体某一个代码块的复用。
说到了抽象,我就不能不提到曾让我头痛的Java接口和Java抽象类了,这也是本文我想说的重点。
既然面向对象设计的重点在于抽象,那Java接口和Java抽象类就有它存在的必然性了。
Java接口(interface)和Java抽象类(abstract class)代表的就是抽象类型,就是我们需要提出的抽象层的具体表现。OOP面向对象的编程,如果要提高程序的复用率,增加程序 的可维护性,可扩展性,就必须是面向接口的编程,面向抽象的编程,正确地使用接口、抽象类这些有用的抽象类型作为你结构层次上的顶层。
Java接口和Java抽象类有太多相似的地方,又有太多特别的地方,究竟在什么地方,才是它们的最佳位置呢?把它们比较一下,你就可以发现了。
- Java接口和Java抽象类最大的一个区别,就在于Java抽象类可以提供某些方法的部分实现,而Java接口不可以(就是interface中只能定义方法,而不能有方法的实现,而在abstract class中则可以既有方法的具体实现,又有没有具体实现的抽象方法),这大概就是Java抽象类唯一的优点吧,但这个优点非常有用。如果向一个抽象类里加入一个新的具体方法时,那么它所有的子类都一下子都得到了这个新方法,而Java接口做不到这一点,如果向一个Java接口里加入一个 新方法,所有实现这个接口的类就无法成功通过编译了,因为你必须让每一个类都再实现这个方法才行,这显然是Java接口的缺点。这个在我的另外一篇博客mapreduce 新旧API 区别中有提到类似的问题,在新的mapreduce api中更倾向于使用抽象类,而不是接口,因为这更容易扩展。原因就是上面划线部分所说的。
- 一个抽象类的实现只能由这个抽象类的子类给出,也就是说,这个实现处在抽象类所定义出的继承的等级结构中,而由于Java语言的单继承性,所以抽象类作为类型定义工具的效能大打折扣。在这一点上,Java接口的优势就出来了,任何一个实现了一个Java接口所规定的方法的类都可以具有这个接口的类型,而一个类可以实现任意多个Java接口,从而这个类就有了多种类型。(使用抽象类,那么继承这个抽象类的子类类型就比较单一,因为子类只能单继承抽象类;而子类能够同时实现多个接口,因为类型就比较多。接口和抽象类都可以定义对象,但是只能用他们的具体实现类来进行实例化。)
- 从第2点不难看出,Java接口是定义混合类型的理想工具,混合类表明一个类不仅仅具有某个主类型的行为,而且具有其他的次要行为。
- 结合1、2点中抽象类和Java接口的各自优势,具精典的设计模式就出来了:声明类型的工作仍然由Java接口承担,但是同时给出一个Java 抽象类,且实现了这个接口,而其他同属于这个抽象类型的具体类可以选择实现这个Java接口,也可以选择继承这个抽象类,也就是说在层次结构中,Java 接口在最上面,然后紧跟着抽象类,这下两个的最大优点都能发挥到极至了。这个模式就是“缺省适配模式”。在Java语言API中用了这种模式,而且全都遵循一定的命名规范:Abstract +接口名。(A extends AbstractB implements interfaceC,那么A即可以选择实现(@Override)接口interfaceC中的方法,也可以选择不实现;A即可以选择实现(@Override)抽象类AbstractB中的方法,也可以选择不实现)
Java接口和Java抽象类的存在就是为了用于具体类的实现和继承的,如果你准备写一个具体类去继承另一个具体类的话,那你的设计就有很大问题了。Java抽象类就是为了继承而存在的,它的抽象方法就是为了强制子类必须去实现的。
使用Java接口和抽象Java类进行变量的类型声明、参数是类型声明、方法的返还类型说明,以及数据类型的转换等。而不要用具体Java类进行变量的类型声明、参数是类型声明、方法的返还类型说明,以及数据类型的转换等。
面向对象概述
为什么要讲面向对象
- TS为前端面向对象开发带来了契机
JS语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误。尽管可以使用注释、文档、记忆力,但是它们没有强约束力
TS带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是具有强约束力的。
- 面向对象中有许多非常成熟的模式,能处理复杂问题。
在过去很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验,这很宝贵
虽然函数式编程等方式也可以处理复杂问题,但它需要大量的积累。
nest.js:相当于是前端的 JAVA Spring。它也是基于TS
typeorm:ORM框架,类似于C# EF。还比如:mongoose
什么是面向对象
学开发最重要、最难的是什么?编程的思维
编程思想:
面向过程:一切以功能流程为思考切入点,但大型项目流程会经常发生变动且复杂,故不太适用
函数式编程:以数学运算为思考切入点,适合大型项目
面向对象:Oriented(基于) Object(事物),简称 OO。
- 是一种编程思想,它提出一切以对象为切入点思考问题。
- 或者说,以划分类为思考切入点。类是最小的功能单元
类:是可以产生对象的模板
人(就是一个类):特征:鼻子、眼睛、四肢、性别.. 动作:说话,运动,制造工具
new 人1(),new 人2()
如何学习
- TS中的OOP(面向对象编程,Oriented Object Programing)
- 完整的小游戏练习(中国象棋其实最能锻炼)
理解 -> 想法 -> 实践 -> 进一步理解 -> …不断循环…
类的继承
继承的作用
继承可以描述类与类之间的关系
玩家坦克是坦克,敌方坦克是坦克
如果A和B都是类,并且可以描述为A是B,则A和B形成继承关系:
- B是父类,A是子类
- B派生A,A继承自B
- B是A的基类,A是B的派生类
作用:如果A继承自B,则A中自动拥有B中的所有成员
在markdown中也可以画图,语法如下:puml<br />@startuml<br />Tank <|-- PlayerTank<br />Tank <|-- EnemyTank<br />EnemyTank <|-- BossTank<br />@enduml<br />
成员的重写
重写override:子类中可以覆盖父类中的成员,但是!子类成员不能改变父类成员的类型!
即:无论是属性还是方法,子类都可以对父类相应的成员进行重写,但是一定要保证类型的匹配!
注意this关键字:在继承关系中,this的指向是动态的——调用方法时,根据调用者确定this的指向
super关键字:在子类的方法中,可以使用super关键字读取父类成员
类型匹配
遵循鸭子辨型法
子类的对象,始终可以赋值给类型约束为父类的变量
面向对象中,这种现象,叫做 里氏替换原则
如果需要判断一个数据的具体子类类型,可以使用 instanceof
protected修饰符
readonly:只读修饰符
访问权限修饰符:private、public、protected
protected:受保护的成员。受此修饰的成员,只能在其类自身和其类的子类中使用
单根性和传递性
单根性:每个类最多只有一个父类
如果想要多继承的效果,js可以使用混入,也挺方便
传递性:如果A时B的父类,并且B是C的父类,则可以认为A也是C的父类
抽象类
为什么需要抽象类
puml<br />@startuml<br />棋子 <|-- 马<br />棋子 <|-- 兵<br />棋子 <|-- 炮<br />@enduml<br />
棋子是一个抽象的概念,不应该创建棋子对象
有时,某个类只表示一个抽象概念,主要用于提取子类共有的成员,而不能直接创建它的对象,该类可以作为抽象类。TS中可以在类前加上 关键字abstract 即代表该类是一个抽象类,无法创建抽象类的实例
抽象成员
父类中,可能知道有些成员是必须存在的,但是不知道该成员的值具体是什么。因此需要有一种强约束,让继承该类的子类,必须要实现该成员。
抽象类中,可以有抽象成员(前面加abstract关键字),这些抽象成员必须在子类中实现
抽象类的继承
设计模式 之 模版模式
设计模式:面对一些常见的功能场景,有一些固定的、经过多年实践的成熟的方法,这些方法称之为设计模式
模版模式:有些方法,所有的子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法
abstract class Chess {
x: number = 0;
y: number = 0;
abstract readonly name: string;
move(targetX: number, targetY: number): boolean{
console.log('1. 边界判断')
console.log('2. 目标位置是否有己方棋子')
if(this.rule(targetX, targetY)){
console.log('3. 棋子移动规则判断');
this.x = targetX;
this.y = targetY;
console.log(`${this.name} 移动成功!\n`);
return true;
}
console.log(`${this.name} 移动失败!\n`)
return false;
}
// 因为我不清楚每种棋子的规则啊
protected abstract rule(targetX: number, targetY: number): boolean;
}
class Horse extends Chess {
protected rule(targetX: number, targetY: number): boolean {
return true;
}
readonly name: string = '马';
}
class Artillery extends Chess {
protected rule(targetX: number, targetY: number): boolean {
return false;
}
readonly name: string
constructor(){
super();
this.name = '炮';
}
}
class Soldier extends Chess {
protected rule(targetX: number, targetY: number): boolean {
return true;
}
// name: string = '兵' // 可以去掉readonly但是此处不符合实际
get name(){
return '兵';
}
}
// const c = new Chess();// 这不应该被创建,棋子只是一个抽象概念
const h = new Horse();
const p = new Artillery();
const s = new Soldier();
console.log(h.name, p.name, s.name)
h.move(3, 5);
p.move(1, 3);
s.move(2, 4);
// 几年后,我啥都忘了,就记得个Chess,没关系,TS有提示和一键修复
class King extends Chess {
name: string = '帅';
protected rule(targetX: number, targetY: number): boolean {
throw new Error("Method not implemented.");
}
}
静态成员
什么是静态成员
静态成员是指,附着在类本身上的成员(在js中就是,属于构造函数的成员,比如Number.isNaN)
使用static修饰的成员就是静态成员
实例成员:对象成员,属于某个类的对象
静态成员:非实例成员,属于某个类
静态方法中的this
实例方法中的this指向的是当前对象
而静态方法中的this指向的是当前类
设计模式 之 单例模式
单例模式:某些类的对象,在系统中最多只能有一个,为了避免开发者随意创建多个该类的对象,可以使用单例模式进行强约束
比如:一个棋盘、一个播放器窗口、一个配置对象,此时就可以用单例模式
重点在于 私有化constructor,然后内部创建的唯一的实例要静态化
// 因为棋盘只需要创建一个。播放器
class Board {
private width: number = 500;
private height: number = 700;
init(){
console.log('初始化棋盘')
}
private constructor(){
this.init()
}
// 这种写法的缺陷是,我们不能控制创建棋盘的时机
// static readonly singleBoard = new Board();
private static _board?: Board;
static createBoard(): Board{
if(this._board){
return this._board;
}else{
this._board = new Board();
return this._board;
}
}
}
// const b1 = new Board(); // constructor私有化后,不能这么做了
// const b2 = new Board();// 这是不合理,没意义的
// const b1 = Board.singleBoard;
// const b2 = Board.singleBoard;
const b1 = Board.createBoard();
const b2 = Board.createBoard();
console.log(b1 === b2)
再谈接口
接口用于 约束类、对象、函数,是一个类型契约。
有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,他们各自有各自的技能,技能是可以通过训练改变的
马戏团中有以下常见的技能:
- 火圈表演:单火圈、双火圈 —— 狮子、老虎
- 平衡表演:独木桥、走钢丝 —— 猴子
- 智慧表演:算术题、跳 舞 —— 狗
不使用接口实现时:
- 对动物的能力(成员函数)没有强约束力
- 容易将动物类型和动物能力耦合在一起
系统中缺少对能力的定义 ——>> 接口
面向对象领域中的接口的定义:表达了某个类是否拥有某种能力
某个类具有某种能力,实际上就是某个类实现了(implements)某种接口
类型保护函数
由于TS最终仍会编译为JS,运行态仍是js在处理,所以有些情况仍然不能完美处理,所以产生了类型保护函数,虽然仍是繁复了些,但终归是可以这么写代码了。
// 类型保护函数
function hasFireShow(anim: object): anim is IFireShow {
if((anim as unknown as IFireShow).singleFire && (anim as unknown as IFireShow).doubleFire){
return true;
}
return false;
}
// 2. 所有会火圈表演的动物完成火圈表演
animals.forEach(a => {
//if(a instanceof IFireShow){}// java可以这么写,很遗憾TS不能这样,因为最后运行的还是js,而接口不存在于js中
//目前TS对这里没有特别好的办法处理,因为最终运行时的仍为js
if(hasFireShow(a)){
a.singleFire();
a.doubleFire();
}
})
通过调用该函数,会触发TS的类型保护,该函数必须返回boolean
接口和类型别名最大的区别:接口可以被类实现,而类型别名不可以!而且类型别名不能继承类!
接口可以继承类
表示该类的所有成员都在接口中。
索引器
索引器写法:对象[值] 以前叫做 成员表达式
在TS中,默认情况下,不对索引器(成员表达式)做严格的类型检查,因为它是不确定的
- 使用配置 noImplicitAny 开启对隐式any的检查
隐式any:TS根据实际情况推导出的any类型
在索引器中,键的类型必须是字符串或者是数字
- 在类中,索引器应当书写到所有成员之前!
TS中索引器到作用
- 在严格的检查下,可以实现为类动态的添加成员
- 可以实现动态的操作类成员
在JS中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串
在TS中,如果某个类中,使用了两种类型的索引器,要求两种索引器的值必须匹配(键为number类型的值的类型,必须是键为string类型的值的类型的子类型!)
const methodName = 'sayHello'
class User2 {
// 只要是成员属性就会被这条所约束,但是这个类型改为了any,就意味着什么成员都可以写了
// 如果索引签名的键为number,也会被识别为string类型,因为它本质就是
[prop: string]: any
1 = 2 // 编译到js就是this[1] = 2
constructor(
public name: string,
public age: number,
) {
console.log('start')
}
[methodName]() {
console.log('a')
}
}
const u = new User2('sdf', 18)
u['123'] = 3
u[methodName]()
console.log(u[1]=888)//
console.log(u.name, u.age)
console.log(u.pid)
this指向约束
https://yehudakatz.com/2011/08/10/understanding-javascript-function-invocation-and-this/
在JS中this指向的几种情况
明确:大部分时候,this的指向取决于函数的调用方式
- 如果直接调用函数(全局调用),this指向全局对象 或 undefined—启用严格模式
- 如果使用 对象.方法 调用,this指向对象本身
- 如果是DOM事件的处理函数,this指向事件处理对象
- ES6+ 在类中,默认开启严格模式
特殊情况:
- TS对于对象字面量里的this,被判定为any
- TS对于类中的this,被判定为类的this
配置 noImplicitThis 为 true,表示不允许this隐式的指向any
在TS中,允许在书写函数时,手动声明该函数中this的指向。将this作为函数第一个参数(但它实际上不是参数,所以不会出现在编译的js结果中),并给出约束的类型即可。
interface IUser {
name: string,
age: number,
sayHello2(this: IUser): void,
}
const w: IUser = {
name: 'ssf',
age: 33,
sayHello2(){
console.log(this, this.name, this.age)// this被判定为any
}
}
w.sayHello2();