聊一聊领域驱动和贫血

写在前面

前段时间跟领导讨论技术债概念时不可避免地提到了代码的质量,而影响代码质量的因素向来都不是单一的,诸如项目因素、管理因素、技术选型、人员素质等等,因为是技术债务,自然就从技术角度来分析,单纯从技术角度来看代码质量,其实又细分很多原因,如代码设计、代码规范、编程技巧等等,但我个人觉得这些都是技,并不是代码混乱的根,代码之所以混乱的根要从最基础的层面说起,也就是代码架构。

但是好像代码架构也不像是技术选型的产物,它更像是“润物细无声”的环境影响产物,一个Java Web项目,不谈一个企业,甚至整个行业都有一个共识,应该是Controller、Service、Dao那一套,从代码目录结构到内部细节编写,也不知道是MVC架构的宣传深入人心还是培训机构或者企业培训的连带效应导致的这种浑然天成的共识。就是这种浑然天成的共识导致了一个很奇怪的现象,那就是 :使用标榜面向对象的Java语言所开发的项目却十分的不面向对象。这里也不是说面向对象就比面向过程要强,只是各自适用的领域不一样。超过操作系统到应用层级的应用,不论从需求延申角度、系统规模角度、落地实现角度、扩展维护角度,面向对象都要更好一些。

这也是Java这门语言高居榜首的理由。 Java自诞生之初,各自资料、书籍无一不是在讲它的面向对象特性和面向对象设计,随后的领域驱动设计更是面向对象的集成方法论,在这么多buff的加持下,为什么还是会出现使用面向对象去写面向过程的代码呢?答案就在上面的共识里,好像Java项目的开发和贫血模型一直是强绑定的。

严格来讲,概念上领域驱动其实只有一种概念,并没有贫血充血之分,DDD官方概念中并没有明确定义所谓的贫血模式,贫血模式的诞生其实是对于标准领域驱动的简化,而与之对应的标准的DDD就成了充血。

贫血模型

贫血模式很多人不陌生, 即上文提到的传统的Controller、Service、Dao的框架基础之上,配合以Java的对象实体类概念,但实体类仅有属性和属性的get和set方法,在整个系统中,对象几乎只作传输介质的作用,不会影响到层次的划分,业务逻辑多集中在Service中;随之后续的数据库技术、ORM框架等等,都在这一体系上继续垒加,形成了当下最高复制率的JavaWeb项目结构。

还是转账这个经典的例子,使用贫血实现:

/**
 * 账户业务对象
 */
public class AccountBO {

    /**
     * 账户ID
     */
    private String accountId;

    /**
     * 账户余额
     */
    private Long balance;
    /**
     * 是否冻结
     */
    private boolean isFrozen;

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public Long getBalance() {
        return balance;
    }

    public void setBalance(Long balance) {
        this.balance = balance;
    }

    public boolean isFrozen() {
        return isFrozen;
    }

    public void setFrozen(boolean isFrozen) {
        this.isFrozen = isFrozen;
    }

}

/**
 * 转账业务服务实现
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
        AccountBO fromAccount = accountMapper.getAccountById(fromAccountId);
        AccountBO toAccount = accountMapper.getAccountById(toAccountId);

        /** 检查转出账户 **/
        if (fromAccount.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        if (fromAccount.getBalance() < amount) {
            throw new MyBizException(ErrorCodeBiz.INSUFFICIENT_BALANCE);
        }
        fromAccount.setBalance(fromAccount.getBalance() - amount);

        /** 检查转入账户 **/
        if (toAccount.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        toAccount.setBalance(toAccount.getBalance() + amount);

        /** 更新数据库 **/
        accountMapper.updateAccount(fromAccount);
        accountMapper.updateAccount(toAccount);
        return Boolean.TRUE;
    }
}

贫血模型的本质

21e5a9c4e89b4838820a2bfb8c9fa64c.png

