2019独角兽企业重金招聘Python工程师标准>>>

最近taowen同学连续发起了两起关于贫血模型和领域模型的讨论,引起了大家的广泛热烈的讨论,但是讨论(或者说是争论)的结果到底怎样,我想值 得商榷。问题是大家对贫血模型和领域模型都有自己的看法,如果没有对此达到概念上的共识,那么讨论的结果应该可想而知,讨论的收获也是有的,至少知道了分 歧的存在。为了使问题具有确定性,我想从一个简单例子着手,用我对贫血模型和领域模型的概念来分别实现例子。至于我的理解对与否,大家可以做评判,至少有 个可以评判的标准在这。

一个例子

我要举的是一个银行转帐的例子,又是一个被用滥了的例子。但即使这个例子也不是自己想出来的,而是剽窃的<<POJOs in Action>>中的例子,原谅我可怜的想像力 。当钱从一个帐户转到另一个帐户时,转帐的金额不能超过第一个帐户的存款余额,余额总数不能变,钱只是从一个账户流向另一个帐户,因此它们必须在一个事务内完成,每次事务成功完成都要记录此次转帐事务,这是所有的规则。

贫血模型

我们首先用贫血模型来实现。所谓贫血模型就是模型对象之间存在完整的关联(可能存在多余的关联),但是对象除了get和set方外外几乎就没有其它的方 法,整个对象充当的就是一个数据容器,用C语言的话来说就是一个结构体,所有的业务方法都在一个无状态的Service类中实现,Service类仅仅包 含一些行为。这是Java Web程序采用的最常用开发模型,你可能采用的就是这种方法,虽然可能不知道它有个&ldquo;贫血模型&rdquo;的称号,这要多 亏Martin Flower(这个家伙惯会发明术语!)。

包结构

在讨论具体的实现之前,我们先来看来贫血模型的包结构,以便对此有个大概的了解。

贫血模型的实现一般包括如下包:

  • dao:负责持久化逻辑
  • model:包含数据对象,是service操纵的对象
  • service:放置所有的服务类,其中包含了所有的业务逻辑
  • facade:提供对UI层访问的入口

代码实现

先看model包的两个类,Account和TransferTransaction对象,分别代表帐户和一次转账事务。由于它们不包含业务逻辑,就是一个普通的Java Bean,下面的代码省略了get和set方法。

  1. public   class  Account {
  2. private  String accountId;
  3. private  BigDecimal balance;
  4. public  Account() {}
  5. public  Account(String accountId, BigDecimal balance) {
  6. this .accountId = accountId;
  7. this .balance = balance;
  8. }
  9. // getter and setter ....
  10. }
  1. public   class  TransferTransaction {
  2. private  Date timestamp;
  3. private  String fromAccountId;
  4. private  String toAccountId;
  5. private  BigDecimal amount;
  6. public  TransferTransaction() {}
  7. public  TransferTransaction(String fromAccountId, String toAccountId, BigDecimal amount, Date timestamp) {
  8. this .fromAccountId = fromAccountId;
  9. this .toAccountId = toAccountId;
  10. this .amount = amount;
  11. this .timestamp = timestamp;
  12. }
  13. // getter and setter ....
  14. }
  1. public   interface  TransferService {
  2. TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount)
  3. throws  AccountNotExistedException, AccountUnderflowException;
  4. }
  1. public   class  TransferServiceImpl  implements  TransferService {
  2. private  AccountDAO accountDAO;
  3. private  TransferTransactionDAO transferTransactionDAO;
  4. public  TransferServiceImpl(AccountDAO accountDAO,
  5. TransferTransactionDAO transferTransactionDAO) {
  6. this .accountDAO = accountDAO;
  7. this .transferTransactionDAO = transferTransactionDAO;
  8. }
  9. public  TransferTransaction transfer(String fromAccountId, String toAccountId,
  10. BigDecimal amount) throws  AccountNotExistedException, AccountUnderflowException {
  11. Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0 );
  12. Account fromAccount = accountDAO.findAccount(fromAccountId);
  13. if  (fromAccount ==  null )  throw   new  AccountNotExistedException(fromAccountId);
  14. if  (fromAccount.getBalance().compareTo(amount) <  0 ) {
  15. throw   new  AccountUnderflowException(fromAccount, amount);
  16. }
  17. Account toAccount = accountDAO.findAccount(toAccountId);
  18. if  (toAccount ==  null )  throw   new  AccountNotExistedException(toAccountId);
  19. fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
  20. toAccount.setBalance(toAccount.getBalance().add(amount));
  21. accountDAO.updateAccount(fromAccount);      // 对Hibernate来说这不是必须的
  22. accountDAO.updateAccount(toAccount);        // 对Hibernate来说这不是必须的
  23. return  transferTransactionDAO.create(fromAccountId, toAccountId, amount);
  24. }
  25. }

