[转载]C# 对XML基本操作总结

mikel阅读(924)

[转载]C# 对XML基本操作总结 – work hard work smart – 博客园.

C# 对XML基本操作包括读取节点的数据,添加节点。读取节点属性,修改节点属性等。具体如下:

XML文件:文件在MyDocument文件夹下

<?xml version="1.0" encoding="utf-8"?>

<PersonF xmlns="" Name="(test)work hard work smart!">
<person Name="Person1">
<ID>1</ID>
<Name>XiaoA</Name>
<Age>59</Age>
</person>
<person Name="Person2">
<ID>2</ID>
<Name>XiaoB</Name>
<Age>29</Age>
</person>
<person Name="Person3">
<ID>3</ID>
<Name>XiaoC</Name>
<Age>103</Age>
</person>
<person Name="Person4">
<ID>4</ID>
<Name>XiaoD</Name>
<Age>59</Age>
</person>
</PersonF>

Code:说明都在注释里。

void TestXML()
{
XmlDocument doc = new XmlDocument();
try
{
doc.Load(path);
//1、读取单个节点的数据
XmlNode node = doc.SelectSingleNode("PersonF");
//2、读取多个节点的数据
XmlNodeList nodeList1 = doc.SelectNodes("PersonF/person");
//3.1 读取具体节点的具体值 如:属性为Person2的第二个节点Name的InnerText
XmlNodeList nodeList = doc.DocumentElement.GetElementsByTagName("person");
foreach (XmlNode node2 in nodeList1) //当然也能用nodeList的值
{
if (node2.Attributes["Name"].InnerText == "Person2")
{
Console.WriteLine(node2.ChildNodes[1].InnerText);
}
}
//3.2 读取ID为2所在的节点第二个子节点Name的InnerText
XmlNode node3 = doc.SelectSingleNode("PersonF/person[ID=2]");
string strNode3 = node3.ChildNodes[1].InnerText;
//3.3利用下面的方法可以找到ID为2的节点
XmlNodeList nodeList2 =  doc.SelectNodes("//person//ID");
XmlNode node4 = null;
foreach (XmlNode node5 in nodeList2)
{
if (node5.InnerText  == "2")
{
node4 = node5;
break;
}
}
Console.WriteLine(node4.InnerText);
//4、读取节点的属性
string Name = node.Attributes["Name"].InnerText;
//5 修改节点的属性
node.Attributes["Name"].InnerText = "work hard work smart!";
doc.Save(path);
//6 添加自定义的节点
XmlTextReader reader = new XmlTextReader(path);
XmlElement root = doc.DocumentElement;//获取根节点
XmlElement tagOuter = doc.CreateElement("person");
XmlElement tagIN = doc.CreateElement("Name");
tagIN.InnerText = "work hard work smart!";
tagOuter.AppendChild(tagIN);
root.AppendChild(tagOuter);//添加tagOuter到XML文件的最后
reader.Close();
doc.Save(path);
}
catch (System.Exception e)
{
throw new Exception(e.Message);
}

}

[转载]20款非常漂亮的免费网站 PSD 模板

mikel阅读(1069)

[转载]20款非常漂亮的免费网站 PSD 模板 – 梦想天空(山边小溪) – 博客园.

这篇文章收集了20款免费的网站 PSD 模板分享给大家,您可以免费下载使用。这些高质量的网站 PSD 模板可以让您的工作得心应手,帮助您节省大量的时间和精力。感谢那些优秀的设计师分享他们的劳动成果,让更多的人可以使用他们的创意设计,希望这些免费的 网站 PSD 模板能帮助到您。

Ecommerce Web Template

20款非常漂亮的免费网站 PSD 模板

Redux Business Website Template

20款非常漂亮的免费网站 PSD 模板

3D Creative Layout

20款非常漂亮的免费网站 PSD 模板

Church Website Layout

20款非常漂亮的免费网站 PSD 模板

Gallerise

20款非常漂亮的免费网站 PSD 模板

Sleeko

20款非常漂亮的免费网站 PSD 模板

Agregado LifeStream Theme

20款非常漂亮的免费网站 PSD 模板

Modern Design Studio Template

20款非常漂亮的免费网站 PSD 模板

Creative Portfolio Template

20款非常漂亮的免费网站 PSD 模板

Events PSD Template

20款非常漂亮的免费网站 PSD 模板

Yellow! Minimalist Blog Template

20款非常漂亮的免费网站 PSD 模板

Crisp Presentation

20款非常漂亮的免费网站 PSD 模板

oWire Template

20款非常漂亮的免费网站 PSD 模板

Education Template

20款非常漂亮的免费网站 PSD 模板

Digital Rust Template

20款非常漂亮的免费网站 PSD 模板

Inkfolio

20款非常漂亮的免费网站 PSD 模板

Creative Mass: One Page Portfolio

20款非常漂亮的免费网站 PSD 模板

Clean Tech

20款非常漂亮的免费网站 PSD 模板

Grunge Designer Portfolio

20款非常漂亮的免费网站 PSD 模板

Cafe and Restaurant Template

20款非常漂亮的免费网站 PSD 模板

[原创]File.Create()的进程占用问题

mikel阅读(1081)

最近写File创建的功能总是提示文件被其他进程占用,原来是因为File.Create();时没有释放资源,导致后面的StreamWriter没办法写入。应该File.Create()后再Close(),这样就可以避免问题了

代码如下:

if (!File.Exists(strPath))
{
FileInfo file = new FileInfo(strPath);
file.Create().Close(); //注意要close();
StreamWriter stream = file.AppendText();

stream.WriteLine("");
stream.Close();
}

[转载]C#通过HtmlAgilityPack+XPath来优化网页采集学习笔记

mikel阅读(1055)

[转载]【C#】通过HtmlAgilityPack+XPath来优化网页采集学习笔记 – 悠游人生 – 博客园.

上礼拜我在博客园发布了一篇关于网页数据分析和采集的文章,后来看过文章评论才知道原来现在已经有HtmlAgilityPack这个类库可以更方便地对HTML内容进行分析和提取。因此今天特别学习和实践了一下HtmlAgilityPack和XPath,并作下笔记。

1.下载HtmlAgilityPack.dll并将其添加引用到项目中,然后在代码中声明引用。

下载地址:http://www.codeplex.com/htmlagilitypack

引用:

using HtmlAgilityPack;

2.下载获取HTML页面的步骤和我上篇文章里介绍的差不多,都是先用WebClient或者WebRequest类来下载HTML页面然后处理成字符串。

3.与上次不同的是,这里分析和抓取HTML节点中的数据不再是之前那种STRING字符串操作的方式,而是封装成一个HtmlDocument对象,通过HtmlDocument中的方法来索引你需要抓取HTML节点,进而取出节点中的值。

4.若需要抓取的节点有ID,类似“<div id=’post_list’>value</div>”这种,那很简单只需调用GetElementbyId方法根据节点ID即可获 取所需节点。从而通过HtmlNode中的InnerText或Attribute属性来获取你想要的值。

CODE:

//实例化HtmlAgilityPack.HtmlDocument对象
HtmlDocument doc = new HtmlDocument();
//载入HTML
doc.LoadHtml(mainData);

//根据HTML节点NODE的ID获取节点
HtmlNode navNode = doc.GetElementbyId(post_list);

5.但很多情况下HTML节点是没有ID的,那就需要通过XPATH语言来查找匹配所需节点(XPath教程)。以抓取博客园首页文章为例,通过查看博客园HTML可以发现博客园首页里的文章列表都是放在一个ID为”post_list”的”<div>”节点中的。

这样的话就可以通过调用GetElementbyId方法来定位到这个节点了。

6.在获取到”post_list”节点后,接下来就可以调用SelectSingleNode方法通过XPATH表达式来索引出需要抓取的节点,从而抓取到文章的标题和链接地址。

(PS:在博客园首页”post_list”节点里,平均每4个DIV就是一篇新的文章可以利用这一规律遍历出首页所有文章)

CODE:

//实例化HtmlAgilityPack.HtmlDocument对象
HtmlDocument doc = new HtmlDocument();
//载入HTML
doc.LoadHtml(mainData);

//根据HTML节点NODE的ID获取节点
HtmlNode navNode = doc.GetElementbyId(post_list);
//根据XPATH来索引节点

