面向对象思想理论

文章目录

  • 面向对象的四大特性
  • 面向对象和面向过程的比较
  • 接口和抽象类
    • 定义比较
    • 出发点
    • 抽象类设计思路
  • 模拟抽象类和借口
    • 接口模拟
    • 模拟抽象类
  • 基于接口而非实现编程
  • 多用组合少用继承
    • 为什么不推荐使用继承
    • 组合相比继承有哪些优势?

面向对象的四大特性

  • 封装性:信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据
  • 抽象性:如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的
  • 继承:表示类之间is-a关系,代码复用
  • 多态:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现

面向对象和面向过程的比较

  • 什么是面向过程编程与面向过程编程语言?
    1. 面向对象编程是以类为基本单元,而面向过程编程则是以过程(或方法)作为组织代码的基本单元,最重要的特点是数据和方法分离。
    2. 不支持丰富的面向对象编程特性,如继承,多态,封装。
  • 面向对象编程相比面向过程编程有哪些优势?
    1. 对于大规模复杂程序的开发,程序的处理流程并非单一的是一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发
    2. 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
  • 为什么说面向对象编程语言比面向过程编程语言更高级?
    1. 更接近人的思维逻辑,将事务抽象为类,再去逐步完善行为。而面向过程则更加接近底层,需要我们有计算机思维。
  • 有哪些看似是面向对象实际上是面向过程风格的代码?
    1. 滥用get,set
    2. 滥用全局变量和全局方法。只包含静态方法而不包含任何属性的utils类,是彻彻底底的面向过程的编程风格。
    3. 定义数据和方法分离的类,如mvc编程(贫血模型)
  • 在面向对象编程中,为什么容易写出面向过程风格的代码?
    1. 思维模式的不同,面向过程是一步步按照顺序来,面向对象是一种自底而上的思考方式,不是按照执行流程去分解任务,而是将任务翻译成一个个小的模块,模块之间进行交互
  • 面向过程编程和面向过程语言就真的没有用武之地了吗?
    1. 看使用场景,如计算为主,数据为辅的情况下,就可以使用面向过程

接口和抽象类

定义比较

抽象类:(is-a)

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

接口类:(has-a)

  1. 接口不能包含属性(也就是成员变量)
  2. 接口只能声明方法,方法不能包含代码实现
  3. 类实现接口的时候,必须实现接口中声明的所有方法

出发点

抽象类是多个子类代码重复,对整体重复代码抽离出来,是自下而上的。
接口偏重于解耦,是对行为的抽象,相对于一组协议或者契约,构建接口经常是自上而下的。

抽象类设计思路

public 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;
    }
    
    protected boolean isLoggable(Level level) {
        boolean loggable = enabled & (minPermittedLevel.intValue <= level.intValue);
        return loggable;
    }
}


// 文件子类

class FileLogger extends Logger {
    private Writer writer;
    private String filePath;

    public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) {
        super(name, enabled, minPermittedLevel);
        this.filePath = filePath;
    }
    
    public void log(Level level, String message) {
        if (!isLoggable(level)); return;
        writer.write(message);
    }
}

// 中间间输出子类
class MessageQueueLogger extends Logger {
    private MessageQuesueClient messageQuesueClient;
    
    public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQuesueClient messageQuesueClient) {
        super(name, enabled, minPermittedLevel);
        this.messageQuesueClient = messageQuesueClient;
    }

    public void log(Level level, String message) {
        if (!isLoggable(level)); return;
        messageQuesueClient.send(message);
    }
}

固然说这样子达到了代码复用的目的,但是已经无法使用多态特性了,不能用logger去调用log方法
还有一种,就是写一个空log(),但是相对于抽象类而言,还是脱了裤子放屁,而且定义一个空方法,其他人也不容易去理解,也有可能忘记了该方法的实现

模拟抽象类和借口

接口模拟

接口就是没有定义成员变量,只有方法声明,没有方法实现

  1. 使用抽象类模拟
class Strategy {
	public:
		~Strategy();
		virtual void algorithm()=0;
	protected:
		Strategy(); 
}

没有定义任何属性,且所有方法都声明为virtual类型,这样所有方法都没有代码实现,且继承这个抽象类的子类都必须实现这些方法,从语法特性来说,相当于一个接口。

  1. 用普通类模拟接口
public calss MockInterface {
	protected MockInterface() {}
	public void funcA() {
		throws new MethodUnSupportedException();
	}
}

该类构造方法是protected,只可以继承。且不重写funA,会抛出MethodUnSupportedException异常,这样就强迫子类在继承这个夫类时,都要主动实现夫类的方法。

模拟抽象类

抽象类的概念是有属性,有方法,可以有方法没有实现

用普通类实现抽象类

public calss MockInterface {
	private int value;
	protected MockInterface() {}
	public void funcA() {
		throws new MethodUnSupportedException();
	}
}

和模拟接口类似,将构造方法保护起来,只能通过继承来实现,且未实现方法需要定义抛出异常

基于接口而非实现编程

广泛的对于接口的定义: 一组协议或者约定,是功能提供者提供给使用者的一个功能列表

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

以图片上传为例
现在有一个向阿里云上传图片的类,如下

public class AliyunImageStore {
    // ... 省略属性、构造方法....

    public void createBucketIfNotExisting(String bucketName) {
        // 创建bucket代码逻辑。。。
        // 。。。失败抛出异常
    }

    public String generateAccessToken() {
        // 根据 accesskey / secretkey 等生成access token
    }

    public String uploadToAliyun(Image image, String bucketName, String accessToken) {
        // 上传图片到阿里云
        // 返回图片储存地址
    }

    public Image downloadFromAliyun(String url, String accessToken) {
        // 从阿里云下载图片返回
    }
}


// AliyunImageStore类使用实例
public class ImageProcessingJob {
    private static final String BUCKET_NAME = "al_image_bucket";
    // .....


    public void process() {
        Image image = ...; // 处理图片,并封装为Image对象
        AliyunImageStore imageStore = new AliyunImageStore();
        imageStore.createBucketIfNotExisting(BUCKET_NAME);
        String accessToken = imageStore.generateAccessToken();
        imageStore.uploadToAliyun(image, BUCKET_NAME, accessToken);
    }

}

上述流程分为了三个步骤:创建bucket(存储目录),生成accessToken,携带token上传图片到指定的buckey中。整个流程看起来没有什么问题。

现在不往阿里云上放东西了,改成私有云了,那这个类就需要进行大改,如生成token,类名,方法名等。而且我们对这个行为进行抽象,他们都具有上传和下载功能,姑可以将该方法抽象出来。

public interface ImageStore {
    String uploadImage(Image image, String bucketName);
    Image download(String url);
}

// 阿里云存储
public class AliyunImageStore implements ImageStore{
    // ... 省略属性、构造方法....

    public void createBucketIfNotExisting(String bucketName) {
        // 创建bucket代码逻辑。。。
        // 。。。失败抛出异常
    }

    public String generateAccessToken() {
        // 根据 accesskey / secretkey 等生成access token
    }
    
    @Override
    public String uploadImage(Image image, String bucketName) {
        createBucketIfNotExisting(bucketName);
        String token = generateAccessToken();
        // 上传图片到阿里云
        // 返回图片储存地址
    }

    @Override
    public Image download(String url) {
        String token = generateAccessToken();
        // 从阿里云下载图片返回
    }
}


// 本地云存储
public class PrivateImageStore implements ImageStore{

    public void createBucketIfNotExisting(String bucketName) {
        // 创建bucket代码逻辑。。。
        // 。。。失败抛出异常
    }
    @Override
    public String uploadImage(Image image, String bucketName) {
        createBucketIfNotExisting(bucketName);
        // 上传图片到私有云
        // 返回地址
    }

    @Override
    public Image download(String url) {
        // 从私有云下载图片。。。
    }
}


// 调用
public class ImageProcessingJob {
    private static final String BUCKET_NAME = "al_image_bucket";
    // .....


    public void process() {
        Image image = ...; // 处理图片,并封装为Image对象
        ImageStore imageStore = new AliyunImageStore();
        imageStore.uploadImage(image, BUCKET_NAME);
    }

}

注意:

  1. 最好不要将类写完后反推接口,这可能导致接口定义不够抽象,依赖具体的实现
  2. 只有当系统存在不稳定的实现时,需要将其抽象出来,当系统问题,不需要每一个类都去定义接口。
  3. 定义接口时,一方面,命名要足够通用,不能包换跟具体实现相关的字眼;另一方面,于特定实现有关的方法不要定义在接口中

上述的使用也是存在问题的

ImageStore imageStore = new AliyunImageStore(); //更换图片存储方法不方便

解决思路:

  • 配置文件
  • 构造方法传参
  • 工厂模式,ImageStore imageStore = ImageStoreFactory.newInstance(STORE_TYPE_CONFIG);
  • 依赖注入
  • 策略模式,使用一个Context类,使用聚合持有这个接口实例饮用,其他地方都用这个context类,变动只改动这个context类

多用组合少用继承

为什么不推荐使用继承

继承解决了代码复用问题,但是如果继承层次过深,过复杂,也会影响到代码的可维护性。

例如要设计一个鸟类这样的一个抽象的事物概念,定义为一个抽象类AbstractBird。
现在有需求:鸟会飞,会跑,会下蛋,我们是否要定义这些方法
那不会飞,不会跑,不会下蛋的鸟也很多,那该怎么办呢?
就拿飞举例,给不会飞的鸟抛出异常,这固然是一种方法,还有就是再通过AbstractBird派生出两个更加细分的类,会飞的鸟和不会飞的鸟,哪再加上会跑,2*2就是要定义4个了,再有具体的要求,结构就越来越复杂,而且更改父类逻辑,子类也会收到影响。

组合相比继承有哪些优势?

常用的三种方式:组合,接口,委托,一块解决继承的问题。
对于会飞的特性,定义一个flyable(),去实现这个接口,将实现类放入鸟类的成员变量中去,如果要调用fly的方法,就可以通flyable的实现类进行调用

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。

你可能感兴趣的:(#,设计模式,mvc,java,开发语言,设计模式)