原文地址:kislayverma.com/programming…
原文作者:kislayverma.com/
发布时间:发布于2021年4月10日
什么是自带文档的代码?
我喜欢记录代码和系统。很多人不喜欢。反对文档的一个主要理由是,随着系统的发展,文档会过时。而一个系统发展得越快,它的文档就越快过时。具有讽刺意味的是,这正是需要最新的文档的系统类型。
因此,经常有人提出一个论点,即自我文档化的代码。表面上看,这种代码不需要单独的文档,因为它的设计和实现方式对读者来说是不言自明的。
阅读代码库的人如何理解它呢?首先,他们需要知道这些代码 "应该做什么"。然后,他们就可以逐步弄清楚它是如何做到的。而这正是自文档化代码的问题所在。因为阅读代码本质上就是阅读如何做。"从数据库表中查询一些东西,并将它们处理成一个地图,将它们与其他一些表中的其他东西进行匹配,并将不匹配的东西作为一个列表返回"。写得好的代码让人很容易理解它是如何做某事的。但它并没有告诉读者为什么要做它正在做的事情。因此,读者仍然感到困惑,为了理解系统设计背后的意图,有必要编写文档。
那么,什么样的代码会揭示,即使是以某种有限的方式,为什么它做事情的方式?
我想谈一谈(IMO)代码能够向读者解释自己,从而成为自我文档化的唯一方式。让我们来讨论一下代码背后的模型。
成为自文档化的
在代码之前有一个概念模型,代码是它的物理表现。这是问题和解决方案的心理模型。这可能是领域模型,也可能是代表程序员思维过程的另一种具体方式,以及它将如何实现手头问题的程序化解决方案。
模型是低层次系统设计的核心。它定义了我们正在工作的事物,它们的性质是什么,以及它们在解决手头的问题中起什么作用。在确定API、数据存储、数据流等底层设计的任何其他方面之前,必须首先开发模型。这些都是模型中概念化的东西的物理形式--使模型 "运行 "的方法。模型本身是原因,而代码是方法。
使代码自我记录的唯一方法是使代码显示出它的模型。
这就是为什么使代码自文档化的唯一方法是使代码揭示出它的基础模型。突出核心模型而不是将其隐藏在实现细节中的代码建立了一种叙述方式,这比试图推断代码中某些行的含义要容易得多。这几行代码总是可以被解释的,但是在写得好的代码中所体现出来的模型,为这些代码行的意义提供了背景。
让我们来看看Airbnb预订的一个过于简化的版本。在面向对象、充满REST的世界中,更新预订的代码可能看起来像这样。(如果你禁用JS,请看这个gist)
public class Booking {
String uniqueId;
User guest;
User host;
Date bookingTime;
Date confirmationTime;
Date cancellationTime;
Status status; //PENDING, CONFIRMED, CANCELLED_BY_GUEST, CANCELLED_BY_HOST
User lastUpdatedBy;
}
public class BookingUpdateRequest {
Date updateTime;
Booking updatedBooking;
}
// In Booking Service
public void updateBooking(BookingUpdateRequest request) {
Booking originalBooking = readFromDB(request.updatedBooking.uniqueId);
if ((originalBooking.status == PENDING || originalBooking.status == CONFIRMED) &&
(request.updatedBooking.status == CANCELLED_BY_GUEST)) {
originalBooking.lastUpdatedBy = originalBooking.guest;
originalBooking.cancellationTime = request.updateTime
// Trigger notification to host
// Trigger refund if payment was taken
} else if ((originalBooking.status == PENDING || originalBooking.status == CONFIRMED) &&
(request.updatedBooking.status == CANCELLED_BY_HOST)) {
originalBooking.lastUpdatedBy = originalBooking.host;
// Trigger notification to guest
// Trigger refund if payment was taken
}
// else if ........ more conditions to handle other combinations of new/old variables
}
这种类型的通用更新代码是相当常见的。对我来说,这并不能解释为什么事情会以它们的方式发生。我们是否错过了什么情况?虽然这段代码可以被重构为更简洁的形式,但它并没有揭示出事情为什么会以这种方式发生。
让我们考虑一个替代方案(如果你已经禁用了JS,请看这个gist)。
public class Booking {
String uniqueId;
User guest;
User host;
Date bookingTime;
Date confirmationTime;
Date cancellationTime;
Status status; //PENDING, CONFIRMED, CANCELLED_BY_GUEST, CANCELLED_BY_HOST
User lastUpdatedBy;
}
// In Booking Service
public void cancelBookingByGuest(String bookingId, Date cancellationTime) {
Booking originalBooking = readFromDB(bookingId);
originalBooking.lastUpdatedBy = originalBooking.guest;
originalBooking.cancellationTime = cancelltaionTime;
originalBooking.status = CANCELLED_BY_GUEST;
// Trigger notification to host
// Trigger refund if payment was taken
updateInDB(originalBooking);
}
public void cancelBookingByHost(String bookingId, Date cancellationTime) {
Booking originalBooking = readFromDB(bookingId);
originalBooking.lastUpdatedBy = originalBooking.host;
originalBooking.cancellationTime = cancellationTime;
originalBooking.status = CANCELLED_BY_HOST;
// Trigger notification to guest
// Trigger refund if payment was taken
updateInDB(originalBooking);
}
// Other APIs to handle combinations of new/old variables...
或者是这样(如果你禁用了JS,请看这个要点)。
public class Booking {
String uniqueId;
User guest;
User host;
Date bookingTime;
Date confirmationTime;
Date cancellationTime;
Status status; //PENDING, CONFIRMED, CANCELLED_BY_GUEST, CANCELLED_BY_HOST
User lastUpdatedBy;
}
public class BookingUpdateRequest {
AllowedActionOnBooking action;
Date updateTime;
Booking updatedBooking;
}
// Explicitly define the ways of modiying the Booking entity
public enum AllowedActionOnBooking {
GUEST_CANCELLATION,
HOST_CANCELLATION,
CONFIRM,
DATE_CHANGE
}
// In Booking Service
public void updateBooking(BookingUpdateRequest request) {
Booking originalBooking = readFromDB(request.updatedBooking.uniqueId);
switch (request.action) {
case GUEST_CANCELLATION:
handleGuestCancellationRequest(originalBooking, request);
break;
case HOST_CANCELLATION:
handleHostCancellationRequest(originalBooking, request);
break;
case CONFIRM:
handleConfirmationRequest(originalBooking, request);
break;
case DATE_CHANGE:
handleDateChangeRequest(originalBooking, request);
break;
default: throw new Exception("Unhandled action on booking");
}
}
// Or move all these private methods to their own handler simplifying this class further
private void handleGuestCancellationRequest(Booking originalBooking, BookingUpdateRequest request) {
originalBooking.lastUpdatedBy = originalBooking.guest;
originalBooking.cancellationTime = cancelltaionTime;
originalBooking.status = CANCELLED_BY_GUEST;
// Trigger notification to host
// Trigger refund if payment was taken
updateInDB(originalBooking);
}
private void handleHostCancellationRequest(Booking originalBooking, BookingUpdateRequest request) {
originalBooking.lastUpdatedBy = originalBooking.host;
originalBooking.cancellationTime = cancelltaionTime;
originalBooking.status = CANCELLED_BY_HOST;
// Trigger notification to guest
// Trigger refund if payment was taken
updateInDB(originalBooking);
}
private void handleConfirmationRequest(Booking originalBooking, BookingUpdateRequest request) {
// Business logic
}
private void handleDateChangeRequest(Booking originalBooking, BookingUpdateRequest request) {
// Business logic
}
这有什么区别呢?
看了这些例子,我们就会发现,我们可以通过写代码来明确概念模型,从而减少读者的认知负担。做到这一点的方法是通过抽象。所有的代码都可能有一定量的抽象,但不是所有的抽象都能浮现出代码背后的思维过程。通常情况下,开发者只将模型抽象作为数据载体,而不给它们赋予任何行为或语义上的意义。通用更新API的第一个例子就是这种情况。Booking抽象只是携带数据,所有的意义都被封装在if-else条件中。
另一方面,模型驱动代码使用模型抽象来表示核心元素,并使用它们作为系统中所有其他交互的构建块。它们是系统的核心,所有其他代码只以模型本身定义和控制的方式操纵它们。在第二个和第三个例子中都是如此。
第一个例子中的代码并不是没有模型作为基础。就像不可能有 "无设计"(总是有设计的,即使是无心的和糟糕的),也不可能有 "无模型"。坐在开发者头脑中的解决方案就是模型。第一个例子只是选择了掩盖它,而后两个例子则努力让它变得清晰。
我希望这已经清楚地说明了明确使用概念模型的好处,并围绕它建立系统。这不会使系统(尤其是一个大系统)自动变得不言自明,但它确实在这个方向上走了很长一段路。
通过www.DeepL.com/Translator(免费版)翻译