2025年1月

什么是Kafka

Apache Kafka是一个分布式流处理平台,由LinkedIn开发并开源,后来成为Apache软件基金会的顶级项目。Kafka主要用于构建实时数据管道和流式应用程序。

Kafka 架构

从下面3张架构图中可以看出Kafka Server 实际扮演的是Broker的角色, 一个Kafka Cluster由多个Broker组成, 或者可以说是多个Topic组成。

图 1

图 2

图 3

主要概念(Main Concepts)和术语(Terminology)

Kafka Cluster

一个Kafka集群是一个由多个Kafka代理组成的分布式系统,它们协同工作以处理实时流数据的存储和处理。它为大规模应用程序中高效的数据流和消息传递提供了容错性、可扩展性和高可用性。

Broker

Broker是构成Kafka集群的服务器。 每个Broker负责接收、存储和提供数据。 它们处理来自生产者和消费者的读写操作。 Broker还管理数据的复制以确保容错性。

Topic and Partitions

Kafka中的数据被组织成主题(Topics),这些是生产者发送数据和消费者读取数据的逻辑通道。每个主题被划分为分区(partitions),它们是Kafka中并行处理的基本单位。分区允许Kafka通过在多个Broker 之间分布数据来水平扩展。

Producers

生产者是发布(写入)数据到Kafka主题的客户端应用程序。它们根据分区策略将记录发送到适当的主题和分区,分区策略可以是基于键(key-based)或轮询(round-robin)。

Consumers

消费者是订阅Kafka主题并处理数据的客户端应用程序。它们从主题中读取记录,并且可以是消费者组的一部分,这允许负载均衡和容错。每个组中的消费者从一组独特的分区中读取数据。

Zookeeper

ZooKeeper是一个集中式服务,用于维护配置信息、命名、提供分布式同步和提供群组服务。在Kafka中,ZooKeeper用于管理和协调Kafka Broker。ZooKeeper被展示为与Kafka集群交互的独立组件。

Offsets

偏移量(offsets)是分配给分区中每条消息的唯一标识符。消费者将使用这些偏移量来跟踪他们在消费主题中消息的进度。

Kafka vs RabbitMQ

相同点

  1. 消息队列功能
    • Kafka和RabbitMQ都是流行的消息队列工具,支持生产者-消费者模式,能够解耦系统,提高系统的可扩展性和可靠性。
  2. 异步通信
    • 两者都支持异步通信,允许生产者发送消息后立即返回,消费者可以异步处理消息。
  3. 多种消息传递模式
    • 均支持点对点(P2P)和发布/订阅(Pub/Sub)模式。
  4. 持久化支持
    • Kafka和RabbitMQ都支持消息的持久化,以确保在系统故障或重启后消息不会丢失。
  5. 高可用性
    • 两者都支持集群部署,具有高可用性和容错能力。
  6. 语言支持
    • 提供多种语言的客户端库,支持不同编程语言的集成。


不同点

架构和设计

  1. 数据存储
    • Kafka:基于日志分区存储设计,适合高吞吐量的顺序读写。
    • RabbitMQ:基于AMQP协议,消息存储在队列中,适合低延迟的场景。
  2. 消息消费模式
    • Kafka:消息由消费者主动拉取,支持多次消费。
    • RabbitMQ:消息通过推送方式传递给消费者,消费后消息默认从队列中移除。
  3. 使用场景
    • Kafka:适用于大数据场景(日志聚合、流式处理),擅长处理高吞吐量、大规模消息传递。
    • RabbitMQ:适用于需要复杂路由和消息确认的场景(如事务性消息和实时通信)。

性能与延迟

  1. 高吞吐量
    • Kafka:设计针对高吞吐量场景优化,能够支持百万级消息每秒。
    • RabbitMQ:吞吐量相对较低,但延迟更低。
  2. 延迟
    • Kafka:适合高吞吐量但对实时性要求不高的应用。
    • RabbitMQ:更适合低延迟应用,提供实时性支持。

协议支持

  1. 协议类型
    • Kafka:自定义的二进制协议。
    • RabbitMQ:基于AMQP协议,支持丰富的消息功能(如TTL、优先级)。
  2. 兼容性
    • Kafka:需要Kafka专用客户端。
    • RabbitMQ:支持AMQP标准协议,兼容性较强。

开发一个Producer和一个Consumer

本地docker环境启动一个kafka

version: '2'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.4
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - 22181:2181
  
  kafka:
    image: confluentinc/cp-kafka:7.4.4
    depends_on:
      - zookeeper
    ports:
      - 29092:29092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

使用.NET CORE + Kafka开发一个消息生产者, 一个消息消费者, 客户端需要安装组件** Confluent.Kafka**

InventoryUpdateProducer

public class ProducerService
{
    private readonly IConfiguration _configuration;
    private readonly IProducer<Null, string> _producer;
    private readonly ILogger<ProducerService> _logger;

    public ProducerService(IConfiguration configuration, ILogger<ProducerService> logger)
    {
        _configuration = configuration;
        _logger = logger;
        var config = new ProducerConfig
        {
            BootstrapServers = _configuration["Kafka:BootstrapServers"],

        };

        _producer = new ProducerBuilder<Null, string>(config).Build();
    }

