让你的代码面向未来

水晶球和千里眼:在不可避免的变化世界中证明未来

虽然未来可能是个谜,但您可以设计软件以适应未来的变化。但是,有多少面向未来的设计会阻碍良好的设计?

让你的代码面向未来的概念是一个常年出现在关于软件的对话中的概念。这听起来很神奇——谁不想让您的代码不受未来影响?

当然,现实远没有那么美好,也更加混乱。

在本文中,我将讨论我认为人们所说的“面向未来”的含义、您如何能够实现它,以及何时以及为什么它可能是一个糟糕的选择。

一个人如何“证明”一个人的“未来”?

您可以将面向未来更准确地看作是面向变化的。我们可以将其定义为:

“做出设计、架构或编程决策,使未来的更改更易于管理、花费更少的时间或减少对整体代码的更改。”

这些变化可以分为许多不同的类别:

  • 规模的变化——项目必须做更多它正在做的事情,这是否意味着处理更多的流量或工作项目,更快地处理它们,等等。
  • 需求变更——新信息已进入业务(或已从业务进入工程团队),现在系统需要更改以适应它们。
  • 技术堆栈的变化——例如,切换到不同的数据存储或编程语言。
  • 集成更改——该项目需要与新的第三方应用程序对话,该应用程序可以在现有应用程序之上或代替现有应用程序。
  • 模式更改——我们想要更改定义数据对象的字段。

我应该留下还是应该成长?

面向未来的主要问题是它与软件工程的核心原则之一相冲突:YAGNI,或者你不需要它。 

该原则指出,在您真正知道您将要进行更改之前,您不应该以预期该更改的方式编写软件代码。违反这一原则会导致臃肿、混乱和不必要的抽象,使代码更难理解,而且通常会产生大量的技术债务。

但是,如果您从不着眼于未来,那么您确实会遇到这样的问题,即在前期多做一点工作就可以为您节省数月的时间。

那么……你应该让你的代码面向未来吗?与软件中的几乎所有内容一样,答案是“视情况而定”。

我认为这是各种各样的:

 

当然,每种情况都是独一无二的。但某些类型的变化不太可能值得未来验证,而在其他方面,额外的工作更有可能获得回报,并且(同样重要的是)不会导致重大不利影响。

面向未来的战略

一般来说,任何时候我们想要保护自己免受未来变化的影响,我们都会采用两种策略之一。

模块化

模块化是将代码拆分成更小块的行为。每个软件都有一定程度的模块化——否则,您的代码将由一个巨大的文件组成,其中只有展开的函数和原始类型。(这些程序确实存在,但头脑正常的人都不想与它们有任何关系。) 

增加模块化允许您将更改隔离到单个模块,和/或更容易地相互 交换模块。

您还可以在不同级别进行模块化。将代码提取到一个函数中是一种简单的模块化行为,它很少有缺点,直到您的函数变得如此之小且数量众多以至于难以跟踪。但是,您可以将事物提取到一个类、一个子应用程序、一个微服务,甚至一个单独的集群中;在每个级别上,随着隔离性和灵活性的提高,管理不同组件的认知和管理开销也会增加。

抽象

抽象是您对代码部分的心智模型。低抽象度(或高特异性)的模块、函数、类等更接近地模拟它所采取的动作或它所代表的对象。具有更高抽象的事物会使您的推理更上一层楼。例如,您的代码Cars可以与 一起工作,而不是与 一起工作Vehicles。这样,如果您需要开始处理Motorcycles,这样做所需的更改会因您从未真正开始讨论而得到缓解Cars

抽象程度越高,编写的代码就越灵活,但也越难理解。每个人都知道 aCar是什么,但是当您开始使用 时Vehicles,使用Car- 特定的东西(例如加高座椅和方向盘)会变得更加困难。而且,如果您继续在抽象树上走得更高,您可能会发现自己围绕 , 甚至是普通的旧代码进行编码TransportationManufacturedGoods并且Objects很难弄清楚如何加满油箱。

变化的类型

让我们来看看上面列出的每种更改类型。对于每一个,我都建议您的未来验证工作可能位于频谱的哪个位置。通常,当您当前没有迹象表明会发生更改时,我会应用这些建议。随着更改的可能性越来越大,您会进一步向线的左侧移动。

规模变化

 

 让你的代码和架构健壮并能够处理你扔给它的任何东西是设计的核心方面。但是,该体系结构和设计的一部分必须是您期望它具有什么样的流量。通常,您必须处理的规模越大,您的架构就需要越复杂。

例如,制作一个旨在供少数内部用户使用的简单应用程序可以通过 Rails 或 Django 应用程序使用最少的 JavaScript 来完成。但是,如果您需要能够处理数以千计或数百万的并发用户,那是行不通的。您将需要闪电般快速的服务或微服务、水平缩放和自动缩放,可能还需要响应速度更快的前端以及更定制化和更复杂的缓存机制。

当您没有预料到您的应用程序实际上会扩展到该大小时,预先过度构建所有这些是公然的 YAGNI。事实上,即使您知道在规模发生变化时可能需要重写系统,也有意为您当前的规模构建系统是一种完全有效的策略,有时称为牺牲架构

话虽如此,您应该考虑一些扩展注意事项。遗留大量N+1 查询或有不必要的大或频繁请求是糟糕的工程实践,并且会在用户口中留下不好的印象——无论他们有多少。

要求变更

这种情况(您的代码必须更改或添加其行为)是最难猜测的,也是最有可能发生的情况。

在这种情况下,通用模块化可以为您提供帮助,但我建议不要尝试过早地向您的代码添加抽象。

特别是,如果我们谈论的是业务规则,那么以一种易于测试和更改的方式隔离规则本身是一种总体良好做法。

作为一个简单的例子,下面是一些使用产品的代码,这些代码对什么是有效产品有一些规则:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-variable)">product.save</span> <span style="color:var(--highlight-variable)">if</span> <span style="color:var(--highlight-variable)">product.price</span> <span style="color:var(--highlight-variable)">>=</span> <span style="color:var(--highlight-namespace)">0</span> <span style="color:var(--highlight-variable)">&&</span> <span style="color:var(--highlight-namespace)">!product.name.nil?</span></code></span></span>

产品可能是从应用程序中的许多不同位置保存的。我们可以将它们提取到自己的方法中,而不是检查这些业务规则的调用代码:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-keyword)">class</span> <span style="color:var(--highlight-literal)">Product</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">valid?</span>
    <span style="color:var(--highlight-keyword)">self</span>.price >= <span style="color:var(--highlight-namespace)">0</span> && !<span style="color:var(--highlight-keyword)">self</span>.name.<span style="color:var(--highlight-literal)">nil</span>?
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span>
product.save <span style="color:var(--highlight-keyword)">if</span> product.valid?</code></span></span>

我们甚至可以走得更远,有一个单独的类或模块来处理此检查:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-keyword)">module</span> <span style="color:var(--highlight-literal)">ProductValidator</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">self</span>.<span style="color:var(--highlight-literal)">valid?</span>(product)
    product.price >= <span style="color:var(--highlight-namespace)">0</span> && !product.price.<span style="color:var(--highlight-literal)">nil</span>?
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span>
product.save <span style="color:var(--highlight-keyword)">if</span> ProductValidator.valid?(product)</code></span></span>

这些是相对较小的更改,可以使您的代码保持整洁且易于测试,并且可以将此类进一步的更改隔离到一个地方。 

然而,关键词是“这种”。如果需求更改涉及完全改变功能的行为方式或添加新功能,那么您就需要开始更加坚定地站在 YAGNI 方面,并且只构建您真正知道的东西。

假设您的产品可以购买:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-keyword)">class</span> <span style="color:var(--highlight-literal)">Product</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">purchase</span>
    <span style="color:var(--highlight-comment)"># contact the purchase service and complete transaction</span>
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span></code></span></span>

等一下,你的理由。我们最终可能会扩展到不仅购买产品而且销售产品!也许租用它们?我们应该把它抽象出来:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-keyword)">class</span> <span style="color:var(--highlight-literal)">Product</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">perform_action</span>
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span>

<span style="color:var(--highlight-keyword)">class</span> <span style="color:var(--highlight-literal)">PurchasableProduct</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">perform_action</span>
    <span style="color:var(--highlight-comment)"># contact the purchase service and complete transaction</span>
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span></code></span></span>

这是典型的 YAGNI 过度抽象。阅读您的代码的人不会看到您购买产品,您是在对解决购买的 事情“执行操作” 。

您没有理由相信您的业务实际上会扩展到进一步的业务。您已经引入了一个抽象级别,该级别将您的代码从实际发生的事情和正在建模的事情中移除了一个额外的步骤。

技术栈的变化

 

我会直截了当地说——没有充分的理由以您计划最终改变您的基本技术堆栈的方式构建您的应用程序。

从编程语言的角度来看,这显然几乎是不可能做到的。提供更多隔离的唯一方法是进一步研究微服务——从架构的角度来看这可能有意义,但从面向未来的角度来看则不然。

至于数据存储,没有人——没有人——真正将他们的数据库从 MySQL 更改为 Postgres,反之亦然。如果您使用开源或开放标准方式与您的数据进行交互,请全力以赴,不要回头。在这一点上,相似之处远远超过不同之处,如果您确实出于任何神秘原因需要切换,无论如何您都需要一个完整的回归套件以确保没有其他问题。