优缺点

贫血模型的优点是很明显的:

  1. 被许多程序员所掌握,许多教材采用的是这种模型,对于初学者,这种模型很自然,甚至被很多人认为是java中最正统的模型。
  2. 它非常简单,对于并不复杂的业务(转帐业务),它工作得很好,开发起来非常迅速。它似乎也不需要对领域的充分了解,只要给出要实现功能的每一个步骤,就能实现它。
  3. 事务边界相当清楚,一般来说service的每个方法都可以看成一个事务,因为通常Service的每个方法对应着一个用例。(在这个例子中我使用了facade作为事务边界,后面我要讲这个是多余的)

其缺点为也是很明显的:

  1. 所有的业务都在service中处理,当业越来越复杂时,service会变得越来越庞大,最终难以理解和维护。
  2. 将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣势,随着业务的复杂,业务会在service中多个方法间重复。
  3. 当添加一个新的UI时,很多业务逻辑得重新写。例如,当要提供Web Service的接口时,原先为Web界面提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持业务逻辑一致是很大的挑战。

领域模型

接下来看看领域驱动模型,与贫血模型相反,领域模型要承担关键业务逻辑,业务逻辑在多个领域对象之间分配,而Service只是完成一些不适合放在模型中的业务逻辑,它是非常薄的一层,它指挥多个模型对象来完成业务功能。

包结构

领域模型的实现一般包含如下包:

  • infrastructure: 代表基础设施层,一般负责对象的持久化。
  • domain:代表领域层。domain包中包括两个子包,分别是model和service。model中包含模型对 象,Repository(DAO)接口。它负责关键业务逻辑。service包为一系列的领域服务,之所以需要service,按照DDD的观点,是因为领域中的某些概念本质是一些行为,并且不便放入某个模型对象中。比如转帐操作,它是一个行为,并且它涉及三个对 象,fromAccount,toAccount和TransferTransaction,将它放入任一个对象中都不好。
  • application: 代表应用层,它的主要提供对UI层的统一访问接口,并作为事务界限。

代码实现

现在来看实现,照例先看model中的对象:

  1. public   class  Account {
  2. private  String accountId;
  3. private  BigDecimal balance;
  4. private  OverdraftPolicy overdraftPolicy = NoOverdraftPolicy.INSTANCE;
  5. public  Account() {}
  6. public  Account(String accountId, BigDecimal balance) {
  7. Validate.notEmpty(accountId);
  8. Validate.isTrue(balance == null  || balance.compareTo(BigDecimal.ZERO) >=  0 );
  9. this .accountId = accountId;
  10. this .balance = balance ==  null  ? BigDecimal.ZERO : balance;
  11. }
  12. public  String getAccountId() {
  13. return  accountId;
  14. }
  15. public  BigDecimal getBalance() {
  16. return  balance;
  17. }
  18. public   void  debit(BigDecimal amount)  throws  AccountUnderflowException {
  19. Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0 );
  20. if  (!overdraftPolicy.isAllowed( this , amount)) {
  21. throw   new  AccountUnderflowException( this , amount);
  22. }
  23. balance = balance.subtract(amount);
  24. }
  25. public   void  credit(BigDecimal amount) {
  26. Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0 );
  27. balance = balance.add(amount);
  28. }
  29. }

