【Code】《代码整洁之道》笔记-Chapter11-系统

Windows下MikTeX与VS Code搭建LaTeX环境:从安装配置到高效写作指南 LaTeX作为一种基于标记语言的文档排版系统,其核心原理是将内容与格式分离,通过编译源代码生成高质量、格式一致的PDF文档。这一特性使其在学术论文、技术报告等需要复杂数学公式、交叉引用和参考文献管理的场景中具有不可替代的技术价值。对于Windows用户而言,搭建LaTeX环境常涉及发行版和编辑器的选择。MikTeX发行版以其“按需安装”机制著称,能有效节省存储空间并简化包管理。而Visual Studio Code(VS Code)作为现代代码编辑器,通过LaTeX Workshop等插件可构建强大的集成开 阅读详情

第11章 系统

“复杂要人命。它消磨开发者的生命,让产品难以规划、构建和测试。”

11.1 如何建造一个城市

你能自己掌管一切细节吗?大概不行。即便是管理一个既存的城市,也是靠单人能力无法做到的。不过,城市还是在运转(多数时候)。这是因为每个城市都有各种组织管理不同的部分,如供水系统、供电系统、交通、执法、立法,诸如此类。有些人负责全局,有些人负责细节。

城市能运转,还因为它演化出恰当的抽象等级和模块,好让个人和其所管理的“组件”即便在不了解全局时也能有效地运转。

尽管软件团队往往也是这样组织起来的,但他们所致力的工作却常常没有同样的关注面切分及抽象层级,而整洁的代码可以帮助我们在较低的抽象层级上达成这一目标。本章将讨论如何在较高的抽象层级(系统层级)上保持整洁。

11.2 将系统的构造与使用分开

首先,构造与使用是非常不一样的过程。当我走笔至此,投目窗外的芝加哥,看到有一家酒店正在建设。今天,那只是个框架结构,起重机和升降机布设在外面。忙碌的人们身穿工作服,头戴安全帽。大概一年之后,酒店就将建成。起重机和升降机都会消失无踪。建筑物变得整洁,覆盖着玻璃幕墙和漂亮的漆色。在其中工作和住宿的人,会看到完全不同的景象。

软件系统应将起始过程和起始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在互相缠结的依赖关系。

每个应用程序都该留意起始过程。那也是本章中我们首先要考虑的问题。将关注的方面分离开,是软件技能中最古老也最重要的设计技能。

不幸的是,多数应用程序都没有做分离处理。起始过程代码很特殊,被混杂到运行时逻辑中,下例就是典型的这种情形:

public Service getService() {
  if (service == null)
    service = new MyServiceImpl(...);  // Good enough default for most cases?
  return service;
}

这就是所谓延迟初始化/赋值,它有一些好处,即在真正用到对象之前,无须操心这种架空构造,起始时间也会更短,而且还能保证永远不会返回null值。

然而,我们也得到了MyServiceImpl及其构造器所需一切(我省略了那些代码)的硬编码依赖。不搞好这些被依赖项,程序就无法编译,即便在运行时永不使用这种类型的对象!

测试也会是问题。如果MyServiceImpl是一个重型对象,则我们必须确保在单元测试调用该方法之前,就给服务指派恰当的测试替身(TEST DOUBLE)或仿制对象(MOCK OBJECT)。由于构造逻辑与运行过程相混杂,我们必须测试所有的执行路径(例如,null值测试及其代码块)。有了这些权责,说明方法做了不止一件事,这样就略微违反了单一权责原则。

最糟糕的大概是,我们不知道MyServiceImpl在所有情形中是否都是正确的对象。我在代码注释中做了暗示。为什么该方法所属类必须知道全局情景?我们是否真能知道在这里要用到的正确对象?是否真有可能存在一种放之四海而皆准的类型?

当然,仅出现一次的延迟初始化不算是严重问题。不过,在应用程序中往往有许多种类似的情况出现。于是,全局设置策略(如果有的话)在应用程序中四散分布,缺乏模块组织性,通常也会有许多重复代码。

如果我们勤于打造有着良好格式并且强固的系统,就不该让这类就手小技巧破坏模块组织性,对象构造的起始和设置过程也不例外。应当将这个过程从正常的运行时逻辑中分离出来,确保拥有解决主要依赖问题的全局性一贯策略。

11.2.1 分解main

将构造与使用分开的方法之一是将全部构造过程搬迁到main或被称之为main的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置。

控制流程很容易理解。main函数创建系统所需的对象,再传递给应用程序,应用程序只管使用。注意,看横贯main与应用程序之间隔离的依赖箭头的方向,它们都从main函数向外走,这表示应用程序对main或者构造过程一无所知,它只是简单地指望一切已齐备。

