一起谈.NET技术,走向ASP.NET架构设计——第四章—业务层分层架构(中篇)

简介:   在上一篇文章中,我们讨论了两种组织业务逻辑的模式:Transaction Script和Active Record。在本篇中开始讲述Domain Model和Anemic Model。  Domain Model  在开发过程中,我们常常用Domain Model来对目标的业务领域建模。

  在上一篇文章中,我们讨论了两种组织业务逻辑的模式:Transaction Script和Active Record。在本篇中开始讲述Domain Model和Anemic Model。

  Domain Model

  在开发过程中,我们常常用Domain Model来对目标的业务领域建模。通过Domain Model建模的业务类代表了目标领域中的一些概念。而且,我们会看到通过Domain Model建模的一些对象模拟了业务活动中的数据,有的对象还反映了一些业务规则。

  我们就来看看电子商务系统的开发,在开发中我们建立了一些概念的模型来反映电子商务领域中的一些概念:购物车,订单,订单项等。这些模型有自己的数据,行为。例如一个订单模型,它不仅仅包含一些属性(流水号,创建日期,状态)来包含自己的数据,同时它也包含了一些业务逻辑:下订单的用户时候合法,下订单用户的余额是否充足等。

  一般来说,我们对领域了解的越深,我们在软件中建立的模式越接近现实中的概念,最后实现的软件就越符合客户的需求。同时在建模的过程中,也要考虑模型的可实现行,可能我们对领域进行了很好的建模,和符合目标领域的一些概念,但是在软件实现起来非常的困难,那么就得权衡一下:找出一个比较好的模式,同时也便于实现。

  在以前的文章中其实也提到过一些有关Domain Model的一些东西,其实Domain Model和Active Record的一个区别在于:Domain Model不知道自己的数据时如何持久化的,即PI(Persistence Ignorance).也就是说,通过Domain Model建立的业务类,都是POCO(Plain Old Common Runtime Object)。

  下面我们就用一个银行转账的例子来讲述一下Domain Model的应用。创建一个新的解决方案,命名为ASPPatterns.Chap4.DomainModel,并且添加如下的项目:

  ASPPatterns.Chap4.DomainModel.Model

  ASPPatterns.Chap4.DomainModel.AppService

  ASPPatterns.Chap4.DomainModel.Repository

  ASPPatterns.Chap4.DomainModel.UI.Web

  编译整个,Solution,然后添加引用:

  为Repository项目添加Model 的引用。

  为AppService项目添加Model和Repository的引用。

  为Web项目添加AppService的引用。

  下面就来看看每个项目代表的含义:

  ASPPatterns.Chap4.DomainModel.Model:在这个project中包含了系统中所有的业务逻辑和业务对象,以及业务对象之间的关系。这个project也定义了持久化业务对象的接口,并且用Repository 模式来实现的(Repository 模式我们后面会谈到的)。大家可以看到:这个Model的project没有引用其他的project,也就是说这个Model的project完全关注于业务。

  ASPPatterns.Chap4.DomainModel.Repository:这个Repository的project实现了包含在Model project中定义的持久化接口。而且Repository还引用了Model project,就是用来持久化Model的数据的。

  ASPPatterns.Chap4.DomainModel.AppService:AppService project就扮演者一个应用层的角色,或者理解为门户入口,因为提供了一些比较粗颗粒度的API,并且它和Presenter层之间通过消息的机制来进行通信。(消息模式我们以后也会讲述)而且在AppService中,我们还会定义一些view model,这些view model的就符合也最后要显示的数据结构,view model的数据可能是很多业务对象数据的组合,或者仅仅就是这业务对象数据的格式转换等等。

  ASPPatterns.Chap4.DomainModel.UI.Web:这个Web.UI project主要是负责最后的显示逻辑和一些用户体验的实现。这个project就调用AppService提供的API,获取符合界面显示的强类型的view model,然后显示数据。

  系统的这整个结构如下:

  下面就开始创建保存数据的数据库,和以前一样,为了演示的作用,我们在Web project中添加一个名为BankAccount.mdf的数据库,并且建立如下的表:

  BankAccount 表

  Transaction 表

  下一步就开始为领域建模,因为这里的例子比较简单和常见,建模的过程就省了,最后就得到了表示领域概念的两个领域对象(或者说业务对象):

 
  
public class Transaction
{
public Transaction( decimal deposit, decimal withdrawal, string reference, DateTime date)
{
this .Deposit = deposit;
this .Withdrawal = withdrawal;
this .Reference = reference;
this .Date = date;
}

public decimal Deposit
{
get ; internal set ; }

public decimal Withdrawal
{
get ; internal set ; }

public string Reference
{
get ; internal set ; }

public DateTime Date
{
get ; internal set ; }
}

  在上面的代码中,Transaction对象不包含任何的标识属性(标识对象唯一的属性,常常和数据库中的表的主键对应),因为Transaction对象就是表示订单中的每一笔交易,而且在这个系统中我们往往关心的只是每个Transaction的数据,而不关系这个Transaction到底是那个Transaction。也就是说此时在这个系统中Transaction是一个值对象(后篇讲述DDD会提到)。 

  再看看BankAccount类:

 
 
public class BankAccount
{
private decimal _balance;
private Guid _accountNo;
private string _customerRef;
private IList < Transaction > _transactions;

public BankAccount() : this (Guid.NewGuid(), 0 , new List < Transaction > (), "" )
{
_transactions.Add(
new Transaction(0m, 0m, " account created " , DateTime.Now));
}

public BankAccount(Guid Id, decimal balance, IList < Transaction > transactions, string customerRef)
{
AccountNo
= Id;
_balance
= balance;
_transactions
= transactions;
_customerRef
= customerRef;
}

public Guid AccountNo
{
get { return _accountNo; }
internal set { _accountNo = value; }
}

public decimal Balance
{
get { return _balance; }
internal set { _balance = value; }
}

public string CustomerRef
{
get { return _customerRef; }
set { _customerRef = value; }
}

public bool CanWithdraw( decimal amount)
{
return (Balance >= amount);
}

public void Withdraw( decimal amount, string reference)
{
if (CanWithdraw(amount))
{
Balance
-= amount;
_transactions.Add(
new Transaction(0m, amount, reference, DateTime.Now));
}
}

public void Deposit( decimal amount, string reference)
{
Balance
+= amount;
_transactions.Add(
new Transaction(amount, 0m, reference, DateTime.Now));
}

public IEnumerable < Transaction > GetTransactions()
{
return _transactions;
}
}

  代码中包含了一些保存数据的业务属性,同时还包含了三个简单的业务方法:

  CanWithdraw:是否可以取款

  Withdraw:取款

  Deposit:存款

  为了代码的健壮性,在调用Withdraw方法的时候,如果取款的数量超过了存款的数额,那么就抛出一个余额不足的异常:InsufficientFundsException.其实这里到底是抛异常还是给出其他的返回值,主要是个人的选择,没有一定要,非要什么的。

 
 
public class InsufficientFundsException : ApplicationException
{
}

  所以业务方法Withdraw修改如下:

 
 
public void Withdraw( decimal amount, string reference)
{
if (CanWithdraw(amount))
{
Balance
-= amount;
_transactions.Add(
new Transaction(0m, amount, reference, DateTime.Now));
}
else
{
throw new InsufficientFundsException();
}
}

  最后就考虑下如何持久化业务对象的数据。在上面业务类的设计中,我们尽量的保持业务类的干净------只包含业务逻辑,关系和业务的数据。至于数据从何而来,最后如何保存,我们都委托给了一个Repository的接口IBankAccountRepository。

 
 
public interface IBankAccountRepository
{
void Add(BankAccount bankAccount);
void Save(BankAccount bankAccount);
IEnumerable
< BankAccount > FindAll();
BankAccount FindBy(Guid AccountId);
}

  本系统是一个银行转账的系统,转账的操作不是一个业务对象就能够独立的完成的,往往需要多个业务类,以及数据持久化类的一些相互配合,这些操作放在任何一个业务类中都会把职责搞乱,而且后期的维护还得到处去找这个方法。所以我们在业务层中又剥离一层service,其中service中的每个方法其实和需求中的用例有个对象关系,例如在需求中就有转账的一个用例,那么在service中就有一个Transfer转账的方法,这个方法把很多的业务对象组合在一起完成这个转账的流程,也就是说,在每个业务类中的业务方法都是原子性的,细颗粒度的,可以被重用,而在业务层的service的方法就是粗颗粒度的,目的是为调用者提供简化的API。

 
 
public class BankAccountService
{
private IBankAccountRepository _bankAccountRepository;

public BankAccountService(IBankAccountRepository bankAccountRepository)
{
_bankAccountRepository
= bankAccountRepository;
}

public void Transfer(Guid accountNoTo, Guid accountNoFrom, decimal amount)
{
BankAccount bankAccountTo
= _bankAccountRepository.FindBy(accountNoTo);
BankAccount bankAccountFrom
= _bankAccountRepository.FindBy(accountNoFrom);

if (bankAccountFrom.CanWithdraw(amount))
{
bankAccountTo.Deposit(amount,
" From Acc " + bankAccountFrom.CustomerRef + " " );
bankAccountFrom.Withdraw(amount,
" Transfer To Acc " + bankAccountTo.CustomerRef + " " );

_bankAccountRepository.Save(bankAccountTo);
_bankAccountRepository.Save(bankAccountFrom);
}
else
{
throw new InsufficientFundsException();
}
}
}

  清楚了上面的之后,我们就把Repository那层实现,其实因为我们在业务层中使用的只是Repository的接口,至于采用哪种数据持久化方法可以替换的,例如如果用数据库来保存数据,我们可以选择用Linq To Sql,ADO.NET,EF等。业务层不用关心这些的。

  在下面,就用了最原始的ADO.NET来实现的,大家可以任意替换实现策略:(下面的代码大家过过就行了,可以不用细看)

 
 
public class BankAccountRepository : IBankAccountRepository
{
private string _connectionString;

public BankAccountRepository()
{
_connectionString
= ConfigurationManager.ConnectionStrings[ " BankAccountConnectionString " ].ConnectionString;
}

public void Add(BankAccount bankAccount)
{
string insertSql = " INSERT INTO BankAccounts " +
" (BankAccountID, Balance, CustomerRef) VALUES " +
" (@BankAccountID, @Balance, @CustomerRef) " ;

using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= insertSql;

SetCommandParametersForInsertUpdateTo(bankAccount, command);

connection.Open();

command.ExecuteNonQuery();
}

UpdateTransactionsFor(bankAccount);
}

public void Save(BankAccount bankAccount)
{
string bankAccoutnUpdateSql = " UPDATE BankAccounts " +
" SET Balance = @Balance, CustomerRef= @CustomerRef " +
" WHERE BankAccountID = @BankAccountID; " ;

using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= bankAccoutnUpdateSql;

SetCommandParametersForInsertUpdateTo(bankAccount, command);

connection.Open();

command.ExecuteNonQuery();
}

UpdateTransactionsFor(bankAccount);
}

private static void SetCommandParametersForInsertUpdateTo(BankAccount bankAccount, SqlCommand command)
{
command.Parameters.Add(
new SqlParameter( " @BankAccountID " , bankAccount.AccountNo));
command.Parameters.Add(
new SqlParameter( " @Balance " , bankAccount.Balance));
command.Parameters.Add(
new SqlParameter( " @CustomerRef " , bankAccount.CustomerRef));
}

private void UpdateTransactionsFor(BankAccount bankAccount)
{
string deleteTransactionSQl = " DELETE Transactions WHERE BankAccountId = @BankAccountId; " ;

using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= deleteTransactionSQl;
command.Parameters.Add(
new SqlParameter( " @BankAccountID " , bankAccount.AccountNo));
connection.Open();
command.ExecuteNonQuery();

}

string insertTransactionSql = " INSERT INTO Transactions " +
" (BankAccountID, Deposit, Withdraw, Reference, [Date]) VALUES " +
" (@BankAccountID, @Deposit, @Withdraw, @Reference, @Date) " ;

foreach (Transaction tran in bankAccount.GetTransactions())
{
using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= insertTransactionSql;

command.Parameters.Add(
new SqlParameter( " @BankAccountID " , bankAccount.AccountNo));
command.Parameters.Add(
new SqlParameter( " @Deposit " , tran.Deposit));
command.Parameters.Add(
new SqlParameter( " @Withdraw " , tran.Withdrawal));
command.Parameters.Add(
new SqlParameter( " @Reference " , tran.Reference));
command.Parameters.Add(
new SqlParameter( " @Date " , tran.Date));

connection.Open();
command.ExecuteNonQuery();
}
}
}

public IEnumerable < BankAccount > FindAll()
{
IList
< BankAccount > accounts = new List < BankAccount > ();

string queryString = " SELECT * FROM dbo.Transactions INNER JOIN " +
" dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " +
" ORDER BY dbo.BankAccounts.BankAccountId; " ;

using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= queryString;

connection.Open();

using (SqlDataReader reader = command.ExecuteReader())
{
accounts
= CreateListOfAccountsFrom(reader);
}
}

return accounts;
}

private IList < BankAccount > CreateListOfAccountsFrom(IDataReader datareader)
{
IList
< BankAccount > accounts = new List < BankAccount > ();
BankAccount bankAccount;
string id = "" ;
IList
< Transaction > transactions = new List < Transaction > ();

while (datareader.Read())
{
if (id != datareader[ " BankAccountId " ].ToString())
{
id
= datareader[ " BankAccountId " ].ToString();
transactions
= new List < Transaction > ();
bankAccount
= new BankAccount( new Guid(id), Decimal.Parse(datareader[ " Balance " ].ToString()), transactions, datareader[ " CustomerRef " ].ToString());
accounts.Add(bankAccount);
}
transactions.Add(CreateTransactionFrom(datareader));
}

return accounts;
}

private Transaction CreateTransactionFrom(IDataRecord rawData)
{
return new Transaction(Decimal.Parse(rawData[ " Deposit " ].ToString()),
Decimal.Parse(rawData[
" Withdraw " ].ToString()),
rawData[
" Reference " ].ToString(),
DateTime.Parse(rawData[
" Date " ].ToString()));
}


public BankAccount FindBy(Guid AccountId)
{
BankAccount account;

string queryString = " SELECT * FROM dbo.Transactions INNER JOIN " +
" dbo.BankAccounts ON dbo.Transactions.BankAccountId = dbo.BankAccounts.BankAccountId " +
" WHERE dbo.BankAccounts.BankAccountId = @BankAccountId; " ;

using (SqlConnection connection =
new SqlConnection(_connectionString))
{
SqlCommand command
= connection.CreateCommand();
command.CommandText
= queryString;

SqlParameter Idparam
= new SqlParameter( " @BankAccountId " , AccountId);
command.Parameters.Add(Idparam);

connection.Open();

using (SqlDataReader reader = command.ExecuteReader())
{
account
= CreateListOfAccountsFrom(reader)[ 0 ];
}
}
return account;
}
}

  到现在为止,数据访问,业务逻辑都已经完成了,最后的一步就是显示数据了。

  我们知道:最后在界面显示的数据结构,很多的时候和我们业务对象的数据结构是不一致的,这个时候我们就要进行一定的转换,生成符合界面需要的数据结构,尽量少的让显示层出来过多的逻辑。此时就引入View Model来解决问题。

  AppService就是一个门户:向显示层提供需要的数据。我们在AppService中就处理数据结构不一致的情况:添加两个View Model:

 
 
public class TransactionView
{
public string Deposit { get ; set ; }
public string Withdrawal { get ; set ; }
public string Reference { get ; set ; }
public DateTime Date { get ; set ; }
}

public class BankAccountView
{
public Guid AccountNo { get ; set ; }
public string Balance { get ; set ; }
public string CustomerRef { get ; set ; }
public IList < TransactionView > Transactions { get ; set ; }
}

  然后我们再添加一些辅助的方法来进行数据结构的转换,例如把Transaction转为TranactionViewModel:

 
 
public static class ViewMapper
{
public static TransactionView CreateTransactionViewFrom(Transaction tran)
{
return new TransactionView
{
Deposit
= tran.Deposit.ToString( " C " ),
Withdrawal
= tran.Withdrawal.ToString( " C " ),
Reference
= tran.Reference,
Date
= tran.Date
};
}

public static BankAccountView CreateBankAccountViewFrom(BankAccount acc)
{
return new BankAccountView
{
AccountNo
= acc.AccountNo,
Balance
= acc.Balance.ToString( " C " ),
CustomerRef
= acc.CustomerRef,
Transactions
= new List < TransactionView > ()
};
}
}

  可能现在我们是把这些project部署在一台机器上,如果是考虑到以后的分布式的情况,我们决定让显示层和AppService用消息模式来通信:请求-响应!

 
 
public abstract class ResponseBase
{
public bool Success { get ; set ; }
public string Message { get ; set ; }
}

  其中Success表示方法调用是否成功,Message包含一些信息,如错误信息等。 

  下面就是请求的消息对象:

 
 
public class TransferRequest
{
public Guid AccountIdTo { get ; set ; }
public Guid AccountIdFrom { get ; set ; }
public decimal Amount { get ; set ; }
}

public class WithdrawalRequest
{
public Guid AccountId { get ; set ; }
public decimal Amount { get ; set ; }
}

  然后我们把上面的对象组合在一起,为显示层提供最简化的服务:

 
 
public class ApplicationBankAccountService
{
private BankAccountService _bankAccountService;
private IBankAccountRepository _bankRepository;

public ApplicationBankAccountService() :
this ( new BankAccountRepository(), new BankAccountService( new BankAccountRepository()))
{ }

public ApplicationBankAccountService(IBankAccountRepository bankRepository, BankAccountService bankAccountService)
{
_bankRepository
= bankRepository;
_bankAccountService
= bankAccountService;
}

public ApplicationBankAccountService(BankAccountService bankAccountService, IBankAccountRepository bankRepository)
{
_bankAccountService
= bankAccountService;
_bankRepository
= bankRepository;
}

public BankAccountCreateResponse CreateBankAccount(BankAccountCreateRequest bankAccountCreateRequest)
{
BankAccountCreateResponse bankAccountCreateResponse
= new BankAccountCreateResponse();
BankAccount bankAccount
= new BankAccount();

bankAccount.CustomerRef
= bankAccountCreateRequest.CustomerName;
_bankRepository.Add(bankAccount);

bankAccountCreateResponse.BankAccountId
= bankAccount.AccountNo;
bankAccountCreateResponse.Success
= true ;

return bankAccountCreateResponse;
}

public void Deposit(DepositRequest depositRequest)
{
BankAccount bankAccount
= _bankRepository.FindBy(depositRequest.AccountId);

bankAccount.Deposit(depositRequest.Amount,
"" );

_bankRepository.Save(bankAccount);
}

public void Withdrawal(WithdrawalRequest withdrawalRequest)
{
BankAccount bankAccount
= _bankRepository.FindBy(withdrawalRequest.AccountId);

bankAccount.Withdraw(withdrawalRequest.Amount,
"" );

_bankRepository.Save(bankAccount);
}

public TransferResponse Transfer(TransferRequest request)
{
TransferResponse response
= new TransferResponse();

try
{
_bankAccountService.Transfer(request.AccountIdTo, request.AccountIdFrom, request.Amount);
response.Success
= true ;
}
catch (InsufficientFundsException)
{
response.Message
= " There is not enough funds in account no: " + request.AccountIdFrom.ToString();
response.Success
= false ;
}

return response;
}

public FindAllBankAccountResponse GetAllBankAccounts()
{
FindAllBankAccountResponse FindAllBankAccountResponse
= new FindAllBankAccountResponse();
IList
< BankAccountView > bankAccountViews = new List < BankAccountView > ();
FindAllBankAccountResponse.BankAccountView
= bankAccountViews;

foreach (BankAccount acc in _bankRepository.FindAll())
{
bankAccountViews.Add(ViewMapper.CreateBankAccountViewFrom(acc));
}

return FindAllBankAccountResponse;
}

public FindBankAccountResponse GetBankAccountBy(Guid Id)
{
FindBankAccountResponse bankAccountResponse
= new FindBankAccountResponse();
BankAccount acc
= _bankRepository.FindBy(Id);
BankAccountView bankAccountView
= ViewMapper.CreateBankAccountViewFrom(acc);

foreach (Transaction tran in acc.GetTransactions())
{
bankAccountView.Transactions.Add(ViewMapper.CreateTransactionViewFrom(tran));
}

bankAccountResponse.BankAccount
= bankAccountView;

return bankAccountResponse;
}

}

  最后我们就是处理显示层。

  在本例子中,显示层就是用传统的ASP.NET来实现的,而且用了最简单的实现,如果需要,大家可以采用MVP模式,这点在我的另一文章(走向ASP.NET架构设计—第三章—分层设计,初涉架构(中篇) )中详细的讲述了,这里不在赘述,也希望大家见谅。

  到这里Domain Model就基本讲述完了,我们可以看出:当软件中的业务比较的负责的时候,我们用Domain Model可能比较的好。因为用Domain Model的时候,我们的把所有的精力主要关注在对业务领域的建模,把业务的概念抽象出来,变为软件可以实现的模型。其实抽象出业务模式不是那么容易的事情,往往必须对领域作出比较深入的分析才行。

  同时,在业务建模和可实现性之间要有权衡,有时候,我们把业务分析的很透,但是分析出来的概念无法转为实现,产生了“水至清则无鱼”。希望大家多多的琢磨几种组织业务逻辑模式的区别。 

  代码下载 

目录
相关文章
|
3月前
|
设计模式 Java 应用服务中间件
Tomcat 架构原理解析到架构设计借鉴
Tomcat 架构原理解析到架构设计借鉴
106 0
|
6月前
|
存储 开发框架 前端开发
asp.net与asp.net优缺点及示例
asp.net与asp.net优缺点及示例
|
4月前
|
缓存 架构师 安全
架构篇:什么才是真正的架构设计?
特别特别厉害的一篇文章,今天无意中看到的,转载至CSDN的大佬hguisu的:blog.csdn.net/hguisu/article/details/78258430,谈到了作者对于架构的理解,我看完是真的受益匪浅。
|
30天前
|
开发框架 前端开发 .NET
进入ASP .net mvc的世界
进入ASP .net mvc的世界
28 0
|
30天前
|
存储 消息中间件 算法
深度思考:架构师必须掌握的五大类架构设计风格
数据流风格注重数据在组件间的流动,适合处理大量数据。调用返回风格则强调函数或方法的调用与返回,过程清晰明了。独立构件风格让每个构件独立运作,通过接口交互,提升灵活性和可重用性。虚拟机风格则模拟完整系统,实现资源的高效利用。
深度思考:架构师必须掌握的五大类架构设计风格
|
1月前
|
开发框架 中间件 .NET
C# .NET面试系列七:ASP.NET Core
## 第一部分:ASP.NET Core #### 1. 如何在 controller 中注入 service? 在.NET中,在ASP.NET Core应用程序中的Controller中注入服务通常使用<u>依赖注入(Dependency Injection)</u>来实现。以下是一些步骤,说明如何在Controller中注入服务: 1、创建服务 首先,确保你已经在应用程序中注册了服务。这通常在Startup.cs文件的ConfigureServices方法中完成。例如: ```c# services.AddScoped<IMyService, MyService>(); //
60 0
|
1月前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
95 5
|
3月前
|
开发框架 安全 搜索推荐
分享105个NET源码ASP源码,总有一款适合您
分享105个NET源码ASP源码,总有一款适合您
27 4
|
6月前
|
存储 负载均衡 算法
【系统架构】分布式系统架构设计
【系统架构】分布式系统架构设计
302 0
|
6月前
|
存储 缓存 算法
架构设计第八讲:架构 - 理解架构的模式2 (重点)
架构设计第八讲:架构 - 理解架构的模式2 (重点)
104 0