[关闭]
@qidiandasheng 2021-01-04T10:44:19.000000Z 字数 6812 阅读 2029

面向对象之四大特性(😁)

架构


介绍

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)和对象(object)。面向对象编程从字面上,按照最简单、最原始的方式来理解,就是将对象或类作为代码组织的基本单元,来进行编程的一种编程范式或者编程风格,并不一定需要封装、抽象、继承、多态这四大特性的支持。但是,在进行面向对象编程的过程中,人们不停地总结 发现,有了这四大特性,我们就能更容易地实现各种面向对象的代码设计思路。

面向对象的四大特性:封装、抽象、继承、多态。

下载.jpeg-1182kB

封装(Encapsulation)

介绍

封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性可维护性

除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

苹果官方文档中,有一个配图生动的阐释了封装:
20141207104808375.gif-8.7kB

和面向过程的区别

封装是一种信息隐藏,需要把数据和方法放到一起,而C语言(面向过程)实现的代码,数据和方法是分离的。

优点

示例

这是一个分数类,计算平均分和总分的例子,这里不让成员变量暴露在外,一定程度上保证了数据的安全性。需要读取的两个数据设置为只读,只暴露设置分数的两个函数,在函数内部去做具体的实现(外部无需关心内部是怎么去计算总分和平均分的)。

  1. @interface Score : NSObject
  2. /// 总分
  3. @property(nonatomic, assign, readonly)int totalScore;
  4. /// 平均分
  5. @property(nonatomic, assign, readonly)int averageScoe;
  6. - (void)setCScore:(int)cScore;
  7. - (void)setOcScore:(int)ocScore;
  8. @end
  1. @interface Score(){
  2. int _cScore; // C语言成绩
  3. int _ocScore; // OC成绩
  4. }
  5. /// 总分
  6. @property(nonatomic, assign)int totalScore;
  7. /// 平均分
  8. @property(nonatomic, assign)int averageScoe;
  9. @end
  10. @implementation Score{
  11. int _totalScore;// 总分
  12. int _averageScoe; // 平均分
  13. }
  14. - (void)setCScore:(int)cScore{
  15. _cScore = cScore;
  16. // 计算总分
  17. _totalScore = _cScore + _ocScore;
  18. _averageScoe = _totalScore/2;
  19. }
  20. - (void)setOcScore:(int)ocScore{
  21. _ocScore = ocScore;
  22. // 计算总分
  23. _totalScore = _cScore + _ocScore;
  24. _averageScoe = _totalScore/2;
  25. }
  26. @end
  1. Score *score = [Score new];
  2. [score setCScore:70];
  3. [score setOcScore:90];
  4. NSLog(@"%d",score.totalScore);
  5. NSLog(@"%d",score.averageScoe);

抽象(Abstraction)

介绍

封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功 能,就可以直接使用了。

抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。

我们通常使用接口(Interface)或者或者抽象基类(Abstract Class)这两种语法机制,来实现抽象这一特性。OC中没有抽象基类和接口的存在,而是使用的协议

抽象类和接口的语法特性

抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。

  1. //Java实现抽象类
  2. public abstract class Logger {
  3. private String name;
  4. private boolean enabled;
  5. private Level minPermittedLevel;
  6. public Logger(String name, boolean enabled, Level minPermittedLevel) {
  7. this.name = name;
  8. this.enabled = enabled;
  9. this.minPermittedLevel = minPermittedLevel;
  10. }
  11. public void log(Level level, String message) {
  12. boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intVal)
  13. if (!loggable) return;
  14. doLog(level, message);
  15. }
  16. //抽象方法,子类必须实现
  17. protected abstract void doLog(Level level, String message);
  18. }
  19. // 抽象类的子类:输出日志到文件
  20. public class FileLogger extends Logger {
  21. private Writer fileWriter;
  22. public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filepath) { super(name, enabled, minPermittedLevel);
  23. this.fileWriter = new FileWriter(filepath);
  24. }
  25. @Override
  26. public void doLog(Level level, String mesage) {
  27. // 格式化 level 和 message, 输出到日志文件
  28. fileWriter.write(...);
  29. }
  30. }

接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。

  1. // Java实现接口
  2. public interface Filter {
  3. void doFilter(RpcRequest req) throws RpcException;
  4. }
  5. // 接口实现类:鉴权过滤器
  6. public class AuthencationFilter implements Filter {
  7. @Override
  8. public void doFilter(RpcRequest req) throws RpcException {
  9. //... 鉴权逻辑..
  10. }
  11. }
  12. // 接口实现类:限流过滤器
  13. public class RateLimitFilter implements Filter {
  14. @Override
  15. public void doFilter(RpcRequest req) throws RpcException {
  16. //... 限流逻辑...
  17. }
  18. }

抽象类和接口的区别

示例