11.2.2 工厂

当然,有时应用程序也要负责确定何时创建对象。例如,在某个订单处理系统中,应用程序必须创建LineItem实体,添加到Order对象。在这种情况下,我们可以使用抽象工厂模式让应用自行控制何时创建LineItem,但构造的细节却隔离于应用程序代码之外。

再注意一下,所有依赖都是从main指向OrderProcessing应用程序的,这表示应用程序与如何构建LineItem的细节是分离开来的。其中构建能力由LineItemFactoryImplementation持有,而LineItemFactoryImplementation又是在main这一边的,但应用程序能完全控制LineItem实体何时构建,甚至能传递应用程序特定的构造器参数。

11.2.3 依赖注入

有一种强大的机制可以实现分离构造与使用,那就是依赖注入(Dependency Injection,DI)。控制反转(Inversion of Control,IoC)在依赖管理中的一种应用手段。控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。在依赖管理情景中,对象不应负责实体化对自身的依赖,反之,它应当将这份权责移交给其他“有权力”的机制,从而实现控制的反转。因为初始设置是一种全局问题,所以通常这种授权机制要么是main例程,要么是有特定目的的容器。

JNDI查找是DI的一种“部分”实现。在JNDI中,对象请求目录服务器提供一种符合某个特定名称的“服务”。

MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));

调用对象并不控制真正返回对象的类别(当然前提是它实现了恰当的接口),但调用对象仍然主动解决了依赖问题。

真正的依赖注入还要更进一步。类并不直接解决依赖问题,而是保持完全被动。它提供可用于注入依赖的赋值器方法或构造器参数(或二者皆有)。在构造过程中,DI容器实体化需要的对象(通常按需创建),并使用构造器参数或赋值器方法将依赖连接到一起。至于哪个依赖对象真正得到使用,是通过配置文件或在一个有特殊目的的构造模块中编程决定的。

Spring框架提供了最有名的Java DI容器。用户在XML配置文件中定义互相关联的对象,然后用Java代码请求特定的对象。稍后我们就会看到相关例子。

但延后初始化的好处是什么呢?这种手段在DI中也有其作用。首先,多数DI容器在需要对象之前并不构造对象。其次,许多这类容器提供调用工厂或构造代理的机制,而这种机制可为延迟赋值或类似的优化处理所用。

11.3 扩容

城市由城镇而来,城镇由乡村而来。一开始,道路狭窄,几乎无人涉足,随后逐渐拓宽。小型建筑和空地渐渐被更大的建筑所取代,一些地方最终矗立起摩天大楼。

一开始,供电、供水、下水、互联网(哇!)等服务全部欠奉。随着人口和建筑密度的增加,这些服务也开始出现。

这种成长并非全无阵痛。想想看你有多少次开着车,艰难穿行一个“道路改善”工程时,是否问过自己:“他们为什么不一开始就修条够宽的路呢?!”

不过那无论如何不可能实现。谁敢打包票说在小镇里修建一条六车道的公路并不浪费呢?谁会想要这么一条穿过他们小镇的路呢?

“一开始就做对系统”纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现。

但在系统层面又如何呢?难道系统架构不需要预先做好计划吗?系统理所当然不可能从简单递增到复杂,它能行吗?

与物理系统相比,软件系统比较独特。软件系统的架构可以递增式地增长,只要我们持续将关注面恰当地切分。

如我们将见到的那样,软件系统短生命周期的本质使这一切变得可行。我们先来看一个没有充分隔离关注问题的架构反例。

初始的EJB1和EJB2架构没有恰当地切分关注面,从而给有机增长压上了不必要的负担。比如一个持久Bank类的Entity Bean。Entity Bean是关系数据在内存中的体现,换言之,是表格的一行。

首先,你要定义一个本地(进程内)或远程(分离的JVM)接口,供客户代码使用。代码清单11-1就是一种可能的本地接口:

代码清单11-1 Bank EJB的EJB2本地接口

package com.example.banking;
import java.util.Collections;
import javax.ejb.*;

public interface BankLocal extends java.ejb.EJBLocalObject {
  String getStreetAddr1() throws EJBException;
  String getStreetAddr2() throws EJBException;
  String getCity() throws EJBException;
  String getState() throws EJBException;
  String getZipCode() throws EJBException;
  void setStreetAddr1(String street1) throws EJBException;
  void setStreetAddr2(String street2) throws EJBException;
  void setCity(String city) throws EJBException;
  void setState(String state) throws EJBException;
  void setZipCode(String zip) throws EJBException;
  Collection getAccounts() throws EJBException;
  void setAccounts(Collection accounts) throws EJBException;
  void addAccount(AccountDTO accountDTO) throws EJBException;
}

上面列出了银行地址的几个属性,和一组该银行拥有的账户,其中每个账户的数据都由单独的Account EJB所持有。代码清单11-2展示了Bank Bean的相应实现类。

代码清单11-2 相应的EJB2 Entity Bean实现

package com.example.banking;
import java.util.Collections;
import javax.ejb.*;

public abstract class Bank implements javax.ejb.EntityBean {
  // Business logic...
  public abstract String getStreetAddr1();
  public abstract String getStreetAddr2();
  public abstract String getCity();
  public abstract String getState();
  public abstract String getZipCode();
  public abstract void setStreetAddr1(String street1);
  public abstract void setStreetAddr2(String street2);
  public abstract void setCity(String city);
  public abstract void setState(String state);
  public abstract void setZipCode(String zip);
  public abstract Collection getAccounts();
  public abstract void setAccounts(Collection accounts);
  public void addAccount(AccountDTO accountDTO) {
    InitialContext context = new InitialContext();
    AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
    AccountLocal account = accountHome.create(accountDTO);
    Collection accounts = getAccounts();
    accounts.add(account);
  }
  // EJB container logic
  public abstract void setId(Integer id);
  public abstract Integer getId();
  public Integer ejbCreate(Integer id) { ... }
  public void ejbPostCreate(Integer id) { ... }
  // The rest had to be implemented but were usually empty:
  public void setEntityContext(EntityContext ctx) {} 
  public void unsetEntityContext() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void ejbLoad() {}
  public void ejbStore() {}
  public void ejbRemove() {}
}

我没有列出对应的LocalHome接口,该接口基本上是用来创建对象的,也没有列出你可能添加的Bank查找器(查询)。

最后,你要编写一个或多个XML部署说明,将对象相关映射细节指定给某个持久化存储空间,说明期望的事物行为、安全约束等。

业务逻辑与EJB2应用“容器”紧密耦合。你必须子类化容器类型,必须提供许多个该容器所需要的生命周期方法。

由于存在这种与重量级容器的紧耦合,隔离单元测试就很困难。有必要模拟出容器(这很难),或者花费大量时间在真实服务器上部署EJB和测试。也由于耦合的存在,在EJB2架构之外的复用实际上变得不可能。

最终,连面向对象编程本身也被侵蚀。bean不能继承自另一个bean。留意添加新账号的逻辑。在EJB2 bean中,定义一种本质上是无行为struct的“数据传输对象”(DTO)很常见。这往往会导致拥有同样数据的冗余类型出现,而且也需要在对象之间复制数据的八股式代码。

横贯式关注面

在某些领域,EBJ2架构已经很接近真正的关注面切分。例如,在与源代码分离的部署描述中声明了期待的事务、安全及部分持久化行为。

注意,持久化之类关注面倾向于横贯某个领域的天然对象边界。你会想用同样的策略来持久化所有对象,例如,使用DBMS而非平面文件,表名和列名遵循某种命名约定,采用一致的事务语义,等等。

原则上,你可以从模块、封装的角度推理持久化策略。但在实践上,你却不得不将实现了持久化策略的代码铺展到许多对象中。我们用术语横贯式关注面(Cross-Cutting Concern)来形容这类情况。同样,对于持久化框架和领域逻辑,如果我们孤立地看也可以是模块化的。问题在于横贯这些领域的情形。

实际上,EJB架构处理持久化、安全和事务的方法要早于面向方面编程(aspect-oriented programming,AOP),而AOP是一种恢复横贯式关注面模块化的普适手段。

在AOP中,被称为方面(aspect)的模块构造说明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明或编程机制来实现的。

以持久化为例,可以声明哪些对象和属性(或其模式)应当被持久化,然后将持久化任务委托给持久化框架。行为的修改由AOP框架以无损方式在目标代码中进行。下面来看看Java中的3种方面或类似方面的机制。

11.4 Java代理

Java代理适用于简单的情况,例如在单独的对象或类中包装方法调用。然而,JDK提供的动态代理仅能与接口协同工作。对于代理类,你得使用字节码操作库,比如CGLIB、ASM或Javassist。

代码清单11-3展示了为我们的Bank应用程序提供持久化支持的JDK代理,代码仅覆盖设置和取得账号列表的方法。

代码清单11-3 JDK代理范例

// Bank.java (suppressing package names...)
import java.util.*;

// The abstraction of a bank.
public interface Bank {
  Collection<Account> getAccounts();
  void setAccounts(Collection<Account> accounts);
}

// BankImpl.java
import java.utils.*;

// The "Plain Old Java Object" (POJO) implementing the abstraction.
public class BankImpl implements Bank {
  private List<Account> accounts;

  public Collection<Account> getAccounts() { 
    return accounts; 
  }

  public void setAccounts(Collection<Account> accounts) { 
    this.accounts = new ArrayList<Account>(); 
    for (Account account: accounts) {
      this.accounts.add(account);
    }
  }
}

// BankProxyHandler.java
import java.lang.reflect.*;
import java.util.*;

// "InvocationHandler" required by the proxy API.
public class BankProxyHandler implements InvocationHandler {
  private Bank bank;

  public BankHandler (Bank bank) {
    this.bank = bank;
  }

  // Method defined in InvocationHandler
  public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable {
    String methodName = method.getName();
    if (methodName.equals("getAccounts")) {
      bank.setAccounts(getAccountsFromDatabase());
      return bank.getAccounts();
    } else if (methodName.equals("setAccounts")) {
      bank.setAccounts((Collection<Account>) args[0]);
      setAccountsToDatabase(bank.getAccounts());
      return null;
    } else {
      ...
    }
  }

  // Lots of details here:
  protected Collection<Account> getAccountsFromDatabase() { ... }
  protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
}

// Somewhere else...

Bank bank = (Bank) Proxy.newProxyInstance(
  Bank.class.getClassLoader(), 
  new Class[] { Bank.class },
  new BankProxyHandler(new BankImpl()));

我们定义了将被代理包装起来的接口Bank,还有旧式的Java对象(Plain-Old Java Object,POJO)BankImpl,该对象实现业务逻辑(稍后再来看POJO)。

Proxy API需要一个InvocationHandler对象,用来实现对代理的全部Bank方法调用。BankProxyHandler使用Java反射API将一般方法调用映射到BankImpl中的对应方法,以此类推。

即便对于这样简单的例子,也有许多相对复杂的代码。使用那些字节操作类库也同样具有挑战性。代码量和复杂度是代理的两大弱点,创建整洁代码变得很难!另外,代理也没有提供在系统范围内指定执行点的机制,而那正是真正的AOP解决方案所必需的。

11.5 纯Java AOP框架

幸运的是,编程工具能自动处理大多数代理模板代码。在数个Java框架中,代理都是内嵌的,如Spring AOP和JBoss AOP等,从而能够以纯Java代码实现面向方面编程。在Spring中,你将业务逻辑编码为旧式Java对象。POJO自扫门前雪,并不依赖于企业框架(或其他域),因此,它在概念上更简单、更易于测试驱动,相对简单,也较易于保证正确地实现相应的用户故事,并为未来的用户故事维护和改进代码。

通过使用描述性配置文件或API,你可以把需要的应用程序构架组合起来,包括持久化、事务、安全、缓存、恢复等横贯性问题。在许多情况下,你实际上只是指定Spring或Jboss类库,框架以对用户透明的方式处理使用Java代理或字节代码库的机制。这些声明驱动了依赖注入(DI)容器,DI容器再实体化主要对象,并按需将对象连接起来。

代码清单11-4展示了Spring V2.5配置文件app.xml的典型片段。

代码清单11-4 Spring 2.x的配置文件

<beans>
  ...
  <bean id="appDataSource"
  class="org.apache.commons.dbcp.BasicDataSource" 
  destroy-method="close"
  p:driverClassName="com.mysql.jdbc.Driver" 
  p:url="jdbc:mysql://localhost:3306/mydb" 
  p:username="me"/>

  <bean id="bankDataAccessObject"
  class="com.example.banking.persistence.BankDataAccessObject"
  p:dataSource-ref="appDataSource"/>

  <bean  id="bank"
  class="com.example.banking.model.Bank"
  p:dataAccessObject-ref="bankDataAccessObject"/>
  ...
</beans>

每个bean就像是嵌套“俄罗斯套娃”中的一个,每个由数据存取器对象(DAO)代理(包装)的Bank都有一个域对象,而bean本身又是由JDBC驱动程序数据源代理。

客户代码以为调用的是Bank对象的getAccount()方法,其实它是在与一组扩展Bank POJO基础行为的DECORATOR对象中最外面的那个沟通。

在应用程序中,只添加了少数几行代码,用来向DI容器请求系统中的顶层对象,如XML文件中所定义的那样。

XmlBeanFactory bf =
  new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");

只需区区几行与Spring相关的Java代码,应用程序就几乎与Spring完全分离了,消除了EJB2之类系统中那种紧耦合问题。

尽管XML可能会冗长且难以阅读,配置文件中定义的“策略”还是要比那种隐藏在幕后自动创建的复杂的代理和方面逻辑来得简单。这种类型的架构是如此引人注目,Spring之类的框架最终导致了EJB标准在第3版的彻底变化。使用XML配置文件和/或Java 5注解,EJB3很大程度上遵循了Spring通过描述性手段支持横贯式关注面的模型。

代码清单11-5展示了用EJB3重写的Bank对象。

代码清单11-5 EJB3版本的Bank

package com.example.banking.model;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;

@Entity
@Table(name = "BANKS")
public class Bank implements java.io.Serializable {
  @Id @GeneratedValue(strategy=GenerationType.AUTO)
  private int id;

  @Embeddable // An object "inlined" in Bank’s DB row
  public class Address { 
    protected String streetAddr1; 
    protected String streetAddr2; 
    protected String city; 
    protected String state; 
    protected String zipCode; 
  }

  @Embedded
  private Address address;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, 
             mappedBy = "bank")
  private Collection<Account> accounts = new ArrayList<Account>();

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public void addAccount(Account account) {
    account.setBank(this);
    accounts.add(account);
  }

  public Collection<Account> getAccounts() {
    return accounts;
  }

  public void setAccounts(Collection<Account> accounts) {
    this.accounts = accounts;
  }
}

上述代码要比原本的EJB2代码整洁多了。有些实体细节仍然在注解中存在。不过,因为没有任何信息超出注解之外,代码依然整洁、清晰,也因此而易于测试驱动、易于维护。

如果愿意的话,注解中的一些或全部持久化信息可以转移到XML部署描述中,只留下真正的纯POJO。如果持久化映射细节不会频繁改动,许多团队可能会选择保留注解,但与EJB2那种侵害性相比还是少了很多问题。

11.6 AspectJ的方面

通过方面来实现关注面切分的功能最全的工具是AspectJ语言,它提供“一流的”将方面作为模块构造处理支持的Java扩展。在80%~90%用到方面特性的情况下,Spring AOP和JBoss AOP提供的纯Java实现手段足够使用。然而,AspectJ却提供了一套用以切分关注面的丰富而强有力的工具。AspectJ的弱势在于,需要采用几种新工具,学习新语言构造和使用方式。

借由AspectJ近期引入的“annotation form”(使用Java 5注解定义纯Java代码的方面),采用新工具的问题大大减少。另外,Spring框架也有一些让拥有较少AspectJ经验的团队更容易组合基于注解的方面的特性。

关于AspectJ的全面探讨已经超出本书范围。更多信息可参见[AspectJ]、[Colyer]和[Spring]。

11.7 测试驱动系统架构

通过方面式的手段切分关注面的威力不可低估。假使你能用POJO编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构。采用一些新技术,就能将架构按需从简单演化到精细。没必要先做大设计(Big Design Up Front,BDUF)。实际上,BDUF甚至是有害的,它阻碍改进,因为心理上会抵制丢弃既成之事,也因为架构上的方案选择影响后续的设计思路。

建筑设计师不得不做BDUF,因为一旦建造过程开始,就不可能对大型物理建筑的结构做根本性改动。尽管软件也有物理的一面,但只要软件的构架有效切分了各个关注面,还是有可能做根本性改动的。

这意味着我们可以从“简单自然”但切分良好的架构开始做软件项目,快速交付可开展工作的用户故事,随着规模的增加添加更多基础架构。有些世界上最大的网站采用了精密的数据缓存、安全、虚拟化等技术,获得了极高的可用性和性能,在每个抽象层和范围之内,那些最小化耦合的设计都简单到位,效率和灵活性也随之而来。

当然,这不是说要毫无准备地进入一个项目。对于总的覆盖范围、目标、项目进度和最终系统的总体构架,我们会有所预期。不过,我们必须有能力随机应变。

EJB早期架构就是一种著名的过度工程化而没能有效切分关注面的API。在没能真正得到使用时,设计得再好的API也等于是杀鸡用牛刀。优秀的API在大多数时间都该在视线之外,这样团队才能将创造力集中在要实现的用户故事上。否则,架构上的约束就会妨碍向客户交付优化价值的软件。

概言之,

最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。

11.8 优化决策

模块化和关注面切分成就了分散化管理和决策。在巨大的系统中,不管是一座城市或一个软件项目,无人能做所有决策。

众所周知,对于决策最好是授权给最有资格的人。但我们常常忘记了,延迟决策至最后一刻也是好手段,这不是懒惰或不负责,而是让我们能够基于最有可能的信息做出选择。提前决策是一种预备知识不足的决策。如果决策太早,就会缺少太多客户反馈、关于项目的思考和实施经验。

拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂性也降低了。

11.9 明智使用添加了可论证价值的标准

建筑构造大有可观,既因为新建筑的构建过程(即便是在隆冬季节),也因为那些现今科技所能实现的超凡设计。建筑业是一个成熟行业,有着高度优化的部件、方法和久经岁月历练的标准。

即便是轻量级和更直截了当的设计已足敷使用,许多团队也还是采用了EJB2架构,这是因为EJB2是一个标准。我见过一些团队,纠缠于这个或那个名声大噪的标准,却丧失了对为客户实现价值的关注。

有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合。

11.10 系统需要领域特定语言

建筑,与大多数其他领域一样,发展出一套丰富的语言,有词汇、熟语和清晰而简洁地表达基础信息的句式。在软件领域,领域特定语言(Domain-Specific Language,DSL)最近重受关注。DSL是一种单独的小型脚本语言或以标准语言写就的API,领域专家可以用它编写读起来像是组织严谨的散文一般的代码。

优秀的DSL填平了领域概念和实现领域概念的代码之间的“壕沟”,就像敏捷实践优化了开发团队和甲方之间的沟通一样。如果你用与领域专家使用的同一种语言来实现领域逻辑,就会降低不正确地将领域翻译为实现的风险。

DSL在有效使用时能提升代码惯用法和设计模式之上的抽象层次,它允许开发者在恰当的抽象层级上直指代码的初衷。

领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达。

11.11 小结

系统也应该是整洁的。侵害性架构会湮灭领域逻辑,冲击敏捷能力。如果领域逻辑受到困扰,质量就会堪忧,因为缺陷更易隐藏,用户故事更难实现。当敏捷能力受到损害时,生产力也会降低,TDD的好处遗失殆尽。

在所有的抽象层级上,意图都应该清晰可辨。只有在编写POJO并使用类方面的机制来无损地组合其他关注面时,这种事情才会发生。

无论是设计系统还是单独的模块,别忘了使用大概可开展工作的最简单方案。

KEIL-MDK嵌入式项目必备:AStyle自动化代码格式化实战 代码格式化是嵌入式C开发中保障可维护性与协作效率的基础工程实践,其核心在于统一缩进、括号风格、操作符间距等视觉结构,从而提升静态分析准确率、降低Git Diff噪声、加速调试定位。在KEIL-MDK这类无原生格式化能力的IDE中,AStyle凭借轻量命令行、确定性输出和可嵌入构建流程的特性,成为事实标准工具。结合嵌入式场景定制参数(如K&R风格、2空格缩进、指针星号对齐、宏预处理适配),能有效规避MISRA-C违规、编译器误解析及调试信息错位等问题,广泛应用于STM32、FreeRTOS等实时系统开发。 阅读详情

相关推荐

用Python构建GitHub风格阅读热力图:从数据到自动更新

在个人数据可视化场景中,热力图是一种直观展示时间序列规律的经典工具,而GitHub贡献图正是这一范式的典型代表。它通过日历网格与颜色深浅的映射,将抽象的频率数据转化为一眼可读的视觉信号。理解其背后的原理,包括日期偏移计算、SVG网格布局和颜色分级,是复现类似效果的关键。这种可视化方案不仅适用于代码活跃度统计,也被广泛应用于习惯打卡、阅读时长记录等工程实践场景。结合GitHub Actions的定时任务能力,可以实现数据生成、文件提交与页面展示的全自动化闭环。本文以阅读记录为切入点,完整演示从数据结构设计、P

weixin_34414196的博客 468

代码整洁之道

谈谈代码整洁之道、重构

程序大视界 5687

Python爬虫实战:一键下载全网VIP小说,实现离线阅读自由

网络爬虫是一种自动化程序,用于从互联网上抓取和提取数据。其核心原理是通过模拟浏览器发送HTTP请求,获取网页HTML源码,再使用解析库(如BeautifulSoup)提取目标信息。这项技术在数据采集、信息聚合和自动化处理方面具有重要价值,广泛应用于市场分析、学术研究和内容备份等场景。本文聚焦于一个具体的应用实例——小说内容抓取,通过Python脚本实现自动解析小说网站页面结构,将VIP付费章节内容批量下载并保存为本地TXT或EPUB文件。该工具基于Requests库处理网络请求,并利用BeautifulSo

weixin_34111790的博客 506

代码整洁之道总结

前言 什么是整洁的代码 出自《c++程序设计语言》作者说到 我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做妤一件事 OTI公司创始人,Eclipse 战略教父,Dave Thomas说到: 整洁的代码应可由作者之外的开发者阅读和增补。它应通过所有单元测试和验收测试。它使用有意义的命名。它只提供一种非多种做一件事的途径。它只有尽量少的依赖关系、

代码缔造者的博客 5090

微信小程序+SpringBoot全栈开发:网络安全科普系统实战指南

全栈开发是构建现代Web应用的核心模式,它要求开发者同时掌握前端与后端技术栈,以实现从用户界面到数据存储的完整业务闭环。其原理在于通过前后端分离架构,让前端专注于交互与展示,后端处理业务逻辑与数据持久化,两者通过API进行通信。这种模式的技术价值在于提升了开发效率、系统可维护性和团队协作能力。在应用场景上,全栈开发广泛应用于电商平台、内容管理系统以及在线教育等各类互联网产品中。本文以微信小程序网络安全科普系统为例,深入剖析了如何运用SpringBoot框架构建稳健的后端服务,并集成Swagger进行API文

weixin_33971977的博客 627

基于Spring Boot和Vue的漫画网站毕业设计全攻略:从数据库设计到部署答辩

前后端分离架构已成为现代Web开发的主流实践,其核心在于通过RESTful API解耦前端展示与后端逻辑,并借助JWT等无状态认证机制保障接口安全。在业务复杂度控制上,Redis缓存热点数据能够显著提升系统响应速度,而容器化部署工具则让环境一致性得以保障。这些技术原理共同构成了一个完整的企业级应用底座。当我们将这些能力应用于内容型业务场景时,漫画网站便成为一个极佳的工程实践载体——它天然包含三层嵌套数据模型、多角色权限管理、阅读历史记录等真实业务需求。本文以基于Spring Boot和Vue的漫画网站为实例

weixin_33851604的博客 287

《代码整洁之道》

本文,摘录一下《代码整洁之道》这本书,最让我印象深刻的片段。 一,整洁代码 1,什么是整洁代码 ————————————书本第6页————————————待补充图片 z这里提到了2个关键词:优雅,高效。 优雅就不说了,现在讨论整洁代码的时候,大家都会提到这个词,代码是一种艺术。 高效,这个观点还挺有意思的,尤其是这句: 性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。 二,有意义的命名 1,名副其实 我理解这个和代码自注释意思差不多,当命名做的好,就不需要注释。 ..

nameofcsdn的博客 5749

OpenPCDet安装及其3D检测算法实现

OpenPCDet及pointpillar复现

weixin_58207716的博客 1万+

代码整洁之道 总结

1.什么样的代码才是整洁的 从字面意思上理解,整洁的代码,对于程序员来说非常的一目了然,简单、整洁,结构清晰,逻辑清楚。代码其实是一种语言,传递的是逻辑,如果这份代码可以像我们说话一样快速的将逻辑传递给读者,那么这样一份代码就是一份整洁的代码。可以借助沃德原则:"如果每段代码欧让你感到深合已意,那就是整洁代码" 2.如何保持代码整洁 首先便是要有保持代码整洁的意识,书中反复提到的提到的...

无能力者只知抱怨 7799

地平线Matrix-Client实战:3分钟搞定摄像头调试与3D视图配置(附避坑指南)

本文详细介绍了地平线Matrix-Client在摄像头调试与3D视图配置中的实战技巧,包括环境准备、核心功能快速上手、高阶调试与性能优化等。通过3分钟快速配置指南和避坑建议,帮助开发者高效完成智能视觉系统开发,特别适用于自动驾驶和机器人视觉领域。

w7x8y9z的博客 269

RK3588实战:如何将8路1080P视频流实时拼接成一张超清全景图?聊聊AVS模块的那些事

本文深入解析了RK3588芯片的AVS模块如何实现8路1080P视频流的实时全景拼接技术。通过硬件加速、时间同步、几何校正和色彩均衡等关键技术,RK3588能够高效处理高密度视频流,输出超清全景图。文章还提供了优化策略和实战经验,帮助开发者充分发挥RK3588的潜力。

weixin_27014595的博客 747

K&R《C程序设计语言(第2版)》习题解答的Makefile与CI流水线:递归通配符编译与GitHub Actions实战

