现代化程序开发笔记(3)——多文件与模块

本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记。在这篇文章中,我将对现代编程语言的多文件和模块部分进行一些介绍。

模块化编程

随着现代编程开发项目的代码量越来越大,参与开发维护的人数越来越多,模块化编程这一理念变得十分重要。就像我所说的,模块化编程实际上是一个理念,它倡导的是开发者利用各种手段,将不同作用的代码块隔离。比方说,众所周知,巫师三的两个核心功能是昆特牌功能和与女术士增进感情的功能。假设我们是简陋版巫师三的开发者,就在开发这两个功能。为了简化,假设昆特牌功能有函数playGwent, useMonsterCard,与女术士增进感情功能有函数talk, fight等。那么,任何一个懂得规划的开发者都会知道,不管使用什么编程语言,我们的代码顺序应该是

// Gwent part
void playGwent(Person person);
void useMonsterCard(Card monsterCard);
// ... many more functions

// Sorceress part
void talk(Sorceress sorceress);
void fight(Sorceress sorceress);
// ... many more functions

而不是

void playGwent(Person person);
void talk(Sorceress sorceress);
// ... many more functions
void fight(Sorceress sorceress);
void useMonsterCard(Card monsterCard);
// ... many more functions

这样把各种功能,毫不相关的函数交错放置。只有通过合理地有序组织代码,才能使代码的开发和维护变得轻松一些。试想,如果一个开发者想维护我们的这个代码,他想找到和女术士交谈的函数,与在整个项目代码中一行一行找相比,那必然是直接在女术士相应的代码部分寻找更为轻松。

总而言之,模块化实际上是一种开发分配、代码组织的理念,就是将整个项目的代码分成许多有独立功能的模块,将毫不相关的模块分开。同时,对于没有功能依赖的模块,可以多位开发者并行开发。通过模块化的措施,可以最大程度降低开发、维护的成本和时间。

多文件编程

将项目分为许多模块,由开发者并行开发,这是模块化的理念。那么实际操作中,应该如何贯彻呢?最直观的想法,就是将不同的模块归属到不同的文件、目录下去。假如我们的简陋版巫师三是用JavaScript写的,那么如果和女术士增进感情部分的代码比较少,我们直接将这部分的代码归入到sorceress.js这个文件中;如果昆特牌部分的代码比较多,放不到一个文件中,那就单独设置一个目录gwent, 在其中可能会有monster.js, play.js等多个文件。也就是说,我们的代码结构可能会是

project
├── main.js
├── sorceress.js
└── gwent
    ├── monster.js
    └── play.js

这样的层次结构。此外,我们还可能会使用别人写的库的功能。假设我们有一个库height用于计算跳落的伤害, 其代码结构是

height
├── damage.js
└── distance.js

将代码分到不同的文件、目录中,这不仅需要开发者遵守,编程语言也需要有相应的多文件支持。也就是说,编程语言需要支持多文件编程。

此外,当编程语言支持多文件编程时,还有一个问题引刃而解了——命名冲突。比如说,我们在昆特牌功能中有一个win函数,表示比赛获胜,而在和女术士增进感情的功能中,也有一个win函数,表示获得其芳心。那么,最简单的解决方案,就是一个函数叫winGwent, 一个函数叫winSorceress. 但是,通过编程语言对多文件的支持,或者说其对多模块的支持,只要将其分属于两个模块内,那么都叫win也就没有关系了。

在讨论大多数编程语言的多文件编程支持时,会涉及到三个概念:文件级别,目录级别,以及库级别。根据我们之前的讨论,每个文件都包含一些单独的功能。而对于那些功能有联系的文件,会将其组织在同一个目录下。而有的目录具有的功能是一些辅助性的,可以复用的功能,所以有些目录会被作为一个库发布出去,供别的人使用,也就像我们这里的height库一样。在大多数编程语言的认知中,一个文件被称为一个「模块」,一个目录被称为一个「包」。因此,一个项目本身也是一个包,它是由许多文件和包组成。而组成它的包又有下一层次的文件和包组成。而一个库可能由许多包组成,也可能就是一个包。

对于库而言,我会在后面关于包管理器的文章中专门提到,这里就先只讨论模块和包。在下面具体编程语言的讨论中,模块级别和包级别是我们刚才讲的文件和目录的概念,而非语言具体的名词概念。

Python1

Python是区分模块级别和包级别的。在Python中,一个文件就是一个模块,而一个目录则是一个包。

一个目录如果要声明自己是一个包,则必须要在目录中包含__init__.py文件。一个文件自动是一个模块,不需要声明。

如果要用Python完成我们的简陋版巫师三,那么代码结构应该为

project
├── main.py
├── sorceress.py
└── gwent
    ├── __init__.py
    ├── monster.py
    └── play.py

main.py中,需要按模块引入。同时,引入别的库的模块和引入自己的模块没有差别。引入别的模块的方法是

import sorceress
import gwent.monster
import gwent.play
import height.damage