TransferServiceImpl类:

  1. public   class  TransferServiceImpl  implements  TransferService {
  2. private  AccountRepository accountRepository;
  3. private  TransferTransactionRepository transferTransactionRepository;
  4. public  TransferServiceImpl(AccountRepository accountRepository,
  5. TransferTransactionRepository transferTransactionRepository) {
  6. this .accountRepository = accountRepository;
  7. this .transferTransactionRepository = transferTransactionRepository;
  8. }
  9. public  TransferTransaction transfer(String fromAccountId, String toAccountId,
  10. BigDecimal amount) throws  AccountNotExistedException, AccountUnderflowException {
  11. Account fromAccount = accountRepository.findAccount(fromAccountId);
  12. if  (fromAccount ==  null )  throw   new  AccountNotExistedException(fromAccountId);
  13. Account toAccount = accountRepository.findAccount(toAccountId);
  14. if  (toAccount ==  null )  throw   new  AccountNotExistedException(toAccountId);
  15. fromAccount.debit(amount);
  16. toAccount.credit(amount);
  17. accountRepository.updateAccount(fromAccount);   // 对Hibernate来说这不是必须的
  18. accountRepository.updateAccount(toAccount);     // 对Hibernate来说这不是必须的
  19. return  transferTransactionRepository.create(fromAccountId, toAccountId, amount);
  20. }
  21. }

优缺点

其优点是:

  1. 领域模型采用OO设计,通过将职责分配到相应的模型对象或Service,可以很好的组织业务逻辑,当业务变得复杂时,领域模型显出巨大的优势。
  2. 当需要多个UI接口时,领域模型可以重用,并且业务逻辑只在领域层中出现,这使得很容易对多个UI接口保持业务逻辑的一致(从领域模型的分层图可以看得更清楚)。

其缺点是:

  1. 对程序员的要求较高,初学者对这种将职责分配到多个协作对象中的方式感到极不适应。
  2. 领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。
  3. 对于简单的软件,使用领域模型,显得有些杀鸡用牛刀了。

我的看法

这部分我将提出一些可能存在争议的问题并提出自己的看法。

软件分层

理解软件分层、明晰每层的职责对于理解领域模型以及代码实现是有好处的。软件一般分为四层,分别为表示层,应用层,领域层和基础设施层。软件领域中另外一个著名的分层是TCP/IP分层,分为应用层,运输层,网际层和网络接口层。我发现它们之间存在对应关系,见下表:

TCP/IP分层 软件分层
    表示层 负责向用户显示信息。
应用层 负责处理特定的应用程序细节。如FTP,SMTP等协议。 应用层 定义软件可以完成的工作,指挥领域层的对象来解决问题。它不负责业务逻辑,是很薄的一层。
运输层 两台主机上的应用程序提供端到端的通信。主要包括TCP,UDP协议。 领域层 负责业务逻辑,是业务软件的核心。
网际层 处理分组在网络中的活动,例如分组的选路。主要包括IP协议。
网络接口层 操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。 基础设施层 为上层提供通用技术能力,如消息发送,数据持久化等。

对于TCP/IP来说,运输层和网际层是最核心的,这也是TCP/IP名字的由来,就像领域层也是软件最核心的一层。可以看出领域模型的包结构与软 件分层是一致的。在软件分层中,表示层、领域层和基础设施层都容易理解,难理解的是应用层,很容易和领域层中Service混淆。领域Service属于 领域层,它需要承担部分业务概念,并且这个业务概念不易放入模型对象中。应用层服务不承担任何业务逻辑和业务概念,它只是调用领域层中的对象(服务和模 型)来完成自己的功能。应用层为表示层提供接口,当UI接口改变一般也会导致应用层接口改变,也可能当UI接口很相似时应用层接口不用改变,但是领域层 (包括领域服务)不能变动。例如一个应用同时提供Web接口和Web Service接口时,两者的应用层接口一般不同,这是因为Web Service的接口一般要粗一些。可以和TCP/IP的层模型进行类比,开发一个FTP程序和MSN聊天程序,它们的应用层不同,但是可以同样利用 TCP/IP协议,TCP/IP协议不用变。与软件分层不同的是,当同样开发一个FTP程序时,如果只是UI接口不同,一个是命令行程序,一个是图形界 面,应用层不用变(利用的都是FTP服务)。下图给出领域模型中的分层:

Repository接口属于领域层