    public async Task ProductAsync(string topic, string message)
    {
        var orderPlacedMessage = new Message<Null, string>
        {
            Value = message
        };

        await _producer.ProduceAsync(topic, orderPlacedMessage);

        _logger.LogInformation("Message sent to topic: {Topic}", topic);
    }
}
[Route("api/[controller]")]
[ApiController]
public class InventoryController : ControllerBase
{
    private readonly ProducerService _producerService;

    public InventoryController(ProducerService producerService)
    {
        _producerService = producerService;
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] InventoryUpdateRequest request)
    {
        var message = System.Text.Json.JsonSerializer.Serialize(request);

        await _producerService.ProductAsync("inventory-update", message);

        return Ok("Inventory Updated Successfully...");
    }
}

启动项目,查看Swagger

InventoryUpdateConsumer

消息消费者程序使用.net core BackgroundService开发, 这个类需要在程序启动时注入进去,不要忘记。

public class ConsumerService : BackgroundService
{
    private readonly ILogger<ConsumerService> _logger;
    private readonly IConfiguration _configuration;
    private readonly IConsumer<Ignore, string> _consumer;

    public ConsumerService(ILogger<ConsumerService> logger, IConfiguration configuration)
    {
        _logger = logger;
        _configuration = configuration;

        var consumerConfig = new ConsumerConfig
        {
            BootstrapServers = configuration["Kafka:BootstrapServers"],
            GroupId = "InventoryConsumerGroup",
            AutoOffsetReset = AutoOffsetReset.Earliest
        };

        _consumer = new ConsumerBuilder<Ignore, string>(consumerConfig).Build();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _consumer.Subscribe("inventory-update");

        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                HandleMessage(stoppingToken);

                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Consumer service has been cancelled.");
        }
        catch (Exception ex)
        {
            _logger.LogError($"Error in consuming messages: {ex.Message}");
        }
        finally
        {
            _consumer.Close();
        }
    }

    public void HandleMessage(CancellationToken cancellation)
    {
        try
        {
            var consumeResult = _consumer.Consume(cancellation);

            var message = consumeResult.Message.Value;

            _logger.LogInformation($"Received inventory update: {message}");
        }
        catch (Exception ex)
        {
            _logger.LogError($"Error processing Kafka message: {ex.Message}");
        }
    }
}
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHostedService<ConsumerService>();

运行程序

Publish Message

Consume Message

总结

Apache Kafka不是消息中间件的一种实现。相反,它只是一种分布式流式系统。 不同于基于队列和交换器的RabbitMQ,Kafka的存储层是使用分区事务日志来实现的。Kafka也提供流式API用于实时的流处理以及连接器API用来更容易的和各种数据源集成。

胡言乱语

编程是非常难的,不是说它的内涵有多么深奥,是因为操作门槛太低了(精通门槛不低),导致太多一知半解的人就可以传道授业播种了,反而使得这门学科变得越来越复杂,让人越学越费力。由于本人学识浅薄,只能举一些粗鄙的例子:

  1. 网络教程泛滥成灾。举个例子:创造神创立了官方文档,但是凡人看不懂,自然语言上不行或者理解觉得晦涩(这不一定是坏事,可能最初设计就是要劝退菜鸟)。半神开始出一些教程来解读,凡人(自以为)读懂了,也开始写教程(比如各位博主)。教程良莠不齐,让人选择困难。初学者乍一看感觉民间博客更清晰简单,于是向凡人学习,但有时候提炼的文字缺少了真谛,导致他的读者必然是一知半解的,只能从官方文档重开。官方文档也会被污染。随着不断发展,积极维护的官方文档可能变好,但一部分地方的官方文档则受到菜鸟编著者的污染,而这些编著者的大脑本身更早就受到污染(可能来自他们的菜鸟老师,或者他们写东西的时候也采纳了被污染的网络文本)。
  2. 编程语言种类不断增加:实际上只要几门语言就足够了,奈何现代化的社会需求场景繁多,还有门槛要求,于是便催生了打着“简单易学”旗号的编程语言。他们自己是简单了,但使得整个学问变得困难。
  3. 现代编程中,为了简化,隐藏了一些设计,有很多默认值。比如在配置中,有些东西是默认就有的,不需要手动配置。当你配置了一个与默认项类别一样的东西时,你会担心,默认就有的那些东西需要配置吗?于是你查了一下,但搜索引擎不会给出太匹配的答案,某些人可以将信将疑地 睁只眼闭只眼地就 “不管了,先试试,成了”,但有些“癖”的人,则深深感到担忧,为什么程序可以执行?他们不觉得“就应该是这么被默认的”,他们在Google上查不到答案,作为凡人且初学者,他们面对官方文档也望而却步,他们只能默默痛苦,希望时间能磨平他们到处乱钻的牛角(通过不断学习慢慢就悟了?)。
  4. 最严重的一个问题是名词混乱。相同内涵的名词在不同的文章、书籍、翻译中都各不相同。随着社会和这门学科不断地发展,并没有达到一种和谐融合,统一化的趋势。有些概念/原语本来是艰难的,于是上古大神希望用一些简单的语言来描述并传播,凡人以为自己理解了,甚至也参与传播的行列。导致就像传话游戏一样,里面混了些半聋半哑的人,传着传着就变了样子。直到今天,每学习一个新的东西都让初学者提心吊胆,他们疑惑“当下看到字眼是否会和自己的理解有偏差”。