(注意:这一点经常被用来劝阻ORM的使用。我认为 ORM 有其他非常好的优点,但是切换数据技术的能力并不是使用它们的实际理由。)

集成的变化

 

在这种情况下,我们担心必须改变我们与某些第三方应用程序的对话方式。这些服务可以提供指标、跟踪、警报、日志、功能标志、对象存储、部署等内容。在这种情况下,可以根据成本、功能集自由更改与您开展业务的人员,这很好、易用性等

我最倾向于确保我们围绕集成进行抽象和模块化。在许多情况下,这些第三方应用程序可以很容易地相互切换。例如,一旦您提供了指标或跟踪,无论您与哪个供应商签约,抽象和想法都非常接近。

我更喜欢做的是围绕任何需要与第三方系统对话的代码构建一个外观。通常为了简单起见,这是我选择的任何提供程序的 API 的包装器,它公开了操作所需的所有功能。

这个门面通常只需要几天的时间来构建(如果还没有的 并且可以用于任何需要它的项目。我会经常添加一组最接近我们最常见用例的团队或公司默认值,这样新项目就不需要自己弄清楚了。

这个外观允许您通过不将您的系统绑定到任何特定的供应商来避免供应商锁定——但开销足够小,不会让您或您的团队成员感到困惑。

重要的一件事是允许您在必要时通过访问内部客户端或 API 来突破外观。如果您因为需要使用特定功能而选择了提供商,那么您不需要将手臂绑在背上以阻止您使用它们。然而,这意味着如果您最终确实更换了供应商,则不必对“正常”情况进行任何更改——您只需查看访问内部客户端或 API 的代码并将您的工作集中在那里。

这是我们为内部使用而构建的功能标记库的粗略近似值:

<span style="color:var(--highlight-color)"><span style="background-color:var(--highlight-bg)"><code><span style="color:var(--highlight-keyword)">module</span> <span style="color:var(--highlight-literal)">FlippFlags</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">backend=</span>(backend)
    <span style="color:var(--highlight-keyword)">self</span>.backend = backend
  <span style="color:var(--highlight-keyword)">end</span>

  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">client</span>(config)
    <span style="color:var(--highlight-keyword)">self</span>.backend.new(config)
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span>

<span style="color:var(--highlight-keyword)">class</span> <span style="color:var(--highlight-literal)">FlippFlags::Backend</span>
  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">initialize</span>(config) <span style="color:var(--highlight-comment)"># this is a Ruby version of a constructor, i.e. the "new" keyword</span>
    @client = ... <span style="color:var(--highlight-comment)"># initialize the actual client</span>
  <span style="color:var(--highlight-keyword)">end</span>
 
<span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">enabled?</span>(flag, user)
    @client.enabled?(flag, user)
<span style="color:var(--highlight-keyword)">end</span>

  <span style="color:var(--highlight-keyword)">def</span> <span style="color:var(--highlight-literal)">internal_client</span>
    @client
  <span style="color:var(--highlight-keyword)">end</span>
<span style="color:var(--highlight-keyword)">end</span>

<span style="color:var(--highlight-comment)"># application code</span>
FlippFlags.backend = FlippFlags::MyFlagProvider
client = FlippFlags.client(my_config)
client.enabled?(<span style="color:var(--highlight-variable)">"flag_name"</span>, user_object) <span style="color:var(--highlight-comment)"># true or false</span></code></span></span>

在这里您可以看到,您需要做的唯一更改是切换backend传递到库中的 ,它会无缝切换到不同的提供者——或者甚至可能是一个以相同方式工作的内部类!否则,您将完全按照使用内部客户端的方式使用客户端,但不会将您的代码直接绑定到该实现。

架构的更改

 

我们数据的形状反映了我们对它的理解。随着我们获得更多的了解,我们常常想改变它的形状。这可能意味着添加或删除字段、更改字段类型、默认值等。

通过遵循一些简单的规则,可以使我们的数据模式向后和向前兼容。AvroProtobuf等技术指定了这些规则,并将它们内置到他们的工具中。但是,如果您不使用这些工具,则可以在更改自己的模式时“软执行”类似的规则,以确保您的更改不太可能破坏依赖于您的数据的任何内容。

话虽如此,但在某些情况下您根本无法遵守这些规则——这没关系!这是当您指定从旧数据到新数据的迁移路径时。但规则是这样的,所以你不需要为你经历的每一个数据更改都遵循这个繁琐的过程。

结论

面向未来不一定是软件开发的目标,它只是影响您的设计的众多“功能”之一。在这个范围内过度纠正会导致不必要的工作、技术债务和令人困惑的抽象。不过,找到最佳点可以为您节省时间和精力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值