让你的代码库经得起时间考验的 5 种方法
这是系列文章的第一篇,我和@hecrj将分享我们在过去 3 年里开发一个庞大且快速变化的代码库之后所学到的经验,并且我们对结果非常满意!
如果你是一名网页开发人员,你可能已经习惯了每隔一周就有新的框架、库和技术问世。
我们一直在不断探索更好的工具和模式,但这是否意味着我们的代码注定会变得老旧不堪?
如何让你的项目保持稳定,免受潮流的影响?以下是我们总结的5个行之有效的技巧。
1. 根据领域概念而非技术概念拆分代码
启动一个新项目时,你首先可能会遇到的问题之一就是如何构建项目结构。目前有两种比较流行的做法:一种是按技术概念划分文件,另一种是按领域概念划分文件。
# Split by tech concepts # Split by domain concepts
|- src |- auth
| |- controllers | |- controllers
| | |- auth | |- models
| | |- profile | |- views
| | |- article | |- tests
| |- models |- profile
| |- views |- article
|- test (...)
| |- controllers
| | |- auth
(...)
如果你已经阅读了标题,你可能已经知道我们会推荐什么,但让我们再补充一些想法来佐证。
假设你带着一个具体目标(例如,查找错误、添加功能、删除功能等)来到一个项目的根部。你需要找到相应的代码,浏览相关文件,查看测试,并在感觉足够自信时,对代码库进行相应的更改。
作为开发人员,这个过程是我们的命脉,所以我们必须提高它的效率。
维护一个包含 10 个文件的代码库更容易,还是维护一个包含 100 个文件的代码库更容易?
按领域概念拆分代码可以让你专注于代码库的一小部分,而按技术概念拆分代码则迫使你到处跳来跳去。
2. 为所有领域概念提供公共接口(API)。
想象一下,你的项目中有一个目录,里面存放着所有与💰相关的代码。我们有一系列组件,用于将支付信息存储在数据库中,或连接到像Stripepayments这样的第三方服务。
所有这些组件都是为了履行合同而存在的,也就是说,是为了确保payments它们按预期的方式运行。
需要澄清的是,我们这里讨论的不是你的移动应用用来向用户收费的HTTP API,而是一个内部API,它将你的支付目录转换成一个独立的“微服务”(此处“微服务”一词为引申义)。
你问为什么?
因为拥有明确的 API 可以带来以下好处:
-
清晰描绘预期行为。
-
每个人都能同意并承诺的最低测试覆盖率。
-
可以自由地更改底层实现中的任何内容。
此外,对于此 API 而言,尽可能少地了解用户、权限或环境等外部概念至关重要。这些并非领域知识的一部分。它们是我们解决通信层问题(公共 HTTP 端点本质上是不安全的)或开发工作流程的方式。
例如,我们可以想象拥有:
-
一个面向公众的 API,它公开了一些领域行为,并控制身份验证和授权。
-
一个私有的管理 API + 面板,无需接触任何数据库或控制台即可轻松提供客户支持和查找错误。
-
编写 fixtures、示例和迁移文件的简便方法。
3. 依赖小型接口
这个方法很流行。作为开发者,我们不断被提醒要依赖抽象而不是具体实现,要隔离接口,要反转依赖关系。
理论方面的内容很容易找到,所以我们重点来看一些实际例子。我们的Payments应用程序可能需要与以下接口通信:
-
活动出版商
-
活动订阅者
-
信用卡充电器
-
电子邮件发件人
所有这些接口的作用都很小且明确。稍后,我们将注入具体的实现:
production = Payments.new(
event_publisher: rabbitmq,
event_subscriber: rabbitmq_replicas,
credit_card_charger: stripe,
email_sender: mailgun,
)
development = Payments.new(
event_publisher: in_memory_bus,
event_subscriber: in_memory_bus,
credit_card_charger: stripe_test_mode,
email_sender: muted_mailer,
)
如您所见,小型接口使我们能够创建定义明确的测试,并根据环境为每个操作选择最佳策略。另一方面,我们通常会基于特定技术编写实现,以便将所有知识和辅助函数集中在这些技术周围。
4. 将数据与存储策略解耦
先说明一点:我们认为 ORM 有问题(或者可能是人们使用 ORM 的方式不对)。请看这段Ruby on Rails代码:
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments, dependent: :destroy
scope :authored_by, ->(username) { where(user: User.where(username: username)) }
validates :title, presence: true, allow_blank: false
validates :body, presence: true, allow_blank: false
before_validation do
self.slug ||= “#{title.to_s.parameterize}-#{rand(36**6).to_s(36)}”
end
end
这里面有很多信息需要分析。
首先,我们注意到这个对象描述了关系、级联删除和可空属性。这正是对象关系映射器应有的功能。非常透明!
接下来,我们不妨思考一下:在表示一篇文章时,对我们来说最重要的是什么?
-
我们应该能够充分利用所用语言的全部功能。使用 Java 时,我们希望能够自由地使用面向对象模式和继承。使用 Haskell 时,我们希望能够使用联合类型和记录。
-
我们应该能够以不同的格式和数据库存储数据。这样,我们就可以使用 ElasticSearch 进行高性能搜索,使用 PostgreSQL 保持数据状态的一致性,并使用 Redis 来确保自动保存功能足够快速。
ORM 模型既不提供这些功能,也不提供其他功能,因为它们只是与 SQL 数据库交互的一种方式。我们仍然需要在其他地方表示和操作数据。问题在于,一旦你接受了这个观点,使用 ORM 就显得笨拙或过于复杂。这就是我们的意思:
# Let's say we have a series of entities in our domain that we use to represent an article.
class Article; end # The big picture
class Tag; end
class RichText; end # Headings, bold, cross-references, …
# Now we need an interface to store the article's content in our SQL database.
class ArticleStore
def store(title:, body:, tags:, author:)
# Ruby doesn't have explicit interfaces, but you get the point
raise NotImplementedError
end
end
# Using an ORM creates an additional level of indirection that looks pointless
class ArticleORMStore < ArticleStore
def store(title:, body:, tags:, author:)
ArticleModel.create(title: title, body: body, tags: tags, author: UserModel.get(author.id))
end
end
# A low-level SQL library feels simpler in comparison.
class ArticleSimpleStore < ArticleStore
def store(title:, body:, tags:, author:)
article_table.insert(title: title, body: body, tags: tags, author: author.id)
end
end
关键在于:你可以使用 ORM,但不要将其作为表示和操作数据的唯一方式。这远远偏离了 ORM 的初衷。
5. 使用事件来保持应用程序的连接性,并使代码解耦。
如果应用程序的两个部分是相互连接的,那么它们的代码也必然以某种方式相互连接,对吗?
事件驱动编程在保持应用程序互联互通方面做得非常出色,同时还能让代码易于编写和维护。事实上,它的效果如此显著,以至于类似的理念在移动和前端开发领域以“响应式编程”之名广泛应用,在运维领域也备受青睐,云服务提供商和企业都在大力投资。
基本思想是,对域的每一次更改都表示为一个原子事件。
article_published(…) 1 minute ago
article_draft_created(…) 5 minutes ago
user_signed_in(…) 25 minutes ago
所有事件都通过某种事件总线发布,随机观察者可以订阅并对感兴趣的事件做出反应,而不会过多地干扰其他组件。
一开始确实需要多花些功夫,因为你需要为事件总线打好基础,并考虑每个事件的属性和原子性,但从长远来看,这绝对是值得的。
以下是一些使用事件驱动架构非常容易实现,但在其他架构下却难以考虑和维护的功能示例:
-
监听文章评论并增加计数器(目的:更快地统计评论数)。
-
向新用户发送欢迎邮件。
-
通知文章作者该文章有新的评论。
试着想象一下,你会如何以命令式和被动式的方式完成这些任务。
事件驱动编程避免了冗长的函数及其许多不同的副作用,并使你的测试更简洁、更独立。
下一篇文章我们将解释如何将所有这些部分组合在一起,创建我们自己的架构。
欢迎在评论区留言,告诉我们您对这些想法的看法!
文章来源:https://dev.to/larribas/5-ways-to-make-your-codebase-withstand-the-test-of-time-3652
