`
java183
  • 浏览: 13280 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

《J2EE development without EJB》读书笔记(四)

阅读更多



第7章 Spring框架简介(IOC)
轻量级容器一个重要出发点就是消除J2EE应用中众多自制的工厂和Singleton。
对于服务型对象,Singleton模式足够了。
工厂Bean:FactoryBean本身是在bean工厂中定义的一个bean,同时又是用于创建另一个对象的工厂。实际注册到bean工厂中的是FactoryBean创建的对象,而不是FactoryBean本身。
JndiObjectFactoryBean:通过JNDI查找而获得对象(使用EJB等JNDI树上的资源,EJB结合Spring共同使用)
LocalSessionFactoryBean:创建本地装配的Hibernate SessionFactory
ProxyFactoryBean:经典Spring AOP使用其创建代理
除非你使用EJB,否则基本上不应该在JNDI中保存持久层工具的连接工厂。这样的连接工厂是特定于应用程序的,并不适合在服务器级别共享。使用JNDI也不会带来集群方面的好处:集群环境下的资源锁通常是通过数据库锁或集群缓存来实现的,根本不需要将连接工厂放入JNDI中。
***********************************************
第8章 基于AOP概念的声明性中间件 
衡量OOP成功与否的标准就是它在多大程度上避免了代码重复。而有些横切性的问题导致的代码重复让OO显得力不从心,继承也是爱莫能助。传统EJB使用代码生成解决这种问题,由EJB容器在部署时负责生成子类,添加重复的代码,引入横切行为。虽然开发者避免编写重复代码,但是部署过于复杂,而且只有EJB才可享受这种待遇,另外EJB容器一开始就规定了横切问题导致的不灵活,都使代码生成不足以满足我们的需要。
AOP提供了OO层级模型无法提供的新视角-将横切性问题以一种更加通用的方式模块化,从而提高程序的模块化程度。我们可以使用的策略:代码生成+预编译(EJB)、AOP语言(AspectJ)、动态字节码生成(CGlib)、J2SE动态代理(Spring AOP)。
我们不仅从领域问题中划分出对象层级,还划分出一些横切性的关注点,彻底避免了代码重复。

AOP中的几个概念:
1、关注点(concern)一个特定的问题、概念、或是应用程序的兴趣区间,比如:安全性、事务管理等
2、横切关注点(crosscutting concern)如果一个关注点的实现代码散落在很多个类或方法之中,这样的关注点就是横切关注点。
3、连接点(join point)程序执行过程中的一点,比如:方法调用、字段访问、异常抛出。
4、增强(advice)在特定连接点执行的动作。
前增强(before,pre)在连接点调用之前,首先调用增强。
后增强(after,post)在连接点调用之后,再调用增强。在AspectJ中,后增强又分为三种:
  返回后增强:在调用成功完成(没有异常抛出)之后
  抛出后增强/抛出增强:在抛出某种特定类型(或其子类型)的异常之后
  后增强:在连接点的任何调用之后,不管调用是否抛出异常
环绕增强(around)这类增强可以控制执行流程。除了完成本身的工作之外,它还需要负责主动调用连接点,促使真实的操作发生。
注:环绕增强可以用于实现所有其它类型的增强,只要主动促使实际操作发生即可。从可用性的角度出发,建议选择功能最弱且目的最明确的增强,避免出现错误。比如:前增强和后增强都不会改变连接点的返回值,而环绕增强则有可能。
5、切入点(pointcut)一组连接点的总称,用于指定某个增强应该在何时被调用。
静态标准切入点:根据部署阶段的信息选择增强
动态标准切入点:根据运行时的信息选择增强
6、引介(introduction)为一个现有的Java类或接口添加方法或字段。
7、混入继承(mixin inheritance)一个“混入类”封装了一组功能,这组功能可以被“混入”到现有的类当中,并且无需求助于传统的继承手段。在AOP这里,混入是通过引介来实现的。在Java语言中,通过混入来模拟“多继承”。
8、织入(weaving)将方面整合到完整的执行流程(或完整的类,此时被织入的便是引介)中。
当执行流程进展到连接点时,增强会首先接管流程控制权。当增强允许执行流程继续前进,执行真正的方法调用或字段访问时,我们就说这次操作发生了。
以下几个概念不是出自于AspectJ:
9、拦截器(interceptor)很多AOP框架(如Spring,但不包含AspectJ)用它实现字段和方法的拦截。随之而来的就是在连接点处挂接一条拦截器链(interceptor chain),链条上的每个拦截器通常会调用下一个拦截器。拦截是一种AOP的实现策略。
10、AOP代理(AOP proxy)被增强的对象引用。(AspectJ的增强是直接针对Java类的字节码进行的,因此没有这样的概念。但是对于基于拦截的AOP框架,AOP代理是整个框架的基石,AOP代理可能是使用J2SE的动态代理,或借助字节码操作工具生成。)
11、目标对象(target object)位于拦截器链末端的对象实例。当然,这个概念也是只存在于基于拦截机制的AOP框架之中。有时拦截器链上也会没有目标对象:这种情况很罕见,整个预期行为就是通过拦截器的组合来完成的,比如:一个对象可能有多个“混入”构成。(像AspectJ这种在编译期完成织入的AOP实现没有明确的“目标对象”的概念。目标对象的字节码就是直接包含了所有的增强。)
几个没有完全解决AOP问题的设计模式:Decorator(装饰器)、Observer(观察者)、Chain of Responsilility(责任链)
注:使用OO的设计模式不可能做到既保持被增强方法的强类型性,又不必自己动手为每个方法调用编写转发代码。
EJB如何解决类似AOP问题
EJB容器必须拦截方法的调用,以便插入企业级服务。具体拦截机制有两种(织入策略):动态代理和代码生成。不同的容器实现的策略不相同。
AOP各种实现策略
1、J2SE动态代理
优点:标准Java语言特性
缺点:只能针对接口进行代理、反射调用的额外开销
Spring默认使用动态代理
2、动态字节码生成(dynamic byte code generation)
基于Java反射和类装载机制,针对指定的类动态生成一个子类,并覆盖其中的方法,从而实现方法的拦截。
优点:可以针对类进行代理
缺点:对于final类或者是final方法无法提供代理
Spring对于类的代理就是使用CGLIB,Hibernate也在使用。
3、Java代码生成
生成新的Java源码,在其中执行横切性的代码。传统EJB的实现方式,不再流行。
4、使用定制的类加载器
通过定义一个自制的类加载器,可以在一个类被加载时自动对其进行增强。这样即便用户使用new构造实例,增强也是生效的。JBoss和AspectWerkz都采用这种做法,具体增强信息则是在运行时从XML配置文件中读取。
这种做法风险较大,它偏离了Java的标准,而且在某些应用服务器中,由于需要控制整个类加载的层级体系,因此这种做法会出现问题。
5、语言扩展
让编程语言同时支持AOP和OOP。AspectJ就针对Java进行了扩展,提供自己的编译器。

Spring AOP
增强器(advisor)是Spring AOP中提出的更高级的概念,它包括一个是增强、一个用于说明“在何处进行增强”的切入点。增强器(advisor)完美的模块化了一个方面(aspect)。
引介是通过IntroductionAdvisor对象实现的。但是不同于其它增强器,引介增强器不包含切入点,因为引介是在类的层面上进行的,方法级的匹配没有任何意义。
Spring不支持字段的拦截,这样Spring有意而为之,主要是考虑性能问题。Spring的真正目标是鼓励用户从Spring容器中获得业务对象,从而获得良好的应用结构。
Spring AOP必须要依赖Spring IOC容器,没有提供强类型检查。
自动代理就是通过Bean后置处理器机制实现的。只要配置好,机制就允许Spring容器透明地对任何业务对象进行代理。对于每个业务对象,如果有增强器,就会被代理,而没有任何增强器的切入点匹配业务对象的任何方法,该对象就不会被代理。这样调用者或依赖对象绝不会拿到未经增强的对象。

AOP风险
1、字段拦截
字段拦截是一种破化封装的行为,使AOP和OOP站在了对立面上。只有那些目的性非常明确的情况(Hibernate专注于解决持久化问题)才适合使用字段拦截。而如果应用开发者利用它绕开对字段的封装,风险可想而知。
2、过多的方面
使用AOP之后,我们需要以一种新的方式去看待应用程序的结构。我们推荐“渐进式”AOP用法,每一步都要弄清眼下发生的是什么。只要AOP使用得当,它是可以简化程序结构的,使其更容易理解,因为它提升了程序的模块化程度,将四散的代码规整到方面中,可维护性也提高了。
3、方面执行顺序
很多AOP框架提供了“控制增强顺序”的能力,借此可以允许一个增强依赖于前一个增强的执行结果,这是一项很有用的功能,但切忌不能乱用。对于方面的应用需要文档给以充分的说明,而且使用者要充分的理解。我们可以将常用的、通用的的方面打包在一起,避免错误。如果你要编写应用程序专用的方面,请尽量保持它们彼此的独立,并且不依赖于执行的先后顺序。
4、测试
AOP使得测试更加容易。所谓单元测试,就是应该对一个模块进行隔离测试。我们完全可以将应用代码和方面代码分别测试。不过,为了保证应用代码和方面代码能很好的协作,必要的集成测试仍然至关重要。
5、调试
AOP对调试的影响很大程度取决于AOP框架的质量。另外一个重要因素则是使用增强的个数。我们建议在项目起步之初使用垂直切片来检验一下。
6、性能
由于使用到了反射,拦截机制的开销可能会增大,不过基于Java在反射和垃圾收集上的性能大幅提升,拦截也不会太慢。而AspectJ会比拦截机制的AOP框架性能稍微好一些。
性能问题还取决于AOP如何应用。如果AOP在非常细粒度的对象上使用(持久化对象),性能可能比较严重,但是只是在业务对象的层面上使用(Service或Facade),性能开销是完全可以接受的。
注:在服务器环境下,一次涉及反射的方法调用需要消耗时间通常在几毫秒至十几毫秒,一次数据库只读访问的时间通常是数十毫秒,而通过浏览器访问web页面的网络延迟则是百毫秒级的。很明显,通常情况下反射的消耗完全可以忽略不计。

AOP设计的建议:渐进式、实用至上
开始阶段,我们可以使用AOP应用在业务对象(Service或Facade)。AOP最有价值的用途就是在业务对象的粒度上提供通用的企业级服务。
如果你可以很好的掌握AOP的概念,可以自己实现一些切面,避免一些在OOP中的无法避免的代码重复。或者是在不修改现有代码的前提下增加新功能(如:调用栈跟踪和性能监控、审计、消息通知)。
混入可以是Java实现相当于多继承的机制,而且对象的粒度将会更细,复用性也会更高。但是程序运行时的结构将会变得难以理解。对于这样相当强大的技术,可以尝试,但切莫草率使用。
使用源码级元数据的AOP(注解)
一般来说,最好是避免在源码级元数据中携带太过细节的描述信息。元数据应该用于指示类和方法“需要哪些服务”,而不应该用于描述“如何获得这些服务”。
源码级元数据驱动(注解)的AOP并不是“经典”的AOP,被增强类不能做到对解决横切问题的方面一无所知。但是我们还是认为做法非常有用,对于一些常见的问题,它可以提供简单、符合直觉的解决方法。

使用AOP需要注意的地方
1、最好保持前后一致的命名规范
2、避免依赖AOP基础设施(让AOP框架和应用代码的“无侵入性”)
最重要的是看是否可以在AOP框架之外使用这个业务对象。
3、增强不要引入check异常

*************************************************

第9章 事务管理 
JTA
JTA就是让开发人员不需要关注究竟有多少资源参与了事务,即无需关注分布式事务。让分布式事务处理如同单个资源的事务一样。
J2EE容器作为事务协调器
具有分布式事务能力的服务器应该完整的支持遵循X/Open XA规范的两阶段提交协议(2-Phase-Commit)。XA定义了事务管理器和资源管理器的底层协议,定义了参与事务的资源的底层协议,以及两阶段提交的其它特殊情况的底层协议。不同于JDBC Connection这种本地事务,XA事务协调器总是管理整个事务过程,而不是由资源本身来管理事务。
两阶段提交的第一阶段,事务协调器向所有相关的资源发布“准备”的指令。第二阶段等所有的资源准备好以后触发实际的提交动作。尽管不可能避免所有的出错可能,但是两阶段事务提交能够在某个参与事务的资源发生错误的时候,确保整个事务的一致性。
注意。两阶段事务提交并不会给单个资源的事务管理器带来更多的好处,它只对跨多个资源的事务有用处。一个高效的两阶段事务提交管理器当它在处理单个资源的事务的时候,会切换到简单的一步提交模式上来。当前的J2EE应用服务器实现的事务管理器能力各不相同,有的基础,有的强大。事务管理器仍然是区分不同等级J2EE应用服务器的非常重要的指标。但是仅仅处理单个资源事务,这些区别并没有实际意义。
除了传统的JDBC和JMS的特别支持,J2EE引入了参与事务的资源的统一模型,即Java连接器架构(JCA)。JCA提供了无缝插入容器事务和资源池基础设施的手段,通过标准接口,允许任何资源参与全局分布式容器事务。JCA是可移植的插入J2EE容器事务的资源模型。另外,可以借助于特定服务器特性来获取TransactionManager(事务管理器,而不是UserTransaction(事务))。而像Hibernate这类专注于ORM框架是通过使用一个参与事务的JDBC DataSource来完成所有数据的存取,而此时Hibernate发送的所有JDBC SQL都可以自动参与到容器事务中。
很多J2EE应用程序并不会碰到集成多种事务资源的情况。像Oracle这样的高端关系数据库能够在给J2EE应用程序提供单一的逻辑资源的同时具备巨大的性能可伸缩能力。如果需要JMS处理过程加入到事务中来,J2EE事务协调基础设施是个不错的选择,而且所有的J2EE应用服务器都具有XA能力的JMS提供者,另外第三方的JMS提供者也可以集成到J2EE事务协调器上。在J2EE应用中,集成JMS到事务管理这是最常见的需要分布式事务的场合。

远程事务传播
JTS能够将事务上下文通过远程调用传播到其它的服务器上。一个客户端在一个事务上下文中调用一个session bean的多个方法,或者甚至可以做到在一个事务上下文中使用远程服务器的JTA将事务传播到多个不同的session bean上去。
带有远程事务传播功能的EJB其最主要的价值在于具备能够将一个事务上下文从一个JVM传播到另一个JVM的能力。
由于Hibernate SessionFactory是建立在给定的一个DataSource上的,因此HibernateTransactionManager能够让JDBC代码和Hibernate代码共享一个事务,不需要JTA的支持。

事务管理方式
1、编程式事务管理
2、声明式事务管理

******************************************

第10章 持久化 
不合适的数据访问策略甚至会导致整个项目失败-复杂、效率低下、缺乏可伸缩性。
持久化模式分类
1、事务脚本(SQL)
适合于集合查询和批量更新操作,可以高效的处理
2、领域模型
数据实体通常映射到持久化的Java对象,业务逻辑可以表现为这些对象的交互,而不是直接操作数据库里的表和字段。克服面向对象应用与关系型数据库之间所谓的阻抗不匹配。
没有批量的查询和更新操作
实现方式分为两种:
(1)活动记录:将持久化操作定义在领域类内部
(2)数据映射器(ORM):将业务逻辑与持久化逻辑分离到了不同的类中。
查询对象(Hibernate Session、Hibernate Query):提供查找方法,既可以“按实例查询”,又可以查询语言(HQL)。
工作单元(Hibernate Session):自动侦测对象的修改,并在事务提交时自动将修改持久化到数据库中。使用这种“完全”透明持久化在业务逻辑中根本不需要显示调用“更新领域对象”的操作,而正是由“工作单元”来维护“状态被当前事务中的业务操作修改过”的对象列表。
身份对应表(Hibernate Session):将所有从数据库加载出来的对象都保存在一张对应表中,以确保对象不会被重复加载。
JDBC
JDBC目标不是要给关系型数据提供一个面向对象的视图,也不会牺牲目标数据库专有的强大功能。如果我们要使用目标数据库的某种专有特性或是存储过程,JDBC是不错的选择,而这方面ORM则非常困难。
iBatis
iBatis是构建在JDBC上薄薄的一层,是对JDBC这种底层API的低层次抽象,是一种“语句映射”解决方案。
entity bean
entity bean只是一种实现了活动记录模式的组件框架。
DAO(Data Access Object)
DAO使用接口将持久化操作与业务逻辑隔离开来,接口可以用别的任何持久化技术来实现。传统的DAO都是建立在JDBC之上的。而随着ORM方案的崛起,DAO的价值开始有些不那么清晰了,实践证明,直接使用如Hibernate Sessioon就已经足够抽象了。
选择持久化策略
对于任何项目来说,持久化策略都应该是从一开始就确定下来的。完全抽象化的持久化通常是没有价值的,很少有项目真的会移植到一种全然不同的持久化策略上。数据访问层的修改总是影响深远,即便是使用同一种持久化策略,一般详尽的测试是必不可少的。
并非所有的应用程序都适合ORM风格。大量的批量查询和更新,并且无法在对象映射层放置大量的缓存,这时采用JDBC方式实现的基于数据集的关系型访问倒是不错的选择,而且ORM学习曲线和初始成本较高。
适合使用ORM的场景:
1、单条数据操作
2、批量查询,但不批量修改
3、大量对象需要积极的缓存(通常出现在读操作远多于写操作的情况下,如web应用)
4、领域对象和表、字段之间对应关系自然
5、不需要对SQL进行特别优化。
持久数据的缓存
对于读操作远多于写操作的对象,尤其是仅供参考的数据(如用户个人信息、产品分类等),缓存是非常合适的。当然,缓存都可能造车数据失效,可是它的价值告诉我们这是值得的。某些应用程序,任何数据失效的风险都是不可接受的。是否容忍数据失效,这主要取决于数据的用途。
对缓存的调节并不是易如反掌的事,一般需要针对应用程序的情况和用户的期望来调节。我们不提倡自制缓存。开源的缓存产品和商业的产品足以满足我们的需求。某些持久化工具中,当对象修改时会通知缓存,甚至是提供缓存提供器API,方便第三方产品与其集成。而且允许以编程的方式将特定的对象从缓存中清除出去,比如当Hibernate和JDBC混用时,可以在更新后使相关对象清出缓存,从而避免数据失效。当然,如果有不同的进程进行修改,而且应用程序无法感知的话,必将导致数据失效。往往J2EE应用很少独占使用数据库。
在服务器集群环境下,我们需要借助分布式缓存来保持一致性:应用程序在一台服务器上修改持久对象,其它缓存实例都能收到通知。分布式缓存是一个非常麻烦的问题,对于不允许失效的数据,最好的办法是禁用缓存,将“维持数据一致性”的重担交给数据库。选择缓存时要查看文档,并首先进行测试。
领域对象的行为
最初将领域对象构造为只有setter、getter的哑数据对象是J2EE的为了弥补entity bean的不足而牺牲OO原则的权宜之计——将权宜之计称为“模式”广为传颂是J2EE的一贯作风。当转为透明持久化方案之后,我们应该倡导用OO设计标准决定业务逻辑放在哪里,构造语意丰富的领域对象。业务facade(Service)中的流程方法仍旧负责事务边界的划分,负责取出持久化对象,负责发起多个对象之间的交互,而不在负责处理领域逻辑,而是由领域模型封装领域逻辑。至于哪些逻辑放在领域类、哪些放在流程方法中,无法一概而论,需要注意判断。可以在多个用例之间复用的操作通常应该作为领域类的一部分,而只在一个用例中用到、几乎无法复用的操作应该归属与控制方法。当然,跨多个领域对象的操作更应该放在流程方法中。

资源管理
资源管理主要涉及两类对象:
1、连接工厂(SessionFactory):线程安全
2、连接(Session):线程不安全
Hibernate
1、如果映射JavaBean属性,只跟Hibernate持久化相关的那些getter和setter可以是protected甚至是private的,以便保证由应用程序驱动字段的可见程度。
注:如果将setter声明为private,会导致Hibernate无法使用CGLIB优化的反射机制,只能通过标准的Java反射机制为持久对象,而且是整个对象的所有字段。
数据访问对象DAO
1、DAO模式是J2EE重要模式之一,也是GOF Strategy模式的特殊形式。DAO力图将“与持久化的代码”从业务逻辑中分离出来。DAO接口就是将特定的数据访问实现细节隐藏起来。
2、数据访问对象需要参与到事务之中,但通常不应该由它驱动事务,因为其中操作一般都是细粒度的。应该由调用DAO的业务对象来负责划分事务。
3、不是在任何时候都需要建立一个DAO的。如果业务逻辑就只是数据访问操作,没有别的操作,那么把数据访问代码放在业务操作之中就是完全合适的。另外,如果业务代码与数据访问操作紧密相联,也可以做出“取消DAO”的选择(如出于效率或者复用业务逻辑的考虑而将业务操作委派给存储过程去实现)。而且毕竟业务对象也是接口实现,同样可插拔的,只是可插拔性由数据访问对象换到了业务访问对象层面上。
没有必要太费心在应用程序中区分业务层和持久层。如果你无法清晰的画出业务层和持久层的分界线,那么就首先建立一个业务对象,将数据访问操作放在其中。如果新的需求需要持久化操作的可插拔性,或者代码开始变得混乱,你可以重构出一个DAO。
4、不得不承认很多先进的ORM框架(如Hibernate)的工作单元(如Hibernate Session)已经足够抽象了,完全可以直接使用它们,不必在构建一个DAO层。不过,仍然有理由用DAO在ORM上做薄薄的封装:
(1)在测试阶段很方便的对数据存取操作进行模拟,对于TDD尤为需要。
(2)DAO可以为领域对象提供一个清晰的、强类型的持久化API。ORM提供的通用的API允许对任何对象执行1任何持久化操作,这样可以清晰的看清对于特定领域对象可以执行的操作。
(3)DAO可以提供具有领域含义的查询方法,从而不必在业务对象中维护对象的查询字符串。
5、透明持久化机制之上的DAO和基于JDBC之上的DAO有很大的区别:操作的粒度和是否具有传统的更新方法。
完全可移植的DAO接口至少要实现一个透明持久化和一个JDBC的版本。这可能在浪费时间,因为应用程序通常都会一直使用某一种持久化策略,要么透明持久化,要么JDBC(即便同时使用,也一定能细心划分清两者之间的界限)。如果需要彻底改变数据访问的策略,在调用DAO的业务对象那里进行精化通常也是可以接受的。而对于只读操作,设计通用的DAO接口要容易的多。
DAO的大致分类:
(1)经典的、基于JDBC的DAO:细粒度的更新操作(也可以使用iBatis实现)。
(2)针对特定持久化工具的DAO:充分考虑了该工具在生命周期方面的特性。
(3)通用的透明持久化DAO:可以使用多种ORM实现,使用多个具有特殊语意的访问方法。
(4)可移植的只读DAO:可以使用JDBC或是ORM实现,返回与数据库脱离了关系的对象。
(5)可移植的读写DAO:可以使用JDBC或是ORM实现,要求使用者明确调用更新操作,即便使用ORM(通常不推荐)
6、需要注意的问题:
(1)数据访问操作的粒度:业务对象需要什么粒度的数据访问,就在DAO接口中提供什么粒度的方法。只有在真正需要时才设计粒度更细的方法。一般来讲,DAO接口都需要包含针对特定领域对象的CRUD操作。
(2)透明持久化和对象状态
(3)事务范围和延迟加载
数据访问都是在事务中进行,事务的边界应该由业务对象来划定。一次事务绝对不能将用户思考的时间段包含在内,以免造成不必要的资源死锁。“一次业务操作”通常就是合适的事务范围。在web应用和远程服务这里,事务应该在一次请求之内完成,不应该跨越多次请求。因此,事务通常不影响DAO设计。
由于延迟加载,DAO的设计要注意。由于延迟加载是透明的,因此DAO的加载方法应该说明对象的哪部分被加载到位、或哪部分可能延迟加载。如果对象被传递到事务范围之外(如web层或者远程客户端),就更应该澄清实际加载程度了,因为通常情况下这时延迟加载是不起作用的。但是在web层使用OSIV模式。当然,它不不适合那些视图渲染时间过长。
有一种特殊的事务,它在逻辑上应该生存相当长的时间,甚至是横跨多个请求,这时需要使用乐观锁。对象从一个事务加载上来,而却从另一个事务中储存。
7、基础设施问题,如组装、参与事务、异常处理,完全可以像Spring这样的应用级别的框架来实现。
Spring封装了DataAccessException以及一整套数据访问异常体系的主要价值在于:它使业务对象和数据访问接口不必与特定的数据访问策略相耦合。
Spring的所有模板类都是线程安全的,允许多线程复用。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics