本系列文章以我的个人博客的搭建为线索(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.kt
及sorceress.kt
的第一行,需要
package project
而在monster.kt
和play.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
模块,其下有子模块monster
和play
.
一个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.rs
或gwent.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 gwent
和use gwent::monster
.
访问控制
使用模块化编程后,会带来更进一步的好处,就是访问控制。所谓访问控制,就是谁能对谁干什么。一个访问控制规则可以用一个三元组表示:主体,客体,访问权限。我们的生活中常常会有访问控制的存在,比如说,QQ空间中,仅好友可见,就是一种访问控制规则,表明只有主体为我的好友的人,才能对客体——我的这条说说,进行「读」这一访问权限。
在模块化编程中,访问控制就体现在我此时处在的代码块中,能否调用别的代码块中的函数。为什么要进行访问控制呢?这主要是为了贯彻封装的理念。在一个库中,有的函数也许只是作为库内的辅助函数使用,不暴露给外部,这时候就要对这些函数进行访问控制的保护。
最简单的访问控制,就是private
和public
. 被标记为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
这个结构体的模块只有B
和C
.
-
为了方便开发者,pub(in path)
会有许多的语法糖,比如说pub(crate)
代表在当前crate内能访问,pub(super)
代表父模块能访问,pub(self)
代表只有本模块能访问,也就等同于不加访问控制限定符。
Kotlin8
Kotlin的访问控制限定符则有4个:private
, protected
, internal
和public
. 由于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是最奇葩的一种访问控制策略,它是少数的几种通过变量名来控制访问策略的语言。
首先是对于模块来说,以_
开头的代码不能被import
11. 比方说,我们有如下的一个模块:
# 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
。
https://docs.python.org/3/reference/import.htm ↩︎
https://kotlinlang.org/docs/reference/packages.html ↩︎
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules ↩︎
https://www.typescriptlang.org/docs/handbook/modules.html ↩︎
https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html#ID4 ↩︎
https://doc.rust-lang.org/reference/items/modules.html ↩︎
https://doc.rust-lang.org/reference/visibility-and-privacy.html ↩︎
https://kotlinlang.org/docs/reference/visibility-modifiers.html ↩︎
https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html ↩︎
https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export ↩︎
https://www.typescriptlang.org/docs/handbook/modules.html#export ↩︎ ↩︎
https://docs.python.org/3/tutorial/modules.html#more-on-modules ↩︎