面向过程与面向对象的思想

作者: zsh2517 分类: 未分类 发布时间: 2021-07-07 12:36
  1. 面向过程与面向对象的思想
  2. 类与类之间的关系
  3. 复杂类型的设计原则
  • 面向过程与面向对象的思想(从面向过程过渡到面向对象)
  • 面向对象的基本操作(语法相关的基本语法和规则。暂时略过,后面补上)
  • 类与类之间的关系(继承与派生,接口与实现)
  • 复杂类型的设计原则(OOP 的设计原则,以及一些设计模式等等)

面向过程与面向对象的思想

C/C++/Java 比较混杂,主要以 C/Java 为主,但是也会有少量的 C++。不过看懂应该没问题

(emm有大一的问我小学期选课相关的以及 java 的面向对象相关的东西,正好和软件构造也有博客内容要求,所以会连续写一个系列出来,包括 java、面向对象、软件开发等等一系列的内容)

1 什么是面向过程?

面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。

也就是说,我该怎么做这件事

举个例子,对于一个五子棋的游戏,如果我们拆分成步骤,会是什么?

步骤 内容
1 开始游戏
2 黑子先走
3 绘制画面
4 判断输赢
5 轮到白子
6 绘制画面
7 判断输赢
8 返回步骤2
9 输出最后结果

用代码表示的话

int main() {
    game_init(); // 初始化游戏数据
    game_start(); // 游戏开始

    while(true) { // 反复循环,直到跳出
        black_operation(); // 黑棋操作
        draw(); // 绘制界面
        if(finished()) break; // 判断胜负如果游戏结束,则退出循环

        white_operation(); // 白棋操作
        draw(); // 绘制界面
        if(finished()) break; // 判断胜负如果游戏结束,则退出循环
    }

    output_result();
    exit(0);
}

就像这样,这个程序的运行,就是一系列函数的调用。

我们把变量和函数做成全局,这样整个流程无需传递状态信息即可完成。

2 什么是面向对象

如果说,面向过程是我该怎么做这件事,那么面向对象就是我该找谁做这件事,在这之前,先看几个例子

2.1 class 与 instance

一个类,本质上相当于一个函数(功能)变量(值) 的集合的模板。其中,成员变量的值为这个类提供了保存信息的可能,而成员函数的功能则可以提供外界和自己的值交互的一个接口(注:后面接口均非 Java 的 Interface 概念)

一个类,通过成员变量存储信息,而通过成员函数改变和使用这个信息。

class 是一个模板,描述了这个类的结构,而 instance (实例)是这个类的一个具体个体,他自己具有独立的变量空间,并且成员函数默认对自身生效(暂时可以这么理解)

就比如人

// 这里先不涉及公开与私有的原则。等到后面 public 与 private 再涉及
class People {
    String name;
    int age;

    void eat();
    void sleep();
}

一个 People 类定义了人的概念,人应该有自己独立的姓名、年龄,可以吃饭和睡觉

而具体到某个人呢?

People Alice = new People();
Alice.name = "Alice";
Alice.age = 18;
while(true) {
    Alice.eat();
    Alice.eat();
    Alice.sleep();
    Alice.eat();
    Alice.sleep();
}

People Alice = new People(); 这里创建了一个新的实例,并且赋值给了 Alice 这个变量(这里可以认为 People 是一个“东西”,然后 Alice 作为一个指向“东西”的“指针”指向了刚才新建的“东西”)

之后,就可以针对 Alice 做出各种操作了。

就像上文提到的,面向对象思考的不是该怎么去做这个事,而是该找谁做这件事,比如上文,一个人的各种信息和活动,找到的就是 People 这个 class,这个类包含了作为一个人的基本信息的定义和实现。

2.2 回到上面的五子棋问题

在面向过程的代码中,得到了如下的流程

步骤 内容
1 开始游戏
2 黑子先走
3 绘制画面
4 判断输赢
5 轮到白子
6 绘制画面
7 判断输赢
8 返回步骤2
9 输出最后结果

上面,我们是一个一个函数堆起来的。而采用面向对象的思想(或者简单说,如果要用 Java 实现这个程序),在开始实现之前,先确定这些功能都有谁的参与

模块 功能 具有的成员方法
游戏规则 包含了游戏规则的设定 游戏开始、判断输赢、游戏结束(输出结果)
用户 接收用户的操作,并且加以处理 下棋
绘图 处理绘图有关的东西 绘制

按照上面的原则,可以把不同的功能分发到不同的模块上,然后原来函数的先后调用,就变成了不同模块的调用

public class GameRule {
    public static void main(String[] args) {
        DrawBoard board = new DrawBoard();
        Operator white = new Operator("White");
        Operator black = new Operator("Black");
        GameRule gameRule = new GameRule();

        gameRule.start();
        while(true) {
            black.go();
            board.draw();
            if(gameRule.success()) break;

            white.go();
            board.draw();
            if(gameRule.success()) break;
        }
        gameRule.result();
    }
}

当然,这只是长得象是面向对象的程序的程序,不可否认,确实采取了面向对象的设计思想,但是这个面向对象更像是从面向过程直接迁移过来的

2.3 面向对象 = 结构体指针 + 函数?

面向对象的操作对象是他自己,所以如果给面向过程加一个结构体指针,是不是就能面向对象了?

比如说,我们有一个数据结构:栈(为什么选栈,因为最可以数组+游标实现,简单)

比如说,我们要做一个题目:大中小括号是否匹配(栈的一个经典应用),需要实现一个 stack,就像这样

char stack[100010];
int size = 0;

void push(char c) {
    stack[size] = c;
    size += 1;
}

char top() {
    return stack[size - 1];
}

void pop() {
    size -= 1;
}

而如果学完结构体,就知道可以将这个栈的存储空间和大小打包起来

typedef struct {
    char arr[100010];
    int size;
} Stack;
Stack stack;

void init() {
    stack.size = 0;
}

void push(char c) {
    stack.arr[stack.size] = c;
    stack.size += 1;
}

char top() {
    return stack.arr[stack.size - 1];
}

void pop() {
    size -= 1;
}

既然已经打包了,为什么不直接在函数里面指定要操作的结构体呢?

typedef struct {
    char arr[100010];
    int size;
} Stack;
Stack stack;

void stack_init(Stack* stack) {
    stack->size = 0;
}

void stack_push(Stack* stack, char c) {
    stack->arr[stack->size] = c;
    stack->size += 1;
}

char stack_top(Stack* stack) {
    return stack->arr[stack->size - 1];
}

void stack_pop(Stack* stack) {
    size -= 1;
}

这样,如果需要一个栈,就 Stack s; 创建一下,然后后续需要对他进行操作的时候就可以 stack_push(&s, '('); 这样直接使用对于 s 的操作

写到这里,应该简单理解了面向对象的第一个追求:封装

当然,虽然结构体+指针是可以实现一定程度上的对象的设计,但是并不完全,比如上面的五子棋代码,还可以像下面这样

2.4 封装

public class GameRule {
    private Operator white;
    private Operator black;
    private DrawBoard board;

    public GameRule(white, black, board) { // 构造函数
        // 使用构造函数,可以在创造对象的时候,进行一些初始化操作
        // 比如 GameRule gameRule = new GameRule(whiteInstance, blackInstance, drawBoard);
        // 这样的话,创建对象的时候传递三个新的实例进来,即可对成员赋值
        this.white = white;
        this.black = black;
        this.board = board;
    }

    public boolean success(); // 判断胜负

    public boolean result(); // 打印结果

    public boolean goWhite() { // 走白棋
        this.white.go();
        this.board.draw();
        return success();
    }

    public boolean goBlack() { // 走黑棋
        this.black.go();
        this.board.draw();
        return success();
    }

    public static void main(String[] args) {
        DrawBoard board = new DrawBoard();
        Operator white = new Operator("White", board);
        Operator black = new Operator("Black", board);
        GameRule gameRule = new GameRule(white, black, board);

        while(true) {
            if(gameRule.goWhite()) break;
            if(gameRule.goBlack()) break;
        }
        gameRule.result();
    }
}

当然,这个还可以进一步封装起来,因为每次的游戏都是需要 画板、白棋、黑棋、游戏规则 的实例,这样就可以把这个过程再次打包封装

public class GameRule {
    private Operator white;
    private Operator black;
    private DrawBoard board;

    public GameRule() {}

    public GameRule(white, black, board) { // 构造函数
        // 使用构造函数,可以在创造对象的时候,进行一些初始化操作
        // 比如 GameRule gameRule = new GameRule(whiteInstance, blackInstance, drawBoard);
        // 这样的话,创建对象的时候传递三个新的实例进来,即可对成员赋值
        this.white = white;
        this.black = black;
        this.board = board;
    }

    public boolean success(); 

    public boolean output();

    public boolean goWhite() {
        this.white.go();
        this.board.draw();
        return success();
    }

    public boolean goBlack() {
        this.black.go();
        this.board.draw();
        return success();
    }

    public boolean gameLoop() {
        this.board = new DrawBoard();
        this.white = new Operator("White", board);
        this.black = new Operator("Black", board);

        while(true) {
            if(this.goWhite()) break;
            if(this.goBlack()) break;
        }
        return this.result();
    }

    public static void main(String[] args) {
        GameRule gameRule = new GameRule();
        boolean result = gameRule.gameLoop(); // 只需要调用一个函数就可以了
    }
}

3 面向对象的初衷,是在解决什么问题

是时候提出来这个问题了

可以想象一下,当程序越写越大的时候……

一开始,可能简简单单十几个函数,或者几十个函数就可以完成,但是大了之后呢?

面向过程的程序,以事件作为基本的个体,对于小程序相对方便,但是大了之后就会很麻烦

举个例子吧,打开记事本(Win+R,notepad)

emmmm看起来是个很简单的东西

但是拆分一下,首先,最外层是一个窗口,这个窗口有标题,右上角有功能按钮。

下面是一排菜单,这一排菜单里面很多菜单又有子项目(查看>缩放)或者是弹窗(格式>字体),通过 Alt + 括号内的字母还可以快速触发(比如 Alt + F 相当于鼠标点击 文件 菜单。

然后是一个很大的输入框,可以输入、删除、撤销、复制、粘贴、全选,右键还可以有更多的功能

下面一行是状态栏,随时的和编辑器信息同步(行号和列号)

这样一拆,好像挺复杂的

然后继续,作为一个窗口/组件,他是有信息的,窗口有窗口的位置,长度,宽度。除此之外,主要的数据(标题/文本)等等,然后要支持显示、选中、取消选中、激活、取消激活、大小调整等诸多功能

如果用一系列的函数和结构体(局限在学的 C 语言内),可以想象一下,这些功能得需要多少函数和结构才能满足,即使可以从系统引用(不需要自己实现),那么得有多少不同的名字,而且如此多的东西,描述起来还是一个问题。

但是如果按照部件拆分

外层是一个窗口,里面分别是菜单、输入框、状态栏,其中菜单可以嵌套,状态栏可以划分成多个单元格。

之后,系统以“组件”的形式提供(这里用的是 Win Forms (C#) 网图)各种组件,是不是就简单多了

这还没完,想一下,组件有什么性质?无论什么组件都要有 位置、大小,然后一些组件要有标题等等,一些组件要能够容纳其他组件(比如窗口里面可以放各种子组件)

后文并非某种具体语言的实现

也就是说,如果有一个类 Component,描述了这些公共性质,然后只要其他的组件,从 Component 发展出来,就可以天然的获得这些支持

(上文可以认为,如果有 Person 作为人类的描述,那么可以有从 Person 发展出来的 Student,进一步可以发展出来 CollegeStudent ,每一层具有上一层的各种性质)

这里就涉及到了面向对象的第二个要素:继承,不同的对象之间,可以有继承关系(当然继承的概念并非这么简单,还有各种集成的规则比如抽象类、Interface(接口)、公开程度(public, private, protected)等等)

新的问题来了

前面提到,一个窗口里面可以允许各种组件的存在。那么比如存在一个函数,向窗口里面添加一个组件,那么该传递什么比较好呢?

一个一个实现,比如 addButton, addCheckBox ?太多了,而且如果用户做了新的组件怎么办,还要全写一遍吗?

前面提到,Button, CheckBox 都是源自于 Component 的,也就是说,既然他们都是 Component 衍生出来的,是不是可以在不追求个例的情况下,使用各组件的公共部分,完成这个操作?

这里涉及到了面向对象的第三个要素:多态

在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。

多态的最常见主要类别有:

特设多态:为个体的特定类型的任意集合定义一个共同接口。
参数多态:指定一个或多个类型不靠名字而是靠可以标识任何类型的抽象符号。
子类型(也叫做子类型多态或包含多态):一个名字指称很多不同的类的实例,这些类有某个共同的超类。

多态 (计算机科学) – 维基百科,自由的百科全书 (wikipedia.org)

特设多态(类似于函数重载)

public class AddOperation {
    static int add(int a, int b) {
        return a + b;
    }

    static String add(String a, String b) {
        return a.concat(b);
    }

    public static void main(String[] args) {
        System.out.println(AddOperation.add(2, 3)); // 5
        System.out.println(AddOperation.add("2", "3")); // "23"
    }
}

对于不同的类型,定义不同的函数,实现相同或者不同的功能。

参数多态(模板)

public class ListTest<T> {
    static class Node<T> {
        T elem;
        Node<T> next;
    }
    Node<T> head;
    int length() { ... }
}

子类型(传递接口或者是父类型(接口和父类型))

class Person {
    public int age;
    public String name;
};

class Student extends Person {
    public int grade;
};

class Main {
    Person a = new Person();
    Student b = new Student();

    int getAge(Person person) {
        return person.age;
    }
}

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注