可能有人会将Repository接口,相当于贫血模型中的DAO接口,归于基础设施层,毕竟在贫血模型中DAO是和它的实现放在一起。这就涉及 Repository 接口到底和谁比较密切?应该和domain层比较密切,因为Repository接口是由domain层来定义的。用TCP/IP来类比,网际层支持标准 以太网、令牌环等网络接口,支持接口是在网际层中定义的,没有在网际层定义的网络接口是不能被网际层访问的。那么为什么在贫血模型中DAO的接口没有放在 model包中,这是因为贫血模型中DAO的接口是由service来定义的,但是为什么DAO接口也没有放在service包中,我无法解释,按照我的 观点DAO接口放在service包中要更好一些,将DAO接口放在dao包或许有名称上对应的考虑。对于领域模型,将Repository接口放入 infrastructure包中会引入包的循环依赖,Repository依赖Domain,Domain依赖Repository。然而对于贫血模 型,将DAO接口放入dao包中则不会引入包循环依赖,只有service对DAO和model的依赖,而没有反方向的依赖,这也导致service包很 不稳定,service又正是放置业务逻辑的地方。JDepend这个工具可以检测包的依赖关系。

贫血模型中Facade有何用?

我以前的做一个项目使用的就是贫血模型,使用了service和facade,当我们讨论service和facade有什么区别时,很少有人清 楚,最终结果facade就是一个空壳,它除了将方法实现委托给相应的service方法,不做任何事,它们的接口中的方法都一样。Facade应该是主 要充当远程访问的门面,这在EJB时代相当普遍,自从Rod Johson叫嚷without EJB之后,大家对EJB的热情降了很多,对许多使用贫血模型的应用程序来说,facade是没有必要的。贫血模型中的service在本质上属于应用层 的东西。当然如果确实需要提供远程访问,那么远程Facade(或许叫做Remote Service更好)也是很有用的,但是它仍然属于应用层,只不过在技术层面上将它的实现委托给对应的Service。下图是贫血模型的分层:

从上面的分层可以看出贫血模型实际上相当于取消掉了领域层,因为领域层并没有包含业务逻辑。

DAO到底有没有必要?

贫血模型中的DAO或领域模型中的Repository到底有没有必要?有人认为DAO或者说Repository是充血模型的大敌,对此我无论如 何也不赞同。DAO或Repository是负责持久化逻辑的,如果取消掉DAO或Repository,将持久化逻辑直接写入到model对象中,势必 造成model对象承担不必要的职责。虽然现在的ORM框架已经做得很好了,持久化逻辑还是需要大量的代码,持久化逻辑的掺入会使model中的业务逻辑 变得模糊。允许去掉DAO的一个必要条件就是Java的的持久化框架必须足够先进,持久化逻辑的引入不会干扰业务逻辑,我认为这在很长一段时间内将无法做 到。在rails中能够将DAO去掉的原因就是rail中实现持久化逻辑的代码很简洁直观,这也与ruby的表达能力强有关系。DAO的另外一个好处隔离 数据库,这可以支持多个数据库,甚至可以支持文件存储。基于DAO的这些优点,我认为,即使将来Java的持久化框架做得足够优秀,使用DAO将持久化逻 辑从业务逻辑中分离开来还是十分必要的,况且它们本身就应该分离。

结束语

在这篇文章里,我使用了一个转帐例子来描述领域模型和贫血模型的不同,实现代码可以从附件中下载,我推荐你看下附件代码,这会对领域模型和贫血模型 有个更清楚的认识。我谈到了软件的分层,以及贫血模型和领域模型的实现又是怎样对应到这些层上去的,最后是对DAO(或Repository)的讨论。以 上只是我个人观点,如有不同意见欢迎指出。

争论的焦点到了DAO上啊,呵呵。确实,实践中很多项目最终实施DDD的结果就是把所有的DAO重命名为Repository。但是我认为DAO和Repository很像,但是不是一个东西,因为它们出发点不同。

为什么要有DAO? 
因为之前,很早之前,我们对于框架中立性还很受用。DAO给了我们可以随时把Hibernate换成ibatis的幻觉,所以我们要有一个地方隔离了框架。 
而且DAO集中了所有的查询,方便了性能调优人员。同时也鼓励了查询的重用,同样方便了调优。