工程上的东西就是这样,一旦要考虑流行性,就必须降低严肃性。一旦失去严肃性,就要变得粗鄙,变地不纯净了。喝不纯净的东西,自然觉得卡喉咙了。
还是数学要好一点,至少写书的人,每处新名词都会给个“定义”。但近年来由于发展的又多又快,符号也是混乱(当然不在同一本书中),但这不像编程那些,学习数学的人本能不会认为两本书的约定符号/有细微差异的命题可以通用。就像局部作用域一样,这就有了一定的隔离性。

最后希望有真神降临,统一下编程所有板块,让我这个菜鸟不但能简单学习,还能学的不偏不倚。

在我的很多Winform开发项目中,统一采用了权限管理模块来进行各种权限的控制,包括常规的功能权限(工具栏、按钮、菜单权限),另外还可以进行字段级别的字段权限控制,字段权限是我们在一些对权限要求比较严格的系统里面涉及到的,可以对部分用户隐藏一些敏感的信息,或者禁止不够权限的用户编辑它。本篇随笔介绍基于这一理念,实现在WxPython跨平台开发框架中的模块字段权限的管理。

1、字段权限的设计

字段的权限控制,一般就是控制对应角色人员的对某个模块的一些敏感字段的可访问性:包括可见、可编辑性等处理。

在设计字段权限的时候,我们需要了解这些也是基于RBAC的概念,基于角色进行授权的,而且我们的字段列表是属于具体的业务对象列表的,这里的业务对象是指一些我们具体的业务模块,如客户基础信息、人员基础信息、报价单等等,我们就是基于这些业务进行字段的控制的。

为了实现对业务模型的字段控制,我们在数据库中设计两个数据库,一个用于存储对应实体类名称的信息,如ID,实体类全名,类路径等主体信息;一个存储对应角色配置的字段列表信息,结合起来就可以实现对应角色的字段权限控制了,数据库表设计信息如下所示。

如下界面所示,我们在权限系统里面也可以对其字段进行权限控制,如下图所示。先选择左边的具体角色,然后添加一些业务对象,并设置它们的权限即可。

首次业务对象需要用户加入,这里以wxpython前端框架的实体类进行字段信息的标识处理,如下所示可以加载对应业务信息。

确认后,系统记录对应角色对业务模块中的相关字段的权限处理。

我们在业务对象列表的【显示设置】处可以单击旁边的按钮,在弹出的界面上进行条件的设置,如下界面效果所示。

这样我们就完成了对某个业务对象的各个字段进行配置了,具体的字段控制在业务模块里面添加部分代码即可实现了。

2、业务模块对字段控制的处理

如下面列表界面,对于隐藏的字段,统一进行***符号的遮挡处理,即使导出Excel数据,结果也是***替代。

同时,如果系统界面有新增或者编辑界面,那么我们也需要隐藏才可以达到效果,如下是其的编辑界面效果(隐藏显示年龄字段了)。

如果仅仅是禁止编辑,那么我们配置角色对该字段不允许编辑即可。

那么用户对应客户信息模块的年龄字段可以看到,但是不能编辑。

3、字段权限的列表控制处理

前面我们介绍了在权限系统中进行业务对象的字段权限的设置流程,以及以其中的【客户信息】的业务模块进行的演示,那么我们如何才能在自己的业务模块里面进行控制处理的呢?

首先我们需要在业务列表绑定的时候,需要获取我们当前用户能够访问的字段列表,默认是全部可见,但是如果用户设置了条件,那么就需要获取对应的权限列表进行控制了,具体的控制代码如下所示。

在我的WxPython跨平台开发框架中,我们对于常规窗体列表界面做了抽象处理,一般绝大多数的逻辑封装在基类上,基类提供一些可重写的函数给子类实现弹性化的处理。

有了基类的挡箭牌,我们就可以统一在上面进行列表界面中字段权限的判断处理了。

我们在基类窗体的类上,对更新表格数据的处理进行调整,增加对字段权限的获取,在传递给实际更新表格的接口即可,如下代码所示。

由于在窗体基类,可以获得对泛型类型的一些属性,因此我们也就直接获得该对象的字段权限的配置了,如下代码所示。

    async def get_columns_permit(self) ->dict[str, int]:"""获取当前用户对该模块的列权限字典"""

        #1、获得对象的模块路径+类名
        entityFullName = f"{self.model.__module__}.{self.model.__name__}"

        #2、获取当前用户对该模块的列权限字典
        result =await api_fieldpermit.get_columns_permit(
entityFullName, settings.CurrentUser.id
)
return result

因为对客户信息的实体类,我们在数据库的表中配置的记录如下所示。

根据实体类全称和字段名称,以及对应该用户的角色ID,我们就能唯一确定该字段的权限了。

因此我们在自定义的表格数据对象中,设置返回的单元格值进行控制处理即可进行脱敏处理了。

由于我们的表格数据默认就是只读的,因此只需要判断隐藏的进行脱敏处理即可。

