当前位置: 首页 > news >正文

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

第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 logicpublic 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 InvocationHandlerpublic 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 rowpublic class Address { protected String streetAddr1; protected String streetAddr2; protected String city; protected String state; protected String zipCode; }@Embeddedprivate 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并使用类方面的机制来无损地组合其他关注面时,这种事情才会发生。

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

相关文章:

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

第11章 系统 “复杂要人命。它消磨开发者的生命&#xff0c;让产品难以规划、构建和测试。” 11.1 如何建造一个城市 你能自己掌管一切细节吗&#xff1f;大概不行。即便是管理一个既存的城市&#xff0c;也是靠单人能力无法做到的。不过&#xff0c;城市还是在运转&#…...

MySQL数据库编程总结

MySQL数据库编程总结 一、数据库概述 数据库定义 • 数据库是管理数据的软件系统&#xff0c;用于高效存储、管理和检索数据&#xff0c;减少冗余。 • 核心功能&#xff1a;通过SQL语言定义、操作数据&#xff0c;维护完整性和安全性。 常见数据库 • MySQL、Oracle、SQL Ser…...

MySQL学习笔记7【InnoDB】

Innodb 1. 架构 1.1 内存部分 buffer pool 缓冲池是主存中的第一个区域&#xff0c;里面可以缓存磁盘上经常操作的真实数据&#xff0c;在执行增删查改操作时&#xff0c;先操作缓冲池中的数据&#xff0c;然后以一定频率刷新到磁盘&#xff0c;这样操作明显提升了速度。 …...

HTML应用指南:利用GET请求获取全国汉堡王门店位置信息

在当今快节奏的都市生活中&#xff0c;餐饮品牌的门店布局不仅反映了其市场策略&#xff0c;更折射出消费者对便捷、品质和品牌认同的追求。汉堡王&#xff08;Burger King&#xff09;作为全球知名的西式快餐品牌之一&#xff0c;在中国市场同样占据重要地位。自进入中国市场以…...

STM32+EC600E 4G模块 与华为云平台通信

前言 由于在STM32巡回研讨会上淘了一块EC600E4G模块以及刚办完电信卡多了两张副卡&#xff0c;副卡有流量刚好可以用一下&#xff0c;试想着以后画一块ESP32板子搭配这个4G模块做个随身WIFI&#xff0c;目前先用这个模块搭配STM32玩一下云平顺便记录一下。 实验目的 实现STM…...

【Spring】IoC详解:五大类注解、类Bean的存储(上)

1.IoC本质 IoC&#xff08;Inversion of Control&#xff0c;控制反转&#xff09; 是Spring框架的灵魂&#xff0c;它颠覆了传统编程中“谁用谁造”的逻辑。简单来说&#xff0c;IoC就是把对象创建和管理的控制权从程序员手中“反转”给一个外部容器&#xff0c;让代码更灵活…...

图片压缩后失真?3款工具还原高清细节

在当今&#xff0c;图片的使用无处不在。为了便于存储和传输&#xff0c;我们常常会对图片进行压缩。然而&#xff0c;不少人发现&#xff0c;压缩后的图片往往变得模糊&#xff0c;失去了原本的清晰度和细节。那么&#xff0c;当遇到这种情况时&#xff0c;我们该如何将模糊的…...

2025中国移动云智算大会|彩讯企业级AI应用产品引关注

2025中国移动以“由云向智&#xff0c;共绘算网新生态”为主题&#xff0c;精心打造了一场智能科技展。中国移动携手生态伙伴带来涵盖算力、工具、模型、应用等覆盖多样化场景的AI应用服务&#xff0c;赋能生产方式、生活方式、社会治理方式的数智化解决方案&#xff0c;充分释…...

在新一代人工智能技术引领下的,相互联系、层层递进的明厨亮灶开源了

明厨亮灶视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。AI技术可以24小时…...

修图自由!自建IOPaint服务器,手机平板随时随地远程调用在线P图

前言&#xff1a;在这个人人都想当摄影师的时代&#xff0c;一张完美的照片简直比中彩票还难。但别担心&#xff0c;今天我来给大家揭秘一个超级神器——IOPaint&#xff01;这款免费开源的AI工具不仅能一键移除照片中的杂物和路人&#xff0c;还能智能扩展图片内容&#xff0c…...