贫血模式本质是在架构指导下技术框架趋于成熟的演进产物;换句话说是MVC架构随着技术的发展而不断变体而来。MVC的初衷是一个三层架构,即包含数据结构和业务逻辑的模型Model层、应用程序的用户界面(UI)展示与交互的视图层View、用于协调Model与View的剧中调度层Controller,旨在将数据、逻辑和交互解耦。但伴随着ORM框架、Web框架的进步,慢慢形成了Controller、Service、Dao(ORM)的格局,之后前后端分离的浪潮袭来,导致Controller变成了路由,Dao变成了JDBC的简化,Do变成了数据结构,头和尾都明确定位的夹击下,所有的业务功能只能聚焦于中间的Service里,需求大都集中在了Service,“Service用来承载业务逻辑”的说法更加助长了设计懒惰,形成了 “service=数据+逻辑”的风格,学过C语言的或多或少都听过一句“程序=数据+算法”,所以贫血就是在输入输出都固定框架模式下的面向过程。

贫血模型下的开发特点

贫血模式的service层的定义导致它的底层思维是 “程序=算法+数据” ,与之而来的开发流程或者说特点也就十分明了了。即 功能与数据密不可分、代码与数据密不可分。最后面向数据编程。

d39ec834a3944d218b8eea5043f7146f.png

聚焦功能忽视业务

长期使用贫血模式开发的人员,眼里看到的多是UI原型或者功能,对于系统的全貌,永远是站在功能的角度去描述和了解。往往专注于功能的开发,尽可能地去简化实现,点到实现功能为止,不做过多的设计与业务的扩展思考。实现往往是单维度的,仅仅是为了满足某个功能或某个页面,当需求分析和系统规划不够优秀时,后续功能开发时,不可避免会陷入前后矛盾的情景。

面向数据开发

在接手系统时,程序员一般先看两部分,数据库里的表和功能,最后才是代码,或者接到需求时,首先做的就是根据UI或者产品的描述去设计存储用的表结构,然后再基于表结构去进行代码设计。也就是说,在实现角度,数据库的表结构才是根基。

另一方面就是SQL建模,其实也可以说是重SQL轻代码,绝大多数做Java Web开发地研发人员从根据需求设计表结构、到实现时进行SQL建模十分娴熟,甚至我见过很多高手,能用一个SQL完成功能开发,把功能和数据的关系极尽所能地压缩到一个或者几个SQL中,代码反而只是一个传输媒介。

贫血模式的开发者倾向于将注意力集中在数据上,他们直接在数据表上建模。语言框架更多的功能在于数据库的访问和UI的绘制,虽然语言可能是Java这种完全面向对象语言,但其实应用里并没有什么客观对象,除了数据库的容器和访问者

贫血模型的合理性

说到这好像显得贫血模式一无是处,其实也不绝对,毕竟 “存在即合理”,裁剪了繁重体系的领域驱动后,贫血模式就变得十分轻便,本身面向过程的特点又让它复制性和落地效率十分突出,开发人员只需要UI页面和数据库的表结构,随便一个研发都可以快速接手并完成一个功能的开发 ,降低了软件设计的复杂度,这恰恰和很多开发团队快速响应的诉求不谋而合。

贫血得以盛行的另一个助力就是Spring,Spring家族作为Java系的行业老大哥框架,积累和沉淀非常丰富,各方面已经封装的很全面,所以留出的“自由度”就比较少,它的宗旨就是让开发人员减少重复造轮子,仅仅专注功能的开发就好;这些特点使得Spring本身就带有限制性质,例如Spring的根基Bean管理机制,把对象的管控牢牢把握在框架中,这种将实体类的管理也交由Spring本身也降低了二次开发时面向对象的属性,导致在Spring中进行bean之间的引用改造会面临大范围的bean嵌套构造器的调用问题。所以使用Spring也就大概率默认使用贫血。

贫血模型的问题

不可否认,面对大多数简单而生命周期短暂的项目面向数据开发是一种高效的方式,且当需求发生变动,Service按部就班地追加相应的逻辑也是可以快速响应,可是这些都是建立在系统简单、项目周期短的前提下,一旦面临长生命周期且业务复杂的大型系统,贫血模式的问题就是:

  1. 随着系统体积的增长和需求的蔓延,部分功能设计问题显露,贫血模式下的代码特点难以快速重构来响应这一级别的诉求
  2. 面向过程的形式,让代码陷入 “又臭又长的if else”死胡同,随着不断地开发,维护和改动成本指数级上涨
  3. 长时间的“水多了加面,面多了加水”的开发习惯,团队人员思维固化,难以提出有效地优化或改进,恶性循环

充血模型

充血简单来讲就是OO思想的体系方法论,相比贫血的面向数据,它提出领域的概念,即将业务和数据拆开,对业务部分进行领域划分并进行OOA,代码上更重抽象出的领域实体类,将业务逻辑和判定等内容均内聚到实体类中,Service层仅仅充当组合实体对象的画布,负责简单封装部分业务和事务权限管理等;

为了更直观感受,使用充血模型,对上面的例子进行修改:

/**
 * 账户业务对象
 */
public class AccountBO {

    /**
     * 账户ID
     */
    private String accountId;

    /**
     * 账户余额
     */
    private Long balance;

    /**
     * 是否冻结
     */
    private boolean isFrozen;

    /**
     * 出借策略
     */
    private DebitPolicy debitPolicy;

    /**
     * 入账策略
     */
    private CreditPolicy creditPolicy;

    /**
     * 出借方法
     * 
     * @param amount 金额
     */
    public void debit(Long amount) {
        debitPolicy.preDebit(this, amount);
        this.balance -= amount;
        debitPolicy.afterDebit(this, amount);
    }

    /**
     * 转入方法
     * 
     * @param amount 金额
     */
    public void credit(Long amount) {
        creditPolicy.preCredit(this, amount);
        this.balance += amount;
        creditPolicy.afterCredit(this, amount);
    }

    public boolean isFrozen() {
        return isFrozen;
    }

    public void setFrozen(boolean isFrozen) {
        this.isFrozen = isFrozen;
    }

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public Long getBalance() {
        return balance;
    }

    /**
     * BO和DO转换必须加set方法这是一种权衡
     */
    public void setBalance(Long balance) {
        this.balance = balance;
    }

    public DebitPolicy getDebitPolicy() {
        return debitPolicy;
    }

    public void setDebitPolicy(DebitPolicy debitPolicy) {
        this.debitPolicy = debitPolicy;
    }

    public CreditPolicy getCreditPolicy() {
        return creditPolicy;
    }

    public void setCreditPolicy(CreditPolicy creditPolicy) {
        this.creditPolicy = creditPolicy;
    }
}


/**
 * 入账策略实现
 */
@Service
public class CreditPolicyImpl implements CreditPolicy {

    @Override
    public void preCredit(AccountBO account, Long amount) {
        if (account.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }        
    }

    @Override
    public void afterCredit(AccountBO account, Long amount) {
        System.out.println("afterCredit");
    }
}

/**
 * 出借策略实现
 */
@Service
public class DebitPolicyImpl implements DebitPolicy {

    @Override
    public void preDebit(AccountBO account, Long amount) {
        if (account.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        if (account.getBalance() < amount) {
            throw new MyBizException(ErrorCodeBiz.INSUFFICIENT_BALANCE);
        }
    }

    @Override
    public void afterDebit(AccountBO account, Long amount) {
        System.out.println("afterDebit");
    }
}

/**
 * 转账业务服务实现
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Resource
    private AccountMapper accountMapper;
    @Resource
    private CreditPolicy creditPolicy;
    @Resource
    private DebitPolicy debitPolicy;

    @Override
    public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
        AccountBO fromAccount = accountMapper.getAccountById(fromAccountId);
        AccountBO toAccount = accountMapper.getAccountById(toAccountId);
        //此处采用轻量化地方式解决了自身面向对象和Spring的bean管理权矛盾
        fromAccount.setDebitPolicy(debitPolicy);
        toAccount.setCreditPolicy(creditPolicy);

        fromAccount.debit(amount);
        toAccount.credit(amount);
        accountMapper.updateAccount(fromAccount);
        accountMapper.updateAccount(toAccount);
        return Boolean.TRUE;
    }
}

充血模型的开发特点

抽象业务

5531817e85a7431ebabcd39ef2703285.png

相比于贫血聚焦于功能、UI页面,充血模式在面对设计时,需要更上升一层,聚焦于业务。将业务内容抽象出业务对象、对象关系、业务流程、对象依赖,以排列组合对象来满足业务需求。面对需求,需要层层分析,设计,模型场景推演,做到重业务对象,轻数据,从而实现业务、功能、代码、数据的清晰划分,避免直译式的粘连耦合实现。

通过领域概念将业务与技术分离

66db0d0a2fa140fc89c11a5ecdc1bd53.png

领域驱动中突出领域的概念,包含:通用域、支撑域、核心域三部分,其中通用域和支撑域则是纯技术流和通用内容,包括持久化、通信技、安全、性能、通用组件、规则等,核心域则是具体业务分析得出的业务块,其中包含通用的业务语言、领域内的上下文、复杂变化的业务逻辑等。通用域和支撑域负责数据管理、包装接口、绘制界面、收接消息等系统的基本技术能力,以此来支撑核心域的业务流转、运作。 通用域、支撑域不与核心域耦合,让技术归技术、业务归业务,做到技术和业务的分离。

充血模型的问题

很多书籍和资料都是在说充血模型是解决贫血模型的良药,其实也过于吹捧了,之前也提到了,贫血模型有其存在的必要性,充血模型和贫血模型只是适用范围不同,如果贫血模型的缺点都是由于应用场景导致,那充血确是良药。但如果单纯是管理问题、团队人员技术问题导致的问题,那强行套用充血才是灾难,至于贫血换充血的理由,会在后面提到。这里单纯以批评视角说一说充血的问题。

单纯看充血模型存在的问题,就两点:

  • 理论过于理想,实践较为困难
  • 边界难以把控

其实这两点归于一点也不为过,那就是充血模型的实践土壤过于苛刻,因为其脱身于面向对象,着重以面向对象思想解决系统开发中熵增的问题,由此诞生了非常繁重的体系理论,而每一处体系理论都透露着对于团队人员素质以及业务场景理解的高要求;对于团队人员素质,这一点是最难的,因为在一个群体中很难把每个研发对于面向对象的理解拉到同一个维度,只能是不要有太大偏差。其次就是设计者很容易陷入到 “设计师的锤子”理论中,即 “手里有锤子,处处是钉子”,很容易过度设计,导致无意义地加重复杂度,毕竟把握“精准平衡” 这四个字对于 架构师、设计师、研发工程师的要求不是一纸证书那么简单。

从贫血到充血的前制条件

聊怎么做之前,先谈一谈必要性,即贫血一定要切换为充血么,其实从贫血过度到充血也不是必须品,至于要不要使用充血模式,取决于两个方面:

  • 领域特性
  • 团队成熟度

领域特性

衡量是否必要的第一点就是领域逻辑的复杂度,充血模式最适合具有复杂领域逻辑的应用。如果业务比较复杂,随着我们对领域逻辑的了解,就能很快感受到基于数据的做法带来的限制。仅从数据出发,CRUD系统无法创建出好的业务模型。业务的复杂性可以体现在精巧的商业模式、复杂的制造过程、精益的管理方法等方面。

第二点就是领域的稳定性,这里的稳定性是指内在商业逻辑是稳定的,未来的需求主要集中在支撑功能和业务量的扩展上。当然,稳定性指标并不是绝对的,要看它所处的阶段。有的软件功能在时间长度上,几年内将不断变化,但一旦改变就不是简单的变更,例如金融、政策领域的一些法规。或者,虽然业务现阶段处于变化之中,但它的商业模式一旦形成,将作为企业的核心资产,例如新兴的移动互联网应用中的各种商业模式。它们都适合采用DDD,即便业务逻辑处在变化之中,我们也可以从DDD的另一个特性——快速测试和验证领域逻辑中收益,它可以为高频发布类应用提供很好的支持。

团队成熟度

另一个说必要性有点不够准确,应该说是前置条件,那就是团队的成熟度。

团队的成熟度首先表现在团队的技术素养上,如果团队大多数人还需要花费大量时间去学习和体会面向对象技术(这些技术包括UML语言、面向对象设计、理解面向接口编程、基于服务的架构、设计模式、开发流程、需求分析等),那么在构建通用语言、模型设计和架构解耦上投入的精力就会受到限制,冒然转为充血模式的领域驱动,将是灾难性质的。

其次就是团队中一定要有 “领域专家”角色,这个领域专家严格来说只是一种角色,它可以泛指那些对业务领域的政策、工作流程、关键节点和特性都有深刻理解的所有人。一个判断标准是,他们对领域的论述是有体系的,而不是散乱的,而且十分清楚规则的应用范围。没有领域专家,就不会有通用语言和与语言一致的模型和代码。谁来保证我们是“领域驱动”,还是过度设计?即便是最简的设计,谁来验证呢?这个“最简”只能是个伪概念,领域专家的重要性是不可替代的。

说起前置条件不得不提一下项目的周期。相对来说,领域驱动更适合周期长的项目。交付时间过于紧张的项目,团队成员的注意力都会集中在功能的开发上,这时候强调领域知识的学习和领域模型的精炼,显然会和各利益相关者产生工作安排重点的冲突。相反,周期越长的项目,比如核心产品的研发,随着时间的推移,提炼出的领域模型就会逐步释放出它的威力。因此,项目生命周期越长,收益越大。

如何从贫血到充血

主要是思考角度的转换,角度转换主要指两方面:需求视角, 不要过分纠结具体的功能,而是上升一层,去理解业务,抽象出业务对象和它们之间的关系;实现视角不要拘泥于具体代码或实现框架,要站在架构一层上,屏蔽掉系统对于数据的依赖细节,以面向对象的思维去构建系统。

设计视角:将业务转换为对象和对象关系

be5dcc14bd3a48d4a8cde2f83bab27e6.png

贫血模式所对应的开发方式更为“简单粗暴”,针对需求和功能页面,1:1的进行技术实现,直接针对功能复刻存储结构、参照MVC,逐层进行编码开发,中间几乎没有分析与设计可言,输出的成果本质上是对功能UI页面负责;这个流程其实是百分百信任或者说依赖产品经理的能力,而产品经理是否在产出产品说明书之前就进行了高质量的需求分析、是否进行了业务到功能的有效映射和转换、是否进行了有效的功能设计又都是研发不可知的。这也是诸多研发人员即便参与工作或者某个项目很久,仍旧无法说清楚所谓“业务”的原因,因为在贫血模式下的开发都是公式化的功能代码转化,没有业务理解和设计。

切入DDD的开发方式,对研发和产品都是挑战,都要有成为领域专家的意识。意味着产品经理角色自己要明白业务、领域的概念,而不是执著于几个功能或者交互;研发人员则需要同产品经理一起参与需求的分析,对于功能的设计和规划不是拘泥于一点,而是根据业务,通过绘制用例图等手段理清整个业务线,搞清楚其中牵扯的对象,并抽象出通用语言和具有业务意义的业务对象,再进一步根据业务流程 去梳理出对象间的关系、对象间的通信,从而得到完整的一套业务模型,最后结合数据情况,设计数据对接部分。

实现视角:切断数据与代码强耦合

所有的模型、被二级制化的领域逻辑和数据都不可能一直活跃在内存中,而需要被存储在数据库或文件中。此时,代码就要与具体的持久化技术框架产生耦合。如果不能将这部分代码分离出去,领域层的独立性就无从谈起了。我们也不可能脱离技术复杂度而独立开发领域逻辑,所以领域模型要想保持自己的独立性,离不开存储库将其与持久化机制解耦。

贫血模式下很多情况是数据库表、SQL模型、ORM配置、实体类到Service逻辑集于一人开发,这就导致了一个问题,当个人拥有“穿透权限”时是很容易忽视边界感的,久而久之就会懈怠去维护层与层之间的边界,业务模型与底层数据通道耦合,难以重构和复用。

92bf45ba82824bcdb972e481ecedf90d.png

解决这种情况技术上就要建立DDD中支撑域的概念,在业务之下,创建真正的数据层,数据层需要反向依赖业务层中定义的需求接口,即数据层是纯技术实现层,实现参照就是各个领域中列出的interface类;对应的开发习惯也需要进行改变,即业务开发人员不要参与数据层的逻辑设计与开发,只需要在Domain中定义数据诉求接口即可,由专门的开发团队去完成数据层或者说数据底座的开发,这样能够保证边界感,同时也能防止代码泛滥。通过支撑域数据层和核心域业务层的依赖倒置,实现代码上的领域模型与数据的解耦。保证业务模型能够在不受底层技术影响的情况下进行演化,让我们可以独立开发领域模型,无须关注架构的技术细节。

架构视角补充:利用领域概念进行技术整合

541610ec19c74ca2b30cc7c00d6ec3c8.png

使用DDD概念后,类似于包装接口、描绘界面、参数校验、数据封装、各类Util工具包, 包括上文中提到的支撑域中的数据管理则都可以划分到纯技术中,对于体量较大的系统,核心领域则可以拆分为单独的module,配合整个平台统一的数据支撑域module,形成模块化组装形态,各个领域不依赖数据形态、数据获取方式,仅提出数据接口需求,由支撑域完成核心域的需求,技术角度上,支撑域是一个大而全的数据底座,依赖于业务领域绘制的需求进行开发。

对于微服务架构,DDD更多的是利用其领域划分提供服务划分依据,技术上为保证各服务的独立性,需要弱化支撑域的概念,转而技术上将权限下放给各个服务,独立的各个微服务内部可以有自身独立的数据交互、数据持久化通道,保证各微服务的独立管理和演进,将支撑域中的内容抽离为通用域的公共组件,进行技术上的统一化管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/768198.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UOS系统中JavaFx笔锋功能

关于笔锋功能&#xff0c;网上找了很久&#xff0c;包括Java平台客户端&#xff0c;Android端&#xff0c;相关代码资料比较少&#xff0c;找了很多经过测试效果都差强人意&#xff0c;自己也搓不出来&#xff0c;在UOS平台上JavaFX也获取不到压力值&#xff0c;只能用速度的变…

c++习题07-求小数的某一位

目录 一&#xff0c;问题 二&#xff0c;思路 三&#xff0c;代码 一&#xff0c;问题 二&#xff0c;思路 被除数a的类型设置为long long类型&#xff0c;a变量需要变大&#xff0c;需要更大的数据类型来存储除数b和指定的小数位置n为int类型&#xff0c;这两个变量的的…

计算机图形学入门23:蒙特卡洛路径追踪

1.前言 前面几篇文章介绍了Whitted-style光线追踪&#xff0c;还介绍了基于物理渲染的基础知识&#xff0c;包括辐射度量学、BRDF以及渲染方程&#xff0c;但并没有给出解渲染方程的方法&#xff0c;或者说如何通过该渲染方程计算出屏幕上每一个坐标的像素值。 Whitted-style光…

未来的钥匙在于过去:学历史的真正意义,震惊!历史竟然是偶然的?从历史中寻找未来的方向!

我们自幼接受的教育是&#xff0c;学历史是为了相信历史是必然的。中国人民必然战胜日寇的侵略&#xff0c;解放思想和改革开放必定会发生&#xff0c;和平和发展必定是世界的主题&#xff0c;中国经济必定是高速增长…… 然而&#xff0c;在真正的历史学家眼中&#xff0c;历史…

1分钟了解,预写日志WAL的核心思路...

上一篇《刷盘&#xff0c;还是不刷盘&#xff0c;是一个问题》中我们遇到了哪些问题&#xff1f; 1. 已提交事务未提交事务的ACID特性怎么保证&#xff1f; 画外音&#xff1a;上一篇中遇到的问题&#xff0c;主要是原子性与持久性。 2. 数据库崩溃&#xff0c;怎么实施故障恢复…

新声创新20年:无线技术给助听器插上“娱乐”的翅膀

听力损失并非现代人的专利&#xff0c;古代人也会有听力损失。助听器距今发展已经有二百多年了&#xff0c;从当初单纯的声音放大器到如今的全数字时代助听器&#xff0c;助听器发生了翻天覆地的变化&#xff0c;现代助听器除了助听功能&#xff0c;还具有看电视&#xff0c;听…

AD导入.step 3D封装

在网站查找想要的3D封装 https://www.3dcontentcentral.cn/ 下载 AD导入 在封装库下导入

融云上线 HarmonyOS NEXT 版 SDK,全面适配「纯血鸿蒙」生态

6 月 21 日&#xff0c;“2024 华为开发者大会”正式发布使用自研内核的原生鸿蒙系统 HarmonyOS NEXT&#xff0c;即 “纯血鸿蒙”。 同时&#xff0c;华为宣布开放“鸿蒙生态伙伴 SDK 市场”&#xff0c;甄选各类优质、安全的 SDK 加入聚合平台&#xff0c;助力各行业开发者轻…

数据结构初阶 堆的问题详解(三)

题目一 4.一棵完全二叉树的节点数位为531个&#xff0c;那么这棵树的高度为&#xff08; &#xff09; A 11 B 10 C 8 D 12 我们有最大的节点如下 假设最大高度为10 那么它的最多节点应该是有1023 假设最大高度为9 那么它的最多节点应该是 511 所以说这一题选B 题目二 …

08 docker Registry搭建docker私仓

目录 本地镜像发布流程 1. docker pull registry 下载镜像 2. docker run 运行私有库registry 3. docker commit 构建镜像 4. docker tag 修改新镜像&#xff0c;使之符合私服规范tag 5. 修改配置文件使之支持http 6. curl验证私服库上有什么镜像 7. push推送 pull拉取 …

Jenkins教程-13-参数化任务构建

上一小节我们学习了发送html邮件测试报告的方法&#xff0c;本小节我们讲解一下Jenkins参数化任务构建的方法。 很多时候我们需要根据不同的条件去执行构建&#xff0c;如自动化测试中执行test、stg、prod环境的构建&#xff0c;Jenkins是支持参数化构建的。 以下是Jenkins官…

kaggle量化赛金牌方案(第七名解决方案)(下)

— 无特征工程的神经网络模型&#xff08;得分 5.34X&#xff09; 比赛进入最后阶段&#xff0c;现在是时候深入了解一些关于神经网络模型的见解了。由于 Kaggle 讨论区的需求&#xff0c;我在这里分享两个神经网络模型。第一个是 LSTM 模型&#xff0c;第二个是卷积网络&…

pmp顺利通关总结

目录 一、背景二、总结三、过程 一、背景 人活着总是想去做一些事情&#xff0c;通过这些事情来证明自己还活着。 而我证明自己还会活着并且活得很好的方式和途径&#xff0c;是通过这些东西去让自己有一个明确的边界节点&#xff1b;借此知识来验证自己的学习能力。 我坚定认…

汇凯金业:投资交易如何才能不亏损

投资交易中永不亏损是一个理想化的目标&#xff0c;现实中无法完全避免亏损。然而&#xff0c;通过科学的方法、合理的策略和严格的风险管理&#xff0c;投资者可以大幅减少亏损&#xff0c;并提高长期盈利的概率。以下是一些关键策略和方法&#xff0c;帮助投资者在交易中尽量…

Android线性布局的概念与属性

线性布局(LinearLayout)是Android中最简单的布局方式&#xff0c;线性布局方式会使得所有在其内部的控件或子布局按一条水平或垂直的线排列。如图所示&#xff0c;图a是纵向线性布局示意图&#xff0c;图b是横向线性布局示意图。 a&#xff09;纵向线性布局示意图 …

2024年电子信息工程与电气国际学术会议 (EIEEE 2024)

2024年电子信息工程与电气国际学术会议 &#xff08;EIEEE 2024&#xff09; 2024 International Academic Conference on Electronic Information Engineering and Electrical Engineering 【重要信息】 大会地点&#xff1a;北京 大会官网&#xff1a;http://www.iceieee.co…

昂科烧录器支持MindMotion灵动微电子的32位微控制器MM32L052NT

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中MindMotion灵动微电子的32位微控制器MM32L052NT已经被昂科的通用烧录平台AP8000所支持。 MM32L052NT使用高性能的ARM Cortex-M0为内核的32位微控制器&#xff0c;最高工作频率…

语音唤醒入门(基于ESP-skainet)

主要参考资料&#xff1a; ESP-SR 用户指南: https://docs.espressif.com/projects/esp-sr/zh_CN/latest/esp32s3/index.html 目录 ESP提供的模型直接初始化和使用模型AFE声学前端算法 使用模型 自定义模型 ESP提供的模型 乐鑫提供了经过训练的 WakeNet 和 MultiNet 模型&…

【C++】多态(详解)

前言&#xff1a;今天学习的内容可能是近段时间最难的一个部分的内容了&#xff0c;C的多态&#xff0c;这部分内容博主认为难度比较大&#xff0c;各位一起慢慢啃下来。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&#xff23…

【深海王国】小学生都能玩的语音模块?ASRPRO打造你的第一个智能语音助手(4)

Hi~ (o^^o)♪, 各位深海王国的同志们&#xff0c;早上下午晚上凌晨好呀~ 辛勤工作的你今天也辛苦啦(/≧ω) 今天大都督继续为大家带来系列——小学生都能玩的语音模块&#xff0c;帮你一周内快速学会语音模块的使用方式&#xff0c;打造一个可用于智能家居、物联网领域的语音助…