//div[2]表示文章链接a位于post_list里面第3个div节点中
HtmlNode navNode2 = navNode.SelectSingleNode(//div[2]/h3/a);
//获取文章链接地址
string articleTitle = navNode2.Attributes[href].Value.ToString();
//获取文章标题
string articleName = navNode2.InnerText;

对比一下之前使用string截取字符串的方法:

//以”<a class=\”titlelnk\” href=\””作为抓取点开始采集
mainData=mainData.Substring(mainData.IndexOf(<a class=\”titlelnk\” href=\”) + 26);

//获取文章页面的链接地址
string articleAddr = mainData.Substring(0,mainData.IndexOf(\”));

//获取文章标题
string articleTitle = mainData.Substring(mainData.IndexOf(target=\”_blank\”>) + 16,
mainData.IndexOf(
</a>) mainData.IndexOf(target=\”_blank\”>) 16);

可见与之前相比,使用HtmlAgilityPack.HtmlDocument类来实现前台HTML的分析和采集在页面编码和数据分析上面处理更加合理、优化,代码也更加简洁。

目前只是粗浅地学了一下HtmlAgilityPack提取HTML节点数值的应用,更多应用以及采集速度的优化处理等方面尚未仔细研究,还望高人指点。

[转载]监控 SQL Server 的运行状况

mikel阅读(1071)

[转载]监控 SQL Server 的运行状况 – kudychen – 博客园.

Microsoft SQL Server 2005 提供了一些工具来监控数据库。方法之一是动态管理视图。动态管理视图 (DMV) 和动态管理函数 (DMF) 返回的服务器状态信息可用于监控服务器实例的运行状况、诊断问题和优化性能。

常规服务器动态管理对象包括:

  • dm_db_*:数据库和数据库对象
  • dm_exec_*:执行用户代码和关联的连接
  • dm_os_*:内存、锁定和时间安排
  • dm_tran_*:事务和隔离
  • dm_io_*:网络和磁盘的输入/输出

此部分介绍为监控 SQL Server 运行状况而针对这些动态管理视图和函数运行的一些常用查询。

示例查询

您可以运行以下查询来获取所有 DMV 和 DMF 名称:

SELECT * FROM sys.system_objects
WHERE name LIKE 'dm_%'
ORDER BY name

监控 CPU 瓶颈

CPU 瓶颈通常由以下原因引起:查询计划并非最优、配置不当、设计因素不良或硬件资源不足。下面的常用查询可帮助您确定导致 CPU 瓶颈的原因。

下面的查询使您能够深入了解当前缓存的哪些批处理或过程占用了大部分 CPU 资源。

SELECT TOP 50 
      SUM(qs.total_worker_time) AS total_cpu_time, 
      SUM(qs.execution_count) AS total_execution_count,
      COUNT(*) AS  number_of_statements, 
      qs.sql_handle 
FROM sys.dm_exec_query_stats AS qs
GROUP BY qs.sql_handle
ORDER BY SUM(qs.total_worker_time) DESC

下面的查询显示缓存计划所占用的 CPU 总使用率(带 SQL 文本)。

SELECT 
      total_cpu_time, 
      total_execution_count,
      number_of_statements,
      s2.text
      --(SELECT SUBSTRING(s2.text, statement_start_offset / 2, ((CASE WHEN statement_end_offset = -1 THEN (LEN(CONVERT(NVARCHAR(MAX), s2.text)) * 2) ELSE statement_end_offset END) - statement_start_offset) / 2) ) AS query_text
FROM 
      (SELECT TOP 50 
            SUM(qs.total_worker_time) AS total_cpu_time, 
            SUM(qs.execution_count) AS total_execution_count,
            COUNT(*) AS  number_of_statements, 
            qs.sql_handle --,
            --MIN(statement_start_offset) AS statement_start_offset, 
            --MAX(statement_end_offset) AS statement_end_offset
      FROM 
            sys.dm_exec_query_stats AS qs
      GROUP BY qs.sql_handle
      ORDER BY SUM(qs.total_worker_time) DESC) AS stats
      CROSS APPLY sys.dm_exec_sql_text(stats.sql_handle) AS s2

下面的查询显示 CPU 平均占用率最高的前 50 个 SQL 语句。

SELECT TOP 50
total_worker_time/execution_count AS [Avg CPU Time],
(SELECT SUBSTRING(text,statement_start_offset/2,(CASE WHEN statement_end_offset = -1 then LEN(CONVERT(nvarchar(max), text)) * 2 ELSE statement_end_offset end -statement_start_offset)/2) FROM sys.dm_exec_sql_text(sql_handle)) AS query_text, *
FROM sys.dm_exec_query_stats 
ORDER BY [Avg CPU Time] DESC

下面显示用于找出过多编译/重新编译的 DMV 查询。

select * from sys.dm_exec_query_optimizer_info
where 
      counter = 'optimizations'
      or counter = 'elapsed time'

下面的示例查询显示已重新编译的前 25 个存储过程。plan_generation_num 指示该查询已重新编译的次数。

select top 25
      sql_text.text,
      sql_handle,
      plan_generation_num,
      execution_count,
      dbid,
      objectid 
from sys.dm_exec_query_stats a
      cross apply sys.dm_exec_sql_text(sql_handle) as sql_text
where plan_generation_num > 1
order by plan_generation_num desc

效率较低的查询计划可能增大 CPU 占用率。

下面的查询显示哪个查询占用了最多的 CPU 累计使用率。

SELECT 
    highest_cpu_queries.plan_handle, 
    highest_cpu_queries.total_worker_time,
    q.dbid,
    q.objectid,
    q.number,
    q.encrypted,
    q.[text]
from 
    (select top 50 
        qs.plan_handle, 
        qs.total_worker_time
    from 
        sys.dm_exec_query_stats qs
    order by qs.total_worker_time desc) as highest_cpu_queries
    cross apply sys.dm_exec_sql_text(plan_handle) as q
order by highest_cpu_queries.total_worker_time desc

下面的查询显示一些可能占用大量 CPU 使用率的运算符(例如 ‘%Hash Match%’、‘%Sort%’)以找出可疑对象。

select *
from 
      sys.dm_exec_cached_plans
      cross apply sys.dm_exec_query_plan(plan_handle)
where 
      cast(query_plan as nvarchar(max)) like '%Sort%'
      or cast(query_plan as nvarchar(max)) like '%Hash Match%'

如果已检测到效率低下并导致 CPU 占用率较高的查询计划,请对该查询中涉及的表运行 UPDATE STATISTICS 以查看该问题是否仍然存在。

然后,收集相关数据并将此问题报告给 PerformancePoint 规划支持人员。

如果您的系统存在过多的编译和重新编译,可能会导致系统出现与 CPU 相关的性能问题。

您可以运行下面的 DMV 查询来找出过多的编译/重新编译。

select * from sys.dm_exec_query_optimizer_info
where 
counter = 'optimizations'
or counter = 'elapsed time'

下面的示例查询显示已重新编译的前 25 个存储过程。plan_generation_num 指示该查询已重新编译的次数。

select top 25
sql_text.text,
sql_handle,
plan_generation_num,
execution_count,
dbid,
objectid 
from sys.dm_exec_query_stats a
cross apply sys.dm_exec_sql_text(sql_handle) as sql_text
where plan_generation_num > 1
order by plan_generation_num desc

如果已检测到过多的编译或重新编译,请尽可能多地收集相关数据并将其报告给规划支持人员。

内存瓶颈

开始内存压力检测和调查之前,请确保已启用 SQL Server 中的高级选项。请先对 master 数据库运行以下查询以启用此选项。

sp_configure 'show advanced options'
go
sp_configure 'show advanced options', 1
go
reconfigure
go

首先运行以下查询以检查内存相关配置选项。

sp_configure 'awe_enabled'
go
sp_configure 'min server memory'
go
sp_configure 'max server memory'
go
sp_configure 'min memory per query'
go
sp_configure 'query wait'
go

运行下面的 DMV 查询以查看 CPU、计划程序内存和缓冲池信息。

select 
cpu_count,
hyperthread_ratio,
scheduler_count,
physical_memory_in_bytes / 1024 / 1024 as physical_memory_mb,
virtual_memory_in_bytes / 1024 / 1024 as virtual_memory_mb,
bpool_committed * 8 / 1024 as bpool_committed_mb,
bpool_commit_target * 8 / 1024 as bpool_target_mb,
bpool_visible * 8 / 1024 as bpool_visible_mb
from sys.dm_os_sys_info

I/O 瓶颈

检查闩锁等待统计信息以确定 I/O 瓶颈。运行下面的 DMV 查询以查找 I/O 闩锁等待统计信息。

select wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms, wait_time_ms / waiting_tasks_count
from sys.dm_os_wait_stats  
where wait_type like 'PAGEIOLATCH%'  and waiting_tasks_count > 0
order by wait_type

如果 waiting_task_countswait_time_ms 与正常情况相比有显著变化,则可以确定存在 I/O 问题。获取 SQL Server 平稳运行时性能计数器和主要 DMV 查询输出的基线非常重要。

这些 wait_types 可以指示您的 I/O 子系统是否遇到瓶颈。

使用以下 DMV 查询来查找当前挂起的 I/O 请求。请定期执行此查询以检查 I/O 子系统的运行状况,并隔离 I/O 瓶颈中涉及的物理磁盘。

select 
    database_id, 
    file_id, 
    io_stall,
    io_pending_ms_ticks,
    scheduler_address 
from  sys.dm_io_virtual_file_stats(NULL, NULL)t1,
        sys.dm_io_pending_io_requests as t2
where t1.file_handle = t2.io_handle

在正常情况下,该查询通常不返回任何内容。如果此查询返回一些行,则需要进一步调查。

您还可以执行下面的 DMV 查询以查找 I/O 相关查询。

select top 5 (total_logical_reads/execution_count) as avg_logical_reads,
                   (total_logical_writes/execution_count) as avg_logical_writes,
           (total_physical_reads/execution_count) as avg_physical_reads,
           Execution_count, statement_start_offset, p.query_plan, q.text
from sys.dm_exec_query_stats
      cross apply sys.dm_exec_query_plan(plan_handle) p
      cross apply sys.dm_exec_sql_text(plan_handle) as q
order by (total_logical_reads + total_logical_writes)/execution_count Desc

下面的 DMV 查询可用于查找哪些批处理/请求生成的 I/O 最多。如下所示的 DMV 查询可用于查找可生成最多 I/O 的前五个请求。调整这些查询将提高系统性能。

select top 5 
    (total_logical_reads/execution_count) as avg_logical_reads,
    (total_logical_writes/execution_count) as avg_logical_writes,
    (total_physical_reads/execution_count) as avg_phys_reads,
     Execution_count, 
    statement_start_offset as stmt_start_offset, 
    sql_handle, 
    plan_handle
from sys.dm_exec_query_stats  
order by  (total_logical_reads + total_logical_writes) Desc

阻塞

运行下面的查询可确定阻塞的会话。

select blocking_session_id, wait_duration_ms, session_id from 
sys.dm_os_waiting_tasks
where blocking_session_id is not null

使用此调用可找出 blocking_session_id 所返回的 SQL。例如,如果 blocking_session_id 是 87,则运行此查询可获得相应的 SQL。

dbcc INPUTBUFFER(87)

下面的查询显示 SQL 等待分析和前 10 个等待的资源。

select top 10 *
from sys.dm_os_wait_stats
--where wait_type not in ('CLR_SEMAPHORE','LAZYWRITER_SLEEP','RESOURCE_QUEUE','SLEEP_TASK','SLEEP_SYSTEMTASK','WAITFOR')
order by wait_time_ms desc

若要找出哪个 spid 正在阻塞另一个 spid,可在数据库中创建以下存储过程,然后执行该存储过程。此存储过程会报告此阻塞情况。键入 sp_who 可找出 @spid;@spid 是可选参数。

create proc dbo.sp_block (@spid bigint=NULL)
as
select 
    t1.resource_type,
    'database'=db_name(resource_database_id),
    'blk object' = t1.resource_associated_entity_id,
    t1.request_mode,
    t1.request_session_id,
    t2.blocking_session_id    
from 
    sys.dm_tran_locks as t1, 
    sys.dm_os_waiting_tasks as t2
where 
    t1.lock_owner_address = t2.resource_address and
    t1.request_session_id = isnull(@spid,t1.request_session_id)

以下是使用此存储过程的示例。

exec sp_block
exec sp_block @spid = 7

[转载]Android之断点续传下载

mikel阅读(921)

[转载]Android之断点续传下载 – 青春流水指间、 – 博客园.

今天学习了Android开发中比较难的一个环节,就是断点续传下载,很多人看到这个标题就感觉头大,的确,如果没有良好的逻辑思维,这块的确很难搞明白。下面我就将自己学到的知识和一些见解写下供那些在这个环节还烦恼的人参考。这里我以下载mp3文件为例。

断点续传下载,顾名思义,那就是我们在一次下载未结束时,退出下载,第二次下载时会接着第一次下载的进度继续下载。那么怎么记录第一次下载的数据呢,这里肯定就要用到数据库了。下面就是我创建数据库的一个SQLiteOpenHelper类。用来首次运行时创建数据库。

DBHelper类:

 1 package cn.yj3g.DBHelper;
 2 
 3 import android.content.Context;
 4 import android.database.sqlite.SQLiteDatabase;
 5 import android.database.sqlite.SQLiteOpenHelper;
 6 
 7     /**
 8      * 建立一个数据库帮助类
 9      */
10 public class DBHelper extends SQLiteOpenHelper {
11     //download.db-->数据库名
12      public DBHelper(Context context) {
13         super(context, "download.db", null, 1);
14     }
15     
16     /**
17      * 在download.db数据库下创建一个download_info表存储下载信息
18      */
19     @Override
20     public void onCreate(SQLiteDatabase db) {
21         db.execSQL("create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "
22                 + "start_pos integer, end_pos integer, compelete_size integer,url char)");
23     }
24     @Override
25     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
26 
27     }
28 
29 }