PyTorch实现二维卷积与边缘检测:从原理到实战

本文通过PyTorch实现二维互相关运算、自定义卷积层&#xff0c;并演示如何通过卷积核检测图像边缘。同时&#xff0c;我们将训练一个卷积核参数&#xff0c;使其能够从数据中学习边缘特征。 1. 二维互相关运算的实现 互相关运算&#xff08;Cross-Correlation&#xff09;是卷…...

解决Server doesn‘t support Accept-Ranges问题

Cannot download differentially, fallback to full download: Error: Server doesnt support Accept-Ranges (response code 200) 解决方案 修改nginx配置文件支持Accept-Ranges&#xff08;范围请求&#xff09; server {...location ^~/ {default_type multipart/byterang…...

处理Excel表不等长时间序列用tsfresh提取时序特征

我原本的时间序列格式是excel表记录的&#xff0c;每一行是一条时间序列&#xff0c;时间序列不等长。 要把excel表数据读取出来之后转换成extract_features需要的格式。 1.读取excel表数据 import pandas as pd import numpy as np from tsfresh import extract_features mda…...

Linux __命令和权限

目录 一、几个指令 bc uname -r 指令 重要的几个热键 二、Shell命令以及运行原理 为什么有外壳 外壳是如何工作的 什么是操作系统&#xff0c;为什么要有操作系统 三、文件类型 1、Linux的文件类型 2、文件类型 四、用户 用户问题和切换问题 增加普通用户 root -&…...

IO流——字符输入输出流:FileReader FileWriter

一、文件字符输入流&#xff1a;FileReader 作用&#xff1a;以内存为基准&#xff0c;可以把文件中的数据以字符的形式读入到内存中去 public class Test5 {public static void main(String[] args) {try (Reader fr new FileReader("E:\\IDEA\\JavaCodeAll\\file-io-t…...

【大模型理论篇】DeepResearcher论文分析-通过在真实环境中的强化学习实现深度研究

1. 背景与问题 大模型&#xff08;LLMs&#xff09;配合网络搜索功能已经展现出在深度研究任务中的巨大潜力。然而&#xff0c;目前的方法主要依赖两种途径&#xff1a; 人工设计的提示工程&#xff08;Prompt Engineering&#xff09;&#xff1a;这种方法依靠手动设计的工作流…...

大数据(7.5)Kafka Edge在5G边缘计算中的革新实践:解锁毫秒级实时处理的无限可能

目录 一、5G时代边缘计算的算力革命1.1 传统架构的延迟困境1.2 5G网络特性与Kafka适配 二、Kafka Edge核心架构设计2.1 分层处理架构2.2 关键技术创新点2.2.1 协议优化2.2.2 轻量化存储引擎 三、5G场景落地实践3.1 智能工厂预测性维护3.2 全息远程医疗会诊 四、性能优化深度实践…...

【基于开源insightface的人脸检测,人脸识别初步测试】

简介 InsightFace是一个基于深度学习的开源人脸识别项目,由蚂蚁金服的深度学习团队开发。该项目提供了人脸检测、人脸特征提取、人脸识别等功能,支持多种操作系统和深度学习框架。本文将详细介绍如何在Ubuntu系统上安装和实战InsightFace项目。 目前github有非常多的人脸识…...

kafka怎么保证消息不被重复消费

在 Kafka 中&#xff0c;要保证消息不被重复消费&#xff0c;可从消费者端和生产者端分别采取不同策略&#xff0c;下面为你详细介绍&#xff1a; 消费者端实现幂等消费 幂等消费是指对同一条消息&#xff0c;无论消费多少次&#xff0c;产生的业务结果都是一样的。 业务层面…...

一个批量文件Dos2Unix程序(Microsoft Store,开源)

这个程序可以把整个目录的文本文件改成UNIX格式&#xff0c;源码是用C#写的。 目录 一、从Microsoft Store安装 二、从github获取源码 三、功能介绍 3.1 运行 3.2 浏览 3.3 转换 3.4 转换&#xff08;无列表&#xff09; 3.5 取消 3.6 帮助 四、源码解读 五、讨论和…...

Python及Javascript的map 、 filter 、reduce类似函数的对比汇总

A. 在Python中&#xff0c;map 和 filter 是两个非常有用的内置函数&#xff0c;它们分别用于对可迭代对象中的每个元素执行某种操作&#xff0c;并返回结果。在JavaScript中&#xff0c;虽然没有内置的 map 和 filter 函数&#xff0c;但是可以使用数组的 map() 和 filter() …...

Linux中OS的管理和进程的概念

一、OS的管理 1.1操作系统宏观的理解 OS的本质是一款进行资源管理的软件 图示&#xff1a; 1.2OS存在的意义 1.2.1计算机的分层式管理结构 最底层的硬件部分遵循“冯诺依曼体系” &#xff0c;每一种硬件都在驱动层中有着自己对应的“驱动程序” 在OS中&#xff0c;驱动管…...

Spring定时任务修仙指南:从@Scheduled到分布式调度的终极奥义

各位被Thread.sleep()和while(true)折磨的Spring道友们&#xff01;今天要解锁的是Spring生态自带的定时任务三件套——Scheduled、TaskScheduler、Async定时组合技&#xff01;无需引入外部依赖&#xff0c;轻松实现从简单定时到分布式调度的全场景覆盖&#xff01;准备好抛弃…...

Node.js多版本共存管理工具NVM(最新版本)详细使用教程(附安装包教程)

目录 前言 一、Nvm下载 二、Nvm安装 三、配置nodeJS 前言 NVM&#xff08;Node Version Manager&#xff09;是一个用于管理多个Node.js版本的工具&#xff0c;主要帮助开发者在同一设备上轻松安装、切换和卸载不同版本的Node.js&#xff0c;解决项目间版本冲突问题。 一、…...

管道魔法木马利用Windows零日漏洞部署勒索软件

微软披露&#xff0c;一个现已修复的影响Windows通用日志文件系统&#xff08;CLFS&#xff09;的安全漏洞曾被作为零日漏洞用于针对少数目标的勒索软件攻击中。 01 攻击目标与漏洞详情 这家科技巨头表示&#xff1a;"受害者包括美国信息技术&#xff08;IT&#xff09…...

Devops之Argo:Argo 是什么,和现在常用的Jenkins之间的区别

Argo CD&#xff08;Argo Continuous Delivery 的缩写&#xff09;是一款基于 GitOps 的声明式 Kubernetes 持续交付工具。它提供了一种以 Git 为中心的方法来管理和部署应用程序到 Kubernetes 集群。Argo CD 遵循 GitOps 的原则&#xff0c;即将应用程序的预期状态存储在 Git …...

从 60 FPS 掉帧到 7.6 倍提速Rust + WebAssembly 优化《生命游戏》的实战指南

一、构建 FPS 统计器&#xff1a;用 performance.now() 实时观察性能变化 要优化&#xff0c;就要先 测量。我们在 JavaScript 端添加一个 fps 对象&#xff0c;结合 performance.now() 来监控每一帧的耗时&#xff0c;并统计最近 100 帧的平均 FPS、最小 FPS、最大 FPS&#…...

jmeter 集成ZAP进行接口测试中的安全扫描 实现方案

以下是将 JMeter 集成 ZAP(OWASP Zed Attack Proxy)进行接口测试中安全扫描的实现方案: 1. 环境准备 JMeter 安装:从 JMeter 官方网站(https://jmeter.apache.org/download_jmeter.cgi)下载并安装 JMeter,确保其版本稳定。ZAP 安装:从 ZAP 官方网站(https://www.zapr…...

Hyperlane 文件分块上传服务端

Hyperlane 文件分块上传服务端&#xff1a;高效、可靠、易用的文件上传解决方案 引言 在现代 Web 开发中&#xff0c;文件上传是许多应用的核心功能之一。然而&#xff0c;随着文件大小的增加和网络环境的复杂性&#xff0c;传统的单次文件上传方式已经难以满足需求。Hyperla…...

BT面板docker搭建excalidraw遇到的问题

1.傻瓜式拉取镜像 2.点击创建容器 3.暴露端口 4.放行端口和服务器安全组&#xff0c;如果用的是轻量型服务器&#xff0c;那就关闭防火墙 下面放图...

Qt之OpenGL使用Qt封装好的着色器和编译器

代码 #include "sunopengl.h"sunOpengl::sunOpengl(QWidget *parent) {}unsigned int VBO,VAO; float vertices[]{0.5f,0.5f,0.0f,0.5f,-0.5f,0.0f,-0.5f,-0.5f,0.0f,-0.5f,0.5f,0.0f };unsigned int indices[]{0,1,3,1,2,3, }; unsigned int EBO; sunOpengl::~sunO…...

【仿Mudou库one thread per loop式并发服务器实现】项目介绍+前置技术知识点

HTTP协议模块实现 1. 项目实现的目标2. 项目储备知识2.1 HTTP服务器2.2 Reactor模型 3. 功能模块划分3.1 SERVER模块3.1.1 Buffer模块3.1.2 Socket模块3.1.3 Channel模块3.1.4 Poller模块3.1.5 EventLoop模块3.1.6 Connection模块3.1.7 7. Acceptor模块3.1.8 TimerQueue模块3.1…...

Open Interpreter:重新定义人机交互的开源革命

引言 在人工智能技术蓬勃发展的今天&#xff0c;人机交互的方式正经历着前所未有的变革。Open Interpreter&#xff0c;作为一个开源项目&#xff0c;正在重新定义我们与计算机的互动方式。它允许大型语言模型&#xff08;LLMs&#xff09;在本地运行代码&#xff0c;通过自然…...

Shell编程之条件语句

目录 一.条件测试操作 1.文件测试 2.整数值比较 3.字符串比较 4.逻辑测试 二&#xff1a;if条件语句 1.if语句的结构 &#xff08;1&#xff09;单分支if语句 &#xff08;2&#xff09;双分支if语句 &#xff08;3&#xff09;多分支if语句 2.if语句应用示例 &…...

Python编程快速上手 让繁琐工作自动化笔记

编程基础 字符串使用单引号...

高性能文件上传服务

高性能文件上传服务 —— 您业务升级的不二选择 在当今互联网数据量激增、文件体积日益庞大的背景下&#xff0c;高效、稳定的文件上传方案显得尤为重要。我们的文件分块上传服务端采用业界领先的 Rust HTTP 框架 Hyperlane 开发&#xff0c;凭借其轻量级、低延时和高并发的特…...

【从零开始学习JVM | 第二篇】HotSpot虚拟机对象探秘

对象的创建 1.类加载检查 虚拟机遇到一条new的指令&#xff0c;首先去检查这个指令的参数能否在常量池中定位到这个类的符号引用&#xff0c;并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有&#xff0c;那必须先执行类的加载过程。 2.分配内存 在类…...

浅谈前端开发中的 npm、cnpm、pnpm、yarn各自特点

在前端开发中的 npm、cnpm、pnpm、yarn 等工具都是包管理器&#xff08;Package Manager&#xff09;&#xff0c;用于安装/更新/卸载 JavaScript 项目的依赖。 下面我详细地给你梳理下这些包管理器的作用、特点和适用场景&#x1f447; 一. npm&#xff08;Node Package Mana…...

【数据结构】包装类和泛型

目录 1.包装类 1.1 基本数据类型和对应的包装类 1.2 装箱和拆箱 1.3 自动装箱和自动拆箱 2.泛型 2.1泛型的概念 2.2引出泛型 3.语法 4.泛型类的使用 5.泛型的上界 1.包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基…...

红帽9运行容器一

运行容器&#xff1a;容器概念&#xff0c;构建&#xff0c;存储和运行容器的核心技术&#xff08;用户资源管理的控制组&#xff0c;进程隔离的命名空间&#xff0c;加强安全边界的SELinux和Seccomp&#xff09; 软件运行需要环境&#xff0c;系统库&#xff0c;配置文件和服…...

使用poi+itextpdf把word转成pdf

使用 Apache POI 和 iTextPDF 将 Word 转换为 PDF 需要分两步操作&#xff1a;先用 POI 读取 Word 内容&#xff0c;再用 iText 生成 PDF。 apache poi官方文档:Apache POI™ - Javadocs 以下是详细的代码实现示例&#xff1a; 环境准备 在 pom.xml 中添加依赖&#xff1a; …...

民安智库:开启零售行业客户满意度提升新征程​

在当今这个瞬息万变的商业世界中&#xff0c;零售市场的竞争愈发激烈&#xff0c;犹如一场没有硝烟的战争。各大零售企业为了抢占市场份额&#xff0c;纷纷使出浑身解数&#xff0c;从商品种类的丰富到店铺环境的优化&#xff0c;从价格策略的调整到服务质量的提升&#xff0c;…...

自行搭建一个Git仓库托管平台

1.安装Git sudo apt install git 2.Git本地仓库创建&#xff08;自己选择一个文件夹&#xff09; git init 这里我在 /home/test 下面初始化了代码仓库 1. 首先在仓库中新建一个txt文件&#xff0c;并输入一些内容 2. 将文件添加到仓库 git add test.txt 执行之后没有任何输…...

无锡无人机超视距驾驶证怎么考?

无锡无人机超视距驾驶证怎么考&#xff1f;在近年来&#xff0c;无人机技术的迅猛发展使得无人机的应用场景变得愈发广泛&#xff0c;其不仅在环境监测、农业喷洒、快递配送等领域展现出真金白银的价值&#xff0c;同时也推动了无人机驾驶证的需求。尤其是在无锡&#xff0c;随…...

pyautogui是什么:自动化鼠标和键盘操作

pyautogui是什么:自动化鼠标和键盘操作 目录 pyautogui是什么:自动化鼠标和键盘操作安装方法主要功能及使用示例1. 鼠标操作2. 键盘操作3. 获取屏幕信息应用场景注意事项pyautogui 是一个用于自动化鼠标和键盘操作的 Python 第三方库,它允许开发者通过编写 Python 代码来模拟…...

小白学习java第12天:IO流之缓冲流

1.IO缓冲流&#xff1a; 之前我们学习的都是原始流&#xff08;FileInputStream字节输入流、FileOutputStream字节输出流、FIleReader字符输入流、FIleWriter字符输出流&#xff09;其实我们可以知道对于这些其实性能都不是很好&#xff0c;要么太慢一个一个&#xff0c;要么就…...

智能导诊系统方案:人体画像导诊实现从症状到科室推荐及院内导航链路拆解(python示范 TensorFlow Embedding 层源码)

本文面向医院信息科负责人、医疗AI开发者、医院管理者&#xff0c;解决传统分诊依赖人工经验&#xff0c;效率低且易出错&#xff1b;患者跨科室就诊路径不清晰等痛点问题&#xff0c;实现症状到科室的精准推荐及动态导航链路优化。 如需获取智慧医院导航导诊系统解决方案请前往…...

声学测温度原理解释

已知声速&#xff0c;就可以得到温度。 不同温度下的胜诉不同。 25度的声速大约346m/s 绝对温度-273度 不同温度下的声速。 FPGA 通过测距雷达测温度&#xff0c;固定测量距离&#xff0c;或者可以测出当前距离。已知距离&#xff0c;然后雷达发出声波到接收到回波的时间&a…...

30天学Java第九天——线程

并行与并发的区别 并行是多核 CPU 上的多任务处理&#xff0c;多个任务在同一时间真正的同时执行并发是单核 CPU 上的多任务处理&#xff0c;多个任务在同一时间段内交替执行&#xff0c;通过时间片轮转实现交替执行&#xff0c;用于解决 IO 密集型任务的瓶颈 线程的创建方式…...

SaaS微服务架构的智慧工地源码,基于Spring Cloud +UniApp +MySql开发

基于微服务架构JavaSpring Cloud UniApp MySql技术开发&#xff0c;saas模式的一套智慧工地云平台源码&#xff0c;支持多端展示&#xff1a;PC端、大屏端、手机端、平板端。包含项目人员管理、视频监控管理、危大工程监管、绿色施工管理、现场物料管理、安全隐患排查等功能。 …...