为什么要有Repository? 
在我看来,与其说PublicationRepository,不如说Publications。Repository是一个集合对象,它封装了集合的逻辑。因为它具有封装性,所以它应该负责保持这个集合的状态,比如拒绝一些非法的的修改

  1. class  PublicationRepository {
  2. public   void  save(Publication pub) {
  3. if  (hasSameName(pub)) {
  4. throw   new  InvalidPublicationException();
  5. }
  6. dao.save(pub);
  7. }
  8. }
  1. class PublicationRepository {
  2. public void save(Publication pub) {
  3. if (hasSameName(pub)) {
  4. throw new InvalidPublicationException();
  5. }
  6. dao.save(pub);
  7. }
  8. }

另外,Repository只应该负责Aggregate Root。对于被Aggregate的对象,应该用Navigation,也就是在关系之间游走来获取。所以不是所有的查询都必须由Repository来完成,比如说:

  1. class  Contact {
  2. private  List<ContactNote> contactNotes =  new  ArrayList<ContactNote>();
  3. public   void  contactedBy(User accountManager, DateTime time){
  4. ContactNote contactNote = new  ContactNote( this , accountManager, time);
  5. if  (isDuplicated(contactNote)) {
  6. throw   new  InvalidContactNote();
  7. }
  8. contactNotes.add(contactNote);
  9. }
  10. private   boolean  isDuplicated(ContactNote contactNote) {
  11. // 查询contactNotes
  12. return  xxx;
  13. }
  14. }
  1. class Contact {
  2. private List<ContactNote> contactNotes = new ArrayList<ContactNote>();
  3. public void contactedBy(User accountManager, DateTime time){
  4. ContactNote contactNote = new ContactNote(this, accountManager, time);
  5. if (isDuplicated(contactNote)) {
  6. throw new InvalidContactNote();
  7. }
  8. contactNotes.add(contactNote);
  9. }
  10. private boolean isDuplicated(ContactNote contactNote) {
  11. // 查询contactNotes
  12. return xxx;
  13. }
  14. }

现状是,对象之间的关联不可查询导致了,很多这样的查询必须通过xxxDao,xxxRepository来完成。其实它们都不应该插手。

理想情况下,只有业务开始的时候用repository加载对象,在结束的时候用repository把对象存储回去,中间都是领域对象在互相作 用。而DAO,可以以Generic Query Builder的形式存在,不过和它之前被发明出来的意图已经不是一个东西了。

DAO原本的作用就是隔离数据库的影响,没有业务逻辑。而Repository更抽象,从概念上来说是一个可以全局访问的集合,从这个意义上来讲对你所举 PublicationRepository,使用add(Publication pub)作为方法签名要更好一些。Repository也负责保持完整对象的完整性,PublicationRepository的例子也说明了这一点, 另外一个例子,但从数据库重建一个对象时,由于外部原因,对象已经变得不完整,将它恢复为一个完整的对象或者直接抛异常也是Repostory的责任,它 可以将这种保证对象完整性的责任委托给别的对象(如Factory)。将Repository和DAO联合起来用应该很有用,谢谢你的提醒!

至于

引用

  1. class  Contact {
  2. private  List<ContactNote> contactNotes =  new  ArrayList<ContactNote>();
  3. public   void  contactedBy(User accountManager, DateTime time){
  4. ContactNote contactNote = new  ContactNote( this , accountManager, time);
  5. if  (isDuplicated(contactNote)) {
  6. throw   new  InvalidContactNote();
  7. }
  8. contactNotes.add(contactNote);
  9. }
  10. private   boolean  isDuplicated(ContactNote contactNote) {
  11. // 查询contactNotes
  12. return  xxx;
  13. }
  14. }

转载于:https://my.oschina.net/newchaos/blog/889454