Kotlin2

使用Kotlin编写的Android项目依然有模块级别和包级别的概念。但是,从名词术语的角度来看,Kotlin的模块并不指单个文件,而是一个项目;Kotlin的包则指一个目录。

一个目录不需要声明,自动是一个包。一个文件需要在开头用package关键词表明自己所属的包。

如果要用Kotlin完成我们的简陋版巫师三,那么代码结构应该为

project
├── main.kt
├── sorceress.kt
└── gwent
    ├── monster.kt
    └── play.kt

main.ktsorceress.kt的第一行,需要

package project

而在monster.ktplay.kt的第一行,则要

package project.gwent

main.kt中,需要按包引入。一个文件会自动引入同一个包下的别的文件,如果要引入别的包或者库(实际上也是包),则需要

import project.gwent
import height

JavaScript/TypeScript34

自ECMAScript 2015之后,JavaScript有了模块的概念。在JavaScript的视角下,目录仅仅是目录的作用,并没有特殊的包的作用。因此,JavaScript只有模块的概念。

一个文件如果要表明自己是一个模块,则必须有export语句。

如果要用JavaScript或TypeScript完成我们的简陋版巫师三,那么代码结构应该为

project
├── main.js
├── sorceress.js
└── gwent
    ├── monster.js
    └── play.js

对于main.js,引入别的模块的方法是

import './sorceress.js';
import './gwent/monster.js';
import './gwent/play.js';
import 'path/to/height/damage.js';

值得注意的是,TypeScript在进行import的时候,不需要带扩展名,也就是

import './sorceress';
import './gwent/monster';
import './gwent/play';
import 'path/to/height/damage';

Swift5

Swift没有模块级别和包级别的概念,其「模块」指的是库的概念。

如果要用Swift完成我们的简陋版巫师三,其代码结构与之前无异:

project
├── main.swift
├── sorceress.swift
└── gwent
    ├── monster.swift
    └── play.swift

在同一个模块下(也就是我们理解的在同一个库内),所有文件都是默认导入的,我们不需要import来导入同项目下别的文件或目录。但是,需要使用import语句导入别的库,也就是Swift中的模块:

import height

Rust6

Rust的模块系统和JavaScript相近,没有包的概念。但其目录和文件的地位是相同的,都是一个模块,而模块可以拥有子模块。

具体而言,就是Rust把「模块」和「包」的概念等同了,gwent目录实际上就是gwent模块,其下有子模块monsterplay.

一个Rust的文件自动是一个模块,但需要在其父模块中声明。一个Rust的目录必须包含mod.rs作为当前模块。

如果要用Rust来完成我们的简陋版巫师三,其代码结构为

project
├── main.rs
├── sorceress.rs
└── gwent
    ├── mod.rs
    ├── monster.rs
    └── play.rs

project
├── main.rs
├── sorceress.rs
├── gwent.rs
└── gwent
    ├── monster.rs
    └── play.rs

gwent/mod.rsgwent.rs中,必须要有

mod monster;
mod play;

来声明其子模块。

main.rs中如果要想引入别的模块,需要

mod sorceress; // declare sub module
mod gwent; // declare sub module
use gwent::monster; // use sub module
use gwent::play; // use sub module
use height::damage; // use library module

由于Rust中目录和文件的地位都是模块,所以我们也可以同时use gwentuse gwent::monster.

访问控制

使用模块化编程后,会带来更进一步的好处,就是访问控制。所谓访问控制,就是谁能对谁干什么。一个访问控制规则可以用一个三元组表示:主体,客体,访问权限。我们的生活中常常会有访问控制的存在,比如说,QQ空间中,仅好友可见,就是一种访问控制规则,表明只有主体为我的好友的人,才能对客体——我的这条说说,进行「读」这一访问权限。

在模块化编程中,访问控制就体现在我此时处在的代码块中,能否调用别的代码块中的函数。为什么要进行访问控制呢?这主要是为了贯彻封装的理念。在一个库中,有的函数也许只是作为库内的辅助函数使用,不暴露给外部,这时候就要对这些函数进行访问控制的保护。

最简单的访问控制,就是privatepublic. 被标记为private的代码只能被逻辑上处于同一个代码块的别的代码调用,而被标记为public的代码却能被所有的代码访问到。这一个思想作为基础,在此之上有许多的变种。

Rust7

最符合逻辑的访问控制操作是Rust. 它将模块视作访问控制的最小单元,其的原则只有一个:如果一个模块能访问某些代码,那么它的所有子模块都能访问该代码。根据这一宗旨,Rust的访问控制实际上只分为两种:

  • pub(in path::to::module)

    • 在适当的代码前加上这个限定符,代表当前的代码能够被指定的模块和其子模块访问。比如说以下的代码:

      mod A {
          mod B {
              mod C { }
          }
      }
      
      mod D {
          pub(in super::A::B) struct Foo { }
      }
      

      那么,Foo这个结构体本身位于D这个模块,但它指定B模块可以访问自己,那么总共可以访问Foo结构体的模块有B, C, D.

  • 不加访问控制限定符

    • 不加访问控制限定符则默认为私有。私有的代码只能被当前模块和其子模块访问。比如说以下的代码:

      mod A {
          mod B {
              struct Foo { }
              mod C { }
          }
      }
      
      mod D { }
      

      那么,能够访问到Foo这个结构体的模块只有BC.