K&R《C程序设计语言(第2版)》是无数程序员的C语言入门经典,而配套的**习题解答**项目更是自学路上的得力助手。本项目用一套巧妙的**递归通配符Makefile**,就能一键编译全部8章、上百个习题的C源码;再配合**GitHub Actions CI流水线**,每次提交代码后自动完成全量编译校验。本文面向零基础新手,用通俗语言拆解这套构建系统与持续集成的实战细节,让你既能读懂源码,也能亲手跑

gitblog_00271的博客 1049

microblaze之Video Processing Subsystem调试误区

内容:总结video processing subsystem调试中遇到的问题,以及在解决问题中的思路方法论,引为前车之鉴。 1,我的目的是希望采用vps中的scaler模块对图像进行拉伸缩小。IP的配置如下: 2,整个系统的框架图如下,仅比上期的内容增加了scaler模块,如下: 3,查手册,可以知道这个scaler only组件分成了hscale,vscale和GPIO,其中GPIO是控制上下游stream流控设备复位用,暂时忽略。 hscale和vscale的寄存器空间如下,主要关注图

YDY5659150的专栏 3556

Legal RAG:基于 LangChain 的轻量化法律智能问答系统

面向中国法律领域的检索增强生成(RAG)系统:以 LangChain 为编排框架,通过硅基流动(SiliconFlow) 云端 API 调用 DeepSeek-V3.2 生成模型与 BGE-M3 嵌入模型,结合 ChromaDB 向量数据库,实现了从文档加载、智能分块、向量化存储到语义检索与生成的完整 RAG 流程。

m0_59777389的博客 227

HFSS实战项目——高带外抑制特性SIW带通滤波器

本文对单级滤波器和双级滤波器的设计、工作原理及仿真结果进行了详细分析,并验证了双级SIW带通滤波器在性能方面的优越性,为微波通信系统中滤波器的设计和应用提供了重要参考。

2401_87464239的博客 1816

第39步 深度学习图像识别:Inception V3建模(Tensorflow)

第39步 深度学习图像识别:Inception V3建模(Tensorflow)

Jet4505的博客 3171

Linux | Bash Shebang 语法与结构及常见问题排查

……

u013669912的博客 1161

034、MLCA 多尺度局部上下文注意力的 YOLOv11 适配:轻量级金字塔式空间上下文聚合

本文介绍了一种将多尺度局部上下文注意力(MLCA)模块集成到YOLOv11模型中的方法,以解决小目标检测中空间信息丢失的问题。MLCA采用金字塔式局部上下文聚合策略,通过不重叠的网格划分实现轻量级空间注意力计算,相比全局注意力机制能更好地保留小目标特征。 作者详细记录了实现过程,包括模块定义、参数调优和训练技巧,特别指出避免使用BN而改用GroupNorm以提升训练稳定性。在VisDrone数据集上的实验表明,MLCA-YOLOv11在参数量和计算量基本不变的情况下,mAP提升2.5%,其中行人检测提升3.

sundong_81的博客 285

代码之美——代码整洁之道

代码之美有很多方面,可以谈高级编程语言以及各语言的框架/类库,也可以聊算法和数据结构,还可以谈操作系统内核或是层出不穷的中间件,不过这里我们看最不起眼的代码整洁之道。

京东零售技术的博客 2875

ARM CoreLink 系列 5.1.1 -- CI-700 System Address Map 】

所有的CHI 命令都包含一个 Source ID 和 Target ID, 其中 Source ID 可以来自于 RN Node, Target ID 可以来自于HN node或者其他 SN node. 其中 Target ID 是通过 SAM解析传入的地址得到的。对于 CI-700中的所有 RN Node 中的 SAM 多会进行配置,因为RN Node 访问的是整个地址空间,不同的地址空间会有不同的属性(I/O, DRAM),所以会对RN SAM 配置多个不同属性的区域。

CodingCos的博客 771

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

《代码的整洁之道》

代码的整洁之道

大张 1252

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

CleanCode---代码整洁之道笔记

一、整洁代码 糟糕 的代码想做太多事,它意图混乱、目的含混。整洁的代码只做好一件事,力求每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。如果对象功能太多,最好是切分为两个或者多个对象。如果方法功能太多,可以使用抽取手段进行重构,从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。 整洁的代码从 不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。 简单的代码有以下几点要求: 能通过所有测试。高效可用的代码是第一位。 没有重复的代码。如果同

zhaozhen的博客 1841

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

上一篇: 【Code】《代码整洁之道》笔记-Chapter10-类
下一篇: 【Code】《代码整洁之道》笔记-Chapter12-迭进
江湖人称菠萝包
博客等级 码龄10年 762粉丝 314原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值