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

模块化编程

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

// Gwent partvoid playGwent(Person person);

void useMonsterCard(Card monsterCard);

// ... many more functions

// Sorceress partvoid talk(Sorceress sorceress);

void fight(Sorceress sorceress);

// ... many more functions

而不是

void playGwent(Person person);

void talk(Sorceress sorceress);

// ... many more functionsvoid 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库一样。在大多数编程语言的认知中,一个文件被称为一个「模块」,一个目录被称为一个「包」。因此,一个项目本身也是一个包,它是由许多文件和包组成。而组成它的包又有下一层次的文件和包组成。而一个库可能由许多包组成,也可能就是一个包。

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

Python^1

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

Kotlin^2

使用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/TypeScript^3

自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';

Swift^5

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

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

project

├── main.swift

├── sorceress.swift

└── gwent

├── monster.swift

└── play.swift

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

import height

Rust^6

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 modulemod gwent;// declare sub moduleusegwent::monster;// use sub moduleusegwent::play;// use sub moduleuseheight::damage;// use library module

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

访问控制

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

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

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

Rust^7

最符合逻辑的访问控制操作是Rust. 它将模块视作访问控制的最小单元,其的原则只有一个:如果一个模块能访问某些代码,那么它的所有子模块都能访问该代码。根据这一宗旨,Rust的访问控制实际上只分为两种:pub(in path::to::module)在适当的代码前加上这个限定符,代表当前的代码能够被指定的模块和其子模块访问。比如说以下的代码:

```rust mod A { mod B { mod C { } } }

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

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

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

```rust mod A { mod B { struct Foo { } mod C { } } }

mod D { } ```

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

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

Kotlin^8

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)的访问控制,不能做到任意包路径的访问控制。

Swift^9

Swift比Kotlin多了一个访问控制限定符,共有5个:open, public, internal, fileprivate, private. 首先,我们还是先来看其定义:open能被所有代码访问,并且能被Swift语义下的模块(即我们眼中的库级别)外的类继承

public能被所有代码访问internal能被Swift语义下的模块内的代码访问

fileprivate能被当前文件内的代码访问private仅能被当前逻辑上的实体访问

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

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

JavaScript/TypeScript^10

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。

python质数列_现代化程序开发笔记(3)——多文件与模块相关推荐

  1. 现代化程序开发笔记(11)——异步编程杂谈

    本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记.在这篇文章中,我将以我的理解从头开始梳理一遍异步编程. 从 ...

  2. 现代化程序开发笔记(4)——包管理工具

    本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记.在这篇文章中,我会就项目构建工具和包管理工具做一些讨论,先 ...

  3. 微信小程序开发笔记(1.1)滚动选择器picker的使用

    微信小程序开发笔记(1.1)滚动选择器picker的使用 前言 滚动选择器picker 普通选择器 多列选择器 时间选择器 日期选择器 省市区选择器 前言 最近被拉来做小程序,因为时间比较赶,其他方面 ...

  4. 微信小程序开发笔记,你收藏了吗?

    ** 微信小程序开发笔记,你收藏了吗? ** 最近在开发微信小程序,把自己在项目中经常遇到的知识点记录下来,以便下次开发的时候查看. 开发小程序开发工具推荐vscode写代码,微信开发工具用于查看效果 ...

  5. 微信小程序开发笔记 进阶篇④——getPhoneNumber 获取用户手机号码(小程序云)

    文章目录 一.前言 二.前端代码wxml 三.前端代码js 四.云函数 五.程序流程 一.前言 微信小程序开发笔记--导读 大部分微信小程序开发者都会有这样的需求:获取小程序用户的手机号码. 但是,因 ...

  6. 微信小程序开发笔记 进阶篇⑤——getPhoneNumber 获取用户手机号码(基础库 2.21.2 之前)

    文章目录 一.前言 二.前端代码wxml 三.前端代码js 四.后端java 五.程序流程 六.参考 一.前言 微信小程序开发笔记--导读 大部分微信小程序开发者都会有这样的需求:获取小程序用户的手机 ...

  7. 微信小程序开发笔记二(WXSS和CSS样式美化)

    微信小程序开发笔记二(WXSS和CSS样式美化) 一.CSS基本知识 1.Class选择器的定义 2.ID选择器的定义 3.ID选择器和class选择器的区别 4.CSS中设置颜色 5.CSS中的文本 ...

  8. 微信小程序开发笔记——wsdchong

    微信小程序开发笔记 一.小程序简介 小程序起源于微信的webview:此类API最初是提供给腾讯内部一些业务使用,很多外部开发者发现后,照葫芦画瓢,逐渐成为微信中网页的事实标准.2015年初,微信发布 ...

  9. 微信小程序开发笔记 进阶篇③——onfire.js事件订阅和发布在微信小程序中的使用

    文章目录 一.前言 二.onfire.js介绍 三.API介绍 四.实例应用 五.onfire源码 六.实例源码 一.前言 微信小程序开发笔记--导读 二.onfire.js介绍 一个简单实用的事件订 ...

最新文章

  1. ASP连接Access2013
  2. SQLServer数据库自增长标识列的更新修改操作
  3. jyphon 环境变量配置
  4. Spring Cache抽象-缓存注解
  5. 分布式架构的分布式文件系统
  6. 【机器学习】机器学习一些概念的整理(不断更新中)
  7. html 如何去除浮动,CSS浮动? 如何清除浮动?
  8. ios macos_设计师可以从iOS 14和macOS Big Sur中学到什么?
  9. IntelliJ IDEA2017 激活方法 最新的
  10. 剑灵傲雪区最新服务器,12.8日势力优化具体内容 各大区服务器互通情况
  11. html5可以用flash,HTML5网页可以直接看视频,不用flash吗,另外WP7为何不支持flash。。。HTML5网页...
  12. 【英语学习】【WOTD】opusculum 释义/词源/示例
  13. 解决python在pycharm中可以import本地文件,但命令行运行时报错:no model named xxxx本地文件
  14. php读写文件要加锁
  15. express 模板 及 文件上传
  16. oracle关键字作为字段名使用方法
  17. SPI协议通信时序详解
  18. Hadoop环境搭建(6) -- 克隆
  19. prepareStatement的批量处理数据
  20. java提取一个字符串中的整数和小数部分

热门文章

  1. mpvue template compiler 中文版教程
  2. windows下安装composer抛出Composer\Downloader\TransportException异常解决办法
  3. 《税的真相》—— 读后总结
  4. 浅谈Junit测试中反射和Jmock的应用
  5. Java后台请求远程链接
  6. SCCM 2007 R2部署、操作详解系列之部署篇
  7. 百度地图API地理位置和坐标转换
  8. SOAP消息机制简介
  9. nginx最大并发连接数的思考:worker_processes、worker_connections、worker_rlimit_nofile
  10. Laravel自定义验证规则的实例与框架使用正则实例