.NET 云原生架构师训练营(基于 OP Storming 和 Actor 的大型分布式架构三)--学习笔记

目录

  • 为什么我们用 Orleans
  • Dapr VS Orleans
  • Actor 模型
  • Orleans 的核心概念
  • 结合 OP Storming 的实践

结合 OP Storming 的实践

  • 业务模型
  • 设计模型
  • 代码实现

代码实现

  • HelloOrleans.Host
  • Orleans.Providers.MongoDB
  • HelloOrleans.Contract
  • HelloOrleans.BlazorWeb

Orleans.Providers.MongoDB

接下来我们把它改为 MongoDB:Orleans.Providers.MongoDB: http://github.com/OrleansContrib/Orleans.Providers.MongoDB

引入 negut 包

<PackageReference Include="Orleans.Providers.MongoDB" Version="3.4.1" />

配置 MongoDB

builder.Host.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();
    // silo.AddMemoryGrainStorage("hello-orleans");
    silo.UseMongoDBClient("mongodb://localhost")
        .AddMongoDBGrainStorage("hello-orleans", options =>
        {
            options.DatabaseName = "hello-orleans";
            options.CollectionPrefix = "";
        });
});

我们启动项目测试一下

Create 方法入参

{
	"title": "第一个职位",
	"description": "第一个职位"
}

可以看到方法调用成功,返回的 job 里面包含了 identity

{
    "title": "第一个职位",
    "description": "第一个职位",
    "location": null,
    "identity": "c83725f2-44da-45e0-bc2d-d849563cf924"
}

接着我们打开 MongoDB

可以看到 JobGrain 中有对应的 identity 的记录

因此我们可以看到 Storage 的切换是不会影响代码的

HelloOrleans.Contract

持久化之后我们再给它改变一下模式,现在我们是 silo 内模式,客户端和服务端是放到一起的

我们可以把它改变成 silo 外模式,客户端和服务端分开

创建一个类库项目 HelloOrleans.Contract

接着把 Contract 目录下的文件都搬到 HelloOrleans.Contract 项目中

添加 Orleans 的 nuget 包,它不需要添加 Server 的包

<ItemGroup>
    <PackageReference Include="Microsoft.Orleans.Core" Version="3.6.5" />
    <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.6.5" />
</ItemGroup>

接着在 Host 项目中添加 Contract 的项目引用,即可生成成功

HelloOrleans.BlazorWeb

创建一个 Blazor Server 项目 HelloOrleans.BlazorWeb

添加 Orleans 的 nuget 包

<PackageReference Include="Microsoft.Orleans.Client" Version="3.6.5" />

在 Program 中添加 Orleans 客户端的配置

var builder = WebApplication.CreateBuilder(args);

var clientBuilder = new ClientBuilder();
clientBuilder.UseLocalhostClustering();

和服务端一样使用 UseLocalhostClustering

通过单例将 client 配置进去

var builder = WebApplication.CreateBuilder(args);

var clientBuilder = new ClientBuilder();
clientBuilder.UseLocalhostClustering();
builder.Services.AddSingleton(sp =>
{
    var client = clientBuilder.Build();
    client.Connect().Wait();
    return client;
});

接着在 Blazor 的 Index Page 尝试获取我们刚刚保存的 Job,需要添加 Contract 的项目引用

@page "/"
@using Orleans
@using HelloOrleans.Host.Contract.Entity
@using HelloOrleans.Host.Contract.Grain

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

职位标题:@Model.Title
<br/>
职位描述:@Model.Description

@code
{
    [Inject]
    public IClusterClient ClusterClient { get; set; }

    public Job Model { get; set; } = new Job();

    protected override async Task OnInitializedAsync()
    {
        var jobId = "c1eb55ff-74bc-4747-a782-5786cc3cdf80";
        var jobGrain = ClusterClient.GetGrain<IJobGrain>(jobId);
        Model = await jobGrain.Get();
    }
}

相当于获取 Job,然后将 Job 的相关信息展现在页面上

接着我们测试一下,先启动 Host,再启动 Blazor

可以看到我们已经获取到了职位的信息

源码链接:http://github.com/MingsonZheng/HelloOrleans

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

本文转载于网络 如有侵权请联系删除

相关文章

  • AIX 7.1查看硬件配置信息 查看端口IBM POWER 750 P750

    大家好,又见面了,我是你们的朋友全栈君。查看CPU型号host01:/home/oracle$lsattr-Elproc0 frequency3500000000ProcessorSpeedFalse smt_enabledtrueProcessorSMTenabledFalse smt_threads4ProcessorSMTthreadsFalse stateenableProcessorstateFalse typePowerPC_POWER7ProcessortypeFalse host01:/home/oracle$复制从这里可以看出,CPU型号是PowerPC_POWER7;查看cpu个数#lsdev-Ccprocessor|grepproc|wc-l 32 安装了32个cpu复制查看cpu核心数及频率host01:/home/oracle$pmcycles-m CPU0runsat3500MHz CPU1runsat3500MHz CPU2runsat3500MHz CPU3runsat3500MHz CPU4runsat3500MHz CPU5runsat3500MH

  • 无分类编址 CIDR(构造超网)

    它的正式名字是无分类域间路由选择CIDR(ClasslessInter-DomainRouting)。 网络前缀CIDR消除了传统的A类、B类和C类地址以及划分子网的概念,因而可以更加有效地分配IPv4的地址空间。CIDR使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号。IP地址从三级编址(使用子网掩码)又回到了两级编址。无分类的两级编址的记法是: CIDR使用“斜线记法”(slashnotation),它又称为CIDR记法,即在IP地址后面加上一个斜线“/”,然后写上网络前缀所占的位数(这个数值对应于三级编址中子网掩码中1的个数)。例如:220.78.168.0/24CIDR把网络前缀都相同的连续的IP地址组成“CIDR地址块”。128.14.32.0/20表示的地址块共有212个地址(因为斜线后面的20是网络前缀的位数,所以这个地址的主机号是12位,因为总共是32位)。这个地址块的起始地址是128.14.32.0。在不需要指出地址块的起始地址时,也可将这样的地址块简称为“/20地址块”。128.14.32.0/20地址块的最小地址:128

  • Prometheus配置企业微信告警

    kubernetesoperator安装的Prometheus,如果不会可以参考使用Operator部署Prometheus 前提:创建企业微信,创建应用然后配置altermanager.yamlglobal: resolve_timeout:5m receivers: -name:wechat wechat_configs: -agent_id:"100000x" api_secret:Nm7PRrxxxxG8EpkyWuXDAWMLmFo corp_id:wwf9d3833cd2323ewdd send_resolved:true to_user:Joker route: group_by: -job group_interval:5m group_wait:30s receiver:wechat repeat_interval:12h routes: -match: alertname:Watchdog receiver:wechat 复制 然后删除原有的alertmanager-mainkubectldeletesecretalertmanager-mai

  • mac上通过brew包管理器安装pyth

    mac上通过brew包管理器安装python3.6缘由:最近在学习pythonTDD(测试驱动开发时)遇到了一个问题,根据指导需要使用Django1.12版本,但是在跑服务启动命令pythonmanage.pyrunserver复制的时候,出现了如下的报错:File"<frozenimportlib._bootstrap>",line1006,in_gcd_import File"<frozenimportlib._bootstrap>",line983,in_find_and_load File"<frozenimportlib._bootstrap>",line967,in_find_and_load_unlocked File"<frozenimportlib._bootstrap>",line677,in_load_unlocked File"<frozenimportlib._bootstrap_external>",

  • AssetBundle使用模式

    原文链接:https://www.jianshu.com/p/22a6876d39b5本系列中的上一篇文章覆盖了AssetBundle的基础知识,尤其是各种加载API的底层行为。这篇文章讨论的则是实际应用中使用AssetBundles可能遇到的,方方面面的问题与解决方法。4.1.管理已加载Assets在内存紧张的环境中,小心控制加载Objects的大小和数量尤为重要。Objects被移出激活的场景时,Unity不会自动卸载他们。Asset的清理会在特定的时间触发,当然也可以手动触发。必须小心的管理AssetBundles自身文件。一个AssetBundle在本地存储(不论是在UnityCache中,还是通过AssetBundle.LoadFromFile加载的文件)中以一个文件的形式存在时,其占用的内存开销很小,几乎不会超过10-40kb。但如果有大量的AssetBundles存在,这开销依旧不容忽视。因为多数工程允许用户重复体验某内容(比如重玩一个关卡),所以知道什么时候去加载或卸载一个AssetBundle就尤为重要了。如果一个AssetBundle被不恰当的卸载了,这可能会引起O

  • python——发送邮件

    利用程序来发邮件的作用挺多的,在脚本运行时发送一个邮件给你,然后你可以知道脚本运行了,做到一个实时的监控。importsmtplib复制fromemail.headerimportHeader复制defsendtheback(str): sendemaillsddress="你的邮箱"#用于发送邮件的地址 sendemailpassword="授权码"#邮箱的密码 sendemailhost="smtp.qq.com"#邮箱的服务器地址 sendemailport="465"#端口 recuveremailaddress="接收的邮箱"#接收的邮箱 emailsubject="关机" emalicontent=str message=MIMEText(emalicontent,'plain',"utf-8") message["From"]=Header(sendemaillsddress,"ut

  • 每日一道面试题 【征集令】& 【eBay】 面试经验分享

    最近参加了不少面试,深深感受到了面试的侧重点和平时工作所用到的东西差异还是不小的,或许是我本来基础缺失,所以有些面试经历都不是很愉快。前几天去参加了eBay的面试,很明显地感觉到了国内公司和外企在面试上的区别,不清楚从什么时候起,国内各大中小公司不论公司规模,业务方向或者真正的技术实力,都是一言不合就会跟你聊源码,问框架等等,但是本次eBay面试过程中,面试官看到我实际毕业只有两年,全程专注于JavaSE部分以及算法和数据结构这块,结果可想而知,长久以来一直在不停的学框架,同时为了应付国内公司的面试,也假装在不停的看源码。可是无论你怎么引导,eBay面试官始终坚持在对于基础的考察上。简述一下整个面试过程吧,首先会有个电话面试,电话面试聊的就比较广泛,基础、框架、算法或者各种中间件都有可能涉及到(具体的题我会总结在下文),电话面试通过之后会安排现场面试。电话面试的面试题我大概回忆了下,大概有以下内容:面试现场有个专门的小会议室,一张桌子,两个凳子,一块挂在墙上的白板,一看这个就是专门为了面试而安排的地方。第一位面试过来之后看了看简历,互相寒暄了几句就开始面试了,首先描述了自己的项目,因为

  • 06-字符串使用基础

    python中,单双引号没有区别,表示一样的含义sentence='tom\'spetisacat'#单引号中间还有单引号,可以转义 sentence2="tom'spetisacat"#也可以用双引号包含单引号 sentence3="tomsaid:\"helloworld!\"" sentence4='tomsaid:"helloworld"' #三个连续的单引号或双引号,可以保存输入格式,允许输入多行字符串 words=""" hello world abcd""" print(words) py_str='python' len(py_str)#取长度 py_str[0]#第一个字符 'python'[0] py_str[-1]#最后一个字符 #py_str[6]#错误,下标超出范围 py_str[2:4]#切片,起始下标包含,结束下标不包

  • 优化安卓应用内存的神秘方法以及背后的原理,一般人我不告诉他

    安卓应用一般都害怕自己被杀,内存占用高是被杀的重要原因之一,所以大家都想尽各种招数应对,但效果都一般。但有一招:WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);复制几乎没有人提及。这段时间的实战,发现效果还不错,但要掌握好这个函数的用法,需要仔细理解背后的原理,毕竟这个调用相当于在局部时间内让应用的一系列GPU缓存被清理,相当于硬件加速失效。文章分三大部分,第一大部分用简单的方式描述安卓绘制系统框架,第二大部分说明绘制过程中GPU产生缓存的原因。第三大部分说明startTrimMemory能够清理的GPU缓存以及一些误区。(一)简介安卓绘制系统框架安卓绘制系统比较复杂,网上很多文章讲得很细,但不容易抓住核心要点,其实我们只要抓到12个关键的对应关系和概念,就可以掌握清晰基本框架,对debug和性能优化都有价值。1)一个activity对应一个window,当然,没有activity耶可以有window,比如通知栏,window大家都知道,有各种属性,比如层次,位置等等2)一个window对应

  • HDUOJ----4509湫湫系列故事——减肥记II

    湫湫系列故事——减肥记IITimeLimit:5000/2000MS(Java/Others)    MemoryLimit:65535/32768K(Java/Others) TotalSubmission(s):2176    AcceptedSubmission(s):921 ProblemDescription  虽然制定了减肥食谱,但是湫湫显然克制不住吃货的本能,根本没有按照食谱行动! 于是,结果显而易见…   但是没有什么能难倒高智商美女湫湫的,她决定另寻对策——吃没关系,咱吃进去再运动运动消耗掉不就好了?   湫湫在内心咆哮:“我真是天才啊~\(≧▽≦)/~”   可是,大家要知道,过年回家多忙啊——帮忙家里做大扫除,看电影,看小说,高中同学聚餐,初中同学聚餐,小学同学聚餐,吃东西,睡觉,吃东西,睡觉,吃东西,睡觉……所以锻炼得抽着时间来。   但是,湫湫实在太忙了,所以没时间去算一天有多少时间可以用于锻炼,现在她把每日行程告诉你,拜托你帮忙算算吧~   皮埃斯:一天是24小时,每小时60分钟Input输入数据包括多组测试用例。 每组测试数据首先是一个整数n,表

  • 【零一】#操作教程贴#从0开始,教你如何做数据分析#中阶#第十篇

    大家好,我是零一。这一篇给大家介绍聚类/分类。我的公众微信号是start_data,欢迎大家关注。我们先讲一讲聚类。上一篇的探索关系,很多朋友反映说非常有趣,这一篇,聚类分析也是相当有趣的。聚类分析简称聚类,俗话说物以类聚,人以群分,聚类就是划分子类的过程。算法上面多用k-means和k-medoids,当然,大家可以跳过这些算法的过程,用程序来完成即可。说简单一点,通过聚类,可以将我们的数据进行分类,并且描述每个类的特征。聚类应用非常广泛,包括在电商领域的应用也是多不胜数。比如(1)对客户数据进行聚类分析得到多个客户群组,并且得到各个群组的特征,这可以帮助我们发现客户的共性和差异性;(2)竞争对手数据进行聚类分析得到多个对手群组和各自的特征,这一样可以让我们找到对手们的共性和差异性;(3)对行业数据进行聚类分析得到多个行业群组和各自的特征,这个可以来发现不同行业之间的共性和差异性(4)对销售数据进行聚类分析(比如以其中的地域聚类),可以告诉我们那些地域之间的共性和差异性不难发现,我举的4个例子都是在发现共性和差异性。对的!我们了解了这些信息,可以指导我们的运营决策,对不同群组制定不同

  • 入门干货之用DVG打造你的项目主页-Docfx、Vs、Github

    由于这三项技术涉及到的要点以及内容较多,希望大家有空能自己挖掘一下更多更深的用法。   0x01、介绍     VS,即VS2017以及以上版本,宇宙最好的IDE,集成了宇宙最有前景的平台,前阶段也支持了宇宙最好的语言。     Github,知名的代码/项目托管平台,不想赘述了,如果干两三年了这个都不认识,自觉转行吧,我不在文章里说什么,但你得晓得,我肯定偷偷的鄙视你了。     Docfx,类似JSDoc或Sphinx,可以从源代码中提取注释生成文档之外,而且还有语法支持你加入其他的文件链接到API添加额外的说明,DocFX会扫描你的源代码和附加的文件为你生成一个完整的HTML模版网站,你可以自己通过模版定制,目前已经内嵌了几个模版,包括静态的HTML页面和AngularJS页面。     哎呀,说白了,就是根据你的.cs文件生成一些API,注释,首页之类的,就是说,你用吹灰之力就能免去文档的烦恼,就这码事。   0x02、吐槽     我觉得VS上的github插件巨难用,反正我用它上不来气儿。之前本人用惯了PC端,高端大气上档次,同时也保证了简洁清爽功能全。

  • hdoj:2043

    #include<iostream> #include<string> usingnamespacestd; booljudgeSize(stringstr) { intsize=str.size(); if(size<8||size>16) returnfalse; returntrue; } intisA(stringstr) { for(auto&c:str) { if(c>='A'&&c<='Z') return1; } return0; } intisa(stringstr) { for(auto&c:str) { if(c>='a'&&c<='z') return1; } return0; } intis0(stringstr) { for(auto&c:str) { if(c>='0'&&c<='9') return1; } return0; } intisOther(stringstr) { for(auto&c:

  • 300. 最长递增子序列

    300.最长递增子序列 给你一个整数数组nums,找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。 示例1: 输入:nums=[10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是[2,3,7,101],因此长度为4。 复制 动态规划 dp[i]表示以下标i为结尾的严格递增子序列的长度。 更新规则:dp[i]=max(dp[i],dp[j]+1),0<=j<i 边界条件:初始化dp数组全为1即可,因为至少有其本身是子序列。 最后:注意dp数组的含义,因此最后要遍历数组,找到最大的那个值。 classSolution{ public: intlengthOfLIS(vector<int>&nums){ //动态规划经典题 //dp[i]表示以下标`i`为结尾的严格递增子序列的长度。 //更新时需要和前dp[i]比较 intn=nums.size(); if(!n)return0; vecto

  • 03 Nginx虚拟主机&amp;图片服务器&amp;访问日志

    2020年11月4日study 2021年5月12日review 01Nginx虚拟主机 搭建前端静态服务器 什么是虚拟主机? 指在一台物理主机服务器上划分出多个磁盘空间,每个磁盘空间都是一个虚拟主机,每个虚拟主机都可以对外提供Web服务,并且互不干扰,就类似虚拟机。 利用虚拟主机可以把多个不同域名的网站部署在同一台服务器上,节省了服务器硬件成本和相关的维护费用。 (注意:修改配置文件之前,做好备份,以免配置出错可以回滚。) Nginx虚拟主机配置 nginx.conf中的一个server就是一个虚拟主机; 在win10下,进入C:\Windows\System32\drivers\etc,修改hosts文件: 47.115.23.41dawson.com 47.115.23.41rose.com 复制 通过域名进行区分,不在location进行区分; server{ listen80; server_namedawson.com; location/{ roothtml; indexxdclass.html; } } server{ listen80; se

  • Cortex-A8和Android应用开发视频教程

    基于ARMCortex-A8和Android4.x的联动报警系统(Android、A8、Linux、驱动、NDK)课程分类:Android适合人群:高级课时数量:38(70节)课时用到技术:Android、A8、210、Linux、驱动、NDK、报警涉及项目:联动报警系统咨询QQ:1840215592 1.课程研发环境本课程包括JAVA应用、C语言驱动、NDK(应用调用驱动)等方面内容,课程涉及主要工具如下:开发工具:Eclipse、SourceInsight交叉编译工具:arm-linux-gcc4.5.1其他工具:SecureCRT、Minitools、VMware等都会提供与项目匹配的安装程序,并且是破解版2.内容简介本教程共分五大部分内容,1Android应用开发2Android系统移植3Cortexa8裸机接口开发4Android设备驱动开发5综合项目实战。第一部分课程从最基础的Android应用开发环境搭建开始,简单讲解了Android界面及事件处理之后,深入剖析AndroidHandler多线程机制,重点讲解AndroidNDK应用层与驱动的通信;第二部分内容,先简单讲解

  • Python爬虫-scrapyd框架部署

    爬虫项目部署 1脚本文件部署 linux内置的cron进程能帮我们实现这些需求,cron搭配shell脚本,非常复杂的指令也没有问题。 1.1crontab的使用 crontab[-uusername]    //省略用户表表示操作当前用户的crontab -e(编辑工作表) -l(列出工作表里的命令) -r(删除工作) 复制 我们用crontab-e进入当前用户的工作表编辑,是常见的vim界面。每行是一条命令。 crontab的命令构成为时间+动作,其时间有分、时、日、月、周五种,操作符有 *****取值范围内的所有数字 /每过多少个数字 -从X到Z ,散列数字 代表意义 分钟 小时 日期 月份 周 命令 数字范围 0~59 0~23 1~31 1~12 0~6 就命令 1.2为当前用户创建cron服务 可以键入crontab-e编辑crontab服务文件 举例: *****#每分钟都执行 复制 实例1:每5分钟执行一次文档写入 */1****echo'helloworld'>>/home/poppies/Documents/xialuo/ps

  • 统一异常处理

    一、什么是统一异常处理 1、制造异常 //异常测试 @ApiOperation(value="异常测试") @GetMapping("exceptionTest") publicRexceptionTest(){ inta=1/0; returnR.ok(); }复制 2、什么是统一异常处理 我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么需要统一异常处理。 二、统一异常处理 1、创建统一异常处理器 packagecom.stu.service.base.handler; importcom.stu.service.base.exception.CustomException; importcom.stu.service.base.result.R; importcom.stu.service.base.utils.ExceptionUtils; importlombok.extern.slf4j.Slf4j; importorg.springframework.web.bind.annotation.ControllerAdvice; import

  • Weblogic11g下调WebService出现的一系列问题

                             Weblogic11g下调WebService出现的一系列问题     今天在远程测试机上测试前天写的调用WebService接口方法,遇到的问题还真多啊! 首先说明一下weblogic加载jar包的顺序: 加载顺序: weblogic11g和之前部署的不一样,首先是,他先加载自己的jar包,然后才是你部署到什么程序的war包。 更改加载顺序: 从网上找的的方法是,在你开发的Java项目下的web-inf下,先写一个weblogic.xml的配置,这里的内容是: <?xmlversion="1.0"encoding="UTF-8"?> <!DOCTYPEhibernate-configurationPUBLIC "-//Hi

  • 智能眼镜(安卓系统)安装和卸载和查看当前包名ABD 命令

    adbdevices adbinstall adbuninstallbzh.ama.xperteye.vk adbuninstallbzh.ama.glasscam adbshellpmlistpackages-3    以下是其他博友网络总结参考如下:  查了其他细节的adb命令如下参考 1.查看当前连接设备或者虚拟机的所有包 adbshellpmlistpackages 2.只输出系统的包 adbshellpmlistpackages-s 3.输出所有第三方包 adbshellpmlistpackages-3 4.输出包和包相关联的文件(安装路径) adbshellpmlistpackages-f 5.输出包和安装信息(安装来源) adbshellpmlistpackages-i 6.输出包含过滤条件的包 adbshellpmlistpackages“lzy” 7.只输出启用的包 adbshellpmlistpackages-e 8.只输出禁用的包 adbshellpmlistpackages-d 9.只输出包和未安装包信息(安装来源) adb

  • 抽象类和接口的区别,使用场景

    shared fromhttp://yinny.iteye.com/blog/1152430 1接口是核心,其定义了要做的事情,包含了许多的方法,但没有定义这些方法应该如何做。 2如果许多类实现了某个接口,那么每个都要用代码实现那些方法 3如果某一些类的实现有共通之处,则可以抽象出来一个抽象类,让抽象类实现接口的公用的代码,而那些个性化的方法则由各个子类去实现。 所以,抽象类是为了简化接口的实现,他不仅提供了公共方法的实现,让你可以快速开发,又允许你的类完全可以自己实现所有的方法,不会出现紧耦合的问题。 应用场合很简单了 1优先定义接口 2如果有多个接口实现有公用的部分,则使用抽象类,然后集成它。 接口和抽象类的区别--相信你看完不会再混淆了 我想,对于各位使用面向对象编程语言的程序员来说,“接口”这个名词一定不陌生,但是不知各位有没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,作为程序员,一定经常听到“面向接口编程”这个短语,那么它是什么意思?有什么思想内涵

相关推荐

推荐阅读