通过上面的控制,就可以实现列表中对字段的统一控制处理了。对于不同的业务表对象,都不需要单独的进行设置,已经在基类统一处理相关的逻辑了。

3、字段权限的显示窗体控制处理

如果在开发界面的时候,把列表的展示统一放在wx.Grid列表控件进行展示,不再独立设计展示窗体,那么上面列表控制就已经达到了字段权限的控制目的了:可见或不可见、可编辑或只读等处理。

在我的wxpython开发框架中,我一般倾向于设计一个界面来展示业务对象的内容,一般新增,查看或者编辑都放在这个窗体上展示信息,比较直观,那么这种对字段权限的控制也需要延伸到这个显示窗体上;

对于普通的编辑控件,我们只能控制控件的可读写、可见与否的处理。为了避免重复对字段权限的请求,我们在列表界面请求获得的字段权限列表,可以传递给新增、编辑对话框界面,如下代码是在子类列表界面,打开新增、编辑对话框的时候进行的处理。

    async def OnEditById(self, entity_id: Any |str):"""子类重写-根据主键值打开编辑对话框"""
        #使用列表窗体获得的字段权限
        dlg = FrmCustomerEdit(self, entity_id, columns_permit=self.columns_permit)#if dlg.ShowModal() == wx.ID_OK:
        if await AsyncShowDialogModal(dlg) ==wx.ID_OK:
            #更新grid列表数据
await self.update_grid()
dlg.Destroy()

可以看到,已经对字段权限的字典对象进行了传递,方便使用,避免重复请求了。

一般常规的新增、编辑界面,我们对它们也是进行了基类的封装处理,以便对界面元素进行更好的统一控制。

详细的基类设计如下所示,如对于客户信息的新增、编辑对话框,关系图如下所示。

由于新增/编辑对话框中,往往使用我们自定义的控件进行处理,因此在自定义控件中,可以再初始化函数的时候,添加一个 permit 的属性字段,然后进行判断控件的处理即可。

        #字段权限,默认0表示不限制,1表示只读,2 隐藏
        self.permit =permitif permit == 2:
self.Hide()
#隐藏 elif permit == 1:
self.ReadOny()
#只读 else:pass #不限制

而对于非自定义的控件,我们就需要再外部初始化的时候,对它们进行一些额外的处理控制显示了。

    age_permit = self.columns_permit.get("age", 0)
self.txtAge
= wx.SpinCtrl(panel, permit=age_permit)if age_permit == 2:
self.txtAge.Hide()
#隐藏 elif age_permit == 1:
self.txtAge.Disable()
#只读

那么用户对应客户信息模块的年龄字段可以看到,但是不能编辑。

由于新增、编辑界面是属于子类完全自定义控件和展示的逻辑,因此无法在基类统一进行处理,但是可以通过自定义控件的方式,减少过多的干预处理。

对于列表界面,则是 可以进行统一的字段的控制展示,比较一致。

以上就是字段权限的设计思路,实现控制过程,这样我们在权限里面实现了
功能权限、菜单权限、数据记录权限、字段权限的综合控制
,基本上能够满足大多数业务规则的要求了,从而提高了权限管理系统在整个应用开发中的通用性、便利性,一致性。

组合模式(Composite Pattern)

组合模式(Composite Pattern)是一种结构型设计模式,它用于将对象组织成
树形结构
,以表示
部分-整体
的层次结构。通过组合模式,客户端可以统一对待单个对象和组合对象,从而简化了客户端代码的复杂性。

组合模式的核心思想

  1. 统一的接口
    :通过抽象类或接口将
    单个对象

    组合对象
    统一起来;
  2. 递归组合

    组合对象
    中可以包含单个对象或其他组合对象;
  3. 透明性
    :客户端可以
    一致地调用
    单个对象和组合对象的方法,而无需区分两者的差异。

组合模式的角色

  1. 组件(Component)
    定义单个对象和组合对象的公共接口,例如通用操作(
    add

    remove

    getChild
    等)。

  2. 叶子节点(Leaf)
    表示树形结构中的基本单元,不能再包含其他对象。它实现了组件接口,但不支持添加或移除操作。

  3. 组合对象(Composite)
    表示树形结构中的复杂单元,可以包含叶子节点或其他组合对象。它实现组件接口,并负责管理其子对象的操作。

示例代码

组合模式解析XML或 HTML 元素结构的代码示例。我们将 XML/HTML 元素看作“部分-整体”结构,其中:

  • 叶子节点(leaf)
    :表示没有子节点的元素(如
    <img>

    <input>
    )。
  • 组合节点(Composite)
    :表示可以包含其他子元素的元素(如
    <div>

    <body>
    )。

两种节点使用同一种的顶层抽象,属于同一类对象,统称为元素(节点)。

类图

image

1. 抽象组件

HTML顶层元素抽象类。你也可以定义一个顶层接口,然后在抽象类中实现基础功能。

public abstract class HTMLElement {
    protected String name;

    public HTMLElement(String name) {
        this.name = name;
    }

    public abstract void render(int level);

    public HTMLElement getChild(int index) {
        throw new UnsupportedOperationException();
    }

    // 默认行为:叶子结点禁止新增元素
    public void addChild(HTMLElement element) {
        throw new UnsupportedOperationException();
    }
    // 默认行为:叶子结点禁止移除子元素
    public void removeChild(HTMLElement element) {
        throw new UnsupportedOperationException();
    }