下面来看主界面的布局,在这里,我只设计了一个ListView来显示下载的音乐的名称,和一个开始下载按钮和一个暂停按钮。

布局文件如下:

main.xml:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:orientation="vertical"
 4     android:layout_width="fill_parent"
 5     android:layout_height="fill_parent"
 6     android:id="@+id/llRoot">
 7     <ListView android:id="@android:id/list"
 8         android:layout_width="fill_parent"
 9         android:layout_height="fill_parent">
10     </ListView>
11 </LinearLayout>

list_item.xml:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3            android:orientation="vertical"
 4            android:layout_width="fill_parent"
 5            android:layout_height="wrap_content">
 6     <LinearLayout
 7            android:orientation="horizontal"
 8            android:layout_width="fill_parent"
 9            android:layout_height="wrap_content"
10            android:layout_marginBottom="5dip">
11         <TextView 
12             android:layout_width="fill_parent"
13             android:layout_height="wrap_content"
14             android:layout_weight="1"
15             android:id="@+id/tv_resouce_name"/>
16         <Button
17             android:layout_width="fill_parent"
18             android:layout_height="wrap_content"
19             android:layout_weight="1"
20             android:text="下载"
21             android:id="@+id/btn_start"
22             android:onClick="startDownload"/>
23         <Button
24             android:layout_width="fill_parent"
25             android:layout_height="wrap_content"
26             android:layout_weight="1"
27             android:text="暂停"
28             android:id="@+id/btn_pause"
29             android:onClick="pauseDownload"/>
30       </LinearLayout>
31 </LinearLayout>

主界面运行效果如下:

下面我们来看具体实现下载的方法。首先,我们要定义一个记录在下载时各个时期的数据的类,这里我创建了一个DownloadInfo类来记录。代码如下:

DownloadInfo:

 1 package cn.yj3g.entity;
 2 /**
 3  *创建一个下载信息的实体类
 4  */
 5 public class DownloadInfo {
 6     private int threadId;//下载器id
 7     private int startPos;//开始点
 8     private int endPos;//结束点
 9     private int compeleteSize;//完成度
10     private String url;//下载器网络标识
11     public DownloadInfo(int threadId, int startPos, int endPos,
12             int compeleteSize,String url) {
13         this.threadId = threadId;
14         this.startPos = startPos;
15         this.endPos = endPos;
16         this.compeleteSize = compeleteSize;
17         this.url=url;
18     }
19     public DownloadInfo() {
20     }
21     public String getUrl() {
22         return url;
23     }
24     public void setUrl(String url) {
25         this.url = url;
26     }
27     public int getThreadId() {
28         return threadId;
29     }
30     public void setThreadId(int threadId) {
31         this.threadId = threadId;
32     }
33     public int getStartPos() {
34         return startPos;
35     }
36     public void setStartPos(int startPos) {
37         this.startPos = startPos;
38     }
39     public int getEndPos() {
40         return endPos;
41     }
42     public void setEndPos(int endPos) {
43         this.endPos = endPos;
44     }
45     public int getCompeleteSize() {
46         return compeleteSize;
47     }
48     public void setCompeleteSize(int compeleteSize) {
49         this.compeleteSize = compeleteSize;
50     }
51 
52     @Override
53     public String toString() {
54         return "DownloadInfo [threadId=" + threadId
55                 + ", startPos=" + startPos + ", endPos=" + endPos
56                 + ", compeleteSize=" + compeleteSize +"]";
57     }
58 }

在下载时,我们有进度条来显示进度,怎么确定进度条的进度,大小和起始位置呢?这里我定义了一个LoadInfo类来记录下载器详细信息。代码如下:

LoadInfo:

 1 package cn.yj3g.entity;
 2 /**
 3  *自定义的一个记载下载器详细信息的类 
 4  */
 5 public class LoadInfo {
 6     public int fileSize;//文件大小
 7     private int complete;//完成度
 8     private String urlstring;//下载器标识
 9     public LoadInfo(int fileSize, int complete, String urlstring) {
10         this.fileSize = fileSize;
11         this.complete = complete;
12         this.urlstring = urlstring;
13     }
14     public LoadInfo() {
15     }
16     public int getFileSize() {
17         return fileSize;
18     }
19     public void setFileSize(int fileSize) {
20         this.fileSize = fileSize;
21     }
22     public int getComplete() {
23         return complete;
24     }
25     public void setComplete(int complete) {
26         this.complete = complete;
27     }
28     public String getUrlstring() {
29         return urlstring;
30     }
31     public void setUrlstring(String urlstring) {
32         this.urlstring = urlstring;
33     }
34     @Override
35     public String toString() {
36         return "LoadInfo [fileSize=" + fileSize + ", complete=" + complete
37                 + ", urlstring=" + urlstring + "]";
38     }
39 }

下面是最最重要的一步,那就是定义一个下载器来进行下载了,这里我就不多说,具体解释在代码中都有注解,我就直接将代码附下,供大家研究参考

Downloader:

  1 package cn.yj3g.service;
  2 
  3 import java.io.File;
  4 import java.io.InputStream;
  5 import java.io.RandomAccessFile;
  6 import java.net.HttpURLConnection;
  7 import java.net.URL;
  8 import java.util.ArrayList;
  9 import java.util.List;
 10 import android.content.Context;
 11 import android.os.Handler;
 12 import android.os.Message;
 13 import android.util.Log;
 14 import cn.yj3g.Dao.Dao;
 15 import cn.yj3g.entity.DownloadInfo;
 16 import cn.yj3g.entity.LoadInfo;
 17 
 18 public class Downloader {
 19     private String urlstr;// 下载的地址
 20     private String localfile;// 保存路径
 21     private int threadcount;// 线程数
 22     private Handler mHandler;// 消息处理器
 23     private Dao dao;// 工具类
 24     private int fileSize;// 所要下载的文件的大小
 25     private List<DownloadInfo> infos;// 存放下载信息类的集合
 26     private static final int INIT = 1;//定义三种下载的状态:初始化状态,正在下载状态,暂停状态
 27     private static final int DOWNLOADING = 2;
 28     private static final int PAUSE = 3;
 29     private int state = INIT;
 30 
 31     public Downloader(String urlstr, String localfile, int threadcount,
 32             Context context, Handler mHandler) {
 33         this.urlstr = urlstr;
 34         this.localfile = localfile;
 35         this.threadcount = threadcount;
 36         this.mHandler = mHandler;
 37         dao = new Dao(context);
 38     }
 39     /**
 40      *判断是否正在下载 
 41      */
 42     public boolean isdownloading() {
 43         return state == DOWNLOADING;
 44     }
 45     /**
 46      * 得到downloader里的信息
 47      * 首先进行判断是否是第一次下载,如果是第一次就要进行初始化,并将下载器的信息保存到数据库中
 48      * 如果不是第一次下载,那就要从数据库中读出之前下载的信息(起始位置,结束为止,文件大小等),并将下载信息返回给下载器
 49      */
 50     public LoadInfo getDownloaderInfors() {
 51         if (isFirst(urlstr)) {
 52             Log.v("TAG", "isFirst");
 53             init();
 54             int range = fileSize / threadcount;
 55             infos = new ArrayList<DownloadInfo>();
 56             for (int i = 0; i < threadcount - 1; i++) {
 57                 DownloadInfo info = new DownloadInfo(i, i * range, (i + 1)* range - 1, 0, urlstr);
 58                 infos.add(info);
 59             }
 60             DownloadInfo info = new DownloadInfo(threadcount - 1,(threadcount - 1) * range, fileSize - 1, 0, urlstr);
 61             infos.add(info);
 62             //保存infos中的数据到数据库
 63             dao.saveInfos(infos);
 64             //创建一个LoadInfo对象记载下载器的具体信息
 65             LoadInfo loadInfo = new LoadInfo(fileSize, 0, urlstr);
 66             return loadInfo;
 67         } else {
 68             //得到数据库中已有的urlstr的下载器的具体信息
 69             infos = dao.getInfos(urlstr);
 70             Log.v("TAG", "not isFirst size=" + infos.size());
 71             int size = 0;
 72             int compeleteSize = 0;
 73             for (DownloadInfo info : infos) {
 74                 compeleteSize += info.getCompeleteSize();
 75                 size += info.getEndPos() - info.getStartPos() + 1;
 76             }
 77             return new LoadInfo(size, compeleteSize, urlstr);
 78         }
 79     }
 80 
 81     /**
 82      * 初始化
 83      */
 84     private void init() {
 85         try {
 86             URL url = new URL(urlstr);
 87             HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 88             connection.setConnectTimeout(5000);
 89             connection.setRequestMethod("GET");
 90             fileSize = connection.getContentLength();
 91 
 92             File file = new File(localfile);
 93             if (!file.exists()) {
 94                 file.createNewFile();
 95             }
 96             // 本地访问文件
 97             RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
 98             accessFile.setLength(fileSize);
 99             accessFile.close();
100             connection.disconnect();
101         } catch (Exception e) {
102             e.printStackTrace();
103         }
104     }
105 
106     /**
107      * 判断是否是第一次 下载
108      */
109     private boolean isFirst(String urlstr) {
110         return dao.isHasInfors(urlstr);
111     }
112 
113     /**
114      * 利用线程开始下载数据
115      */
116     public void download() {
117         if (infos != null) {
118             if (state == DOWNLOADING)
119                 return;
120             state = DOWNLOADING;
121             for (DownloadInfo info : infos) {
122                 new MyThread(info.getThreadId(), info.getStartPos(),
123                         info.getEndPos(), info.getCompeleteSize(),
124                         info.getUrl()).start();
125             }
126         }
127     }
128 
129     public class MyThread extends Thread {
130         private int threadId;
131         private int startPos;
132         private int endPos;
133         private int compeleteSize;
134         private String urlstr;
135 
136         public MyThread(int threadId, int startPos, int endPos,
137                 int compeleteSize, String urlstr) {
138             this.threadId = threadId;
139             this.startPos = startPos;
140             this.endPos = endPos;
141             this.compeleteSize = compeleteSize;
142             this.urlstr = urlstr;
143         }
144         @Override
145         public void run() {
146             HttpURLConnection connection = null;
147             RandomAccessFile randomAccessFile = null;
148             InputStream is = null;
149             try {
150                 URL url = new URL(urlstr);
151                 connection = (HttpURLConnection) url.openConnection();
152                 connection.setConnectTimeout(5000);
153                 connection.setRequestMethod("GET");
154                 // 设置范围,格式为Range:bytes x-y;
155                 connection.setRequestProperty("Range", "bytes="+(startPos + compeleteSize) + "-" + endPos);
156 
157                 randomAccessFile = new RandomAccessFile(localfile, "rwd");
158                 randomAccessFile.seek(startPos + compeleteSize);
159                 // 将要下载的文件写到保存在保存路径下的文件中
160                 is = connection.getInputStream();
161                 byte[] buffer = new byte[4096];
162                 int length = -1;
163                 while ((length = is.read(buffer)) != -1) {
164                     randomAccessFile.write(buffer, 0, length);
165                     compeleteSize += length;
166                     // 更新数据库中的下载信息
167                     dao.updataInfos(threadId, compeleteSize, urlstr);
168                     // 用消息将下载信息传给进度条,对进度条进行更新
169                     Message message = Message.obtain();
170                     message.what = 1;
171                     message.obj = urlstr;
172                     message.arg1 = length;
173                     mHandler.sendMessage(message);
174                     if (state == PAUSE) {
175                         return;
176                     }
177                 }
178             } catch (Exception e) {
179                 e.printStackTrace();
180             } finally {
181                 try {
182                     is.close();
183                     randomAccessFile.close();
184                     connection.disconnect();
185                     dao.closeDb();
186                 } catch (Exception e) {
187                     e.printStackTrace();
188                 }
189             }
190 
191         }
192     }
193     //删除数据库中urlstr对应的下载器信息
194     public void delete(String urlstr) {
195         dao.delete(urlstr);
196     }
197     //设置暂停
198     public void pause() {
199         state = PAUSE;
200     }
201     //重置下载状态
202     public void reset() {
203         state = INIT;
204     }
205 }