为了方便开发者,pub(in path)会有许多的语法糖,比如说pub(crate)代表在当前crate内能访问,pub(super)代表父模块能访问,pub(self)代表只有本模块能访问,也就等同于不加访问控制限定符。

Kotlin8

Kotlin的访问控制限定符则有4个:private, protected, internalpublic. 由于Rust并不具有OOP的全部特性,所以其访问控制可以通过简单的修饰符达到完美的效果。但是,Kotlin等语言则是OOP更强的语言,所以其访问控制的修饰符也更多了一些。

首先,我们来看每个符号的定义:

  • private
    • 对于顶层代码(即不写在class内部的代码),是仅能由本文件内部访问
    • 对于class内的代码,仅能由该类内部访问
  • protected
    • 对于class内的代码,仅能由该类及其子类访问
  • internal
    • 对于顶层代码,能由整个Kotlin语义下的模块(即我们眼中的库级别)访问
    • 对于class内的代码,能由该Kotlin语义下的模块内,能访问该类的代码访问
  • public
    • 对于顶层代码,能被所有代码访问
    • 对于class内的代码,能由能访问该类的代码访问

就像我们刚刚所说的,正是由于拥有了继承关系,Kotlin的访问限定符因此多了一个protected, 以及其他每个符号也多了class内的含义。但是,其与Rust相比,仅能表示当前文件内(即private),或当前库级别内(即internal)的访问控制,不能做到任意包路径的访问控制。

Swift9

Swift比Kotlin多了一个访问控制限定符,共有5个:open, public, internal, fileprivate, private. 首先,我们还是先来看其定义:

  • open
    • 能被所有代码访问,并且能被Swift语义下的模块(即我们眼中的库级别)外的类继承
  • public
    • 能被所有代码访问
  • internal
    • 能被Swift语义下的模块内的代码访问
  • fileprivate
    • 能被当前文件内的代码访问
  • private
    • 仅能被当前逻辑上的实体访问

这样看来,实际上和Kotlin是类似的,只不过open多了一个能不能被继承的限定。

上述的三种语言使用的是传统意义上的访问控制,我们可以看到,Rust由于没有OOP的完整特性,所以比较灵活。但虽然Kotlin和Swift不能指定路径上的访问控制,但在实际工程中,库级别和文件级别的访问控制实际已经能够胜任了。

JavaScript/TypeScript1011

JavaScript的访问控制就比较粗糙了,但也是一个很有效的策略,它的思想就是:只要我export的,你就能用;只要我没有export的,你就不能用。

我们之前讲过,JavaScript和TypeScript中一个文件就是一个模块。在JavaScript或TypeScript中,所有代码都可以被同一模块内的其他代码使用。但是,如果想让别的模块使用某些代码,则必须将相应的代码导出去。比如说,我们有下面的代码:

// foobar.ts
function foo() { }
export interface Foo { }
export function bar() { }
export default function baz() { }

那么,在别的模块中,我们可以使用

import baz, { bar, Foo } from 'foobar'

来导入相应的代码。

Python

Python是最奇葩的一种访问控制策略,它是少数的几种通过变量名来控制访问策略的语言。

首先是对于模块来说,以_开头的代码不能被import11. 比方说,我们有如下的一个模块:

# foo.py
def bar():
    print("bar")

def _baz():
    print("barz")

那么,当我们在别的模块使用

from foo import *

这个语法的时候,并不会将_baz也一并导入。但是,要注意的是,如果我们单独

from foo import _baz

是可以成功的。

其次,是对于类来说,以双下划线__开头的代码被认为是不能被子类改写的12。比方说以官方文档中的代码为例:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

那么,当我们实例化一个MappingSubclass的时候,即使子类提供了__update, 其构造函数也不会调用子类的__update


  1. https://docs.python.org/3/reference/import.htm ↩︎

  2. https://kotlinlang.org/docs/reference/packages.html ↩︎

  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules ↩︎

  4. https://www.typescriptlang.org/docs/handbook/modules.html ↩︎

  5. https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html#ID4 ↩︎

  6. https://doc.rust-lang.org/reference/items/modules.html ↩︎

  7. https://doc.rust-lang.org/reference/visibility-and-privacy.html ↩︎

  8. https://kotlinlang.org/docs/reference/visibility-modifiers.html ↩︎

  9. https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html ↩︎

  10. https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export ↩︎

  11. https://www.typescriptlang.org/docs/handbook/modules.html#export ↩︎ ↩︎

  12. https://docs.python.org/3/tutorial/modules.html#more-on-modules ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值