    // 辅助方法:生成缩进
    protected String generateIndent(int level) {
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < level * 2; i++) {
            indent.append(" "); // 每层缩进2个空格
        }
        return indent.toString();
    }
}

2. 组合结点

表示可以包含子元素的HTML标签

public class HTMLComposite extends HTMLElement {
    private List<HTMLElement> children = new LinkedList<>();

    public HTMLComposite(String name) {
        super(name);
    }

    @Override
    public void addChild(HTMLElement element) {
        children.add(element);
    }

    @Override
    public void removeChild(HTMLElement element) {
        children.remove(element);
    }

    @Override
    public HTMLElement getChild(int index) {
        return children.get(index);
    }

    @Override
    public void render(int level) {
        System.out.println(generateIndent(level) + "<" + name + ">");
        for (HTMLElement child : children) {
            child.render(level + 1); // 子节点递归调用
        }
        System.out.println(generateIndent(level) + "</" + name + ">");
    }
}

3.叶子节点

表示没有子元素的HTML标签

public class HTMLLeaf extends HTMLElement {
    public HTMLLeaf(String name) {
        super(name);
    }

    @Override
    public void render(int level) {
        System.out.println(generateIndent(level) + "<" + name + " />");
    }
}

测试

public class CompositePatternHTMLDemo {
    public static void main(String[] args) {
        // 创建HTML结构
        HTMLElement html = new HTMLComposite("html");
        HTMLElement body = new HTMLComposite("body");
        HTMLElement div = new HTMLComposite("div");
        HTMLElement img = new HTMLLeaf("img");
        HTMLElement input = new HTMLLeaf("input");
        HTMLElement p = new HTMLLeaf("p");


        // 组合结构
        html.addChild(body);
        body.addChild(div);
        body.addChild(input);
        div.addChild(img);
        div.addChild(p);

        // 渲染HTML结构
        html.render(0);

        // 去除某个节点
        div.removeChild(p);
        html.render(0);
    }
}

测试结果:

<html>
  <body>
    <div>
      <img />
      <p />
    </div>
    <input />
  </body>
</html>
<html>
  <body>
    <div>
      <img />
    </div>
    <input />
  </body>
</html>

从类图或测试类(使用者)中可以看出,使用者直接依赖于具体的类,属于高耦合的一种编程方式。

简单优化(结合其它设计模式)

加入一个工厂类来创建组合节点和叶子结点

类图结构
变为

image

工厂类代码

public class HTMLElementFactory {
    private static Map<String, Class<? extends HTMLElement>> elementRegistry = new HashMap<>();