在这边下载器类的定义中,我们用到了许多关于进行数据库操作的方法,这里我定义了一个数据库工具类,来提供这些方法,代码如下:

Dao:

 1 package cn.yj3g.Dao;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import android.content.Context;
 6 import android.database.Cursor;
 7 import android.database.sqlite.SQLiteDatabase;
 8 import cn.yj3g.DBHelper.DBHelper;
 9 import cn.yj3g.entity.DownloadInfo;
10 
11 /**
12  * 
13  * 一个业务类
14  */
15 public class Dao {
16     private DBHelper dbHelper;
17 
18     public Dao(Context context) {
19         dbHelper = new DBHelper(context);
20     }
21 
22     /**
23      * 查看数据库中是否有数据
24      */
25     public boolean isHasInfors(String urlstr) {
26         SQLiteDatabase database = dbHelper.getReadableDatabase();
27         String sql = "select count(*)  from download_info where url=?";
28         Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
29         cursor.moveToFirst();
30         int count = cursor.getInt(0);
31         cursor.close();
32         return count == 0;
33     }
34 
35     /**
36      * 保存 下载的具体信息
37      */
38     public void saveInfos(List<DownloadInfo> infos) {
39         SQLiteDatabase database = dbHelper.getWritableDatabase();
40         for (DownloadInfo info : infos) {
41             String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";
42             Object[] bindArgs = { info.getThreadId(), info.getStartPos(),
43                     info.getEndPos(), info.getCompeleteSize(), info.getUrl() };
44             database.execSQL(sql, bindArgs);
45         }
46     }
47 
48     /**
49      * 得到下载具体信息
50      */
51     public List<DownloadInfo> getInfos(String urlstr) {
52         List<DownloadInfo> list = new ArrayList<DownloadInfo>();
53         SQLiteDatabase database = dbHelper.getReadableDatabase();
54         String sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";
55         Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
56         while (cursor.moveToNext()) {
57             DownloadInfo info = new DownloadInfo(cursor.getInt(0),
58                     cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),
59                     cursor.getString(4));
60             list.add(info);
61         }
62         cursor.close();
63         return list;
64     }
65 
66     /**
67      * 更新数据库中的下载信息
68      */
69     public void updataInfos(int threadId, int compeleteSize, String urlstr) {
70         SQLiteDatabase database = dbHelper.getReadableDatabase();
71         String sql = "update download_info set compelete_size=? where thread_id=? and url=?";
72         Object[] bindArgs = { compeleteSize, threadId, urlstr };
73         database.execSQL(sql, bindArgs);
74     }
75     /**
76      * 关闭数据库
77      */
78     public void closeDb() {
79         dbHelper.close();
80     }
81 
82     /**
83      * 下载完成后删除数据库中的数据
84      */
85     public void delete(String url) {
86         SQLiteDatabase database = dbHelper.getReadableDatabase();
87         database.delete("download_info", "url=?", new String[] { url });
88         database.close();
89     }
90 }

下来就是要进行下载和暂停按钮的响应事件了。具体代码和解释如下。

MainActivity:

  1 package cn.yj3g.AndroidDownload;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 import java.util.List;
  6 import java.util.Map;
  7 import android.app.ListActivity;
  8 import android.os.Bundle;
  9 import android.os.Handler;
 10 import android.os.Message;
 11 import android.view.View;
 12 import android.widget.LinearLayout;
 13 import android.widget.LinearLayout.LayoutParams;
 14 import android.widget.ProgressBar;
 15 import android.widget.SimpleAdapter;
 16 import android.widget.TextView;
 17 import android.widget.Toast;
 18 import cn.yj3g.entity.LoadInfo;
 19 import cn.yj3g.service.Downloader;
 20 
 21 public class MainActivity extends ListActivity {
 22     // 固定下载的资源路径,这里可以设置网络上的地址
 23     private static final String URL = "http://192.168.1.105:8080/struts2_net/";
 24     // 固定存放下载的音乐的路径:SD卡目录下
 25     private static final String SD_PATH = "/mnt/sdcard/";
 26     // 存放各个下载器
 27     private Map<String, Downloader> downloaders = new HashMap<String, Downloader>();
 28     // 存放与下载器对应的进度条
 29     private Map<String, ProgressBar> ProgressBars = new HashMap<String, ProgressBar>();
 30     /**
 31      * 利用消息处理机制适时更新进度条
 32      */
 33     private Handler mHandler = new Handler() {
 34         public void handleMessage(Message msg) {
 35             if (msg.what == 1) {
 36                 String url = (String) msg.obj;
 37                 int length = msg.arg1;
 38                 ProgressBar bar = ProgressBars.get(url);
 39                 if (bar != null) {
 40                     // 设置进度条按读取的length长度更新
 41                     bar.incrementProgressBy(length);
 42                     if (bar.getProgress() == bar.getMax()) {
 43                         Toast.makeText(MainActivity.this, "下载完成!", 0).show();
 44                         // 下载完成后清除进度条并将map中的数据清空
 45                         LinearLayout layout = (LinearLayout) bar.getParent();
 46                         layout.removeView(bar);
 47                         ProgressBars.remove(url);
 48                         downloaders.get(url).delete(url);
 49                         downloaders.get(url).reset();
 50                         downloaders.remove(url);
 51 
 52                     }
 53                 }
 54             }
 55         }
 56     };
 57     @Override
 58     public void onCreate(Bundle savedInstanceState) {
 59         super.onCreate(savedInstanceState);
 60         setContentView(R.layout.main);
 61         showListView();
 62     }
 63     // 显示listView,这里可以随便添加音乐
 64     private void showListView() {
 65         List<Map<String, String>> data = new ArrayList<Map<String, String>>();
 66         Map<String, String> map = new HashMap<String, String>();
 67         map.put("name", "mm.mp3");
 68         data.add(map);
 69         map = new HashMap<String, String>();
 70         map.put("name", "pp.mp3");
 71         data.add(map);
 72         map = new HashMap<String, String>();
 73         map.put("name", "tt.mp3");
 74         data.add(map);
 75         map = new HashMap<String, String>();
 76         map.put("name", "You.mp3");
 77         data.add(map);
 78         SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.list_item, new String[] { "name" },
 79                 new int[] { R.id.tv_resouce_name });
 80         setListAdapter(adapter);
 81     }
 82     /**
 83      * 响应开始下载按钮的点击事件
 84      */
 85     public void startDownload(View v) {
 86         // 得到textView的内容
 87         LinearLayout layout = (LinearLayout) v.getParent();
 88         String musicName = ((TextView) layout.findViewById(R.id.tv_resouce_name)).getText().toString();
 89         String urlstr = URL + musicName;
 90         String localfile = SD_PATH + musicName;
 91         //设置下载线程数为4,这里是我为了方便随便固定的
 92         int threadcount = 4;
 93         // 初始化一个downloader下载器
 94         Downloader downloader = downloaders.get(urlstr);
 95         if (downloader == null) {
 96             downloader = new Downloader(urlstr, localfile, threadcount, this, mHandler);
 97             downloaders.put(urlstr, downloader);
 98         }
 99         if (downloader.isdownloading())
100             return;
101         // 得到下载信息类的个数组成集合
102         LoadInfo loadInfo = downloader.getDownloaderInfors();
103         // 显示进度条
104         showProgress(loadInfo, urlstr, v);
105         // 调用方法开始下载
106         downloader.download();
107     }
108 
109     /**
110      * 显示进度条
111      */
112     private void showProgress(LoadInfo loadInfo, String url, View v) {
113         ProgressBar bar = ProgressBars.get(url);
114         if (bar == null) {
115             bar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
116             bar.setMax(loadInfo.getFileSize());
117             bar.setProgress(loadInfo.getComplete());
118             ProgressBars.put(url, bar);
119             LinearLayout.LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, 5);
120             ((LinearLayout) ((LinearLayout) v.getParent()).getParent()).addView(bar, params);
121         }
122     }
123     /**
124      * 响应暂停下载按钮的点击事件
125      */
126     public void pauseDownload(View v) {
127         LinearLayout layout = (LinearLayout) v.getParent();
128         String musicName = ((TextView) layout.findViewById(R.id.tv_resouce_name)).getText().toString();
129         String urlstr = URL + musicName;
130         downloaders.get(urlstr).pause();
131     }
132 }

