发布于 2026-01-06 1 阅读
0

让你的代码库经得起时间考验的 5 种方法

让你的代码库经得起时间考验的 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
    (...)
Enter fullscreen mode Exit fullscreen mode

如果你已经阅读了标题,你可能已经知道我们会推荐什么,但让我们再补充一些想法来佐证。

假设你带着一个具体目标(例如,查找错误、添加功能、删除功能等)来到一个项目的根部。你需要找到相应的代码,浏览相关文件,查看测试,并在感觉足够自信时,对代码库进行相应的更改。

作为开发人员,这个过程是我们的命脉,所以我们必须提高它的效率。

维护一个包含 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,
)
Enter fullscreen mode Exit fullscreen mode

如您所见,小型接口使我们能够创建定义明确的测试,并根据环境为每个操作选择最佳策略。另一方面,我们通常会基于特定技术编写实现,以便将所有知识和辅助函数集中在这些技术周围。

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
Enter fullscreen mode Exit fullscreen mode

这里面有很多信息需要分析。

首先,我们注意到这个对象描述了关系、级联删除和可空属性。这正是对象关系映射器应有的功能。非常透明!

接下来,我们不妨思考一下:在表示一篇文章时,对我们来说最重要的是什么?

  • 我们应该能够充分利用所用语言的全部功能。使用 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
Enter fullscreen mode Exit fullscreen mode

关键在于:你可以使用 ORM,但不要将其作为表示和操作数据的唯一方式。这远远偏离了 ORM 的初衷。

5. 使用事件来保持应用程序的连接性,并使代码解耦。

如果应用程序的两个部分是相互连接的,那么它们的代码也必然以某种方式相互连接,对吗?

事件驱动编程在保持应用程序互联互通方面做得非常出色,同时还能让代码易于编写和维护。事实上,它的效果如此显著,以至于类似的理念在移动和前端开发领域以“响应式编程”之名广泛应用,在运维领域也备受青睐,云服务提供商和企业都在大力投资

基本思想是,对域的每一次更改都表示为一个原子事件。

    article_published(…) 1 minute ago
    article_draft_created(…) 5 minutes ago
    user_signed_in(…) 25 minutes ago
Enter fullscreen mode Exit fullscreen mode

所有事件都通过某种事件总线发布,随机观察者可以订阅并对感兴趣的事件做出反应,而不会过多地干扰其他组件。

一开始确实需要多花些功夫,因为你需要为事件总线打好基础,并考虑每个事件的属性和原子性,但从长远来看,这绝对是值得的。

以下是一些使用事件驱动架构非常容易实现,但在其他架构下却难以考虑和维护的功能示例:

  • 监听文章评论并增加计数器(目的:更快地统计评论数)。

  • 向新用户发送欢迎邮件。

  • 通知文章作者该文章有新的评论。

试着想象一下,你会如何以命令式和被动式的方式完成这些任务。

事件驱动编程避免了冗长的函数及其许多不同的副作用,并使你的测试更简洁、更独立。


下一篇文章我们将解释如何将所有这些部分组合在一起,创建我们自己的架构。

欢迎在评论区留言,告诉我们您对这些想法的看法!

文章来源:https://dev.to/larribas/5-ways-to-make-your-codebase-withstand-the-test-of-time-3652