下面定义了三种动物,人、鱼、青蛙,差异比较大,这里的共性我需要他们游泳,所以使用接口(OC协议)来实现抽象。如果没有协议进行抽象,下面在遍历数组时就需要if判断对象属于哪个类型来调用函数方法。

  1. @protocol SportsProtocol <NSObject>
  2. - (void)swimming;
  3. @end
  1. @interface Human : NSObject<SportsProtocol>
  2. @end
  3. @implementation Human
  4. - (void)swimming{
  5. NSLog(@"人游泳");
  6. }
  7. @end
  8. @interface Fish : NSObject<SportsProtocol>
  9. @end
  10. @implementation Fish
  11. - (void)swimming{
  12. NSLog(@"鱼游泳");
  13. }
  14. @interface Frog : NSObject<SportsProtocol>
  15. @end
  16. @end
  17. @implementation Frog
  18. - (void)swimming{
  19. NSLog(@"青蛙游泳");
  20. }
  21. @end
  1. Human *human = [Human new];
  2. Fish *fish = [Fish new];
  3. Frog *frog = [Frog new];
  4. NSArray *animalArr = @[human,fish,frog];
  5. for (id<SportsProtocol> animal in animalArr) {
  6. [animal swimming];
  7. }

下面这个例子定义了鲤鱼、鲫鱼、草鱼三种鱼,也要游泳。对象之间的共性(都是鱼)大于个性(略微不同的游泳方式)。所以使用抽象基类来实现抽象(基类使用协议)。

  1. @protocol SportsProtocol <NSObject>
  2. - (void)swimming;
  3. @end
  4. @interface Fish : NSObject<SportsProtocol>
  5. @end
  6. @implementation Fish
  7. @end
  1. @interface Carp : Fish
  2. @end
  3. @implementation Carp
  4. - (void)swimming{
  5. NSLog(@"鲤鱼游泳");
  6. }
  7. @end
  8. @interface Crucian : Fish
  9. @end
  10. @implementation Crucian
  11. - (void)swimming{
  12. NSLog(@"鲫鱼游泳");
  13. }
  14. @end
  15. @interface GrassCarp : Fish
  16. @end
  17. @implementation GrassCarp
  18. - (void)swimming{
  19. NSLog(@"草鱼游泳");
  20. }
  21. @end
  1. Carp *carp = [Carp new];
  2. Crucian *crucian = [Crucian new];
  3. GrassCarp *grassCarp = [GrassCarp new];
  4. NSArray *fishArr = @[carp,crucian,grassCarp];
  5. for (Fish *fish in fishArr) {
  6. [fish swimming];
  7. }

继承(Inheritance)

介绍

继承是用来表示类之间的is-a关系,比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承 和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。

继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。

优点

继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。

缺点

过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码, 还需要按照继承关系一层一层地往上查看“父类、父类的父类......”的代码。还有,子类和 父类高度耦合,修改父类的代码,会直接影响到子类。

示例

下面是一个鸟类继承的例子,当我们定义一个鸟类的基类时可能会因为各种不同的特性分出不同的继承类,比如下图鸟类下面分为会飞的和不会飞的,然后再下一层分会叫和不会叫,如果在考虑一层会生蛋和不会生蛋,那我们的继承关系就会爆炸了。

类的继承层次 会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码......一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦 父类代码修改,就会影响所有子类的逻辑。

下载.jpeg-129.1kB

利用组合(composition)、接口、委托(delegation)三个技术手 段,一块儿来解决刚刚继承存在的问题:

  1. //接口
  2. public interface Flyable {
  3. void fly();
  4. }
  5. //实现类
  6. public class FlyAbility implements Flyable {
  7. @Override
  8. public void fly() { //... }
  9. }
  10. // 省略 Tweetable/TweetAbility/EggLayable/EggLayAbility
  11. public class Ostrich implements Tweetable, EggLayable {// 鸵鸟
  12. private TweetAbility tweetAbility = new TweetAbility(); // 组合
  13. private EggLayAbility eggLayAbility = new EggLayAbility(); // 组合
  14. //... 省略其他属性和方法...
  15. @Override
  16. public void tweet() {
  17. tweetAbility.tweet(); // 委托
  18. }
  19. @Override
  20. public void layEgg() {
  21. eggLayAbility.layEgg(); // 委托
  22. }
  23. }

我们知道继承主要有三个作用:表示is-a关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的has-a关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目 中不用或者少用继承关系,特别是一些复杂的继承关系。

多态(Polymorphism)

介绍

多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的扩展性复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

示例

上面抽象中的第二个例子就是使用的多态,我们在for循环中调用Fish父类的swimming方法,得到了不同的响应。

OC指针类型的变量有两个,一个是编译时类型,一个是运行时类型;编译时类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定;如果编译时类型和运行时类型不一致,当访问同一个方法时会做出不同的响应,这时就会出现多态。

  1. Fish *carp = [Carp new];
  2. [carp swimming];
  3. Fish *crucian = [Crucian new];
  4. [carp swimming];
  5. Fish *grassCarp = [GrassCarp new];
  6. [grassCarp swimming];
  1. //输出
  2. 鲤鱼游泳
  3. 鲫鱼游泳
  4. 草鱼游泳
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注