最后我们要在清单文件中添加权限,一个是访问网络的权限,一个是往SD卡写数据的权限。代码如下:

 <uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

这样我们就实现了文件的断点续传下载功能。具体效果图如下:

[转载]分享我对领域驱动设计(DDD)的学习成果

mikel阅读(1024)

[转载]分享我对领域驱动设计(DDD)的学习成果 – netfocus – 博客园.

本文内容提要:

1. 领域驱动设计之领域模型;

2. 为什么建立一个领域模型是重要的;

3. 领域通用语言(Ubiquitous Language);

4. 将领域模型转换为代码实现的最佳实践;

5. 领域建模时思考问题的角度;

6. 领域驱动设计的标准分层架构;

7. 领域驱动设计过程中使用的模式;

8. 设计领域模型的一般步骤;

9. 领域驱动设计的其他一些主题;

10.一些相关的扩展阅读;

领域驱动设计之领域模型

2004Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD。领域驱动设计分为两个阶段:

1. 以一种领域专家、设计人员、开发人员都能理解的“通用语言”作为相互交流的工具,在不断交流的过程中不断发现一些主要的领域概念,然后将这些概念设计成一个领域模型;

2. 由领域模型驱动软件设计,用代码来表现该领域模型。

由此可见,领域驱动设计的核心是建立领域模型。领域模型在软件架构中处于核心地位;软件开发过程中,必须以建立领域模型为中心。

为什么建立一个领域模型是重要的

领域驱动设计告诉我们,在通过软件实现一个业务系统时,建立一个领域模型是非常重要和必要的,因为领域模型具有以下特点:

1. 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反应了我们在领域内所关注的部分;

2. 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等;

3. 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护性,业务可理解性以及可重用性方面都有很好的帮助;

4. 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造;

5. 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,

所以可以防止需求走样,可以让软件设计开发人员做出来的软件真正满足需求;

6. 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使大家对领域的认识不断深入,从而不断细化和完善领域模型;

7. 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是唯一的表达方式,代码或文字描述也能表达领域模型;

8. 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化;

领域通用语言(Ubiquitous Language

我 们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的,但是,那种方法通常会由于一些基础交流的障碍而存在难点。开发人员满脑子都是 类、方法、算法、模式、架构,等等,总是想将实际生活中的概念和程序工件进行对应。他们希望看到要建立哪些对象类,要如何对对象类之间的关系建模。他们会 习惯按照封装、继承、多态等面向对象编程中的概念去思考,会随时随地这样交谈,这对他们来说这太正常不过了,开发人员就是开发人员。但是领域专家通常对这 一无所知,他们对软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。比如,在空中交通监控样例中,领域专家知道飞机、路 线、海拔、经度、纬度,知道飞机偏离了正常路线,知道飞机的发射。他们用他们自己的术语讨论这些事情,有时这对于外行来说很难直接理解。如果一个人说了什 么事情,其他的人不能理解,或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功呢?

在 交流的过程中,需要做翻译才能让其他的人理解这些概念。开发人员可能会努力使用外行人的语言来解析一些设计模式,但这并一定都能成功奏效。领域专家也可能 会创建一种新的行话以努力表达他们的这些想法。在这个痛苦的交流过程中,这种类型的翻译并不能对知识的构建过程产生帮助。

领 域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础。使用模型作为语言的核心骨 架,要求团队在进行所有的交流是都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿需要确保团队使用的语言 在所有的交流形式中看上去都是一致的,这种语言被称为“通用语言(Ubiquitous Language)”。通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通,从而发现要在模型中使用的主要的领域概念。

将领域模型转换为代码实现的最佳实践

拥 有一个看上去正确的模型不代表模型能被直接转换成代码,也或者它的实现可能会违背某些我们所不建议的软件设计原则。我们该如何实现从模型到代码的转换,并 让代码具有可扩展性、可维护性,高性能等指标呢?另外,如实反映领域的模型可能会导致对象持久化的一系列问题,或者导致不可接受的性能问题。那么我们应该 怎么做呢?

我 们应该紧密关联领域建模和设计,紧密将领域模型和软件编码实现捆绑在一起,模型在构建时就考虑到软件和设计。开发人员会被加入到建模的过程中来。主要的想 法是选择一个能够恰当在软件中表现的模型,这样设计过程会很顺畅并且基于模型。代码和其下的模型紧密关联会让代码更有意义并与模型更相关。有了开发人员的 参与就会有反馈。它能保证模型被实现成软件。如果其中某处有错误,会在早期就被标识出来,问题也会容易修正。写代码的人会很好地了解模型,会感觉自己有责 任保持它的完整性。他们会意识到对代码的一个变更其实就隐含着对模型的变更,另外,如果哪里的代码不能表现原始模型的话,他们会重构代码。如果分析人员从 实现过程中分离出去,他会不再关心开发过程中引入的局限性。最终结果是模型不再实用。任何技术人员想对模型做出贡献必须花费一些时间来接触代码,无论他在 项目中担负的是什么主要角色。任何一个负责修改代码的人都必须学会用代码表现模型。每位开发人员都必须参与到一定级别的领域讨论中并和领域专家联络。

领域建模时思考问题的角度

“用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。 《老 子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用户需求。举些例子,一个杯子要装满一杯水,我们在制作杯子 时,制作的是空杯子,即要把水倒出来,之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容纳人的居住。因此, 建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。

所以,我的理解是:

1. 我们设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律作为出发点去思考问题。

2. 领 域模型是排除了人之外的客观世界模型,但是领域模型包含人所扮演的参与者角色,但是一般情况下不要让参与者角色在领域模型中占据主要位置,如果以人所扮演 的参与者角色在领域模型中占据主要位置,那么各个系统的领域模型将变得没有差别,因为软件系统就是一个人机交互的系统,都是以人为主的活动记录或跟踪;比 如:论坛中如果以人为主导,那么领域模型就是:人发帖,人回帖,人结贴,等等;DDD的 例子中,如果是以人为中心的话,就变成了:托运人托运货物,收货人收货物,付款人付款,等等;因此,当我们谈及领域模型时,已经默认把人的因素排除开了, 因为领域只有对人来说才有意义,人是在领域范围之外的,如果人也划入领域,领域模型将很难保持客观性。领域模型是与谁用和怎样用是无关的客观模型。归纳起 来说就是,领域建模是建立虚拟模型让我们现实的人使用,而不是建立虚拟空间,去模仿现实。

Eric EvansDDD之父)在他的书中的一个货物运输系统为例子简单说明一下。

在经过一些用户需求讨论之后,在用户需求相对明朗之后,Eric这样描述领域模型:

1. 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每个Customer承担不同的角色;

2. Cargo的运送目标已指定,即Cargo有一个运送目标;

3. 由一系列满足Specification(规格)的Carrier Movement(运输动作)来完成运输目标;

从 上面的描述我们可以看出,他完全没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。上述这段描述 完全以货物为中心,把客户看成是货物在某个场景中可能会涉及到的关联角色,如货物会涉及到托运人、收货人、付款人;货物有一个确定的目标,货物会经过一系 列列的运输动作到达目的地;其实,我觉得以用户为中心来思考领域模型的思维只是停留在需求的表面,而没有挖掘出真正的需求的本质;我们在做领域建模时需要 努力挖掘用户需求的本质,这样才能真正实现用户需求;

关于用户、参与者这两个概念的区分,可以看一下下面的例子:

试想两个人共同玩足球游戏,操作者(用户)是驱动者,它驱使足球比赛领域中,各个(参与者)的活动。这里立下一个假设,假设操作者A操作某一队员a,而队员a拥有着某人B的信息,那么有以下说法,aB的镜像,a是领域参与者,A是驱动者。

领域驱动设计的标准分层架构

用户界面/展现层

负责向用户展现信息以及解释用户命令。更细的方面来讲就是:

1. 请求应用层以获取用户所需要展现的数据;

2. 发送命令给应用层要求其执行某个用户命令;

应用层

很薄的一层,定义软件要完成的所有任务。对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑。

领域层

负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。

基础设施层

本层为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求;

领域驱动设计过程中使用的模式

所有模式的总揽图:

关联的设计

关联本身不是一个模式,但它在领域建模的过程中非常重要,所以需要在探讨各种模式之前,先讨论一下对象之间的关联该如何设计。我觉得对象的关联的设计可以遵循如下的一些原则:

1. 关联尽量少,对象之间的复杂的关联容易形成对象的关系网,这样对于我们理解和维护单个对象很不利,同时也很难划分对象与对象之间的边界;另外,同时减少关联有助于简化对象之间的遍历;

2. 1对多的关联也许在业务上是很自然的,通常我们会用一个集合来表示1对多的关系。但我们往往也需要考虑到性能问题,尤其是当集合内元素非常多的时候,此时往往需要通过单独查询来获取关联的集合信息;

3. 关联尽量保持单向的关联;

4. 在建立关联时,我们需要深入去挖掘是否存在关联的限制条件,如果存在,那么最好把这个限制条件加到这个关联上;往往这样的限制条件能将关联化繁为简,即可以将多对多简化为1对多,或将1对多简化为11

实体(Entity):

实 体就是领域中需要唯一标识的领域概念。因为我们有时需要区分是哪个实体。有两个实体,如果唯一标识不一样,那么即便实体的其他所有属性都一样,我们也认为 他们两个不同的实体;因为实体有生命周期,实体从被创建后可能会被持久化到数据库,然后某个时候又会被取出来。所以,如果我们不为实体定义一种可以唯一区 分的标识,那我们就无法区分到底是这个实体还是哪个实体。另外,不应该给实体定义太多的属性或行为,而应该寻找关联,发现其他一些实体或值对象,将属性或 行为转移到其他关联的实体或值对象上。比如Customer实体,他有一些地址信息,由于地址信息是一个完整的有业务含义的概念,所以,我们可以定义一个Address对象,然后把Customer的地址相关的信息转移到Address对象上。如果没有Address对象,而把这些地址信息直接放在Customer对象上,并且如果对于一些其他的类似Address的信息也都直接放在Customer上,会导致Customer对象很混乱,结构不清晰,最终导致它难以维护和理解;

值对象(Value Object):

在领域中,并不是没一个事物都必须有一个唯一标识,也就是说我们不关心对象是哪个,而只关心对象是什么。就以上面的地址对象Address为例,如果有两个Customer的地址信息是一样的,我们就会认为这两个Customer的 地址是同一个。也就是说只要地址信息一样,我们就认为是同一个地址。用程序的方式来表达就是,如果两个对象的所有的属性的值都相同我们会认为它们是同一个 对象的话,那么我们就可以把这种对象设计为值对象。因此,值对象没有唯一标识,这是它和实体的最大不同。另外值对象在判断是否是同一个对象时是通过它们的 所有属性是否相同,如果相同则认为是同一个值对象;而我们在区分是否是同一个实体时,只看实体的唯一标识是否相同,而不管实体的属性是否相同;值对象另外 一个明显的特征是不可变,即所有属性都是只读的。因为属性是只读的,所以可以被安全的共享;当共享值对象时,一般有复制和共享两种做法,具体采用哪种做法 还要根据实际情况而定;另外,我们应该给值对象设计的尽量简单,不要让它引用很多其他的对象,因为他只是一个值,就像int a = 3;那么”3”就是一个我们传统意义上所说的值,而值对象其实也可以和这里的”3”一样理解,也是一个值,只不过是用对象来表示。所以,当我们在C#语言中比较两个值对象是否相等时,会重写GetHashCodeEquals这两个方法,目的就是为了比较对象的值;值对象虽然是只读的,但是可以被整个替换掉。就像你把a的值修改为”4”(a = 4;)一样,直接把”3”这个值替换为”4”了。值对象也是一样,当你要修改CustomerAddress对象引用时,不是通过Customer.Address.Street这样的方式来实现,因为值对象是只读的,它是一个完整的不可分割的整体。我们可以这样做:Customer.Address = new Address(…);

领域服务(Domain Service):

领 域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,因为它们本质上就是一些操作,一些动作,而不是事物。这些操作或动作往往会涉及到多个领域 对象,并且需要协调这些领域对象共同完成这个操作或动作。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会 导致对象的职责不明确很混乱。但是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。所以我们需要寻找一种新的模式来表示这种跨多个对象的操 作,DDD认为服务是一个很自然的范式用来对应这种跨多个对象的操作,所以就有了领域服务这个模式。和领域对象不同,领域服务是以动词开头来命名的,比如资金转帐服务可以命名为MoneyTransferService。 当然,你也可以把服务理解为一个对象,但这和一般意义上的对象有些区别。因为一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。需要强调的 是领域服务是无状态的,它存在的意义就是协调领域对象共完成某个操作,所有的状态还是都保存在相应的领域对象中。我觉得模型(实体)与服务(场景)是对领 域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各种现象,有 动有静,有独立有协作。

领域服务还 有一个很重要的功能就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作,这样一 来,领域层可能会把一部分领域知识泄露到应用层。因为应用层需要了解每个领域对象的业务功能,具有哪些信息,以及它可能会与哪些其他领域对象交互,怎么交 互等一系列领域知识。因此,引入领域服务可以有效的防治领域层的逻辑泄露到应用层。对于应用层来说,从可理解的角度来讲,通过调用领域服务提供的简单易懂 但意义明确的接口肯定也要比直接操纵领域对象容易的多。这里似乎也看到了领域服务具有Façade的功能,呵呵。

说到领域服务,还需要提一下软件中一般有三种服务:应用层服务、领域服务、基础服务。

比如应用层有一个资金转帐的服务,该服务主要做以下事情:

1) 获取输入(如一个XML请求);

2) 发送消息给领域层服务,要求其实现转帐的业务逻辑;

3) 领域层服务处理成功,则调用基础层服务发送Email通知;

领域层的服务做以下事情:
1
)获取源帐号和目标帐号,分别通知源帐号和目标帐号进行扣除金额和增加金额的操作;

2)提供返回结果给应用层;

基础层服务做以下事情:

按照应用层的请求,发送Email通知;

所以,从上面的例子中可以清晰的看出,每种服务的职责;

聚合及聚合根(AggregateAggregate Root):

聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。

聚合有以下一些特点:

1. 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体;

2. 聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持  对它的引用的唯一元素;

3. 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;

4. 聚合根负责与外部其他对象打交道并维护自己内部的业务规则;

5. 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象;

6. 聚合内部的对象可以保持对其他聚合根的引用;

7. 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;

关于如何识别聚合以及聚合根的问题:

我觉得我们可以先从业务的角度深入思考,然后慢慢分析出有哪些对象是:

1) 有独立存在的意义,即它是不依赖于其他对象的存在它才有意义的;

2) 可以被独立访问的,还是必须通过某个其他对象导航得到的;

如何识别聚合?

我 觉得这个需要从业务的角度深入分析哪些对象它们的关系是内聚的,即我们会把他们看成是一个整体来考虑的;然后这些对象我们就可以把它们放在一个聚合内。所 谓关系是内聚的,是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当我们在修改一个聚合时,我们必须在事务级 别确保整个聚合内的所有对象满足这个固定规则。作为一条建议,聚合尽量不要太大,否则即便能够做到在事务级别保持聚合的业务规则完整性,也可能会带来一定 的性能问题。有分析报告显示,通常在大部分领域模型中,有70%的聚合通常只有一个实体,即聚合根,该实体内部没有包含其他实体,只包含一些值对象;另外30%的聚合中,基本上也只包含两到三个实体。这意味着大部分的聚合都只是一个实体,该实体同时也是聚合根。

如何识别聚合根?

如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,那么我们可以思考聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互。

工厂(Factory):

DDD中的工厂也是一种体现封装思想的模式。DDD中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操 作。正如对象封装了内部实现一样(我们无需知道对象的内部实现就可以使用对象的行为),工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,工厂的 作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。领域模型中其他元素都不适 合做这个事情,所以需要引入这个新的模式,工厂。工厂在创建一个复杂的领域对象时,通常会知道该满足什么业务规则(它知道先怎样实例化一个对象,然后在对 这个对象做哪些初始化操作,这些知识就是创建对象的细节),如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象;但是如果由于参数无效 等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。当然我们也并不总是需要通过工厂来创建对象,事实上大部分情况下领域 对象的创建都不会太复杂,所以我们只需要简单的使用构造函数创建对象就可以了。

隐藏创建对象的好处是显而易见的,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。

仓储(Repository):

1)仓 储被设计出来的目的是基于这个原因:领域模型中的对象自从被创建出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,然后当需要的时候我 们会重建该对象;重建对象就是根据数据库中已存储的对象的状态重新创建对象的过程;所以,可见重建对象是一个和数据库打交道的过程。从更广义的角度来理 解,我们经常会像集合一样从某个类似集合的地方根据某个条件获取一个或一些对象,往集合中添加对象或移除对象。也就是说,我们需要提供一种机制,可以提供 类似集合的接口来帮助我们管理对象。仓储就是基于这样的思想被设计出来的;

2)仓 储里面存放的对象一定是聚合,原因是之前提到的领域模型中是以聚合的概念去划分边界的;聚合是我们更新对象的一个边界,事实上我们把整个聚合看成是一个整 体概念,要么一起被取出来,要么一起被删除。我们永远不会单独对某个聚合内的子对象进行单独查询或做更新操作。因此,我们只对聚合设计仓储。

3)仓 储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中我们定义仓储的接口,而在基础设施层实现具体的仓储。这样做的原因是:由于仓储背 后的实现都是在和数据库打交道,但是我们又不希望客户(如应用层)把重点放在如何从数据库获取数据的问题上,因为这样做会导致客户(应用层)代码很混乱, 很可能会因此而忽略了领域模型的存在。所以我们需要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可以让它专心的不会 被什么数据访问代码打扰的情况下协调领域对象完成业务逻辑。这种通过接口来隔离封装变化的做法其实很常见。由于客户面对的是抽象的接口并不是具体的实现, 所以我们可以随时替换仓储的真实实现,这很有助于我们做单元测试。

4)尽管仓储可以像集合一样在内存中管理对象,但是仓储一般不负责事务处理。一般事务处理会交给一个叫“工作单元(Unit Of Work)”的东西。这里不准备详细讨论工作单元的工作原理了,大家有兴趣的可以去研究一下。

5)另外,仓储在设计查询接口时,可能还会用到规格模式(Specification Pattern),我见过的最厉害的规格模式应该就是LINQ以及DLINQ查询了。一般我们会根据项目中查询的灵活度要求来选择适合的仓储查询接口设计。通常情况下只需要定义简单明了的具有固定查询参数的查询接口就可以了。只有是在查询条件是动态指定的情况下才可能需要用到Specification等模式。

设计领域模型的一般步骤:

1. 根据需求建立一个初步的领域模型,识别出一些明显的领域概念以及它们的关联,关联可以暂时没有方向但需要有(111NMN)这些关系;可以用文字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息;

2. 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;

3. 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务;

4. 分析关联,通过对业务的更深入分析以及各种软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不需要的关联;

5. 找出聚合边界及聚合根,这是一件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所以,需要我们平时一些分析经验的积累才能找出正确的聚合根;

6. 为聚合根配备仓储,一般情况下是为一个聚合分配一个仓储,此时只要设计好仓储的接口即可;

7. 走查场景,确定我们设计的领域模型能够有效地解决业务需求;

8. 考虑如何创建领域实体或值对象,是通过工厂还是直接通过构造函数;

9. 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地方,比如思考一些对象应该通过关联导航得到还是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等;

领域建模是一个不断重构,持续完善模型的过程,大家会在讨论中将变化的部分反映到模型中,从而是模型不断细化并朝正确的方向走。领域建模是领域专家、设计人员、开发人员之间沟通交流的过程,是大家工作和思考问题的基础。

领域驱动设计的其他一些主题

上面只是涉及到DDD中最基本的内容,DDD中还有很多其他重要的内容在上面没有提到,如:

1. 模型上下文、上下文映射、上下文共享;

2. 如何将分析模式和设计模式运用到DDD中;

3. 一些关于柔性设计的技巧;

4. 如果保持模型完整性,以及持续集成方面的知识;

5. 如何精炼模型,识别核心模型以及通用子领域;

6. ……

这些主题都很重要,因为篇幅有限以及我目前掌握的知识也有限,并且为了突出这篇文章的重点,所以不对他们做详细介绍了,大家有兴趣的可以自己阅读一下。

一些相关的扩展阅读

CQRS架构:

核心思想是将应用程序的查询部分和命令部分完全分离,这两部分可以用完全不同的模型和技术去实现。比如命令部分可以通过领域驱动设计来实现;查询部分可以直接用最快的非面向对象的方式去实现,比如用SQL。这样的思想有很多好处:

1) 实现命令部分的领域模型不用经常为了领域对象可能会被如何查询而做一些折中处理;

2) 由于命令和查询是完全分离的,所以这两部分可以用不同的技术架构实现,包括数据库设计都可以分开设计,每一部分可以充分发挥其长处;

3) 高性能,命令端因为没有返回值,可以像消息队列一样接受命令,放在队列中,慢慢处理;处理完后,可以通过异步的方式通知查询端,这样查询端可以做数据同步的处理;

Event Sourcing

基于DDD的 设计,对于聚合,不保存聚合的当前状态,而是保存对象上所发生的每个事件。当要重建一个聚合对象时,可以通过回溯这些事件(即让这些事件重新发生)来让对 象恢复到某个特定的状态;因为有时一个聚合可能会发生很多事件,所以如果每次要在重建对象时都从头回溯事件,会导致性能低下,所以我们会在一定时候为聚合 创建一个快照。这样,我们就可以基于某个快照开始创建聚合对象了。

DCI架构:

DCI架构强调,软件应该真实的模拟现实生活中对象的交互方式,代码应该准确朴实的反映用户的心智模型。在DCI中有:数据模型、角色模型、以及上下文这三个概念。数据模型表示程序的结构,目前我们所理解的DDD中 的领域模型可以很好的表示数据模型;角色模型表示数据如何交互,一个角色定义了某个“身份”所具有的交互行为;上下文对应业务场景,用于实现业务用例,注 意是业务用例而不是系统用例,业务用例只与业务相关;软件运行时,根据用户的操作,系统创建相应的场景,并把相关的数据对象作为场景参与者传递给场景,然 后场景知道该为每个对象赋予什么角色,当对象被赋予某个角色后就真正成为有交互能力的对象,然后与其他对象进行交互;这个过程与现实生活中我们所理解的对 象是一致的;

DCI的这种思想与DDD中的领域服务所做的事情是一样的,但实现的角度有些不同。DDD中 的领域服务被创建的出发点是当一些职责不太适合放在任何一个领域对象上时,这个职责往往对应领域中的某个活动或转换过程,此时我们应该考虑将其放在一个服 务中。比如资金转帐的例子,我们应该提供一个资金转帐的服务,用来对应领域中的资金转帐这个领域概念。但是领域服务内部做的事情是协调多个领域对象完成一 件事情。因此,在DDD中的领域服务在协调领域对象做事情时,领域对象往往是处于一个被动的地位,领域服务通知每个对象要求其做自己能做的事情,这样就行了。这个过程中我们似乎看不到对象之间交互的意思,因为整个过程都是由领域服务以面向过程的思维去实现了。而DCI则通用引入角色,赋予角色以交互能力,然后让角色之间进行交互,从而可以让我们看到对象与对象之间交互的过程。但前提是,对象之间确实是在交互。因为现实生活中并不是所有的对象在做交互,比如有ABC三个对象,A通知B做事情,A通知C做事情,此时可以认为ABAC之间是在交互,但是BC之间没有交互。所以我们需要分清这种情况。资金转帐的例子,A相当于转帐服务,B相当于帐号1C相当于帐号2。因此,资金转帐这个业务场景,用领域服务比较自然。有人认为DCI可以替换DDD中的领域服务,我持怀疑态度。

四色原型分析模式:

1) 时刻时间段原型(Moment-Interval Archetype

表示在某个时刻或某一段时间内发生的某个活动。使用粉红色表示,简写为MI

2) 参与方地点物品原型(Part-Place-Thing Archetype

表示参与某个活动的人或物,地点则是活动的发生地。使用绿色表示。简写为PPT

3) 描述原型(Description Archetype

表示对PPT的本质描述。它不是PPT的分类!Description是从PPT抽象出来的不变的共性的属性的集合。使用蓝色表示,简写为DESC

举 个例子,有一个人叫张三,如果某个外星人问你张三是什么?你会怎么说?可能会说,张三是个人,但是外星人不知道“人”是什么。然后你会怎么办?你就会说: 张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经可以借用这个例子向大家说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但我们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。所以就有所谓的张三是人的说法。

4) 角色原型(Role Archetype

角色就是我们平时所理解的“身份”。使用黄色表示,简写为Role。为什么会有角色这个概念?因为有些活动,只允许具有特定角色(身份)的PPT(参 与者)才能参与该活动。比如一个人只有具有教师的角色才能上课(一种活动);一个人只有是一个合法公民才能参与选举和被选举;但是有些活动也是不需要角色 的,比如一个人不需要具备任何角色就可以睡觉(一种活动)。当然,其实说人不需要角色就能睡觉也是错误的,错在哪里?因为我们可以这样理解:一个客观存在 只要具有“人”的角色就能睡觉,其实这时候,我们已经把DESC当作角色来看待了。所以,其实角色这个概念是非常广的,不能用我们平时所理解的狭义的“身份”来理解,因为“教师”、“合法公民”、“人”都可以被作为角色来看待。因此,应该这样说:任何一个活动,都需要具有一定角色的参与者才能参与。

用一句话来概括四色原型就是:一个什么什么样的人或组织或物品以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动就是MI

以上这些东西如果在学习了DDD之后再去学习会对DDD有更深入的了解,但我觉得DDD相对比较基础,如果我们在已经了解了DDD的基础之上再去学习这些东西会更加有效和容易掌握。

希望本文对大家有所帮助。

[转载]ASP.NET MVC 使用二级域名来注册Area区域

mikel阅读(1171)

[转载]ASP.NET MVC 使用二级域名来注册Area区域 – jobily – 博客园.

关于ASP.NET MVC 实现二级域名,已有讲解,请见博客园:http://www.cnblogs.com/luanwey/archive/2009/08/12/1544444.html

现在我要说的是怎么用routing engine (System.Web.Routing) 来自动注册区域以实现多个Views。如一个做区域性的网站,像58、赶集网等,每个城市都分站,而每个城市的城市的页面大致相同,只是根据每个城市去取 数据!现在我们要完成一种功能,如网址:cs.8fdc.com 则加载的是长沙的视图,而sz.8fdc.com 则加载的是深圳的视图。而每个城市的视图在结构位置上可能不同,所以又要可以单独去写该城市的视图。

所以我们要的功能则是:cs.8fdc.com 应该是要匹配到 {area}.8fdc.com  这个路上滴! 如图 :我们要匹配到 Areas上的 cd或者 cs文件夹下的视图。

关于 注册 Areas里的视图,微软为我们提供了一种方法:

public class AreaRouting : AreaRegistration
{
public override string AreaName
{
get { return "cs"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Default1", // 路由名称
"cs/{controller}/{action}/{id}", // 带有参数的 URL
new
{
action = "Index",
id = UrlParameter.Optional
},
null,
new string[] { "HB.Controllers.WebSite" } // 选择命名空间,此为HB.Controllers.WebSite的命名空间
);
}
public static void RegisterRoutes(RouteCollection routes)
{
//routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
}
}

继承 AreaRegistration ,重写方法则可注册区域,当我们使用 http://localhost/cs/…, 则会注册区域为cs,并匹配到该路由。

但是我们要的是 http://cs.8fdc.com/…这种 ,也就是我们要使用 http://{area}.8fdc.com 这种方式,根据 ASP.NET MVC 实现二级域名 这篇文章 我们可以这样做

routes.Add("DomainRoute", new DomainRoute(
"{area}.8fdc.com", // Domain with parameters< br />
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
null,
new string[] { "HB.Controllers.WebSite" }
));

但实际上是无法去注册区域的,我们只有将Route Data中的area写入了,并没有将 Data Tokens的 area。可使用 RouteDebug.dll 观察路由信息即可知!所以我们需修改 DomainRoute方法!在DomainRoute.cs 类里修改此处

// 路由数据
RouteData data = null;
if (domainMatch.Success && pathMatch.Success)
{
data = new RouteData(this, RouteHandler);
// 添加默认选项
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key] = item.Value;
#region 此处将area及Namespaces写入DataTokens里
if (item.Key.Equals("area") || item.Key.Equals("Namespaces"))
{
data.DataTokens[item.Key]=item.Value;
}
#endregion
}
}
// 匹配域名路由
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group = domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
#region 新增将area写入到DataTokens中
if (key.Equals("area"))
{
data.DataTokens[key]=group.Value;
}
#endregion
}
}
}
}
// 匹配域名路径
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group = pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
#region 新增将area写入到DataTokens中
if (key.Equals("area"))
{
data.DataTokens[key]= group.Value;
}
#endregion
}
}
}
}
}
return data;

然后在 Global.asax 加入

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("DomainRouteForManage", new DomainRoute(
"manage.8fdc.com", // 固定的二级域名
"{controller}/{action}/{id}", // URL with parameters
new
{
area = "Manage",
controller = "Home",
action = "Index",
id = "",
Namespaces = new string[] { "HB.Controllers.Manage" }
} // Parameter defaults
));
routes.Add("DomainRouteForMutiWebSite", new DomainRoute(
"{area}.8fdc.com", // {area}作为二级域名
"{controller}/{action}/{id}", // URL with parameters
new
{
area = "cs",
controller = "Home",
action = "Index",
id = "",
Namespaces = new string[] { "HB.Controllers.WebSite" }
} // Parameter defaults
));
routes.MapRoute(
"Default", // 路由名称
"{area}/{controller}/{action}/{id}", // 带有参数的 URL
new
{
area = "cs",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
},
null,
new string[] { "HB.Controllers.WebSite" } // 参数默认值
);
}

第一个 routes.Add 是使用固定的二级域名去 注册区域 ,如图:

第二个routes.Add则是使用 {area}作为二级域名。

最后routes.MapRoute 则是默认的路由方法 ,没有注册区域的功能,直接使用根目录下默认的Views里的视图

此方法则可以实现 多个区域使用同一个控制器,而且不要重写AreaRegistration里的方法,可实现根据area的名称来动态注册区域,如果我们增加城市站点的话,只要复制Areas里的cs文件夹之类的就可以了!

项目源代码 :http://download.csdn.net/detail/jobily/3668911

发布在csdn上,因为csdn上的号没分了,希望大家帮我加加分!

再附上一个51cto的链接 :http://down.51cto.com/data/262527

文章写的很差,大家可以下载代码研究,不同可以提问问!

题外话 : 一直说要到博客园上写文章,结果还是没能坚持,真恨自己太懒了。这个功能其实已经写好几个月了,一直说要发布出来,结果还是到了现在才发布出来,而且文章 还是写得那么乱,而且还写了整个晚上,真是没写作的天赋!不过还是得坚持把一些东西写出来,分享给大家一起进步!


[转载]ASP.NET MVC 实现二级域名

