草庐IT

组件通信注解框架实践

杨充 2023-03-28 原文

组件通信注解框架实践

目录介绍

  • 01.为何需要组件间通信
  • 02.实现同级组件通信方式
  • 03.先看一个简单的案例
  • 04.项目组件通信流程
  • 05.逆向简化注册流程
  • 06.这个注解是做什么的
  • 07.注解是如何生成代码
  • 08.如何定义注解处理器
  • 09.项目库的设计和完善
  • 10.封装该库有哪些特点
  • 11.一些常见的报错问题
  • 12.部分原理分析的说明

01.为何需要组件间通信

  • 明确一个前提:各个业务组件之间不会是相互隔离而是必然存在一些交互的;
    • 业务复用:在Module A需要引用Module B提供的某个功能,比如需要版本更新业务逻辑,而我们一般都是使用强引用的Class显式的调用;
    • 业务复用:在Module A需要调用Module B提供的某个方法,例如别的Module调用用户模块退出登录的方法;
    • 业务获取参数:登陆环境下,在Module A,C,D,E多个业务组件需要拿到Module B登陆注册组件中用户信息id,name,info等参数访问接口数据;
  • 这几种调用形式大家很容易明白,正常开发中大家也是毫不犹豫的调用。但是在组件化开发的时候却有很大的问题:
    • 由于业务组件之间没有相互依赖,组件Module B的Activity Class在自己的Module中,那Module A必然引用不到,这样无法调用类的功能方法;由此:必然需要一种支持组件化需求的交互方式,提供平行级别的组件间调用函数通信交互的功能。
  • 项目库开源地址
    • https://github.com/yangchong211/YCLiveDataBus

02.实现同级组件通信方式

  • 至于关于页面跳转
    • 那肯定是首选路由,比如阿里的ARouter。但是涉及到组件之间业务复用,业务逻辑的交互等等,就有点难搞了……那该怎么处理比较方便呢?
  • 组件业务逻辑交互通信
    • 比如业务组件层划分
      • 组件A,组件B,组件C,组件D,组件E等等,这些业务组件并不是相互依赖,它们之间是相同的层级!
    • 举一个业务案例
      • 比如有个选择用户学员的弹窗,代码写到了组件A中,这个时候组件C和组件D需要复用组件A中的弹窗,该业务逻辑如何处理?
      • 比如组件E是我的用户相关的业务逻辑,App登陆后,组件B和组件C需要用到用户的id去请求接口,这个时候如何获取组件E中用户id呢?
    • 该层级下定义一个公共通信组件
      • 接口通信组件【被各个业务组件依赖】,该相同层级的其他业务组件都需要依赖这个通信组件。这个时候各个模块都可以拿到通信组件的类……
  • 需要具备的那些特点
    • 使用简单方便,避免同级组件相互依赖。代码入侵性要低,支持业务交互,自动化等特性。

03.先看一个简单的案例

  • 先说一下业务场景
    • 版本更新业务组件(处理更新弹窗,apk下载,apk的md5校验,安装等逻辑,还涉及到一些业务逻辑,比如更新模式普通或者强更,还有渠道,还有时间段等)
    • 主模块首页,我的组件,设置中心组件等多个module组件中都会用到版本更新功能,除了主模块外,其他组件没有依赖版本更新组件,那么如何调用里面的更新弹窗业务逻辑呢?
  • 创建一个接口通信组件
    • 如上所示,各个同级的业务组件,A,B,C,D等都依赖该接口通信组件。那么这样就会拿到通信组件的类,为了实现通信交互。可以在该接口通信组件中定义接口并暴露抽象更新弹窗方法,那么在版本更新组件中写接口实现类。
    • 创建一个map集合,存储实现类的全路径,然后put到map集合中;这样可以get拿到实现类的路径,就可以利用反射创建实例对象。
  • 通信组件几个主要类
    • BusinessTransfer,主要是map集合中get获取和put添加接口类的对象,利用反射机制创建实例对象。该类放到通信组件中
    • IUpdateManager,该类是版本更新接口类,定义更新抽象方法。该类放到通信组件中
    • UpdateManagerImpl,该类是IUpdateManager接口实现类,主要是具体业务逻辑的实现。该类放到具体实现库代码中,比如我的组件
  • 主要实现的代码如下所示//接口 public interface IUpdateManager extends Serializable { void checkUpdate(UpdateManagerCallBack updateManagerCallBack); interface UpdateManagerCallBack { void updateCallBack(boolean isNeedUpdate); } } //接口实现类 public class UpdateManagerImpl implements IUpdateManager { @Override public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) { try { IConfigService configService = DsxxjServiceTransfer.$().getConfigureService(); String data = configService.getConfig(KEY_APP_UPDATE); if (TextUtils.isEmpty(data)) { if (updateManagerCallBack != null) { updateManagerCallBack.updateCallBack(false); } return; } ForceUpdateEntity xPageUpdateEntity = JSON.parseObject(data, ForceUpdateEntity.class); ForceUpdateManager.getInstance().checkForUpdate(xPageUpdateEntity, updateManagerCallBack); } catch (Exception e) { e.printStackTrace(); } } } //如何使用 //在初始化时注入,建议放在application中设置,调用setImpl其实就是把路径字符串put到map集合中 BusinessTransfer businessTransfer = BusinessTransfer.$(); businessTransfer.setImpl(BusinessTransfer.BUSINESS_IMPL_UPDATE_MANAGER, PACKAGE_NAME + ".base.businessimpl.UpdateManagerImpl");
  • 那么如何调用呢?可以在各个组件中调用,代码如下所示……//版本更新 BusinessTransfer.$().getUpdate().checkUpdate(new IUpdateManager.UpdateManagerCallBack() { @Override public void updateCallBack(boolean isNeedUpdate) { } });
  • 反射创建接口的实现类对象String className = implsMap.get(key); try { return (T) Class.forName(className).newInstance(); } catch (InstantiationException e) { e.printStackTrace(); }
  • 这种方式存在几个问题
    • 1.注入的时候要填写正确的包名,否则在运行期会出错,且不容易找到;
    • 2.针对接口实现类,不能混淆,否则会导致反射找不到具体的类,因为是根据类的全路径反射创建对象;所以每次写一个接口+实现类,都要在混淆文件中添加一下,比较麻烦……
    • 3.每次添加新的接口通信,都需要手动去注入到map集合,稍微有点麻烦,能否改为自动注册呢?
    • 4.每次还要在Transfer的类中,添加获取该接口对象的方法,能否自动一点?
    • 5.可能出现空指针,一旦忘记没有注入或者反射创建对象失败,则直接导致崩溃……

04.项目组件通信流程

  • 具体实现方案
    • 比方说,主app中的首页有版本更新,业务组件用户中心的设置页面也有版本更新,而版本升级的逻辑是写在版本更新业务组件中。这个时候操作如下所示

05.逆向简化注册流程

  • 在module通信组件中定义接口,注意需要继承IRouteApi接口public interface IUpdateManager extends IRouteApi { void checkUpdate(UpdateManagerCallBack updateManagerCallBack); interface UpdateManagerCallBack { void updateCallBack(boolean isNeedUpdate); } }
  • 在需要实现服务的组件中写接口实现类,注意需要添加注解@RouteImpl(IUpdateManager.class) public class UpdateImpl implements IUpdateManager { @Override public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) { //省略 } }
  • 如何获取服务的实例对象//无返回值的案例 //设置监听 IUpdateManager iUpdateManager = TransferManager.getInstance().getApi(IUpdateManager.class); iUpdateManager.checkUpdate(new IUpdateManager.UpdateManagerCallBack() { @Override public void updateCallBack(boolean isNeedUpdate) { } }); //有返回值的案例 userApi = TransferManager.getInstance().getApi(IUserManager.class); String userInfo = userApi.getUserInfo();
  • 关于get/put主要是存属什么呢/** * key表示的是自定义通信接口 * value表示自定义通信接口的实现类 */ private Map<Class, Class> apiImplementMap = new HashMap<>();
  • 代码混淆-keep class com.yc.api.**{*;} -keep public class * implements com.yc.api.** { *; }
  • 不需要在额外添加通信接口实现类的混淆代码
    • 因为用到了反射,而且是用Class.forName(name)创建反射对象。所以必须保证name路径是正确的,否则找不到类。
    • 该库,你定义的实现类已经继承了我定义的接口,因为针对继承com.yc.api.**的子类,会忽略混淆。已经处理……所以不需要你额外处理混淆问题!

06.这个注解是做什么的

  • 这个注解有什么用呢
    • 框架会在项目的编译器扫描所有添加@RouteImpl注解的XxxImpl接口实现类,然后传入接口类的class对象。这样就可以通过注解拿到接口和接口的实现类……
  • apt编译后生成的代码
    • build--->generated--->ap_generated_sources--->debug---->out---->com.yc.api.contract
    • 这段代码什么意思:编译器生成代码,并且该类是继承自己自定义的接口,调用IRegister接口中的register方法,key是接口class,value是接口实现类class,直接在编译器把接口和实现类存储起来。用的时候直接取……
    public class IUpdateManager$$Contract implements IRouteContract { @Override public void register(IRegister register) { register.register(IUpdateManager.class, UpdateImpl.class); } }

07.注解是如何生成代码

  • 如何拿到注解标注的类,看个案例@RouteImpl(IUserInfoManager.class) public class Test implements IUserInfoManager { @Override public String getUserId() { return null; } } private void test(){ //这个地方先写个假的业务代码,实际apt中是通过roundEnvironment对象拿到注解标记的类 Class c = Test.class; //Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement); //找到修饰了注解RouteImpl的类 RouteImpl annotation = (RouteImpl) c.getAnnotation(RouteImpl.class); if (annotation != null) { try { //获取ContentView的属性值 Class value = annotation.value(); String name = value.getName(); System.out.println("注解标记的类名"+name); } catch (RuntimeException e) { e.printStackTrace(); System.out.println("注解标记的类名"+e.getMessage()); } } }
  • 手动编程还是自动生成
    • 在代码的编写过程中自己手动实现,也可以通过apt生成。作为一个框架,当然是自动解析RouteImpl注解然后生成这些类文件更好了。要想自动生成代码的映射关系,那么便要了解apt和javapoet了。

08.如何定义注解处理器

  • apt工具了解一下
    • APT是Annotation Processing Tool的简称,即注解处理工具。它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。
  • 定义注解处理器
    • 用来在编译期扫描加入@RouteImpl注解的类,然后做处理。这也是apt最核心的一步,新建RouteImplProcessor 继承自 AbstractProcessor,然后实现process方法。在项目编译期会执行RouterProcessor的process()方法,我们便可以在这个方法里处理RouteImpl注解了。
  • 初始化自定义Processor@AutoService(Processor.class) public class RouteImplProcessor extends AbstractProcessor { }
  • 在init方法中初始化获取文件生成器信息/** * 初始化方法 * @param processingEnvironment 获取信息 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //文件生成器 类/资源 filer = processingEnv.getFiler(); //节点工具类 (类、函数、属性都是节点) elements = processingEnv.getElementUtils(); }
  • 在process方法中拿到注解标记的类信息@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { for (TypeElement typeElement : set) { Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement); for (Element apiImplElement : annotated) { //被 RouteImpl 注解的节点集合 RouteImpl annotation = apiImplElement.getAnnotation(RouteImpl.class); if (annotation == null || !(apiImplElement instanceof TypeElement)) { continue; } ApiContract<ClassName> apiNameContract = ElementTool.getApiClassNameContract(elements, annotationValueVisitor,(TypeElement) apiImplElement); if (RouteConstants.LOG){ System.out.println("RouteImplProcessor--------process-------apiNameContract---"+apiNameContract); } } } return true; }
  • 然后生成代码,主要是指定生成代码路径,然后创建typeSpec注解生成代码。
    • 这个javapoet工具,目前还紧紧是套用ARouter,创建类名,添加接口,添加注解,添加方法,添加修饰符,添加函数体等等。也就是说将一个类代码拆分成n个部分,然后逆向拼接到一起。最后去write写入代码……
    //生成注解类相关代码 TypeSpec typeSpec = buildClass(apiNameContract); String s = typeSpec.toString(); if (RouteConstants.LOG){ System.out.println("RouteImplProcessor--------process-------typeSpec---"+s); } try { //指定路径:com.yc.api.contract JavaFile.builder(RouteConstants.PACKAGE_NAME_CONTRACT, typeSpec) .build() .writeTo(filer); } catch (IOException e) { e.printStackTrace(); }
  • 来看看怎么创建注解类
    • 大概思路就是,将我们平时的类,拆分,然后拼接成实体。ParameterSpec是创建参数的实现,MethodSpec是函数的生成实现等等……
    private TypeSpec buildClass(ApiContract<ClassName> apiNameContract) { String simpleName = apiNameContract.getApi().simpleName(); //获取 com.yc.api.IRouteContract 信息,也就是IRouteContract接口的路径 TypeElement typeElement = elements.getTypeElement(RouteConstants.INTERFACE_NAME_CONTRACT); ClassName className = ClassName.get(typeElement); String name = simpleName + RouteConstants.SEPARATOR + RouteConstants.CONTRACT; //这里面又有添加方法注解,添加修饰符,添加参数规格,添加函数题,添加返回值等等 MethodSpec methodSpec = buildMethod(apiNameContract); //创建类名 return TypeSpec.classBuilder(name) //添加super接口 .addSuperinterface(className) //添加修饰符 .addModifiers(Modifier.PUBLIC) //添加方法【然后这里面又有添加方法注解,添加修饰符,添加参数规格,添加函数题,添加返回值等等】 .addMethod(methodSpec) //创建 .build(); }

09.项目库的设计和完善

  • ModuleBus主要由三部分组成,包括对外提供的api调用模块、注解模块以及编译时通过注解生产相关的类模块。
    • api-compiler 编译期解析注解信息并生成相应类以便进行注入的模块
    • api-manager 注解的声明和信息存储类的模块,以及开发调用的api功能和具体实现
  • 编译生成代码发生在编译器
    • 编译期是在项目编译的时候,这个时候还没有开始打包,也就是没有生成apk呢!框架在这个时期根据注解去扫描所有文件,然后生成路由映射文件。这些文件都会统一打包到apk里!
  • 无需初始化操作
    • 先看ARouter,会有初始化,主要是收集路由映射关系文件,在程序启动的时候扫描这些生成的类文件,然后获取到映射关系信息,保存起来。这个封装库不需要初始化,简化步骤,在获取的时候如果没有则在put操作map集合。具体看代码!

10.封装该库有哪些特点

  • 注解生成代码自动注册
    • 使用apt注解在编译阶段生成服务接口与实现的映射注册帮助类,其实这部分就相当于是替代了之前在application初始化注入的步骤,获取服务时自动使用帮助类完成注册,不必手动调用注册方法。
  • 避免空指针崩溃
    • 无服务实现注册时,使用空对象模式 + 动态代理的设计提前暴露调用错误,主要抛出异常,在测试时就发现问题,防止空指针异常。
  • 代码入侵性低
    • 无需改动之前的代码,只需要在之前的接口和接口实现类按照约定添加注解规范即可。其接口+接口实现类还是用之前的,完全无影响……
  • 按照你需要来加载
    • 首次获取接口服务的时候,用反射生成映射注册帮助类的实例,再返回实现的实例。
  • 丰富的代码案例
    • 代码案例丰富,提供丰富的案例,然后多个业务场景,尽可能完善好demo。
  • 该库注解生成代码在编译器
    • 在编译器生成代码,并且该类是继承自己自定义的接口,存储的是map集合,key是接口class,value是接口实现类class,直接在编译器把接口和实现类存储起来。用的时候直接取……

11.一些常见的报错问题

  • Didn't find class "com.yc.api.contract.IUserManager$$Contract" on path
    • 注解生成的代码失败导致出现这个问题。为什么会出现这种情况?修改gradle的构建版本……
    public class IUpdateManager$$Contract implements IApiContract { @Override public void register(IRegister register) { register.register(IUpdateManager.class, UpdateImpl.class); } }
  • 关于apt编译器不能生成代码的问题,可能会有这么一些关键点
    • 第一查看module的依赖,如果没有依赖请先添加依赖
    implementation project(path: ':api-manager') annotationProcessor project(path: ':api-compiler')
    • 第二查看写完wirter的流没有关闭,会造成生成文件,但文件内容为空,或者不全;
    • 第三可能是Android Gradle及构建版本问题,我的是3.4.1 + 5.2.1,会出现不兼容的情况,大神建议3.3.2 + 4.10.1以下都可以。听了建议降低版本果然构建编译,新的文件生成了。

12.部分原理分析的说明

  • 注解是如何生成代码的?也就是javapoet原理……
    • 这个javapoet工具,目前还紧紧是套用ARouter,创建类名,添加接口,添加注解,添加方法,添加修饰符,添加函数体等等。也就是说将一个类代码拆分成n个部分,然后逆向拼接到一起。最后去write写入代码……
    • 但是,怎么拼接和并且创建.java文件的原理,待完善。目前处于会用……
  • Class.forName(name)反射如何找到name路径的这个类,从jvm层面分析?
    • 待完善
  • new和Class.forName("").newInstance()创建对象有何区别?A a = (A)Class.forName("com.yc.demo.impl.UpdateImpl").newInstance(); A a = new A();
    • 它们的区别在于创建对象的方式不一样,前者(newInstance)是使用类加载机制,后者(new)是创建一个新类。
    • 为什么会有两种创建对象方式?
      • 主要考虑到软件的可伸缩、可扩展和可重用等软件设计思想。
    • 从JVM的角度上看:
      • 我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用newInstance()方法的时候,就必须保证:1、这个类已经加载;2、这个类已经连接了。
      • 而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载 java API的那个加载器。
      • 现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。
    • 区别
      • 首先,newInstance( )是一个方法,而new是一个关键字;其次,Class下的newInstance()的使用有局限,因为它生成对象只能调用无参的构造函数,而使用 new关键字生成对象没有这个限制。

项目地址:https://github.com/yangchong211/YCLiveDataBus

有关组件通信注解框架实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  5. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  6. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  7. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

  8. ruby - sinatra 框架的 MVC 模式 - 2

    我想开始使用“Sinatra”框架进行编码,但我找不到该框架的“MVC”模式。是“MVC-Sinatra”模式或框架吗? 最佳答案 您可能想查看Padrino这是一个围绕Sinatra构建的框架,可为您的项目提供更“类似Rails”的感觉,但没有那么多隐藏的魔法。这是使用Sinatra可以做什么的一个很好的例子。虽然如果您需要开始使用这很好,但我个人建议您将它用作学习工具,以对您来说最有意义的方式使用Sinatra构建您自己的应用程序。写一些测试/期望,写一些代码,通过测试-重复:)至于ORM,你还应该结帐Sequel其中(imho

  9. Ruby 最佳实践 : working with classes - 2

    参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin

  10. ruby - 存储外部 API 的密码 - 最佳实践 - 2

    如果我构建了一个应用程序来访问来自Gmail、Twitter和Facebook的一些数据,并且我希望用户只需输入一次他们的身份验证信息,并且在几天或几周后重置,那会怎样是在Ruby中动态执行此操作的最佳方法吗?我看到很多人只是拥有他们客户/用户凭证的配置文件,如下所示:gmail_account:username:myClientpassword:myClientsPassword这看起来a)非常不安全,b)如果我想为成千上万的用户存储此类信息,它就无法工作。推荐的方法是什么?我希望能够在这些服务之上构建一个界面,因此每次用户进行交易时都必须输入凭据是不可行的。

随机推荐