贫血模式or领域模式(转载)相关推荐

  1. 领域驱动设计战术模式:领域服务

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第四篇,重心讲解领域服务模式. 在建模时,有时会遇 ...

  2. IIS7 经典模式和集成模式的区别分析(转载)

    经典模式是为了与之前的版本兼容,使用ISAPI扩展来调用ASP.NET运行库,原先运行于IIS6.0下的Web应用程序迁移到IIS7.0中只要将应用程序配置成经典模式,代码基本不用修改就可以正常运行. ...

  3. 转载:建设工程中常见的项目建设管理模式有哪些(DBB模式、EPC模式)

    原文标题:建设工程中常见的项目建设管理模式有哪些? - 知乎 (zhihu.com) 一.DBB模式 即设计-招标-建造(Design-Bid-Build)模式,这是最传统的一种工程项目管理模式.该管 ...

  4. 并发模式与 RPS 模式之争,性能压测领域的星球大战

    Photo by Daniel Cheung on Unsplash 本文是 <如何做好性能压测>系列专题分享的第四期,该专题将从性能压测的设计.实现.执行.监控.问题定位和分析.应用场景 ...

  5. 生产者消费者模式详解(转载)

    ★简介 在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类.函数.线程.进程等).产生数据的模块,就形象地称为生产者:而处理 ...

  6. MySQL的关系模式集是什么,[转载]数据库的关系模型、关系模式、主码。。。。...

    1.关系模型:用二维表格结构表示实体集,外键表示实体间联系的数据模型称为关系模型.关系模型是由若干个关系模式组成的集合. 2.关系模式:关系模式实际上就是记录类型.它包括:模式名,属性名,值 ... ...

  7. 自然语言处理中的模式(模式1.概率化模式)

    /* 版权声明:可以任意转载,转载时请务必标明文章原始出处和作者信息 .*/ 自然语言处理中的模式(模式1.概率化模式) CopyMiddle: 张俊林                         ...

  8. SOA系列文章(二):服务设计原理:服务模式和反模式

    服务设计系列的法则已经发展到最佳通信实践和取样相关编码的程度.本文提供了设计和实现网络服务的基本原理,并且对面向服务的体系结构(SOA)的相关概念做了一个简要的回顾,以及有关于几种模式和反模式的详细讨 ...

  9. 设计模式C++实现(4)——原型模式、模板方法模式

    软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径.设计模式中运用了面向对象编程语言的重要特性:封装.继承.多态,真正领悟设计模式的精髓是可能一个漫长的过程,需要大量实践经验的积累.最 ...

  10. Atitit orm的实现模式 data-mapper模式和active-record模式有什么区别

    Atitit orm的实现模式  data-mapper模式和active-record模式有什么区别 1.1. 这是来自Node.js路线有关混合两种ORM模式Active Record(活动记录模 ...

最新文章

  1. Javascript 函数声明和函数表达式的区别
  2. 二级联动,三级联动,初学者,纯javascript,不含jQuery
  3. linux如何把postgresql添加到环境变量_如何搞清楚PostgreSQL的环境变量 ?
  4. Leetcode--22. 括号生成
  5. 历经7年双11实战,阿里巴巴是如何定义云原生混部调度优先级及服务质量的?
  6. AIX 文件操作和AIX 目录操作
  7. [erlang] mnesia
  8. 中兴手机官宣吴京代言 以科技为勇敢者助力
  9. 一个真正成熟的人不会过度在意别人的眼光
  10. uva11922(强行用rope替代spaly)
  11. 吴恩达神经网络和深度学习-学习笔记-12-RMSprop算法
  12. 基于解释的学习一个例子
  13. 《HTML与CSS设计》课程总结,网页设计课程学习心得总结
  14. Python 房贷计算器小工具
  15. 信息学竞赛与 计算机科学,在信息学奥林匹克竞赛中我与学生共成长
  16. Windows下解压tar.gz压缩文件
  17. 《假如给我三天光明》读后感及其摘录(2)
  18. 韶音骨传导耳机怎么样?南卡和韶音在线评测对比
  19. IObit Driver Booster 无法更新驱动的解决办法
  20. 这可能是最完整的进藏攻略

热门文章

  1. Windows系统过滤病毒功能吗
  2. 二十一天学通VC++之创建工作者线程
  3. 4.6 GoogLeNet CNN、tensorflow实现——python实战
  4. 优化理论07-----拟牛顿法、拟牛顿方程、对称秩二更新公式、BFGS、DFS、Broyden族、Huang’s Family
  5. python爬虫股票数据分析判断股票好坏_教你用Python爬虫股票评论,简单分析股民用户情绪...
  6. hive查看表中列的信息命令_走近大数据之Hive入门(四、Hive的管理)
  7. Flutter高级第5篇:官方推荐的状态管理库 provider 的使用
  8. Android studio 导包时,容易出现的问题【包括最新版本的问题】
  9. 【推荐】 Neutralizer 安卓上特殊的均衡器
  10. WebFrom 【文件上传】