番茄架构是遵循Common Sense Manifesto的软件架构的一种方法

Common Sense Manifesto

  • 选择适合你的软件的架构而不是流行的的架构: 在盲目追随流行人物的建议之前,要考虑软件最佳实践。
  • 不要过度设计: 努力保持简单,而不是过度工程化,试图猜测未来十年的需求。
  • 进行研究与开发,选择一项技术并拥抱它,而不是为了可替代性而创建抽象层。
  • 确保你的解决方案作为整体工作正常,而不仅仅是单个组件。

架构图

tomato-architecture.png

实施指南(架构核心:关注点分离):

  1. 按功能进行打包

    将代码按照功能分成不同的包是一种常见的模式,通常会根据技术层次(如 controllers, services, repositories等)进行划分。如果你正在构建一个专注于特定模块或业务能力的微服务,那么这种方法可能是可行的。

如果你正在构建一个单体应用或者模块化单体应用,强烈建议首先按照功能而非技术层进行划分。

详细信息请阅读链接: https://phauer.com/2020/package-by-feature/

  1. “应用核心”独立于交付机制(Web, Scheduler Jobs, CLI)

    应用核心应该公开可以从主方法调用的API。为了实现这一点,“应用核心”不应该依赖于其调用上下文。这意味着“应用核心”不应依赖于任何HTTP/Web层的库。同样,如果你的应用核心被用于定时任务或命令行接口,任何调度逻辑或命令行执行逻辑都不应泄露到应用核心中。

  2. 将业务逻辑执行与输入源(Web Controllers, Message Listeners, Scheduled Jobs等)分离

    输入源,如Web Controllers, Message Listeners, Scheduled Jobs等,应该是很薄的一层,在提取请求数据后将实际的业务逻辑执行委托给“应用核心”。

比如:

坏味道:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
class CustomerController {
private final CustomerService customerService;

@PostMapping("/api/customers")
void createCustomer(@RequestBody Customer customer) {
if(customerService.existsByEmail(customer.getEmail())) {
throw new EmailAlreadyInUseException(customer.getEmail());
}
customer.setCreateAt(Instant.now());
customerService.save(customer);
}
}

纠正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
class CustomerController {
private final CustomerService customerService;

@PostMapping("/api/customers")
void createCustomer(@RequestBody Customer customer) {
customerService.save(customer);
}
}

@Service
@Transactional
class CustomerService {
private final CustomerRepository customerRepository;

void save(Customer customer) {
if(customerRepository.existsByEmail(customer.getEmail())) {
throw new EmailAlreadyInUseException(customer.getEmail());
}
customer.setCreateAt(Instant.now());
customerRepository.save(customer);
}
}

采用这种方法,无论你是通过REST API调用还是通过CLI创建一个客户,所有的业务逻辑都会集中在应用核心中。

坏味道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
class OrderProcessingJob {
private final OrderService orderService;

@Scheduled(cron="0 * * * * *")
void run() {
List<Order> orders = orderService.findPendingOrders();
for(Order order : orders) {
this.processOrder(order);
}
}

private void processOrder(Order order) {
...
...
}
}

纠正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
class OrderProcessingJob {
private final OrderService orderService;

@Scheduled(cron="0 * * * * *")
void run() {
List<Order> orders = orderService.findPendingOrders();
orderService.processOrders(orders);
}
}

@Service
@Transactional
class OrderService {

public void processOrders(List<Order> orders) {
...
...
}
}