    static {
        // 注册类
        try {
            HTMLElementFactory.registerElement("composite", (Class<? extends HTMLElement>) Class.forName("org.example.composite.htmldemo.HTMLComposite"));
            HTMLElementFactory.registerElement("leaf", (Class<? extends HTMLElement>) Class.forName("org.example.composite.htmldemo.HTMLLeaf"));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    // 注册类
    public static void registerElement(String type, Class<? extends HTMLElement> clazz) {
        elementRegistry.put(type, clazz);
    }

    // 创建实例
    public HTMLElement createElement(String type, String name) {
        Class<? extends HTMLElement> clazz = elementRegistry.get(type);
        if (clazz == null) {
            throw new IllegalArgumentException("未知元素类型: " + type);
        }
        try {
            return clazz.getDeclaredConstructor(String.class).newInstance(name);
        } catch (Exception e) {
            throw new RuntimeException("错误创建元素对象: " + e.getMessage(), e);
        }
    }
}

测试代码

public class CompositePatternHTMLDemo {
    public static void main(String[] args) {
        // 创建HTML结构
//        HTMLElement html = new HTMLComposite("html");
//        HTMLElement body = new HTMLComposite("body");
//        HTMLElement div = new HTMLComposite("div");
//        HTMLElement img = new HTMLLeaf("img");
//        HTMLElement input = new HTMLLeaf("input");
//        HTMLElement p = new HTMLLeaf("p");

        HTMLElementFactory htmlElementFactory = new HTMLElementFactory();
        // 创建对象
        HTMLElement html = htmlElementFactory.createElement("composite", "html");
        HTMLElement body = htmlElementFactory.createElement("composite", "body");
        HTMLElement div = htmlElementFactory.createElement("composite", "div");
        HTMLElement input = htmlElementFactory.createElement("leaf", "input");
        HTMLElement img = htmlElementFactory.createElement("leaf", "img");
        HTMLElement p = htmlElementFactory.createElement("leaf", "p");

        // 组合结构
        html.addChild(body);
        body.addChild(div);
        body.addChild(input);
        div.addChild(img);
        div.addChild(p);

        // 渲染HTML结构
        html.render(0);

        // 去除某个节点
        div.removeChild(p);
        html.render(0);
    }
}

测试结果和前面的一样。

除了结合工厂模式外,还可以结合其它设计模式。比如,结合迭代器模式来递归遍历组合树。

总结

组合模式(Composite Pattern)是一种结构型设计模式,它用于将对象组织成
树形结构
,以表示
部分-整体
的层次结构。同时可以结合其它设计模式,使组合模式变得更加灵活和高效。

优点

  1. 透明性
    :客户端无需区分单个对象和组合对象。
  2. 灵活性
    :可以方便地动态组合对象,拓展系统。
  3. 符合开闭原则
    :添加新的组件类无需修改现有代码。

开闭原则
的核心是:

  • 对扩展开放
    :可以通过扩展系统的功能,而不是修改已有的代码来实现新需求。
  • 对修改关闭
    :已有的代码逻辑不需要因需求变化而被改动。

关于
开闭原则的“修改关闭”主要是指不修改核心业务逻辑
。对于工厂类这样的“管理型”代码,适当的修改是可以接受的,因为它并不属于核心业务逻辑。

缺点

  1. 复杂性增加
    :系统中的类和对象数量可能会增多。
  2. 单一职责原则的可能破坏
    :组合对象需要管理其子对象,可能职责过多。

使用场景

  1. 文件系统(文件和文件夹);
  2. GUI(窗口、按钮、文本框等控件);
  3. 公司组织架构(员工与部门);
  4. XML或HTML的元素结构。

image

什么是设计模式?

单例模式及其思想

设计模式--原型模式及其编程思想

掌握设计模式之生成器模式

掌握设计模式之简单工厂模式

掌握设计模式之工厂方法模式

掌握设计模式--装饰模式


超实用的SpringAOP实战之日志记录

2023年下半年软考考试重磅消息

通过软考后却领取不到实体证书?

计算机算法设计与分析(第5版)

Java全栈学习路线、学习资源和面试题一条龙

软考证书=职称证书?

软考中级--软件设计师毫无保留的备考分享

时序逻辑电路概述

  • 时序逻辑电路分类:
    • 按照触发器的动作特点:
      • 同步时序逻辑电路:
        所有触发器的状态变化都是在同一个时钟信号作用下同时发生的
      • 异步时序逻辑电路:
        没有统一的时钟脉冲信号,各触发器状态的变化不在同一时间,而是有先后顺序
    • 按照输出信号的特点:
      • Mealy型:
        输出状态不仅与存储电路的状态有关,而且与外部的输入有关
      • Moore型:
        输出状态仅与存储电路的状态有关,而且与外部输入无关

时序逻辑电路设计方法

  • 时序逻辑电路设计要点
    • 只有时钟信号和复位信号可以放在敏感列表里,如果敏感变量列表中,有一个信号是边沿触发,那么所有信号都得使用边沿触发;
      • posedge:时钟上升沿触发
      • negedge:时钟下降沿触发
    • 使用非阻塞赋值,即使用“<="进行赋值
    • 不需要对所有分支进行描述,对于未描述的分支,变量将保持原值

时序逻辑电路设计实例

不同结构功能和不同用途的触发器和锁存器,是基本的时序电路元件,是时序逻辑电路设计的基础

  • 实例1:基本锁存器

要求描述:
基本锁存器电路是一个
电平触发型
的电路,当时钟clk为高电平时,其输出q的值才会随输入d的数据变化而更新;当时钟clk为低电平时,锁存器将保持原来高电平时锁存的值。

//实现代码
module latch_1(
	input clk, d,
	output reg q);
	
	always@(clk,d)
		if(clk)
			q<=d;
		
endmodule 

//仿真代码
`timescale 1ns/1ns
module latch_1_tb();
	reg clk, d;
	wire q;

	latch_1 U1(clk, d, q);
	always #5 clk = ~clk;
	initial begin
		clk=0; d=1;
		#20 d=0;
		#20 d=1;
		#20 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例2:含复位控制的锁存器

要求描述:
当rstn为低电平时,输出q的值复位为0;当rstn为高电平时,q的输出行为与基本锁存器一致。

//实现代码
module latch_2(
	input clk, d, rstn,
	output reg q);

	always@(clk, d, rstn)	begin
		if(~rstn)	q<=1'b0;
		else if(clk)	q<=d;
	end
endmodule

//仿真代码
`timescale 1ns/1ns
module latch_2_tb();
	reg clk, d, rstn;
	wire q;

	latch_2 U1(clk, d, rstn, q);
	always #5 clk = ~clk;
	initial begin
		rstn=0;d=0;
		#10 rstn=1; d=0;
		#10 rstn=1; d=1;
		#10 rstn=1; d=0;
		#10 rstn=1; d=1;
		#10 rstn=0;
		#10 $finish;
	end
endmodule 

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例3:基本D触发器

要求描述:
触发器是指边沿触发的存储单元,常见的有D型、JK型、T型,通常在时钟上升沿存储(更新)数据。其中D触发器是最常用的触发器,几乎所有的逻辑电路都可以描述成D触发器与组合逻辑电路

//实现代码
module dff_1(
	input clk, d,
	output reg q);

	always@(posedge clk)
		q<=d;

endmodule

//仿真代码
`timescale 1ns/1ns
module dff_1_tb();
	reg clk, d;
	wire q;

	dff_1 U1(clk, d, q);
	always #10 clk = ~clk;
	initial begin
		clk=0;	d=0;
		#20 d=1;
		#30 d=0;
		#10 d=1;
		#20 d=0;
		#10 $finish;
	end
endmodule 

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例4:含异步复位信号的D触发器

要求描述:
异步复位信号reset的高电平能够在任意时刻复位D触发器,而不受时钟信号控制

//实现代码
module dff_2(
	input clk, d, reset,
	output reg q);

	always@(posedge clk, posedge reset)	begin//敏感信号列表中只能同时出现脉冲边沿信号或者是电平信号,不能混合使用
		if(reset)	q<=1'b0;
		else	q<=d;
	end
endmodule 

//仿真代码
`timescale 1ns/100ps
module dff_2_tb();
	reg clk, d, reset;
	wire q;

	dff_2 U1(clk, d, reset, q);
	always #5 clk=~clk;
	initial begin
		clk=0; reset=1; d=0;
		#6 reset=0; d=1;
		#6 reset=0; d=0;
		#6 reset=0; d=1;
		#6 reset=0; d=0;
		#6 reset=0; d=1;
		#6 reset=1;
		#10 $finish;
	end
endmodule 

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例5:含同步复位信号的D触发器

要求描述:
与时钟同步,同步复位信号reset的高电平只有当时钟信号有效时才起作用,而当时钟信号没有到来时,该控制信号不起作用

//实现代码
module dff_3(
	input clk, d, reset,
	output reg q);

	always@(posedge clk)	begin
		if(reset)	q<=1'b0;
		else	q<=d;
	end
endmodule 

//仿真代码
`timescale 1ns/100ps
module dff_3_tb();
	reg clk, d, reset;
	wire q;

	dff_3 U1(clk, d, reset, q);
	always #5 clk=~clk;
	initial begin
		clk=0; reset=1; d=0;
		#6 reset=0; d=1;
		#6 reset=0; d=0;
		#6 reset=0; d=1;
		#6 reset=0; d=0;
		#6 reset=0; d=1;
		#6 reset=1;
		#10 $finish;
	end
endmodule 

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例6:含异步复位和同步使能的D触发器

要求描述:
更加实用的D触发器包含一个额外的控制信号en,能够控制触发器进行输入值采样。使能信号en只有在时钟上升沿来临时才会生效,所以他是同步信号,如果en没有置1,触发器将保持先前的值。

//实现代码
module dff_4(
	input clk, d, reset, en,
	output reg q
);

	always@(posedge clk, posedge reset)	begin
		if(reset)	q<=1'b0;
		else if(~en)	q<=q;
		else	q<=d;
	end
endmodule

//仿真代码
module dff_4_tb();
	reg clk, d, reset, en;
	wire q;

	dff_4 U1(clk, d, reset, en, q);
	always #5 clk=~clk;

	initial begin
		clk=0; reset=1; en=0; d=0;
		#8 reset=0;
		#4 en=1;
		#5 d=1;
		#10 d=0;
		#10 d=1;
		#10 d=0;
		#10 en=0; d=1;
		#3 en=1; reset=1;
		#10 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下:

D触发器的使能常用于
实现同步快子系统和慢子系统

假设快子系统和慢子系统的时钟频率分别为50MHz和1MHz。我们可以生成一个周期性的使能信号,每50个时钟周期使能一个时钟周期,而不是另外派生出一个1MHz的时钟信号来驱动慢子系统,慢子系统在其余49个时钟周期中是保持原来状态的。

锁存器和触发器的区别:

  • 锁存器:
    没有时钟输入端,对脉冲电平敏感的存储电路,在特定输入脉冲电平作用下改变状态;
  • 触发器:
    每一个触发器有一个时钟输入端,对脉冲边沿敏感的存储电路,在时钟脉冲的上升沿或下降沿的变化瞬间改变状态。

    锁存器消耗的门资源比DFF要少;

    ASIC中会使用一定数量的锁存器;而FPGA中几乎没有标准的锁存器单元,更多的是触发器;

    锁存器会让时序变得极为复杂,静态时序分析非常困难。故在绝大多数设计中应尽量避免产生锁存器,能使用触发器则不使用锁存器。
  • 实例7:1位寄存器

要求描述:
一个触发器构成一个一位寄存器;N个触发器级联,构成一个N位寄存器。为了设计一个一位寄存器,可以在需要时从输入线in_data加载一个值,我们给D触发器增加一根使能信号load,当想要从in_data加载一个值时,就把load设置为1,那么在下一个时钟上升沿到来时,in_data的值将被存储在q中。
本质上,一位寄存器就是一个异步复位和同步使能的触发器

//实现代码
module reg_1(
	input clk, in_data, reset, load,
	output reg q);

	always@(posedge clk, posedge reset)	begin
		if(reset)	q<=1'b0;
		else if(load)	q<=in_data;
	end
endmodule

//仿真代码
`timescale 1ns/1ns //默认值是1ns/1ps
module reg_1_tb();
	reg clk, in_data, reset, load;
	wire q;

	reg_1 U1(clk, in_data, reset, load, q);
	always #10 clk=~clk;

	initial begin
		clk=0; reset=1; load=0; in_data=0;
		#5 reset=0;
		#5 load=1;
		#5 in_data=1;
		#5 in_data=0;
		#10 in_data=1;
		#5 load=0;
		#5 load=1;reset=1;
		#10 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例8:N位寄存器

要求描述:
把N个1位寄存器模块组合起来,就可以构成一个N位寄存器

//实现代码
module reg_2#(parameter N=4)(
	input clk, reset, load,
	input [N-1:0] in_data,
	output reg [N-1:0] q);

	always@(posedge clk, posedge reset)	begin
		if(reset)	q<=1'b0;
		else if(load)	q<=in_data;
	end
endmodule

//仿真代码
`timescale 1ns/100ps
module reg_2_tb#(parameter N=4)();
	reg clk, reset, load;
	reg [N-1:0] in_data;
	wire [N-1:0] q;

	reg_2 U1(clk, reset, load, in_data, q);
	always #1 clk=~clk;
	
	initial begin
		clk=0; reset=1; load=0; in_data=4'b0000;
		#5 reset=1;
		#5 load=1;
		#5 in_data = 4'b0001;
		#5 in_data = 4'b0010;
		#5 in_data = 4'b0100;
		#5 in_data = 4'b1000;
		#5 load = 0;
		#5 reset=1;
		#10 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例9:具有同步预置功能的8位移位寄存器

将若干个D触发器串接级联在一起构成的具有移位功能的寄存器
移位寄存器的分类:

  • 按照
    移动方式
    分类:
    • 单向移位寄存器:左移位寄存器/右移位寄存器
    • 双向移位寄存器
  • 按照
    输出方式
    分类:
    • 串入串出
    • 串入并出

要求描述:
当时钟上升沿到来时,过程被启动,如果此时预置使能端口load为高电平,则输入端口din的8位二进制数被同步并行移入寄存器,用作串行右移的初始值;
如果此时预置使能端口load为低电平,则执行赋值语句:
\(reg8[6:0]<=reg8[7:1]\)
,这样完成一个时钟周期后,将把上一时钟周期的高七位值更新至此寄存器的低七位,实现右移一位的操作,连续赋值语句把移位寄存器最低为通过qb端口输出。

//实现代码
module reg_3(
	input clk, load, 
	input [7:0] din,
	output qb
);

	reg [7:0] reg8;
	always@(posedge clk)	begin
		if(load)	reg8<=din;
		else	reg8[6:0]<=reg8[7:1];
	end

	assign qb=reg8[0];
endmodule

//仿真代码
`timescale 1ns/100ps
module reg_3_tb();
	reg clk, load;
	reg [7:0] in;
	wire qb;