mikel阅读(825)

[转载]ASP.NET MVC 实现二级域名 – 大陆响尾蛇 – 博客园.

自从微软发布 ASP.NET MVC 和routing engine (System.Web.Routing)以来,就设法让我们明白你完全能控制URL和routing,只要与你的application path相结合进行扩展,任何问题都迎刃而解。如果你需要在所处的域或者子域处理数据标记的话,强制使用Default。

遗憾的是,ASP.NET MVC是基于虚拟目录的,在实际项目却有各种各样的需求方案。

例如:

1:应用程序是多语言的,像cn.example.com应该被匹配到“www.{language}example.com”路由上。

2:应用程序是多用户的,像username.example.com应该被匹配到“www.{clientname}.example.com”路由上。

3:应用程序是多子域的,像mobile.example.com应该被匹配到”www.{controller}.example.com/{action}….” 。

坐下来,深呼吸,开始我们ASP.NET MVC的神奇之旅吧。

定义routes

下面是我们定义简单的route,不带任何controller控制的route:


routes.Add(DomainRoute, new DomainRoute(
home.example.com, // Domain with parameters
{action}/{id}, // URL with parameters
new { controller = Home, action = Index, id = “” } // Parameter defaults
));

另一个例子是用我们的controller控制域名:


routes.Add(DomainRoute, new DomainRoute(
{controller}.example.com, // Domain with parameters< br />    “{action}/{id}”, // URL with parameters
new { controller = Home, action = Index, id = “” } // Parameter defaults
));

打算用controller 和action完全控制域名?


routes.Add(DomainRoute, new DomainRoute(
{controller}-{action}.example.com, // Domain with parameters
{id}, // URL with parameters
new { controller = Home, action = Index, id = “” } // Parameter defaults
));

接下来是多语言route:


routes.Add(DomainRoute, new DomainRoute(
{language}.example.com, // Domain with parameters
{controller}/{action}/{id}, // URL with parameters
new { language = en, controller = Home, action = Index, id = “” } // Parameter defaults
));

HtmlHelper 扩展方法

因为我们不希望所有的URL所产生HtmlHelper ActionLink要使用full URLs,第一件事我们会添加一些新的ActionLink,其中载有boolean flag是否要full URLs或没有。利用这些,现在您可以添加一个链接到一个Action如下:

<%= Html.ActionLink(About, About, Home, true)%>

跟你以往的习惯没有什么不同,不是吗?
以下是一小段代码:


public static class LinkExtensions
{
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, bool requireAbsoluteUrl)
{
return htmlHelper.ActionLink(linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary(), requireAbsoluteUrl);
}

// more of these

public static string ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool requireAbsoluteUrl)
{
if (requireAbsoluteUrl)
{
HttpContextBase currentContext
= new HttpContextWrapper(HttpContext.Current);
RouteData routeData
= RouteTable.Routes.GetRouteData(currentContext);

routeData.Values[controller] = controllerName;
routeData.Values[
action] = actionName;

DomainRoute domainRoute = routeData.Route as DomainRoute;
if (domainRoute != null)
{
DomainData domainData
= domainRoute.GetDomainData(new RequestContext(currentContext, routeData), routeData.Values);
return htmlHelper.ActionLink(linkText, actionName, controllerName, domainData.Protocol, domainData.HostName, domainData.Fragment, routeData.Values, null);
}
}
return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
}
}

在这没什么特别的:有许多的扩展方法,把扩展的URL加到域名上。这是一个预设ActionLink helpers,我的精神食粮来了DomainRoute class(详见:Dark Magic)

Dark magic

瞥眼之间,您可能已经看到了我的DomainRoute类代码段。这个类实际上是提取子域,并增加了象征性支持域部分的传入的URL,

我们将扩展基类,它已经给了我们一些属性和方法,但是我们得重写他们!


public class DomainRoute : Route
{
//

public string Domain { get; set; }

//

public override RouteData GetRouteData(HttpContextBase httpContext)
{
// 构造regex
domainRegex = CreateRegex(Domain);
pathRegex
= CreateRegex(Url);

// 请求信息
string requestDomain = httpContext.Request.Headers[host];
if (!string.IsNullOrEmpty(requestDomain))
{
if (requestDomain.IndexOf(:) > 0)
{
requestDomain
= requestDomain.Substring(0, requestDomain.IndexOf(:));
}
}
else
{
requestDomain
= httpContext.Request.Url.Host;
}
string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

//匹配域名和路由

Match domainMatch
= domainRegex.Match(requestDomain);
Match pathMatch
= pathRegex.Match(requestPath);

// Route 数据

RouteData data
= null;
if (domainMatch.Success && pathMatch.Success)
{
data
= new RouteData(this, RouteHandler);

// 添加默认选项
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key]
= item.Value;
}
}

// 匹配域名路由
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group
= domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key]
= group.Value;
}
}
}
}

// 匹配域名路径
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group
= pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key]
= group.Value;
}
}
}
}
}

return data;
}

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
}

public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
{
// 获得主机名

string hostname = Domain;
foreach (KeyValuePair<string, object> pair in values)
{
hostname
= hostname.Replace({ + pair.Key + }, pair.Value.ToString());
}

// Return 域名数据

return new DomainData
{
Protocol
= http,
HostName
= hostname,
Fragment
= “”
};
}

//
}

哇,这是一串按照我们定义的route转换传入请求的URL到tokens的代码,我们这样做是转换{controller}和按照regex然后再尝试匹配route规则,在我们的DomainRoute class里还有其他的helper方法,需要更多的功能可以自己研究扩展。

附代码:附件
(如果要在使用Visual Studio开发Web服务器,务必添加把二级域名添加到hosts文件)(貌似本地测试不用)
原文地址:http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
其实有的人为什么要这么麻烦用这种方式,URL重写或者二级域名直接绑定都可以。但是既然微软给url rouring,就应该能做到。

[转载]json的使用 前后台统一以对象的方式编程

mikel阅读(1134)

[转载]痕网,痕网博客,痕网工作室 – henw.

前台插件的介绍

JQuery.json 插件{JQuery插件}

主要方法:

$.toJSON(json对象): 将json对象转化为字符串

$.evalJSON(str): 将字符串转化为json对象

插件下载: jquery.json-2.3.min.js

后台工具的介绍

json.net

主要方法:

将对象转为json字符串:

1 User u = new User() { Id = 1000, Name = "小白", Age = 50 };
2 string returnStr=JsonConvert.SerializeObject(u);

将json格式字符串转为对象:

1 User user = JsonConvert.DeserializeObject<User>(Request["data"]);

下载: Newtonsoft.Json.zip

前台json的读取

方式jQuery:

jQuery.getJSON(url, [data], [callback])    返回值:XMLHttpRequest

参数
urlString

发送请求地址。

data (可选)Map

待发送 Key/value 参数。

callback (可选)Function

载入成功时回调函数。

示例:

1. 两个参数

1 jQuery.getJSON("Json.aspx", function (json) {
2 _json = json;
3 $("#TextArea1").val(json);
4 $("#Text1").val(json.Id);
5 $("#Text2").val(json.Name);
6 $("#Text3").val(json.Age);
7 })

2. 3个参数

1 jQuery.getJSON("SetJson.aspx", { data: $.toJSON(_json) }, function (json) {
2 $("#Text4").val(json.Id);
3 $("#Text5").val(json.Name);
4 $("#Text6").val(json.Age);
5 })

{ data: $.toJSON(_json) } : 可以是字符串, 也可以是json数据

$.toJSON: 是 jquery.json 插件提供的方法, 将json对象转换为字符串

后台json的处理:

需要用到 Json.net

用户类:

01 public class User
02 {
03 private long id;
04
05 public long Id
06 {
07 get { return id; }
08 set { id = value; }
09 }
10
11 private string name;
12
13 public string Name
14 {
15 get { return name; }
16 set { name = value; }
17 }
18
19 private int age;
20
21 public int Age
22 {
23 get { return age; }
24 set { age = value; }
25 }
26 }

将对象转为json字符串:

1 User u = new User() { Id = 1000, Name = "小白", Age = 50 };
2 string returnStr=JsonConvert.SerializeObject(u);

将json格式字符串转为对象:

1 User user = JsonConvert.DeserializeObject<User>(Request["data"]);

整项目简单示例:

1. 主页面

01 <head runat="server">
02 <title></title>
03 <script src="Scripts/jquery-1.4.1.min.js" type="text/JavaScript"></script>
04 <script src="Scripts/jquery.json-2.3.min.js" type="text/JavaScript"></script>
05 <script type="text/javascript">
06 var _json;
07 function GetServerJson()
08 {
09 jQuery.getJSON("Json.aspx", function (json) {
10 _json = json;
11 $("#Text1").val(json.Id);
12 $("#Text2").val(json.Name);
13 $("#Text3").val(json.Age);
14 })
15 }
16 function SetServerUser() {
17 _json.Id = $("#Text1").val();
18 _json.Name = $("#Text2").val();
19 _json.Age = $("#Text3").val();
20 jQuery.getJSON("SetJson.aspx", { data: $.toJSON(_json) }, function (json) {
21 $("#Text4").val(json.Id);
22 $("#Text5").val(json.Name);
23 $("#Text6").val(json.Age);
24 })
25 }
26 </script>
27 </head>
28 <body>
29 <form id="form1" runat="server">
30 <div>
31 <input id="Button1" type="button" value="获取Json" onclick="GetServerJson()" />
32 </div>
33 <br />
34 id:<input id="Text1" type="text" />
35 名字:<input id="Text2" type="text" />
36 年龄:<input id="Text3" type="text" />
37 <input id="Button2" type="button" value="修改" onclick="SetServerUser()" />
38 </form>
39 <div>修改之后的值: <br />
40 id:<input id="Text4" type="text" />
41 名字:<input id="Text5" type="text" />
42 年龄:<input id="Text6" type="text" /></div>
43 </body>

image

点击”获取Json” 从Json.aspx 获取User对象

点击”修改”将第一行修改后的对象 传到SetJson.aspx中, 然后恢复对象接着返回恢复的对象 在页面第二行数据中显示

2. Json.aspx

1 protected void Page_Load(object sender, EventArgs e)
2 {
3 User u = new User() { Id = 1000, Name = "小白", Age = 50 };
4 returnStr=JsonConvert.SerializeObject(u);
5 }

3. SetJson.aspx

1 protected void Page_Load(object sender, EventArgs e)
2 {
3 User user = JsonConvert.DeserializeObject<User>(Request["data"]);
4 returnStr = JsonConvert.SerializeObject(user);
5 }

项目源码下载: Json.zip