采用这种方法,你可以将订单处理逻辑与调度程序解耦,可以在没有通过调度程序触发的情况下独立进行测试。

  1. 不要让“外部服务集成”对“应用核心”产生太大影响

    从应用核心,我们可能需要与数据库、消息代理或第三方Web服务等进行通信。必须注意的是,业务逻辑执行器不应过度依赖于外部服务集成。

    例如,假设你正在使用Spring Data JPA进行持久化,而你想从CustomerService中使用分页获取客户。

    坏味道:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Service
    @Transactional
    class CustomerService {
    private final CustomerRepository customerRepository;

    PagedResult<Customer> getCustomers(Integer pageNo) {
    Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
    Page<Customer> cusomersPage = customerRepository.findAll(pageable);
    return convertToPagedResult(cusomersPage);
    }
    }

    纠正:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    @Transactional
    class CustomerService {
    private final CustomerRepository customerRepository;

    PagedResult<Customer> getCustomers(Integer pageNo) {
    return customerRepository.findAll(pageNo);
    }
    }

    @Repository
    class JpaCustomerRepository {

    PagedResult<Customer> findAll(Integer pageNo) {
    Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
    return ...;
    }
    }

    这种方式下,任何持久化库的修改都只会影响到持久化层。

  2. 将领域(domain)逻辑处理放在领域对象中

    如果一个方法仅仅影响领域对象中的状态或者是根据领域对象状态计算某些内容的方法,则该方法应该是属于领域对象。

    坏味道:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Cart {
    List<LineItem> items;
    }

    @Service
    @Transactional
    class CartService {

    CartDTO getCart(UUID cartId) {
    Cart cart = cartRepository.getCart(cartId);
    BigDecimal cartTotal = this.calculateCartTotal(cart);
    ...
    }

    private BigDecimal calculateCartTotal(Cart cart) {
    ...
    }
    }

    纠正:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Cart {
    List<LineItem> items;

    public BigDecimal getTotal() {
    ...
    }
    }

    @Service
    @Transactional
    class CartService {

    CartDTO getCart(UUID cartId) {
    Cart cart = cartRepository.getCart(cartId);
    BigDecimal cartTotal = cart.getTotal();
    ...
    }
    }
  3. 不要创建没有必要的接口

    不要创建接口并希望有一天我们可以为此接口添加另一个实现。如果那一天真的到来,那么利用我们现在拥有的强大的 IDE,只需敲击几下键盘即可提取界面。如果创建接口的原因是为了使用 Mock 实现进行测试,我们有像 Mockito 这样的模拟库,它能够在不实现接口的情况下模拟类。

    因此,除非有充分的理由,否则不要创建接口。

  4. 拥抱框架强大的功能和灵活性

    通常,创建库和框架是为了满足大多数应用程序所需的常见要求。因此,您应该选择一个库/框架来更快地构建应用程序时。

    与其利用所选框架提供的功能和灵活性,不如在所选框架之上创建间接或抽象,并希望有一天您可以将框架切换到其他框架,这通常是一个非常糟糕的主意。

    例如,Spring框架为处理数据库事务、缓存、方法级安全性等提供了声明性支持。引入我们自己的类似注释并通过将实际处理委托给框架来重新实现相同的功能支持是不必要的。

    相反,最好直接使用框架的注释,或者根据需要使用附加语义来组合注释。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Transactional
    public @interface UseCase {
    @AliasFor(
    annotation = Transactional.class
    )
    Propagation propagation() default Propagation.REQUIRED;
    }
  5. 不仅单元测试,也需要集成测试。

    我们绝对应该编写单元测试来测试单元(业务逻辑),如果需要,可以通过模拟外部依赖项。但更重要的是验证整个功能是否正常工作。

    即使我们的单元测试以毫秒为单位运行,我们是否可以放心地投入生产?当然不是。我们应该通过测试实际的外部依赖项(例如数据库或消息代理)来验证整个功能是否正常工作。这让我们更有信心。

    我想知道“我们应该拥有完全独立于外部依赖项的核心域”哲学的整个想法来自于使用真实依赖项进行测试非常具有挑战性或根本不可能的时代。

    幸运的是,我们现在拥有更好的技术(例如:testcontainer 运行测试时动态启动依赖的中间件容器技术)来测试真正的依赖关系。使用真正的依赖项进行测试可能会花费稍多的时间,但与好处相比,这是一个可以忽略不计的成本。

学习于原文:https://github.com/wenPKtalk/tomato-architecture

Comment and share

DDD是什么

  1. DDD并不是一种技术架构,而是一种划分业务领域范围的方法论。
  2. DDD不是一种架构,而是一种架构方法论,目的就是将复杂问题领域简单化,帮助我们设计出清晰的领域和边界,可以很好的实现技术架构的演进。

DDD 包括战略设计和战术设计两部分。

  1. DDD战略部分战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
  2. DDD战术部分战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

DDD与微服务

DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从 业务视角 去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。

  • DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
  • 微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。

实践步骤:

第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。

第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。

第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。

image-20220411183114240

总结

DDD可以理解为:ddd 是一个事件风暴 (分类划分),进而知道组织划分(也就是中台)、系统划分(微服务)、代码划分/设计的思想方法

Comment and share

概述

最近20年软件设计发生了天翻地覆的变化,但是SOLID原则至今仍然是软件设计的最佳实践。

SOLID 原则对于对于创建高质量软件是久经测试的标题。但是在现代多范式编程(函数式编程等)和云计算兴起的年代,它依然能够坚挺吗?我将通过如下文章解释SOLID代表了什么,为什么它依然适用现代软件,并且分享一些例子来解释。

什么是SOLID

SOLID是Robert C. Martin在2000年提取出来的一系列原则。它被建议去作为面向对象(OO)编程质量的特殊思考方式。总得来讲SOLID在这几个方面:代码如何切分,代码私有和对外暴露,代码之间如何调用提出了建议。我下面将深入研究每个字母(S.O.L.I.D)的原始含义,并且扩展到面向对象编程之外的使用。

有哪些改变?

21世纪早期,是Java和C++称霸的时期,理所当然我的大学很多课程都使用Java语言来当作训练。Java的流行催生出来了一些书籍,课程 和其它资料使得人们从写代码过度到写出好的代码。

因此,软件工业发生了深远的影响。有几个值得注意的点:

  • **动态类型语言(Dynamically-typed languages) ** 比如 Python,Ruby, 尤其JavaScript 变的和Java一样流行—甚至在某些行业和某类公司已经超过了Java
  • 非面向对象范式(Non-object-oriented paradigms) 最值得注意的是函数式编程(FP),在这些新语言中也比较常见。甚至Java本身也引入了lambdas!元编程技术(增加和改变对象的方法和特性)等技术也越来越流行。还有拥有“软面向对象”特征的Go语言,它具有静态类型但是没有继承。所有的这些都表明了现代软件中类和集成没有过去重要。
  • 开源软件(Open-source software) 的扩散。早期,更多的通用软件是闭源(closed-source)人们使用的都是编译后的软件,现在通常人们依赖的软件都是开源的。因此,在编写库是曾经对必不可少的逻辑和数据隐藏不再哪么重要。
  • 微服务和软件即服务(Saas) 爆炸式地出现。与其将应用程序部署为将所有依赖项链接在一起的大型可执行文件,不如部署一个与其他服务(自己的活第三方提供支持的服务)调用的小型服务。

整体来看,SOLID真正关心的许多事情——例如类和接口,数据隐藏性和多态——不再是程序员每天都要处理的事情。

有什么没发生变化?

现在工业界有很多不一样了,但是也还有一些东西未发生改变。包括:

  • 代码是由人类编写和修改的 。代码被编写一次并且被读很多很多次。所以对内部和外部需要很好的代码说明文档,尤其很好的API文档。
  • 代码被组织成模块。在某些语言中,这些是类。在其他情况下,他们可能是单独的源文件。在JavaScript中,它们可能是导出对象。无论如何,总是存在某种方式去隔离和组织代码成为独立,有界的单元。因此,总是需要决定如何最好的将代码组织在一起。
  • 代码可以是内部的或者外部的。一些编写出来的代码是被你自己或者你的团队使用,一些可能会被其它团队甚至其他顾客通过API的方式使用。这也意味着需要某种方式来决定哪些代码是“可见的”哪些是隐藏的。

“现代”SOLID

在接下来的文章中,我将把SOLID中的每一项原则都表述为更一般的描述,并且说明是如何应用在OO,FP,多范式编程中的,并距离说明。在许多情况下,这些原则甚至可以应用在整个服务或者系统中。

需要注意的是我将使用“模块”代指一组代码,可以是类,包,文件等等。

单一职责 (Single responsibility principle)

原始定义: “一个类改变的原因不会超过一个”

如果您编写的类有很多关注点或“更改的原因”,那么这些关注点中任何一个需要更改,您就需要更改相同的代码。这增加了对一个特性功能的修改就会破坏另外一个特性功能的可能性。

一个例子,如下是一个永远不应该应用在生产环境的Franken-class:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FrankenClass {
public void savaUserDetail(User user) {
//...
}

public void performOrder(Order order) {
//...
}

public void shipItem(Item item, String address) {
//...
}
}

新的定义: “每个模块应该做一件事,并且做好”。

这个原则和高内聚(high cohesion)话题紧密联系。本质上,您的代码不应该将很多角色或者用途混在一起。

如下是使用JavaScript的同一示例的函数式编程(FP)版本:

1
2
3
4
5
const saveUserDetails = (user) => {...}
const performOrder = (order) => { ...}
const shipItem = (item, address) => { ... }
export { saveUserDetails, performOrder, shipItem };
import { saveUserDetails, performOrder, shipItem } from "allActions";

这也适用在微服务设计;如果你有一个单独服务来处理所有这三个功能,它就会尝试做太多事情。

开闭原则(Open-closed principle)

原始定义: “软件实体应该对扩展开放,对修改关闭。”

这也是Java语言设计的一部分—你可以创建一个子类来继承一个类,但是不能去修改原始的类。

“对扩展开放”的原因之一是限制了对类作者的依赖——如果你需要改动一个类,你不得不去等待类的原始作者去修改,或者你深入研究这个类后再去修改。更重要的是这个类承担了太多的关注点这将打破单一职责原则。

“对修改关闭”的原因是我们不相信下游的使用者能够完全理解我们所有的“private”私有代码,我们希望保护它免收不熟练的人的修改带来的损害。

1
2
3
4
5
6
7
8
9
10
11
12
class Notifier {
public void notify(String message) {
//send an e-mail
}
}

class LoggingNotifier extends Notifier {
public void notify(String message) {
super.notify(message);//keep parent behaiver
//also log the message;
}
}

新定义: “应该能够在不重写模块的情况下使用和添加模块”。

这在面向对象领域是免费的(AOP)。在函数式编程代码必须明确定义Hook point以允许修改。这事一个示例,其中不仅仅允许使用前后hooks,而且甚至可以通过将函数传递给您的函数来覆盖其基本行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//library code

const saveRecord = (record, save, beforeSave, afterSave) => {
const defaultSave = (record) => {
//default save function;
}

if (beforeSave) beforeSave(record);
if (save) {
save(record);
} else {
defaultSave(record);
}
if(afterSave) afterSave(record);
//calling code
}

//calling code
const customSave = (record) => {...}
saveRecord(myRecord, customSave);

里氏替换原则

原始定义: “如果类型S是T的子类型,那么类型T可以被替换为S类型而不用修改程序的任何所需属性”。

这也是面向对象语言的基础属性。它意味着你可以使用任意子类替换它们的父类。所以你可以对这一个约定充满信心:你可以安全的使用任何 “is a” type T的对象而像T一样去使用。如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vehicle {
public int getNumberOfWheels() {
return 4;
}
}

class Bicycle extends Vehicle {
public int getNumberOfWheels() {
return 2;
}
}

// calling code
public static int COST_PER_TIRE = 50;

public int tireCost(Vehicle vehicle) {
return COST_PER_TIRE * vehicle.getNumberOfWheels();
}

Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); //100

新定义: 你可以使用一个东西代替另一个东西只要它们声明的行为方式一样。

在动态语言中,重要的是如果你的程序“promises”去做某些事情(例如实现一个接口或者函数),你需要遵守你的“promises”不要给客户端不符合“promises”的东西。

许多动态语言使用“duck typing”(不知道什么意思 戳这儿 )去达到这一点。本质上,你的function正式或者非正式的声明它期望其输入以特定方式运行并根据该假设运行。

例如Ruby:

1
2
3
4
# @param input [#to_s]
def split_lines(input)
input.to_s.split("\n")
end

在这个例子中,这个function并不在乎输入类型——只关心它有一个to_s函数,它的行为方式与所有to_s函数的行为方式相同:即把输入变成字符串。许多动态语言没有办法去强制这种行为,因此这更像一种纪律问题而不是一种形式化技术。

接下来是函数式编程TypeScript例子,在这个例子中高阶函数引入了一个过滤器输入一个数字返回一个boolean值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const isEven = (x: number) : boolean => x % 2 == 0;
const isOdd = (x: number) : boolean => x % 2 == 1;

const printFiltered = (arr: number[], filterFunc: (int) => boolean) => {
arr.forEach((item) => {
if (filterFunc(item)) {
console.log(item);
}
})
}

const array = [1,2,3,4,5,6];
printFiltered(array, isEven);
printFiltered(array, isOdd);

接口隔离原则(Interface segregation principle)

原始定义: “许多客户端(client-specific)接口要好于一个通用的(general-purpose)接口。”

在面向对象语言中,你可以理解为是为你的类提供了一个“视图”(view)。与其提供给你一个大而全的实现给你的客户端,而是仅使用与该客户端相关的方法在它们之上创建接口,并要求您的客户端使用这些接口。

正如单一职责原则一样,接口隔离原则隔离了系统之间的耦合,并且确保客户端不需要了解它所依赖的无关功能。

如下例子通过SPR测试:

1
2
3
4
5
class PrintRequest {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

这段代码通常只有一个“原因去更改”——它都与打印请求有关,它们都是同一个域的一部分,并且所有三种方法都可能会更改相同的状态。但是,创建请求的客户端不太可能是处理请求的客户端。将这些接口隔离开会更有意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface PrintRequestModifier {
public void createRequest();
public void deleteRequest();
}

interface PrintRequestWorker {
public void workOnRequest()
}

class PrintRequest implements PrintRequestModifier, PrintRequestWorker {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

新的定义: “不要向客户端展示它不需要看到的东西。”

只记录你客户端需要知道的内容。这意味着使用文档生成器只输入“public”function 或路由,而“private”没必要输出。

在微服务时代,你可以使用文档或者真正的隔离增加清晰度。例如,你外部客户可能只能以用户身份登录,但你的内部服务可能获取用户列表或者其他属性。你也可以创建一个单独的“仅限外部”用户服务来调用你的主服务,或者你可以只为隐藏内部路由的外部用户输出特定文档。

依赖倒置原则(Dependency inversion principle)

原始定义: “依赖抽象,而不是依赖具体实现”

在面向对象语言中,这意味着客户端应该尽可能依赖接口而不是具体实现类。这确保了代码应该依赖尽可能小的面积——事实上,客户端不必要依赖所有的代码,只需要依赖一个定义代码应该如何表现的契约。和其它原则一样这降低了修改一处而导致破坏了其它功能的风险。下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Logger {
public void write(String message);
}

class FileLogger implements Logger {
public void write(String message) {
//write to file
}
}

class StandardOutLogger implements Logger {
public void write(String message) {
//write to standard out
}
}

// call
public void doStuff(Logger logger) {
//do stuff
logger.write("some message");
}

如果你正在写的代码需要一个logger, 你不想去限制自己仅仅只是去写入文件中,因为你不在乎。你仅仅只需要调用 write方法而让具体实现去输出。

新的定义: “依赖抽象,而不是具体实现。”

对的,这个例子我将定义保留原样!保持东西依赖抽象依然是重要的,即使现代代码中的抽象机制不像严格的面向对象世界那样强大。

尤其,这和上面讨论的里氏替换原则是一样的。主要的区别是它没有默认实现。因此,该部分中涉及鸭子类型和钩子函数的讨论通用适用于依赖倒置。

你也可以使用抽象对于微服务。比如,你可以将服务之间的直接通信替换为消息总线活队列平台,例如Kafka或者其他消息中间件。这样允许服务将消息发送到单个通用位置,而无需关心哪个特定服务将接收这些消息并执行其任务。

总结

再次重新梳理“现代SOLID”一次:

  • 不要惊讶别人读到你的代码。
  • 不要惊讶别人使用你的代码。
  • 不要让阅读你代码的人感到迷惑。
  • 为你的代码使用合理的边界。
  • 使用正确的耦合级别——尘归尘,土归土。

好的代码就是好的代码——从未改变,SOLID也是,实践是坚实的基础。

原文连接: https://stackoverflow.blog/2021/11/01/why-solid-principles-are-still-the-foundation-for-modern-software-architecture/?utm_source=programmingdigest&utm_medium=web&utm_campaign=447

Comment and share

纠葛的问题:REST和RPC的对比?

思想上,概念上,使用范围 RPC和REST都完全不一样,本质上并不是同一类东西,只有部分功能重合。REST和RPC在思想上存在差异的核心是:抽象目标不一样,也就是面向资源的编程思想和面向过程的编程思想之间的差异。
REST:本质上不是一种协议,而是一种风格。

理解REST

REST 概念的提出来自于罗伊·菲尔丁(Roy Fielding)在 2000 年发表的博士论文:《Architectural Styles and the Design of Network-based Sorftware Architectures》

REST全称为:Representational State Transfer 即表征状态转移。

为何常常使用HTTP搭配REST

REST实际上是HTT(Hyper Text Transfer,超文本传输)的进一步抽象,就想是接口和实现类之间的关系。(即REST是接口,HTT是实现)

  • Representation 表征 (对Resource的转换):

    浏览器向客户端请求资源的HTML格式,那么返回的这个HTML我们就可以称之为:表征。也可以理解为MVC模式中的:表示层。

  • State 状态:

    特定语境中产生的上下文信息称之为“状态”。注意Stateful 和 Stateless是针对的服务端没有记录上下文。浏览器每次携带上下文来请求服务端,比如:JWT技术

  • Transfer 转移:

    服务端通过浏览器提供的信息,由当前的表示层转移到新的标识层,这就被成为:表征状态转移。

如何评价一个系统是否符合RESTful ?

Fielding 认为,一套理想的、完全满足 REST 的系统应该满足以下六个特征。

  1. 服务端与客户端分离(Client-Server)

  2. 无状态(Stateless)

    REST希望服务器不能负责维护状态,每一次从客户端发送的请求中,应该包括所有必要的上下文信息,会话信息也由客户端保存维护。服务器端依据客户端传递的状态信息来进行业务处理,并且驱动整个应用的状态变迁。提升了系统的可见性、可靠性和可伸缩性(大多数系统达不到这个要求,越复杂,越大型的系统越是如此。)

  3. 可缓存(Cacheability)

    REST希望客户端和中间的通讯传递者(代理)可以将部分服务端应答缓存起来。通过良好的缓存机制,减少客户端和服务端的交互。

  4. 分层系统(Layered System)

    不是传统的MVC这样的分层。而是指客户端一般不需要知道是否直接链接到了最终的服务器,或者是链接到了路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存机制,提高系统的可扩展性。这样也便于缓存,伸缩和安全策略的部署。

  5. 统一接口(Uniform Interface)

    REST希望开发者面向资源编程,希望软件系统设计重点放放在抽象系统有哪些资源上,而不是抽象系统该有哪些行为上。

    举例:

    几乎每个系统都会有登录和注销功能,如果你登录对应的是login() 注销对应于loginout()。那么如果面向资源可以理解成为:登录是PUT session 注销是DELETE session,这样你只需要设计一种“session”资源即可。查询登录用户使用GET session。

    Felding给出三个建议:

    1. 系统要做到每次请求中都包含资源的ID,所有操作都通过ID完成。
    2. 每个资源都应该有自描述信息。
    3. 通过超文本来驱动应用状态的转移。
  6. 按需代码(Code-On-Demand)

RESTful带来的优点

  1. 降低了服务接口的学习成本

    它把资源的标准操作都映射到了标准的HTTP方法上(GET,PUT,POST,DELETE)对每个资源的语义一致。无需额外定义。

  2. 资源天然具有集合与层次结构

    1
    GET /book/{bookId}/chapter/{chapterNumber}

    天热的集合与层次关系。

  3. RESTful绑定于HTTP协议

    面向资源编程并不是必须构筑在 HTTP 之上。

    但是HTTP 协议已经有效运作了 30 年,与其相关的技术基础设施已是千锤百炼,无比成熟。而它的坏处自然就是,当你想去考虑那些 HTTP 不提供的特性时,就束手无策了。

RESTful缺点

  1. 面向资源只适合做CRUD,只有面向过程,面向对象编程才能处理真正复杂的业务逻辑。

  2. REST与HTTP完全绑定,不适用于要求高性能传输场景当中。

    面向资源编程与协议无关,但是 REST(特指 Fielding 论文中所定义的 REST,而不是泛指面向资源的思想)的确依赖着 HTTP 协议的标准方法、状态码和协议头等各个方面。

  3. REST 不利于事务支持。

  4. REST 没有传输可靠性支持。

    应对传输可靠性最简单粗暴的做法,就是把消息再重发一遍。这种简单处理能够成立的前提,是服务具有幂等性(Idempotency),也就是说服务被重复执行多次的效果与执行一次是相等的。

  5. REST 缺乏对资源进行“部分”和“批量”的处理能力。

    就是缺少对资源的“部分”操作的支持。要解决批量操作这类问题,目前一种从理论上看还比较优秀的解决方案是GraphQL(但实际使用人数并不多)。GraphQL 是由 Facebook 提出并开源的一种面向资源 API 的数据查询语言。它和 SQL 一样,挂了个“查询语言”的名字,但其实 CRUD 都能做。

参考链接

周志明的软件架构课:https://time.geekbang.org/column/article/317164

李锟谈 Fielding 博士 REST 论文中文版发布:https://www.infoq.cn/article/2007/07/dlee-fielding-rest/

Fielding论文:https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

REST和RPC对比结论:

无论是思想上,概念上,还是使用范围上,REST和RPC都不完全一样,本质上并不是同一个类型的东西。充其量是相似,在应用中会存在重合功能。

  1. 思想上差异:抽象目标不一样,REST是面向资源的编程思想,RPC是面向过程。
  2. 概念上:REST并不是一种远程服务调用协议,也可以说它就不是一种协议,只是一种风格。RPC是作为一种远程调用协议的存在。
  3. 试用范围:REST和RPC作为主流的两种远程调用方式,在使用上确实有重合之处。

Comment and share

理解RPC

in Architecture

什么是RPC(Remote procedure call)

Remote procedure call is the synchronous language-level transfer of control between programs in address spaces whose primary communication is a narrow channel.—— Bruce Jay Nelson,Remote Procedure Call,Xerox PARC,1981

RPC是一种语言级别的通讯协议,它允许运行于一台计算机上的程序以某种管道作为通讯媒介(即某种网络传输协议),去调用另外一个地址空间(通常为网络上的另外一台计算机)。

所有RPC协议都需要解决哪些问题?

基于 TCP/IP 网络的、支持 C 语言的 RPC 协议,后来也被称为是ONC RPC(Open Network Computing RPC/Sun RPC)。这两个RPC鼻祖。

  1. 如何表示数据
  2. 如何传递数据
  3. 如何标识方法

如何表示数据

将交互双方涉及的数据,转换为某种事先约定好的中立数据流格式来传输,将数据流转换回不同语言中对应的数据类型来使用,即序列化与反序列化

如何传递数据

这里“传递数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等传输层协议来完成的。

如何标识方法

标识调用方法入口,能够找到这个方法。

已经实现的RPC框架

RPC 框架有明显朝着更高层次(不仅仅负责调用远程服务,还管理远程服务)与插件化方向发展的趋势,不再选择自己去解决表示数据、传递数据和表示方法这三个问题,而是将全部或者一部分问题设计为扩展点,实现核心能力的可配置,再辅以外围功能,如负载均衡、服务注册、可观察性等方面的支持。这一类框架的代表,有 Facebook 的 Thrift 和阿里的 Dubbo(现在两者都是 Apache 的)。尤其是断更多年后重启的 Dubbo 表现得更为明显,它默认有自己的传输协议(Dubbo 协议),同时也支持其他协议,它默认采用 Hessian 2 作为序列化器,如果你有 JSON 的需求,可以替换为 Fastjson;如果你对性能有更高的需求,可以替换为Kryo、FST、Protocol Buffers 等;如果你不想依赖其他包,直接使用 JDK 自带的序列化器也可以。这种设计,就在一定程度上缓解了 RPC 框架必须取舍,难以完美的缺憾。

RMI(Sun/Oracle)、Thrift(Facebook/Apache)、Dubbo(阿里巴巴 /Apache)、gRPC(Google)、Motan2(新浪)、Finagle(Twitter)、brpc(百度)、.NET Remoting(微软)、Arvo(Hadoop)、JSON-RPC 2.0(公开规范,JSON-RPC 工作组)

Comment and share

  • page 1 of 1
Author's picture

Topsion

Fullstack Developer


Coder


Xi'an China