	reg_3 U1(clk, load, in, qb);
	always #1 clk=~clk;
	
	initial begin
		clk=0; load=0; in=8'b11011010;
		#5 load=1;
		#1 load=0;
		#10 load=1; in=8'b10101010;
		#5 load=0;
		#10 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例10:4位双向移位寄存器

要求描述:
功能如下图,试用功能描述风格对其建模

//实现代码
module reg_4(
	input clk, clr, dsl, dsr, s1, s0,
	input [3:0] din,
	output reg [3:0] q
);

	always@(posedge clk, negedge clr)	begin
		if(~clr)	q<=4'b0000;
		else	begin
			case({s1, s0})
				2'b00: q<=q;
				2'b01: q<={dsr,q[3:1]};
				2'b10: q<={q[2:0],dsl};
				2'b11: q<=din;
			endcase
		end
	end
endmodule

//仿真代码
`timescale 1ns/100ps
module reg_4_tb();
	reg clk, clr, dsl, dsr, s1, s0;
	reg [3:0] din;
	wire [3:0] q;

	reg_4 U1(clk, clr, dsl, dsr, s1, s0, din, q);
	always #1 clk=~clk;

	initial begin
		clk=0; clr=0; dsl=0; dsr=0; s1=0; s0=0; din=4'b1111;
		#5 clr=1; s1=1; s0=1;
		#5 s1=0; s0=1;
		#5 s1=1; s0=0; dsl=1;
		#10 clr=0;
		#5 $finish;
	end
endmodule 

仿真得到的波形图如下:

综合出的电路结构图如下:

  • 实例11:模M计数器

计数器的基本功能是对输入时钟脉冲进行计数,它也可以用于分频、定时、产生节拍脉冲和脉冲序列以及进行数字运算等。
计数器的分类如下:

要求描述:
模M计数器的计数值从0增加到M-1,然后循环。其中参数M指定了计数模值,参数N指定了计数器所需的位数,采用高电平同步复位。

//实现代码
module counter_mod_m #(
	parameter M=10,
	parameter N=4
)(
	input clk, reset,
	output reg [N-1:0] qd,
	output cout //进位信号
);

	

	always@(posedge clk)	begin
		if(reset)
			qd<=0;	
		else if(qd<M-1)
			qd<=qd+1;
		else 
			qd<=0;
	end

	assign cout = (qd==(M-1))? 1'b1:1'b0;
endmodule

//仿真代码
`timescale 1ns/100ps
module counter_mod_m_tb();
	reg clk, reset;
	wire cout;
	wire [3:0] qd;

	counter_mod_m U1(clk, reset, qd, cout);

	always #1 clk=~clk;
	initial begin
		clk=0; reset=1;
		#5 reset=0;
		#20 reset=1;
		#5 $finish;
	end
endmodule

仿真得到的波形图如下:

综合出的电路结构图如下: