面向对象编程


前言

本文主要介绍什么是面向对象编程,以及面向对象编程与面向过程编程的差异。并总结了实际开发中,几种看似是面向对象编程,实际为面向过程编程的常见情形。


一、什么是面向对象编程

关于面向对象编程可以总结为以下两句话:

  1. 面向对象是一种编程范式或者编程风格,它以类或者对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
  2. 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

谈面向对象编程时不可避免地要与面向过程编程进行对比,以下是总结出的面向过程编程的定义:

  1. 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
  2. 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。

注意:另外的说法是三大特性:封装、继承、多态,不包含抽象

下面通过代码对比,加深对面向过程与面向对象的理解:
假设我们有一个记录了用户信息的文本文件 users.txt,每行文本的格式是 name&age&gender(比如,xiaowang&28&male)。我们希望写一个程序,从 users.txt 文件中逐行读取用户信息,然后格式化成 name#age#gender这种文本格式,并且按照 age 从小到大排序之后,重新写入到另一个文本文件formatted_users.txt.

面向过程编码:

public class FileFormatProcess {
    private void format(String originalFile, String targetFile) throws IOException {
        Stream<String> lines = Files.lines(new File(originalFile).toPath());
        Stream<String[]> users = formatToMapList(lines);
        Stream<String[]> result = sortByAge(users);
        formatToText(result, targetFile);
    }
    private void formatToText(Stream<String[]> result, String targetFile) throws FileNotFoundException {
        try (PrintWriter writer =
                     new PrintWriter(new FileOutputStream(new File(targetFile)))) {
            result.map(each -> String.join("#", each)).forEach(each -> {
                writer.write(each);
                writer.println();
            });
        }
    }

    private Stream<String[]> sortByAge(Stream<String[]> infos) {
        return infos.sorted(Comparator.comparingInt(u -> Integer.parseInt(u[1])));
    }

    private Stream<String[]> formatToMapList(Stream<String> lines) {
        return lines.map(line -> {
            return line.split("&");
        });
    }

    public static void main(String[] args) throws IOException {
        new FileFormatProcess().format("G:\\github\\userfile.txt",
                "G:\\github\\fomatteduserfile.txt");
    }
}

面向对象编码

public class User {
    private String name;
    private int age;
    private String gender;

    public User(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public static User parseFrom(String userInfoText) {
        String[] array = userInfoText.split("&");
        if (array.length != 3) {
            // error
        }
        // 这里要检查用户名,年龄,姓名的合法性,省略
        return new User(array[0], Integer.parseInt(array[1]), array[2]);
    }

    public String formattedUserText() {
        StringBuilder builder = new StringBuilder();
        builder.append(name).append("#")
                .append(age).append("#")
                .append(gender);
        return builder.toString();
    }
    
    public int getAge() {
        return age;
    }
}

public class UserFileFormatter {
    private void format(String originalFile, String targetFile) throws Exception {
        Stream<String> lines = Files.lines(new File(originalFile).toPath());
        Stream<User> users = formatToUserList(lines);
        Stream<User> result = sortByAge(users);
        formattedUserText(result, targetFile);
    }

    private void formattedUserText(Stream<User> users, String targetFile) throws Exception {
        try (PrintWriter writer =
                     new PrintWriter(new FileOutputStream(new File(targetFile)))) {
            users.map(User::formattedUserText).forEach(each -> {
                writer.write(each);
                writer.println();
            });
        }
    }

    private Stream<User> sortByAge(Stream<User> users) {
        return users.sorted(Comparator.comparingInt(User::getAge));
    }

    private Stream<User> formatToUserList(Stream<String> lines) {
        return lines.filter(line -> !"".equals(line)).map(User::parseFrom);
    }

    public static void main(String[] args) throws Exception {
        new UserFileFormatter().format("G:\\github\\userfile.txt", "G:\\github\\fomatteduserfile.txt");
    }
}

从上面的代码中,我们可以看出,面向过程和面向对象最基本的区别就是,代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构(String[]),方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中
当然,这个并不是唯一的区别。

二、面向对象vs面向过程

1)OOP 更加能够应对大规模复杂程序的开发
上面的例子,比较简单。尤其是使用过了jdk8的新特性后,代码都比较整洁。面向对象跟面向过程两者风格貌似相差不大。

对于简单程序的开发来说,不管是用面向过程编程风格,还是用面向对象编程风格,差别确实不会很大。

如果需求足够简单,整个程序的处理流程只有一条主线,很容易被划分成顺序执行的几个步骤,然后逐句翻译成代码,这就非常适合采用面向过程这种面条式的编程风格来实现。

但对于大规模复杂程序的开发来说,整个程序的处理流程错综复杂,并非只有一条主线,而是一个网站结构。这个时候,使用面向对象的编程风格的优势就比较明显了。

面向对象编程是以类为思考对象。面向对象编程,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立怎么调用,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。

同时,比如,我们开发一个电商交易系统,业务逻辑复杂,代码量很大,可能要定义数百个函数、数百个数据结构,那如何分门别类地组织这些函数和数据结构,才能不至于看起来比较凌乱呢?类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段。
2)OOP 风格的代码更易复用、易扩展、易维护

上面的例子比较简单,没有用到面向对象的高级特性:封装,继承,抽象,多态。而这四种特性,能够帮助我们写出更易复用、易扩展、易维护的代码。关于这四大特性的介绍,参考:面向对象的四个特性

面向过程编程是一种非常简单的编程风格,并没有像面向对象编程那样提供丰富的特性。

封装特性是面向对象编程相比于面向过程编程的一个最基本的区别,因为它基于的是面向对象编程中最基本的类的概念。面向对象编程通过类这种组织代码的方式,将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法随意修改。因此,面向对象编程提供的封装特性更有利于提高代码的易维护性。

抽象性,函数本身就是一种抽象,它隐藏了具体的实现。我们在使用函数的时候,只需要了解函数具有什么功能,而不需要了解它是怎么实现的。从这一点上,不管面向过程编程还是是面向对象编程,都支持抽象特性。不过,面向对象编程还提供了其他抽象特性的实现方式。这些实现方式是面向过程编程所不具备的,比如基于接口实现的抽象。基于接口的抽象,可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑,提高了代码的可扩展性。

继承可以提高代码的复用性。

多态性,可以让我们的代码遵从对“修改关闭、对扩展开放”的设计原则,提高代码的扩展性。除此之外,多态还可以提高代码的复用性。

三、面向对象避坑

运用java这种面向对象编程语言,并不能写出真正的面向对象编程的代码。下面只要总结几种看似是面向对象,实际上是面向过程编程风格的代码,并且分析一下,为什么我们很容易写出这样的代码
1)滥用setter、getter方法
先看一段代码

public class ShoppingCart {
    private int itemCount;
    private double totalPrice;
    private List<ShoppingCartItem> items = new ArrayList<>();
    public int getItemCount() {
        return itemCount;
    }
    public void setItemCount(int itemCount) {
        this.itemCount = itemCount;
    }
    public double getTotalPrice() {
        return totalPrice;
    }
    public void setTotalPrice(double totalPrice) {
        this.totalPrice = totalPrice;
    }
    public List<ShoppingCartItem> getItems() {
        return items;
    }
    public void addItem(ShoppingCartItem item) {
        items.add(item);
        itemCount++;
        totalPrice += item.getPrice();
    }
}

上面代码为几乎为每个属性都提供了setter、getter方法。项目中还会经常用到lombock插件。下面分析存在的问题:
itemsCount、totalPrice这两个属性提供了publicsetter方法,是外部可以修改它们的值,这样会导致与items属性值不一致问题。这里setter破坏了封装特性,没有控制访问权限,退化为面向过程编程。
items属性提供的getter方法,返回原始集合的引用,外部可以对其进行修改,也会导致属性不一致问题,比如

