`
webcode
  • 浏览: 5940227 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

解析三层架构(2)----分层究竟分出了那些东西

 
阅读更多

在上篇文章写到我们为什么要分层.有很多读者提出来很多宝贵的意见.让我受益匪浅,深深的感觉到自己的水平"还有很大的提升空间".首先感谢这些朋友们,我会进一步总结完善自己的想法.

截取了部分朋友的留言,感谢他们:

clip_image001

clip_image002

clip_image003

这次我用对比的方式描述一下,分层到底分出了什么.俗话说:有分必有合,那么它是把什么合到了一起.

首先写出两个没有分层的demo:

<1>查询信息demo

   1: Public Class Form2
<!--CRLF-->
   2:     Private sqlCon As String = "Data Source=LSH;Initial Catalog=ComputerLab;User ID=sa;Password=123456"
<!--CRLF-->
   3:     '查询数据库信息信息
<!--CRLF-->
   4:     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
<!--CRLF-->
   5: 
<!--CRLF-->
   6:         Dim conStr As New SqlConnection '数据库连接对象
<!--CRLF-->
   7:         Dim sqlCom As SqlCommand        '数据库执行对象
<!--CRLF-->
   8:         Dim Res As DialogResult         '消息提示框返回类型
<!--CRLF-->
   9:         Dim dr As SqlDataReader         'dataReader对象
<!--CRLF-->
  10:         Dim dt As New DataTable         'Datatable对象
<!--CRLF-->
  11: 
<!--CRLF-->
  12:         Dim sql As String = "select * from TableName where Name=@name"                   'sql插入语句
<!--CRLF-->
  13:         conStr.ConnectionString = sqlCon                                '给数据库连接对象赋值
<!--CRLF-->
  14:         sqlCom = New SqlCommand(sql, conStr)                            '给数据库执行对象赋值
<!--CRLF-->
  15:         sqlCom.Parameters.Add("@name", SqlDbType.VarChar, TextBox1.Text)    '给sql语句参数赋值
<!--CRLF-->
  16: 
<!--CRLF-->
  17:         Res = MessageBox.Show("是否添加", "提示", MessageBoxButtons.OKCancel)
<!--CRLF-->
  18:         '判断是否查询
<!--CRLF-->
  19:         If Res = DialogResult.Yes Then
<!--CRLF-->
  20:             Try
<!--CRLF-->
  21:                 conStr.Open()
<!--CRLF-->
  22:                 dr = sqlCom.ExecuteReader    '执行查询语句
<!--CRLF-->
  23:                 dt.Load(dr)
<!--CRLF-->
  24:             Catch ex As Exception
<!--CRLF-->
  25:                 Throw ex
<!--CRLF-->
  26:             Finally
<!--CRLF-->
  27:                 If Not IsNothing(conStr) Then   '如果数据库打开,则关闭数据库
<!--CRLF-->
  28:                     conStr.Close()
<!--CRLF-->
  29:                 End If
<!--CRLF-->
  30:             End Try
<!--CRLF-->
  31:         End If
<!--CRLF-->
  32: 
<!--CRLF-->
  33:         MsgBox(dt.Rows.Count)   '显示查询到的行数
<!--CRLF-->
  34:     End Sub
<!--CRLF-->

<2>添加信息demo
   1: Public Class Form1
<!--CRLF-->
   2:     Private sqlCon As String = "Data Source=LSH;Initial Catalog=ComputerLab;User ID=sa;Password=123456"
<!--CRLF-->
   3:     '向数据库添加信息
<!--CRLF-->
   4:     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
<!--CRLF-->
   5:         Dim bln As Boolean = False      '存储返回值
<!--CRLF-->
   6:         Dim conStr As New SqlConnection '数据库连接对象
<!--CRLF-->
   7:         Dim sqlCom As SqlCommand        '数据库执行对象
<!--CRLF-->
   8:         Dim Res As DialogResult         '消息提示框返回类型
<!--CRLF-->
   9: 
<!--CRLF-->
  10:         Dim sql As String = "insert into TableName(Name) value(@Name)"  'sql插入语句
<!--CRLF-->
  11:         conStr.ConnectionString = sqlCon                                '给数据库连接对象赋值
<!--CRLF-->
  12:         sqlCom = New SqlCommand(sql, conStr)                            '给数据库执行对象赋值
<!--CRLF-->
  13:         sqlCom.Parameters.Add("@Name", SqlDbType.VarChar, TextBox1.Text) '给sql语句参数赋值
<!--CRLF-->
  14: 
<!--CRLF-->
  15:         Res = MessageBox.Show("是否添加", "提示", MessageBoxButtons.OKCancel)
<!--CRLF-->
  16:         '判断是否同意插入
<!--CRLF-->
  17:         If Res = DialogResult.Yes Then
<!--CRLF-->
  18:             Try
<!--CRLF-->
  19:                 conStr.Open()
<!--CRLF-->
  20:                 bln = sqlCom.ExecuteNonQuery    '执行插入语句
<!--CRLF-->
  21:             Catch ex As Exception
<!--CRLF-->
  22:                 bln = False
<!--CRLF-->
  23:             Finally
<!--CRLF-->
  24:                 If Not IsNothing(conStr) Then   '如果数据库打开,则关闭数据库
<!--CRLF-->
  25:                     conStr.Close()
<!--CRLF-->
  26:                 End If
<!--CRLF-->
  27:             End Try
<!--CRLF-->
  28:         End If
<!--CRLF-->
  29: 
<!--CRLF-->
  30:         MsgBox(bln) '提示返回信息
<!--CRLF-->
  31:     End Sub
<!--CRLF-->
  32: End Class
<!--CRLF-->
<!--CRLF-->
<!--CRLF-->

上面的两个没有分层的代码,当然在功能的实现上是完全没有问题.但是这样设计带来了很多的危机:

(1):写这个模块的人必须是一个能力非常强的人,因为他需要操作数据库,理解业务逻辑

(2):如果有另为一个模块需要这两个操作数据库的方法,只能重写或者调用该窗体下的方法.

(3):如果业务逻辑稍有变动,就需要拆开这个模块,重新编写,对于重新设计的人来说这个模块的所有设计都是可见的,很是不安全.

(4):如果要更换或者改动数据库,结果就是每一个模块都要重新编写

(5):如果有另为一个和这个相似的工程,想要复用以前的东西,难度很大

对于一个公司来说,当然不希望发生这样的事情.所以,合理的分层,降低系统的耦合性是必要的.

第一步:把数据库操作提取出来.

对每一个表,把完全对他的操作单独写成类也就是我们的DAO,数据访问类

每一个表对应一个DAO,每个DAO里面包含该数据库的所有增,删,查,改操作.这样做的好处就是对数据库的操作完全独立,达到功能单一.降低了和其他模块的耦合,写代码的时候,程序员不需要了解业务逻辑.

而且,在维护上和扩展上,如果对数据库改动,就会有针对性的去修改DAO.

当然,在设计的时候,我们尽量去封装不变的操作,这样不但能减少代码量,而且能达到很好的复用效果.在数据库操作中,对于数据库的打开,执行,关闭基本上都是一样的.所以我们单独提取出来sqlHelper(数据库助手类)

   1: Imports System.Data.SqlClient
<!--CRLF-->
   2: Imports System.Configuration
<!--CRLF-->
   3: 
<!--CRLF-->
   4: ''' <summary>
<!--CRLF-->
   5: ''' 运行数据库
<!--CRLF-->
   6: ''' </summary>
<!--CRLF-->
   7: Public Class SqlHelp
<!--CRLF-->
   8:     Private cmd As SqlCommand   'sqlcommand对象
<!--CRLF-->
   9:     Private con As SqlConnection    'sqlconnection对象
<!--CRLF-->
  10:     Private dr As SqlDataReader     'sqlDataReader对象
<!--CRLF-->
  11:     ''' <summary>
<!--CRLF-->
  12:     ''' 获取连接字符串
<!--CRLF-->
  13:     ''' </summary>
<!--CRLF-->
  14:     ''' <returns>连接字符串</returns>
<!--CRLF-->
  15:     ''' <remarks></remarks>
<!--CRLF-->
  16:     Public Function GetCon() As SqlConnection
<!--CRLF-->
  17:         Dim conStr As String
<!--CRLF-->
  18:         conStr = System.Configuration.ConfigurationManager.AppSettings("ConnStr")
<!--CRLF-->
  19: 
<!--CRLF-->
  20:         con = New SqlConnection
<!--CRLF-->
  21:         con.ConnectionString = conStr
<!--CRLF-->
  22: 
<!--CRLF-->
  23:         '打开数据库
<!--CRLF-->
  24:         If (con.State = ConnectionState.Closed) Then
<!--CRLF-->
  25:             con.Open()
<!--CRLF-->
  26:         End If
<!--CRLF-->
  27: 
<!--CRLF-->
  28:         Return con
<!--CRLF-->
  29: 
<!--CRLF-->
  30:     End Function
<!--CRLF-->
  31: 
<!--CRLF-->
  32:     ''' <summary>
<!--CRLF-->
  33:     ''' 增 删 改方法
<!--CRLF-->
  34:     ''' </summary>
<!--CRLF-->
  35:     ''' <param name="sqlStr">数据库语句 活存储过程名</param>
<!--CRLF-->
  36:     ''' <param name="Param">参数数组</param>
<!--CRLF-->
  37:     ''' <param name="commandType">数据库字符串 或者存储过程名 类型</param>
<!--CRLF-->
  38:     Public Function ExecuteNonQuery(ByVal sqlStr As String, ByVal Param() As SqlParameter, ByVal commandType As CommandType) As Boolean
<!--CRLF-->
  39: 
<!--CRLF-->
  40:         cmd = New SqlCommand(sqlStr, Me.GetCon)
<!--CRLF-->
  41:         cmd.CommandType = commandType
<!--CRLF-->
  42: 
<!--CRLF-->
  43:         '添加参数
<!--CRLF-->
  44:         If Param IsNot Nothing Then
<!--CRLF-->
  45:             cmd.Parameters.AddRange(Param)
<!--CRLF-->
  46:         End If
<!--CRLF-->
  47: 
<!--CRLF-->
  48:         '执行语句
<!--CRLF-->
  49:         Try
<!--CRLF-->
  50:             Return CBool(cmd.ExecuteNonQuery)
<!--CRLF-->
  51:         Catch ex As Exception
<!--CRLF-->
  52:             Return False
<!--CRLF-->
  53:         Finally
<!--CRLF-->
  54:             If Not IsNothing(con) Then
<!--CRLF-->
  55:                 con.Close()
<!--CRLF-->
  56:             End If
<!--CRLF-->
  57:         End Try
<!--CRLF-->
  58: 
<!--CRLF-->
  59:     End Function
<!--CRLF-->
  60: 
<!--CRLF-->
  61:     ''' <summary>
<!--CRLF-->
  62:     ''' 查询
<!--CRLF-->
  63:     ''' </summary>
<!--CRLF-->
  64:     ''' <param name="sqlStr">sql语句 或者存储过程名</param>
<!--CRLF-->
  65:     ''' <param name="Param">参数数组</param>
<!--CRLF-->
  66:     ''' <param name="commandType">执行类型</param>
<!--CRLF-->
  67:     Public Function ExecuteQuery(ByVal sqlStr As String, ByVal Param() As SqlParameter, ByVal commandType As CommandType) As DataTable
<!--CRLF-->
  68:         Dim dt As New DataTable
<!--CRLF-->
  69:         cmd = New SqlCommand(sqlStr, Me.GetCon)
<!--CRLF-->
  70:         cmd.CommandType = commandType
<!--CRLF-->
  71: 
<!--CRLF-->
  72:         '添加参数
<!--CRLF-->
  73:         If Param IsNot Nothing Then
<!--CRLF-->
  74:             cmd.Parameters.AddRange(Param)
<!--CRLF-->
  75:         End If
<!--CRLF-->
  76: 
<!--CRLF-->
  77:         '执行语句
<!--CRLF-->
  78:         Try
<!--CRLF-->
  79:             dr = cmd.ExecuteReader
<!--CRLF-->
  80:             dt.Load(dr)
<!--CRLF-->
  81:         Catch ex As Exception
<!--CRLF-->
  82:             Return Nothing
<!--CRLF-->
  83:         Finally
<!--CRLF-->
  84:             If Not IsNothing(con) Then
<!--CRLF-->
  85:                 con.Close()
<!--CRLF-->
  86:             End If
<!--CRLF-->
  87:         End Try
<!--CRLF-->
  88:         Return dt
<!--CRLF-->
  89:     End Function
<!--CRLF-->
提取后数据库访问层代码如下: 
   1: Imports SqlHelper
<!--CRLF-->
   2: Imports System.Data.SqlClient
<!--CRLF-->
   3: Public Class DAL
<!--CRLF-->
   4:     Private sqlHelper As New SqlHelp
<!--CRLF-->
   5:     ''' <summary>
<!--CRLF-->
   6:     ''' 查询记录
<!--CRLF-->
   7:     ''' </summary>
<!--CRLF-->
   8:     ''' <param name="strSelect">关键字</param>
<!--CRLF-->
   9:     ''' <returns>记录集</returns>
<!--CRLF-->
  10:     ''' <remarks></remarks>
<!--CRLF-->
  11:     Public Function SelectInfo(ByVal strSelect As String) As DataTable
<!--CRLF-->
  12:         'sql查询语句
<!--CRLF-->
  13:         Dim sql As String = "select * from TableName where Name=@name"
<!--CRLF-->
  14:         '向sql参数中添加实参
<!--CRLF-->
  15:         Dim Para() As SqlParameter = {New SqlParameter("@name", strSelect)}
<!--CRLF-->
  16:         '调用sqlhelper执行数据库操作
<!--CRLF-->
  17:         Return sqlHelper.ExecuteQuery(sql, Para, Data.CommandType.Text)
<!--CRLF-->
  18:     End Function
<!--CRLF-->
  19:     ''' <summary>
<!--CRLF-->
  20:     ''' 添加记录
<!--CRLF-->
  21:     ''' </summary>
<!--CRLF-->
  22:     ''' <param name="strAdd">记录信息</param>
<!--CRLF-->
  23:     ''' <returns>是否成功</returns>
<!--CRLF-->
  24:     ''' <remarks></remarks>
<!--CRLF-->
  25:     Public Function AddInfo(ByVal strAdd As String) As Boolean
<!--CRLF-->
  26:         'sql插入语句
<!--CRLF-->
  27:         Dim sql As String = "insert into TableName(Name) value(@Name)"
<!--CRLF-->
  28:         '向sql参数中添加实参
<!--CRLF-->
  29:         Dim Para() As SqlParameter = {New SqlParameter("@name", strAdd)}
<!--CRLF-->
  30:         '调用sqlhelper执行数据库操作
<!--CRLF-->
  31:         Return sqlHelper.ExecuteNonQuery(sql, Para, Data.CommandType.Text)
<!--CRLF-->
  32:     End Function
<!--CRLF-->
  33: End Class
<!--CRLF-->
<!--CRLF-->

第二:根据业务逻辑,个人习惯和用例相对应,把相关的逻辑判断,比如添加记录在什么情况下判断,添加前要判断记录是否存在等放到BLL层.

但是对于数据的有效性校验一般还是放在UI层,也可以有其他更好的方法,这些我在以后会总结出来.

代码如下:

   1: Public Class BLL
<!--CRLF-->
   2:     Private dal As New DAL
<!--CRLF-->
   3:     ''' <summary>
<!--CRLF-->
   4:     ''' 业务逻辑层,添加方法
<!--CRLF-->
   5:     ''' </summary>
<!--CRLF-->
   6:     ''' <param name="strAdd">添加信息</param>
<!--CRLF-->
   7:     ''' <remarks></remarks>
<!--CRLF-->
   8:     Public Sub Add(ByVal strAdd As String)
<!--CRLF-->
   9:         '定义用于存储查询结果的记录集
<!--CRLF-->
  10:         Dim dt As New DataTable
<!--CRLF-->
  11:         '执行查询,判断添加的信息是否已经存在
<!--CRLF-->
  12:         dt = dal.SelectInfo(strAdd)
<!--CRLF-->
  13:         If dt.Rows.Count > 0 Then
<!--CRLF-->
  14:             Throw New Exception("记录已存在")
<!--CRLF-->
  15:         Else
<!--CRLF-->
  16:             '添加记录,返回是否成功
<!--CRLF-->
  17:             If dal.AddInfo(strAdd) = True Then
<!--CRLF-->
  18:                 Throw New Exception("添加成功")
<!--CRLF-->
  19:             Else
<!--CRLF-->
  20:                 Throw New Exception("添加失败")
<!--CRLF-->
  21:             End If
<!--CRLF-->
  22:         End If
<!--CRLF-->
  23: 
<!--CRLF-->
  24:     End Sub
<!--CRLF-->
  25: 
<!--CRLF-->
  26: End Class
<!--CRLF-->
第三:剩下的UI层里,除了判断数据有效性以为,只要调用bll的添加方法就可以了.这样制作UI的人就会减轻了很大的压力,让他们专心做美工或者其他.

代码如下:

   1: Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
<!--CRLF-->
   2:         Dim bll As New BLL
<!--CRLF-->
   3:         '调用bll层添加方法
<!--CRLF-->
   4:         Try
<!--CRLF-->
   5:             bll.Add(TextBox1.Text)
<!--CRLF-->
   6:         Catch ex As Exception
<!--CRLF-->
   7:             '显示添加结果给用户
<!--CRLF-->
   8:             MessageBox.Show(ex.Message)
<!--CRLF-->
   9:         End Try
<!--CRLF-->
  10:     End Sub
<!--CRLF-->
<!--CRLF-->

当然三层方式还有其他一些技巧,比如为DAL加一层接口,用抽象工厂加反射的方法实现数据库的灵活操作,替换.这里先不叙述.

上述都是个人的一点总结,欢迎大家指导

接下文:解析三层架构(3)--实体类与面向对象

分享到:
评论

相关推荐

    高可用高并发的一共 9 种技术架构解析.docx

    分层架构是逻辑上的,在物理部署上,三层架构可以部署在同一个物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,即三层结构分别部署在不同的服务器上,是网站拥有更多的计算资源以应对越来越...

    H5智能内核-基于MVC架构的全新Zoomla!逐浪CMS2 x3.8发布

    带有三个逻辑层: 27 (1).jpg 业务层(模型逻辑) 显示层(视图逻辑) 输入控制(控制器逻辑) Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。 通常模型对象负责在数据库中存取数据。 View...

    .NET逻辑分层架构总结

    分层,一些技术功底比较薄弱的程序员听到分层就会联想到三层架构(BLL,DAL之类的),其实不是,分层是一个很大的技术框架思想,三层架构只不过是对普通的信息系统来说,将信息的流转通过三层来分解,

    数据仓库知识(数仓建模以及分层).pdf

    第十三章从0到1简单搭建加载数仓DWD层(业务数据解析) 第十四章从0到1简单搭建加载数仓DWS层 第十五章从0到1简单搭建加载数仓DWT层 第十六章从0到1简单搭建加载数仓ADS层 第十七章 数仓建模以及分层总结(ODS、DIM、...

    解析AUTOSAR CAN诊断实现

    该分层架构由微控制器抽象层、ECU抽象层、服务层、执行时环境(RTE)和应用层组成,前三层被统称为基础软件(BSW)。  AUTOSAR各层软件的通信通过三类接口实现,分别是标准接口、AUTOSAR接口和标准AUTOSAR接口。其中,...

    面向新型互联网架构的移动性管理关键技术研究(博士学位论文-胡章丰)

    4. 针对云计算系统中的虚拟机跨三层网络迁移问题,将新型互联网架构思想和无线网络中的移动性管理思想与云计算技术相结合,提出了一种基于身份与位置分离的虚拟机动态迁移方案。通过将虚拟机的身份标识与位置标识相...

    .NET应用架构设计原则、模式与实践 案例源码

    第二部分的主题是架构与设计的方法和最佳实践,既对架构分层的相关知识进行了详细的阐述,又用大量实战案例对业务层、服务层、数据访问层、数据存储层、显示层的原理和设计进行了深入的剖析;第三部分以一个真实的...

    最全面的门户网站架构设计方案.doc

    2.1.2 WEB应用开发架构思路 1) 应用开发实现MVC架构三层架构进行web应用开发 2) 页面尽可能静态化以减少动态数据访问,如果是资讯类的网站可以考虑采用第三 方开源的CMS系统来生成静态的内容页面。 3) 采用Oscache...

    云管理平台(CMP)功能设计和实践解析

    随着公有/私有云、云原生及底层...为了更好地管理资源,Gartner对云环境进行了分层,主要包含三层:CMP在云计算体系中扮演着「承上启下」的角色,它向上承载和支撑了各类行业应用,向下进行资源的管理和调度,包括异

    asp.net知识库

    2分法-通用存储过程分页(top max模式)版本(性能相对之前的not in版本极大提高) 分页存储过程:排序反转分页法 优化后的通用分页存储过程 sql语句 一些Select检索高级用法 SQL server 2005中新增的排序函数及应用 ...

    单片机与DSP中的解析AUTOSAR CAN诊断实现

    该分层架构由微控制器抽象层、ECU抽象层、服务层、执行时环境(RTE)和应用层组成,前三层被统称为基础软件(BSW)。  AUTOSAR各层软件的通信通过三类接口实现,分别是标准接口、AUTOSAR接口和标准AUTOSAR接口。其中,...

    双鱼林安卓Android代码生成器 v2.0.zip

    真正面向对象设计:系统的整体设计,提供通过使用面向对象的方法,设计所需系统中的基础对象(类),并根据专业级的“三层架构模板”生成专业级的界面和源代码,同时设计系统和数据库:采用“数据映射”建立基础对象...

    Spring面试题

    Spring 框架是一个分层架构,由 7 个定义良好的模块组成。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式,如图 1 所示。 组成 Spring 框架的每个模块(或组件)都可以单独存在,或者...

    尚gg大数据项目实战电商数仓系统开发教程.txt

    64_用户行为数仓_DWD层数据解析脚本.avi 65_用户行为数仓_DWD层事件表加载数据脚本.avi 66_用户行为数仓_今日回顾.avi* e9 W+ P$ {5 ?! x9 [- e5 ]# a 67_用户行为数仓_业务术语.avi 68_用户行为数仓_日期的系统函数...

    Windows Sockets网络编程 总计4个包,part2

    17.3.1 WinSock 2架构 17.3.2 协议独立 17.3.3 名称空间独立 17.4 重叠I/O 17.5 分散和聚合 17.6 服务质量 17.7 socket组 17.8 多点和多播 17.9 有条件的接受 17.10 连接和断开数据 17.11 socket共享 17.12 协议相关...

    Windows Sockets网络编程 可能是最清晰版本(Windows Sockets 2规范解释小组负责人亲自执笔。)总共4个包,part1

    17.3.1 WinSock 2架构 17.3.2 协议独立 17.3.3 名称空间独立 17.4 重叠I/O 17.5 分散和聚合 17.6 服务质量 17.7 socket组 17.8 多点和多播 17.9 有条件的接受 17.10 连接和断开数据 17.11 socket共享 17.12 协议相关...

    Windows内核安全与驱动开发光盘源码

    第1章 内核上机指导 2 1.1 下载和使用WDK 2 1.1.1 下载并安装WDK 2 1.1.2 编写第一个C文件 4 1.1.3 编译一个工程 5 1.2 安装与运行 6 1.2.1 下载一个安装工具 6 1.2.2 运行与查看输出信息 7 1.2.3 在虚拟机...

    Windows内核安全驱动开发(随书光盘)

    第1章 内核上机指导 2 1.1 下载和使用WDK 2 1.1.1 下载并安装WDK 2 1.1.2 编写第一个C文件 4 1.1.3 编译一个工程 5 1.2 安装与运行 6 1.2.1 下载一个安装工具 6 1.2.2 运行与查看输出信息 7 1.2.3 在虚拟机...

    多功能.NET代码自动生成器(含存储过程)

    4、 类库为分层框架工厂模式,依次分为:数据库交互层(SqlHelper)、业务实体层(Model)、数据处理层(DAL)、数据接口层(IDAL)、数据工厂层(DALFactory)、业务层(BLL)6层 5、 业务实体层与业务层直接与UI...

Global site tag (gtag.js) - Google Analytics