@qidiandasheng
2021-01-04T10:44:19.000000Z
字数 6812
阅读 1993
架构
面向对象编程——Object Oriented Programming
,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)和对象(object)。面向对象编程从字面上,按照最简单、最原始的方式来理解,就是将对象或类作为代码组织的基本单元,来进行编程的一种编程范式或者编程风格,并不一定需要封装、抽象、继承、多态这四大特性的支持。但是,在进行面向对象编程的过程中,人们不停地总结 发现,有了这四大特性,我们就能更容易地实现各种面向对象的代码设计思路。
面向对象的四大特性:封装、抽象、继承、多态。
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。
除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。
苹果官方文档中,有一个配图生动的阐释了封装:
和面向过程的区别
封装是一种信息隐藏,需要把数据和方法放到一起,而C语言(面向过程)实现的代码,数据和方法是分离的。
良好的封装能够减少耦合。
类内部的结构可以自由修改。
可以对成员进行更精确的控制。
隐藏信息,实现细节。
这是一个分数类,计算平均分和总分的例子,这里不让成员变量暴露在外,一定程度上保证了数据的安全性。需要读取的两个数据设置为只读,只暴露设置分数的两个函数,在函数内部去做具体的实现(外部无需关心内部是怎么去计算总分和平均分的)。
@interface Score : NSObject
/// 总分
@property(nonatomic, assign, readonly)int totalScore;
/// 平均分
@property(nonatomic, assign, readonly)int averageScoe;
- (void)setCScore:(int)cScore;
- (void)setOcScore:(int)ocScore;
@end
@interface Score(){
int _cScore; // C语言成绩
int _ocScore; // OC成绩
}
/// 总分
@property(nonatomic, assign)int totalScore;
/// 平均分
@property(nonatomic, assign)int averageScoe;
@end
@implementation Score{
int _totalScore;// 总分
int _averageScoe; // 平均分
}
- (void)setCScore:(int)cScore{
_cScore = cScore;
// 计算总分
_totalScore = _cScore + _ocScore;
_averageScoe = _totalScore/2;
}
- (void)setOcScore:(int)ocScore{
_ocScore = ocScore;
// 计算总分
_totalScore = _cScore + _ocScore;
_averageScoe = _totalScore/2;
}
@end
Score *score = [Score new];
[score setCScore:70];
[score setOcScore:90];
NSLog(@"%d",score.totalScore);
NSLog(@"%d",score.averageScoe);
封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功 能,就可以直接使用了。
抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。
我们通常使用接口(Interface)或者或者抽象基类(Abstract Class)这两种语法机制,来实现抽象这一特性。OC中没有抽象基类和接口的存在,而是使用的协议。
抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。
//Java实现抽象类
public abstract class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}
public void log(Level level, String message) {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intVal)
if (!loggable) return;
doLog(level, message);
}
//抽象方法,子类必须实现
protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;
public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filepath) { super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
}
@Override
public void doLog(Level level, String mesage) {
// 格式化 level 和 message, 输出到日志文件
fileWriter.write(...);
}
}
接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。
// Java实现接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//... 鉴权逻辑..
}
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//... 限流逻辑...
}
}
当在共性较多的对象间寻求功能上的差异时,使用抽象基类。抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。
当在差异较大的对象间寻求功能上的共性时,使用接口。接口仅 仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再 抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我 们在编程的时候,一般都是先设计接口,再去考虑具体的实现。
下面定义了三种动物,人、鱼、青蛙,差异比较大,这里的共性我需要他们游泳,所以使用接口(OC协议)来实现抽象。如果没有协议进行抽象,下面在遍历数组时就需要if判断对象属于哪个类型来调用函数方法。
@protocol SportsProtocol <NSObject>
- (void)swimming;
@end
@interface Human : NSObject<SportsProtocol>
@end
@implementation Human
- (void)swimming{
NSLog(@"人游泳");
}
@end
@interface Fish : NSObject<SportsProtocol>
@end
@implementation Fish
- (void)swimming{
NSLog(@"鱼游泳");
}
@interface Frog : NSObject<SportsProtocol>
@end
@end
@implementation Frog
- (void)swimming{
NSLog(@"青蛙游泳");
}
@end
Human *human = [Human new];
Fish *fish = [Fish new];
Frog *frog = [Frog new];
NSArray *animalArr = @[human,fish,frog];
for (id<SportsProtocol> animal in animalArr) {
[animal swimming];
}
下面这个例子定义了鲤鱼、鲫鱼、草鱼三种鱼,也要游泳。对象之间的共性(都是鱼)大于个性(略微不同的游泳方式)。所以使用抽象基类来实现抽象(基类使用协议)。
@protocol SportsProtocol <NSObject>
- (void)swimming;
@end
@interface Fish : NSObject<SportsProtocol>
@end
@implementation Fish
@end
@interface Carp : Fish
@end
@implementation Carp
- (void)swimming{
NSLog(@"鲤鱼游泳");
}
@end
@interface Crucian : Fish
@end
@implementation Crucian
- (void)swimming{
NSLog(@"鲫鱼游泳");
}
@end
@interface GrassCarp : Fish
@end
@implementation GrassCarp
- (void)swimming{
NSLog(@"草鱼游泳");
}
@end
Carp *carp = [Carp new];
Crucian *crucian = [Crucian new];
GrassCarp *grassCarp = [GrassCarp new];
NSArray *fishArr = @[carp,crucian,grassCarp];
for (Fish *fish in fishArr) {
[fish swimming];
}
继承是用来表示类之间的is-a关系,比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承 和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码, 还需要按照继承关系一层一层地往上查看“父类、父类的父类......”的代码。还有,子类和 父类高度耦合,修改父类的代码,会直接影响到子类。
下面是一个鸟类继承的例子,当我们定义一个鸟类的基类时可能会因为各种不同的特性分出不同的继承类,比如下图鸟类下面分为会飞的和不会飞的,然后再下一层分会叫和不会叫,如果在考虑一层会生蛋和不会生蛋,那我们的继承关系就会爆炸了。
类的继承层次 会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码......一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦 父类代码修改,就会影响所有子类的逻辑。
利用组合(composition)、接口、委托(delegation)三个技术手 段,一块儿来解决刚刚继承存在的问题:
//接口
public interface Flyable {
void fly();
}
//实现类
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
// 省略 Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {// 鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); // 组合
private EggLayAbility eggLayAbility = new EggLayAbility(); // 组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
我们知道继承主要有三个作用:表示is-a
关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的has-a
关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目 中不用或者少用继承关系,特别是一些复杂的继承关系。
多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。
上面抽象中的第二个例子就是使用的多态,我们在for循环中调用Fish父类的swimming方法,得到了不同的响应。
OC指针类型的变量有两个,一个是编译时类型,一个是运行时类型;编译时类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定;如果编译时类型和运行时类型不一致,当访问同一个方法时会做出不同的响应,这时就会出现多态。
Fish *carp = [Carp new];
[carp swimming];
Fish *crucian = [Crucian new];
[carp swimming];
Fish *grassCarp = [GrassCarp new];
[grassCarp swimming];
//输出
鲤鱼游泳
鲫鱼游泳
草鱼游泳