// 问题1
ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空购物车

// 问题2
ShoppingCart cart = new ShoppingCart();
cart.add(new ShoppingCartItem(...));
List<ShoppingCartItem> items = cart.getItems();
ShoppingCartItem item = items.get(0);
item.setPrice(19.0); // 这里修改了item的价格属性

针对问题一,用户想要清空购物车时,应该调用ShoppingCart提供的方法,getter应该返回一个不可修改的对象引用。问题二可以返回一个深拷贝对象进行解决。
上面改动

public List<ShoppingCartItem> getItems() {
        List<ShoppingCartItem> tmpShoppingCartItems = new ArrayList<>(items);
        return Collections.unmodifiableList(tmpShoppingCartItems);
    }

    public void clear() {
        items.clear();
        itemCount = 0;
        totalPrice = 0.0;
    }

小结:除非真的需要,否则,尽量不要给属性定义 setter 方法。同时返回集合时要谨慎。

2) 滥用全局变量和全局方法

面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。
我们经常用的有Constants类(存放配置参数等常量)跟Utils类(定义成静态方法),静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。当然,并不是说完全没有好处。下面对这两种类型分别讨论。

Constans

public class Constants {
  public static final String MYSQL_ADDR_KEY = "mysql_addr";
  public static final String MYSQL_DB_NAME_KEY = "db_name";
  public static final String MYSQL_USERNAME_KEY = "mysql_username";
  public static final String MYSQL_PASSWORD_KEY = "mysql_password";
  
  public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
  public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
  public static final int REDIS_DEFAULT_MAX_IDLE = 50;
  public static final int REDIS_DEFAULT_MIN_IDLE = 20;
  public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
  // ...省略更多的常量定义...
}

定义常量类,如果把所有应用场景的常量都放到一个类中,会存在以下几个问题。
首先代码不好维护,每个人都修改该类,容易代码冲突,同时也会增加代码编译时间。另外还会影响代码的复用性,如只需要其中一个常量,不得不引入更无关的常量。
有两种优化思路
第一种是将 Constants 类拆解为功能更加单一的多个类,比如跟 MySQL 配置相关的常量,我们放到 MysqlConstants 类中;跟 Redis 配置相关的常量,我们放到 RedisConstants 类中。另一种是,不单独地设计 Constants 常量类,而是哪个类用到了某个常量,我们就把这个常量定义到这个类中。比如,RedisConfig 类用到了 Redis 配置相关的常量,那我们就直接将这些常量定义在 RedisConfig 中,这样也提高了类设计的内聚性和代码的复用性。

在开发中,Utils类的出现往往是这样一种情景:如果我们有两个类 A 和 B,它们要用到一块相同的功能逻辑,为了避免代码重复,我们可能会想到继承,但A 类和 B 类并不一定具有继承关系,如果仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。这个时候,我们就会用到Utils类。

实际上,只包含静态方法不包含任何属性的 Utils 类,是面向过程的编程风格。但这并不是说,我们就要杜绝使用 Utils 类了。实际上,从刚刚讲的 Utils 类存在的目的来看,它在软件开发中还是挺有用的,能解决代码复用问题。所以,这里并不是说完全不能用 Utils 类,而是说,要尽量避免滥用,不要不加思考地随意去定义 Utils 类。
同样的,在设计Utils时,可以考虑细化一下,针对不同的功能,设计不同的 Utils 类,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等。

3)定义数据与方法分类的类
现在的Web开发中,常用到三层结构:Controller 层、Service 层、Repository 层。
Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。
而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。
这种开发模式叫作基于贫血模型的开发模式,也是我们现在非常常用的一种 Web 项目的开发模式。
与之相对应的是,充血模式。两者比较参考后面的文章


总结

面向过程与面向对象编程各有其优缺点。了解两者的差异后,在实际开发中才能更好地选择哪种开发模式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值