0%

上一篇:《DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(1)

阅读目录:

  1. 抽离 IRepository 并改造 Repository

  2. IUnitOfWork 和 Application Service 的变化

  3. 总结三种设计方案

简单总结上篇所做的两个改进:

  • 从 Repository 和 UnitOfWork 中抽离出 IDbContext,并且它们只依赖于 IDbContext。
  • Repository 和 UnitOfWork 为平级关系,UnitOfWork 负责维护对象状态(增删改),Repository 负责获取对象(查)。

后来,园友 Qlin 在评论中,提出了另外一种方式,大致为:

  • Repository 和 UnitOfWork 还是依赖于 IDbContext。
  • UnitOfWork 只有 Commit,Repository 提供对象的所有操作(增删改查)。

这篇文章我们就按照这种方式实现一下,关于 Repository、IUnitOfWork 和 IDbContext 的设计,以及 Application Service 的调用,上面是两种设计方案,加上上一篇博文开头说到的一种方案,我大致总结了三种,关于它们的优缺点,文章最后我再进行总结。

另外,关于 IDbContext 的接口设计,其实是有些模糊的,因为它并没有真正解耦 EF,比如 DbSet<TEntity> Set<TEntity>() 还是依赖于 EF,没办法,就像我们在 Repository 中返回 IQueryable,你在 Application Service 调用的时候,也必须引用 EF 一样,对于 IDbContext 来说,我们暂时把它看作是一个数据上下文容器,所有对象的持久化最后都通过它来完成,因为我们的解决方案暂时只能使用 EF,所以对于 IDbContext,我们先暂时这样设计。

下面我们开始进行设计。

1. 抽离 IRepository 并改造 Repository

抽离 IRepository 啥意思?我们直接来看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace DDD.Sample.Domain.IRepository
{
public interface IRepository<TAggregateRoot>
where TAggregateRoot : class, IAggregateRoot
{
void Add(TAggregateRoot aggregateRoot);

void Update(TAggregateRoot aggregateRoot);

void Delete(TAggregateRoot aggregateRoot);

TAggregateRoot Get(int id);
}
}

IRepository 是一个泛型接口,类型为 IAggregateRoot,我们在里面定义了增删改查的常用操作,它的作用就是减少 Repository 的冗余代码,我们看下 IStudentRepository 的定义:

1
2
3
4
5
6
7
namespace DDD.Sample.Domain.IRepository
{
public interface IStudentRepository : IRepository<Student>
{
Student GetByName(string name);
}
}

IStudentRepository 需要继承 IRepository,并确定泛型类型为 Student,Student 继承自 IAggregateRoot,因为增删改查常用操作已经定义,所以我们在其它类似的 IStudentRepository 中就不需要定义了。

IRepository 需要进行实现,如果在 StudentRepository 中进行实现,就没有什么作用了,所以我们需要一个 BaseRepository 来实现 IRepository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace DDD.Sample.Repository
{
public abstract class BaseRepository<TAggregateRoot> : IRepository<TAggregateRoot>
where TAggregateRoot : class, IAggregateRoot
{
public readonly IDbContext _dbContext;

public BaseRepository(IDbContext dbContext)
{
_dbContext = dbContext;
}

public void Add(TAggregateRoot aggregateRoot)
{
_dbContext.Set<TAggregateRoot>().Add(aggregateRoot);
}

public void Update(TAggregateRoot aggregateRoot)
{
_dbContext.Entry<TAggregateRoot>(aggregateRoot).State = EntityState.Modified;
}

public void Delete(TAggregateRoot aggregateRoot)
{
_dbContext.Set<TAggregateRoot>().Remove(aggregateRoot);
}

public TAggregateRoot Get(int id)
{
return _dbContext.Set<TAggregateRoot>().FirstOrDefault(t => t.Id == id);
}
}
}

咋一看 BaseRepository 有点像我们上篇的 UnitOfWork,因为我们把增删改放在 Repository 了,因为 Repository 还是和 UnitOfWork 为平级关系,所以我们在 Repository 中用的 IDbContext 而非 IUnitOfWork,这个没什么问题,我们看下 StudentRepository 的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace DDD.Sample.Repository
{
public class StudentRepository : BaseRepository<Student>, IStudentRepository
{
public StudentRepository(IDbContext dbContext)
: base(dbContext)
{
}

public Student GetByName(string name)
{
return base._dbContext.Set<Student>().Where(x => x.Name == name).FirstOrDefault();
}
}
}

StudentRepository 很简单,因为常用操作 BaseRepository 已经实现了,base(dbContext) 的作用就是给 BaseRepository 注入 IDbContext 对象。

Repository 的改造基本上就这些,表面看起来确实很好,另外,如果没有 IUnitOfWork 和 Application Service,我们对 Domain 进行单元测试,也是能满足我们的需求,但需要将 IDbContext 再进行修改下。

2. IUnitOfWork 和 Application Service 的变化

我们先看下 IUnitOfWork 的变化,直接贴下代码:

1
2
3
4
5
6
7
8
9
namespace DDD.Sample.Infrastructure.Interfaces
{
public interface IUnitOfWork
{
bool Commit();

void Rollback();
}
}

因为增删改都移到 Repository 中了,所以 IUnitOfWork 的工作就很简单,只有 Commit 和 Rollback,实现也比较简单,我们看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace DDD.Sample.Infrastructure
{
public class UnitOfWork : IUnitOfWork
{
private IDbContext _dbContext;

public UnitOfWork(IDbContext dbContext)
{
_dbContext = dbContext;
}

public bool Commit()
{
return _dbContext.SaveChanges() > 0;
}

public void Rollback()
{
throw new NotImplementedException();
}
}
}

这个没啥说的,我们直接看下 Application Service 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace DDD.Sample.Application
{
public class StudentService : IStudentService
{
private IUnitOfWork _unitOfWork;
private IStudentRepository _studentRepository;
private ITeacherRepository _teacherRepository;

public StudentService(IUnitOfWork unitOfWork,
IStudentRepository studentRepository,
ITeacherRepository teacherRepository)
{
_unitOfWork = unitOfWork;
_studentRepository = studentRepository;
_teacherRepository = teacherRepository;
}

public Student Get(int id)
{
return _studentRepository.Get(id);
}

public bool Add(string name)
{
var student = new Student { Name = name };
var teacher = _teacherRepository.Get(1);
teacher.StudentCount++;

_studentRepository.Add(student);
_teacherRepository.Update(teacher);
return _unitOfWork.Commit();
}
}
}

StudentService 其实变化不大,只是将原来的 _unitOfWork 添加修改操作,改成了 _studentRepository 和 _teacherRepository,执行下 StudentService.Add 的单元测试代码,发现执行不通过,为什么呢?因为 Repository 和 UnitOfWork 的 IDbContext 不是同一个对象,添加修改对象通过 Repository 注册到 IDbContext 中,最后 UnitOfWork 执行 Commit 却是另一个 IDbContext,所以我们需要确保 Repository 和 UnitOfWork 共享一个 IDbContext 对象,怎么实现呢?

我们进行改造下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace DDD.Sample.Application
{
public class StudentService : IStudentService
{
private IDbContext _dbContext;
private IUnitOfWork _unitOfWork;
private IStudentRepository _studentRepository;
private ITeacherRepository _teacherRepository;

public StudentService(IDbContext dbContext)
{
_dbContext = dbContext;
}

public Student Get(int id)
{
_studentRepository = new StudentRepository(_dbContext);
return _studentRepository.Get(id);
}

public bool Add(string name)
{
_unitOfWork = new UnitOfWork(_dbContext);
_studentRepository = new StudentRepository(_dbContext);
_teacherRepository = new TeacherRepository(_dbContext);

var student = new Student { Name = name };
var teacher = _teacherRepository.Get(1);
teacher.StudentCount++;

_studentRepository.Add(student);
_teacherRepository.Update(teacher);
return _unitOfWork.Commit();
}
}
}

上面对应的测试代码执行通过,其实解决方式很简单,就是手动给 UnitOfWork、StudentRepository 和 TeacherRepository 注入相同的 IDbContext 对象,当然这是一种解决方式,还有人喜欢用属性注入,这都是可以的,无非最后就是想让 Repository 和 UnitOfWork 共享一个 IDbContext 对象。

本篇的相关代码已提交到 GitHub,大家可以参考下:https://github.com/yuezhongxin/DDD.Sample

3. 总结三种设计方案

关于 Repository、IUnitOfWork 和 IDbContext 的设计,以及 Application Service 的调用,我总结了三种设计方式,我觉得也是我们常用的几种方式,下面我大致分别说下。

1. IUnitOfWork -> EfUnitOfWork -> Repository -> Application Service

这种设计应该我们最熟悉,因为我们一开始就是这样设计的,但问题也是最多的,要不然我也不会写上一篇博文了,比如存在的问题:

  • IUnitOfWork 的职责不明确。
  • Repository 的职责不明确。
  • Application Service 很困惑,因为它不知道该使用谁。
  • Application Service 的代码越来越乱。
  • ….

上一篇博文最后分析出来是 IUnitOfWork 的设计问题,因为它做了太多的事,并且 Repository 依赖于 IUnitOfWork,以至于最后在 Application Service 的调用中,Repository 显得非常多余,这种设计最大的问题就是职责不明确

2. IDbContext -> IUnitOfWork/IRepository(only query) -> UnitOfWork/Repository -> Application Service

第二种设计是我比较倾向于的,因为第一种设计出现的问题,所以我对 IUnitOfWork 的设计非常看重,并且我读了《企业应用架构模式》中关于 UnitOfWork 的所有内容,其实就那么几个字可以概括:维护对象状态,统一提交更改。我个人觉得架构设计最重要的地方就是底层接口的设计,就像我们盖一栋摩天大楼,如果地基打不稳,最后的结果肯定是垮塌,所以,我比较坚持 IUnitOfWork 这样的设计:

相对于第一种设计,这种设计还有一个不同就是 IUnitOfWork 和 IRepository 为平级关系,为什么这样设计?因为我们不能通过 IUnitOfWork 提供查询操作,并且 IUnitOfWork 和 ORM 也没什么关系,所以我们最后抽离出来一个 IDbContext,并且用 EF 去实现它。

IRepository 只有查询,这是我们的定义,在 Application Service 的调用中,对象的新增和修改都是通过 IUnitOfWork 进行实现的,因为查询并不需要记录状态,所以我们并不需要将 IDbContext 在 IUnitOfWork 和 IRepository 之间进行共享,有人会说,IRepository 应该提供领域对象的增删改操作啊,我们再看下 Repository 的定义:协调领域和数据映射层,利用类似于集合的接口来访问领域对象。

集合访问领域对象,那 Repository 如果这样设计呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StudentRepository : IStudentRepository
{
private IQueryable<Student> _students;

public StudentRepository(IDbContext dbContext)
{
_students = dbContext.Set<Student>();
}

public Student GetByName(string name)
{
return _students.Where(x => x.Name == name).FirstOrDefault();
}
}

这种 Repository 设计是比较符合定义的,另外,我们如果对 Domain 进行单元测试,集合性质的领域对象也是可以进行维护的,只不过没有持久化而已。

总的来说,第二种设计最大的优点就是职责明确,你想干坏事也干不了(因为接口已经被约束),目前来说没发现什么问题。

3. IDbContext -> IUnitOfWork(only commit)/IRepository -> UnitOfWork/Repository -> Application Service

第三种设计就是本篇博文讲述的,它其实是从第一种和第二种之间取一个中间值,做了一些妥协工作,具体的实现,上面已经详细说明了,我最接受不了的是对 IUnitOfWork 的更改,虽然表面看起来蛮好的,但我总觉得有些不对劲的地方,就像我们“迫于现实做一些违背道德的事”,可能现在觉察不到什么,但出来混的总是要还的。

关于 Repository、IUnitOfWork 和 IDbContext 的设计,以及 Application Service 的调用,我觉得应该是我们在 DDD 架构设计过程中,最普遍遇到的一个问题,但也是最困惑的一个问题,比如最近两个园友写的博文:

对于本篇博文,如果你有什么问题或疑问,欢迎探讨学习。😃

C#软件授权、注册、加密、解密模块源码解析并制作注册机生成license - 吴土炮Jared - 博客园

Excerpt

最近做了一个绿色免安装软件,领导临时要求加个注册机制,不能让现场工程师随意复制。事出突然,只能在现场开发(离开现场软件就不受我们控了)。花了不到两个小时实现了简单的注册机制,稍作整理。 基本原理:1.软件一运行就把计算机的CPU、主板、BIOS、MAC地址记录下来,然后加密(key=key1)生成文


最近做了一个绿色免安装软件,领导临时要求加个注册机制,不能让现场工程师随意复制。事出突然,只能在现场开发(离开现场软件就不受我们控了)。花了不到两个小时实现了简单的注册机制,稍作整理。
        基本原理:1.软件一运行就把计算机的CPU、主板、BIOS、MAC地址记录下来,然后加密(key=key1)生成文件;2.注册机将该文件内容MD5加密后再进行一次加密(key=key2)保存成注册文件;3.注册验证的逻辑,计算机信息加密后(key=key1)加密md5==注册文件解密(key=key2);
另外,采用ConfuserEx将可执行文件加密;这样别人要破解也就需要点力气了(没打算防破解,本意只想防复制的),有能力破解的人也不在乎破解这个软件了(开发这个软件前后只花了一周时间而已);
        技术上主要三个模块:1.获取电脑相关硬件信息(可参考);2.加密解密;3.读写文件;

        1.获取电脑相关硬件信息代码:
[csharp] view plain copy
public class ComputerInfo  
{  
    public static string GetComputerInfo()  
    {  
        string info = string.Empty;  
        string cpu = GetCPUInfo();  
        string baseBoard = GetBaseBoardInfo();  
        string bios = GetBIOSInfo();  
        string mac = GetMACInfo();  
        info = string.Concat(cpu, baseBoard, bios, mac);  
        return info;  
    }  

      private static string GetCPUInfo()  
    {  
        string info = string.Empty;  
        info = GetHardWareInfo(“Win32_Processor”, “ProcessorId”);  
        return info;  
    }  
    private static string GetBIOSInfo()  
    {  
        string info = string.Empty;  
        info = GetHardWareInfo(“Win32_BIOS”, “SerialNumber”);  
        return info;  
    }  
    private static string GetBaseBoardInfo()  
    {  
        string info = string.Empty;  
        info = GetHardWareInfo(“Win32_BaseBoard”, “SerialNumber”);  
        return info;  
    }  
    private static string GetMACInfo()  
    {  
        string info = string.Empty;  
        info = GetHardWareInfo(“Win32_BaseBoard”, “SerialNumber”);  
        return info;  
    }  
    private static string GetHardWareInfo(string typePath, string key)  
    {  
        try  
        {  
            ManagementClass managementClass = new ManagementClass(typePath);  
            ManagementObjectCollection mn = managementClass.GetInstances();  
            PropertyDataCollection properties = managementClass.Properties;  
            foreach (PropertyData property in properties)  
            {  
                if (property.Name == key)  
                {  
                    foreach (ManagementObject m in mn)  
                    {  
                        return m.Properties[property.Name].Value.ToString();  
                    }  
                }  

              }  
        }  
        catch (Exception ex)  
        {  
            //这里写异常的处理    
        }  
        return string.Empty;  
    }  
    private static string GetMacAddressByNetworkInformation()  
    {  
        string key = “SYSTEM\\CurrentControlSet\\Control\\Network\\{4D36E972-E325-11CE-BFC1-08002BE10318}\\“;  
        string macAddress = string.Empty;  
        try  
        {  
            NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();  
            foreach (NetworkInterface adapter in nics)  
            {  
                if (adapter.NetworkInterfaceType == NetworkInterfaceType.Ethernet  
                    && adapter.GetPhysicalAddress().ToString().Length != 0)  
                {  
                    string fRegistryKey = key + adapter.Id + “\\Connection”;  
                    RegistryKey rk = Registry.LocalMachine.OpenSubKey(fRegistryKey, false);  
                    if (rk != null)  
                    {  
                        string fPnpInstanceID = rk.GetValue(“PnpInstanceID”, “”).ToString();  
                        int fMediaSubType = Convert.ToInt32(rk.GetValue(“MediaSubType”, 0));  
                        if (fPnpInstanceID.Length > 3 &&  
                            fPnpInstanceID.Substring(0, 3) == “PCI”)  
                        {  
                            macAddress = adapter.GetPhysicalAddress().ToString();  
                            for (int i = 1; i < 6; i++)  
                            {  
                                macAddress = macAddress.Insert(3 * i - 1, “:”);  
                            }  
                            break;  
                        }  
                    }  

                  }  
            }  
        }  
        catch (Exception ex)  
        {  
            //这里写异常的处理    
        }  
        return macAddress;  
    }  
}  
        2.加密解密代码;
[csharp] view plain copy
public enum EncryptionKeyEnum  
{  
    KeyA,  
    KeyB  
}  
public class EncryptionHelper  
{  
    string encryptionKeyA = “pfe_Nova”;  
    string encryptionKeyB = “WorkHard”;  
    string md5Begin = “Hello”;  
    string md5End = “World”;  
    string encryptionKey = string.Empty;  
    public EncryptionHelper()  
    {  
        this.InitKey();  
    }  
    public EncryptionHelper(EncryptionKeyEnum key)  
    {  
        this.InitKey(key);  
    }  
    private void InitKey(EncryptionKeyEnum key = EncryptionKeyEnum.KeyA)  
    {  
        switch (key)  
        {  
            case EncryptionKeyEnum.KeyA:  
                encryptionKey = encryptionKeyA;  
                break;  
            case EncryptionKeyEnum.KeyB:  
                encryptionKey = encryptionKeyB;  
                break;  
        }  
    }  

      public string EncryptString(string str)  
    {  
        return Encrypt(str, encryptionKey);  
    }  
    public string DecryptString(string str)  
    {  
        return Decrypt(str, encryptionKey);  
    }  
    public string GetMD5String(string str)  
    {  
        str = string.Concat(md5Begin, str, md5End);  
        MD5 md5 = new MD5CryptoServiceProvider();  
        byte[] fromData = Encoding.Unicode.GetBytes(str);  
        byte[] targetData = md5.ComputeHash(fromData);  
        string md5String = string.Empty;  
        foreach (var b in targetData)  
            md5String += b.ToString(“x2”);  
        return md5String;  
    }  

      private string Encrypt(string str, string sKey)  
    {  
        DESCryptoServiceProvider des = new DESCryptoServiceProvider();  
        byte[] inputByteArray = Encoding.Default.GetBytes(str);  
        des.Key = ASCIIEncoding.ASCII.GetBytes(sKey);  
        des.IV = ASCIIEncoding.ASCII.GetBytes(sKey);  
        MemoryStream ms = new MemoryStream();  
        CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);  
        cs.Write(inputByteArray, 0, inputByteArray.Length);  
        cs.FlushFinalBlock();  
        StringBuilder ret = new StringBuilder();  
        foreach (byte b in ms.ToArray())  
        {  
            ret.AppendFormat(“{0:X2}”, b);  
        }  
        ret.ToString();  
        return ret.ToString();  
    }  
    private string Decrypt(string pToDecrypt, string sKey)  
    {  
        DESCryptoServiceProvider des = new DESCryptoServiceProvider();  
        byte[] inputByteArray = new byte[pToDecrypt.Length / 2];  
        for (int x = 0; x < pToDecrypt.Length / 2; x++)  
        {  
            int i = (Convert.ToInt32(pToDecrypt.Substring(x * 2, 2), 16));  
            inputByteArray[x] = (byte)i;  
        }  
        des.Key = ASCIIEncoding.ASCII.GetBytes(sKey);  
        des.IV = ASCIIEncoding.ASCII.GetBytes(sKey);  
        MemoryStream ms = new MemoryStream();  
        CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write);  
        cs.Write(inputByteArray, 0, inputByteArray.Length);  
        cs.FlushFinalBlock();  
        StringBuilder ret = new StringBuilder();  
        return System.Text.Encoding.Default.GetString(ms.ToArray());  
    }  
}  
        注:这边在MD5时前后各加了一段字符,这样增加一点破解难度。
        3.读写文件
[csharp] view plain copy
public class RegistFileHelper  
{  
    public static string ComputerInfofile = “ComputerInfo.key”;  
    public static string RegistInfofile = “RegistInfo.key”;  
    public static void WriteRegistFile(string info)  
    {  
        WriteFile(info, RegistInfofile);  
    }  
    public static void WriteComputerInfoFile(string info)  
    {  
        WriteFile(info, ComputerInfofile);  
    }  
    public static string ReadRegistFile()  
    {  
        return ReadFile(RegistInfofile);  
    }  
    public static string ReadComputerInfoFile()  
    {  
        return ReadFile(ComputerInfofile);  
    }  
    public static bool ExistComputerInfofile()  
    {  
        return File.Exists(ComputerInfofile);  
    }  
    public static bool ExistRegistInfofile()  
    {  
        return File.Exists(RegistInfofile);  
    }  
    private static void WriteFile(string info, string fileName)  
    {  
        try  
        {  
            using (StreamWriter sw = new StreamWriter(fileName, false))  
            {  
                sw.Write(info);  
                sw.Close();  
            }  
        }  
        catch (Exception ex)  
        {  
        }  
    }  
    private static string ReadFile(string fileName)  
    {  
        string info = string.Empty;  
        try  
        {  
            using (StreamReader sr = new StreamReader(fileName))  
            {  
                info = sr.ReadToEnd();  
                sr.Close();  
            }  
        }  
        catch (Exception ex)  
        {  
        }  
        return info;  
    }  
}  
        4.其他界面代码:
        主界面代码:
[csharp] view plain copy
public partial class FormMain : Form  
{  
    private string encryptComputer = string.Empty;  
    private bool isRegist = false;  
    private const int timeCount = 30;  
    public FormMain()  
    {  
        InitializeComponent();  
        Control.CheckForIllegalCrossThreadCalls = false;  
    }  
    private void FormMain_Load(object sender, EventArgs e)  
    {  
        string computer = ComputerInfo.GetComputerInfo();  
        encryptComputer = new EncryptionHelper().EncryptString(computer);  
        if (CheckRegist() == true)  
        {  
            lbRegistInfo.Text = “已注册”;  
        }  
        else  
        {  
            lbRegistInfo.Text = “待注册,运行十分钟后自动关闭”;  
            RegistFileHelper.WriteComputerInfoFile(encryptComputer);  
            TryRunForm();  
        }  
    }  
    /// 

  
    /// 试运行窗口  
    /// 
  
    private void TryRunForm()  
    {  
        Thread threadClose = new Thread(CloseForm);  
        threadClose.IsBackground = true;  
        threadClose.Start();  
    }  
    private bool CheckRegist()  
    {  
        EncryptionHelper helper = new EncryptionHelper();  
        string md5key = helper.GetMD5String(encryptComputer);  
        return CheckRegistData(md5key);  
    }  
    private bool CheckRegistData(string key)  
    {  
        if (RegistFileHelper.ExistRegistInfofile() == false)  
        {  
            isRegist = false;  
            return false;  
        }  
        else  
        {  
            string info = RegistFileHelper.ReadRegistFile();  
            var helper = new EncryptionHelper(EncryptionKeyEnum.KeyB);  
            string registData = helper.DecryptString(info);  
            if (key == registData)  
            {  
                isRegist = true;  
                return true;  
            }  
            else  
            {  
                isRegist = false;  
                return false;  
            }  
        }  
    }  
    private void CloseForm()  
    {  
        int count = 0;  
        while (count < timeCount && isRegist == false)  
        {  
            if (isRegist == true)  
            {  
                return;  
            }  
            Thread.Sleep(1 * 1000);  
            count++;  
        }  
        if (isRegist == true)  
        {  
            return;  
        }  
        else  
        {  
            this.Close();  
        }  
    }  

      private void btnRegist_Click(object sender, EventArgs e)  
    {  
        if (lbRegistInfo.Text == “已注册”)  
        {  
            MessageBox.Show(“已经注册~”);  
            return;  
        }  
        string fileName = string.Empty;  
        OpenFileDialog openFileDialog = new OpenFileDialog();  
        if (openFileDialog.ShowDialog() == DialogResult.OK)  
        {  
            fileName = openFileDialog.FileName;  
        }  
        else  
        {  
            return;  
        }  
        string localFileName = string.Concat(  
            Environment.CurrentDirectory,  
            Path.DirectorySeparatorChar,  
            RegistFileHelper.RegistInfofile);  
        if (fileName != localFileName)  
            File.Copy(fileName, localFileName, true);  

          if (CheckRegist() == true)  
        {  
            lbRegistInfo.Text = “已注册”;  
            MessageBox.Show(“注册成功~”);  
        }  
    }  
}  
        注册机代码:
[csharp] view plain copy
public partial class FormMain : Form  
{  
    public FormMain()  
    {  
        InitializeComponent();  
    }  

      private void btnRegist_Click(object sender, EventArgs e)  
    {  
        string fileName = string.Empty;  
        OpenFileDialog openFileDialog = new OpenFileDialog();  
        if (openFileDialog.ShowDialog() == DialogResult.OK)  
        {  
            fileName = openFileDialog.FileName;  
        }  
        else  
        {  
            return;  
        }  
        string localFileName = string.Concat(  
            Environment.CurrentDirectory,  
            Path.DirectorySeparatorChar,  
            RegistFileHelper.ComputerInfofile);  

          if (fileName != localFileName)  
            File.Copy(fileName, localFileName, true);  
        string computer = RegistFileHelper.ReadComputerInfoFile();  
        EncryptionHelper help = new EncryptionHelper(EncryptionKeyEnum.KeyB);  
        string md5String = help.GetMD5String(computer);  
        string registInfo = help.EncryptString(md5String);  
        RegistFileHelper.WriteRegistFile(registInfo);  
        MessageBox.Show(“注册码已生成”);  
    }  
}  
        最后采用ConfuserEx将可执行文件加密(ConfuserEx介绍),这样就不能反编译获得源码。
        至此全部完成,只是个人的一些实践,对自己是一个记录,同时希望也能对别人有些帮助,如果有什么错误,还望不吝指出,共同进步,转载请保留原文地址。
————————————————
版权声明:本文为CSDN博主「weixin_37691493」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin\_37691493/article/details/79716050

本文转载自: http://www.cnblogs.com/landeanfen/p/4816706.html 作者:landeanfen 转载请注明该声明。

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的。今天看到一篇博文里面写道:越是忙人越有时间写博客。呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西。前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper、仓储模式、WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前看过这篇 博客园的大牛们,被你们害惨了,Entity Framework从来都不需要去写Repository设计模式 文章的朋友应该还记得,不止是该文章的作者,很多园友在评论里面也提到了使用它的不爽。博主的项目中也遇到类似的问题,虽然引入了仓储模式,但是由于没有架构好,把仓储的接口和实现统一放在了数据访问层,导致到后面代码越写越难维护,完全感觉不到仓储带来的好处。所以博主觉得单纯分享仓储模式很容易使读者陷入“为了模式而模式”的误区,再加上最近一段时间在看《领域驱动设计:软件核心复杂性应对之道.Eric.Eva》这本书和博客园大牛dax.net的DDD系列文章,所以打算分享一个Demo来说明仓储模式、Automapper、WCF等知识点。

DDD领域驱动设计初探系列文章:

一、领域驱动设计基本概念

根据《领域驱动设计:软件核心复杂性应对之道.Eric.Eva》书中的观点,领域模型是软件项目的公共语言的核心,是领域专家和开发人员共同遵守的通用语言规则,那么在DDD里面,建模的重要性不用多说,所以要想更好理解领域驱动设计,理解领域模型的划分和建立就变得相当必要。首先来看看DDD里面几个比较重要的概念:

1、领域模型:领域模型与数据模型不同,它表述的是领域中各个类及其之间的关系。从领域驱动设计的角度看,数据库只不过是存储实体的一个外部机制,是属于技术层面的东西。数据模型主要用于描述领域模型对象的持久化方式,应该是先有领域模型,才有数据模型,领域模型需要通过某种映射而产生相应的数据模型,从这点来说,最新的EF的Code First就是一个很好的体现。领域模型对象分为实体、值对象和服务。

2、实体:在领域驱动设计里面,实体是模型中需要区分个体的对象。这里的实体和EntityFramework里面的实体不是一个概念,EF的实体为数据实体,不包含对象的行为。就博主的理解,DDD概念里面的实体就是包括实体数据(EF的Model)和行为的结合体

3、值对象:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。相比实体而言,值对象仅仅是比实体少了一个标识。值对象的设计比较存在争议,我们暂且记住值对象和实体的区别:(1)实体拥有唯一标识,而值对象没有;(2)实体允许变化,而值对象不允许变化;(3)判断两个实体相等的方法是判断实体的标识相等,而判断两个值对象相等的标准是值对象内部所有属性值相等;

4、聚合(以及聚合根):聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。博主觉得关于聚合的划分学问还是挺大的,需要在实践中慢慢积累。同一个实体,在不同的聚合中,它可能是聚合根,也可能不是,需要根据实际的业务决定。聚合根是聚合所表述的领域概念的主体,外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问

5、领域服务:博主的理解,领域模型主张富领域模式,也就是说把领域逻辑尽量写在领域实体里面,也就是常说的“充血模式”,而对于业务逻辑,最好是以服务的形式提供。至于领域逻辑和业务逻辑的界定,这个要根据实际情况来定。总之,领域服务是用来处理那些领域模型里面不好定义或者某些可变逻辑的的时候才会用到。待验证!

6、工厂、仓储等概念留在Demo里面说明。

二、领域驱动设计开始之旅

1、项目分层

领域驱动设计将软件系统分为四层:基础结构层、领域层、应用层和表现层。来看看书中的分层:

其实在dax.net的系列中这张图更能说明这种架构

2、项目架构

博主打算用权限系统的案例说明的领域驱动设计的项目架构。项目严格按照表现层、应用层、领域层、基础设施层来划分。

表现层:MVC的Web项目,负责UI呈现。

应用层:WCF服务,负责协调领域层的调用,向UI层提供需要的接口。

领域层:定义领域实体和领域逻辑。

基础设施层:一些通用的技术,比如AOP、MEF注入、通用的工具类、DTO模型层,这里为什么要有一个DTO模型层,DTO是用于UI展现用的纯数据Model,它不包含实体行为,是一种贫血的模型。

整个项目的调用方式严格按照DDD设计来进行,UI层通过WCF服务调用应用层的WCF接口,WCF服务通过仓储调用领域层里面的接口,基础设施层贯穿其他各层,在需要的项目中都可以引用基础设施层里面的内库。

3、代码示例

接下来,博主就根据自己的理解,从零开始使用这种架构写一个简单的权限管理系统。由于是领域驱动设计,所以,文章的重点会放在领域层,项目使用了EF的Model First来进行,先设计实体,后生成数据库。

3.1 首先来看看表结构

 

根据博友要求,这里说明一下表之间的映射关系:

1表示TB_DEPARTMENT表的主键DEPARTMENT_ID作为TB_USERS表的外键;

2表示TB_USERS表的主键USER_ID作为TB_USERROLE表的外键;

3表示TB_ROLE表的主键ROLE_ID作为TB_USERROLE表的外键;

4表示TB_ROLE表的主键ROLE_ID作为TB_MENUROLE表的外键

5表示TB_MENU表的主键MENU_ID作为TB_MENUROLE表的外键

首先建好对应的表实体,然后根据模型生成数据库

将生成的sql语句执行后就可以得到对应的表结构。

3.2 聚合的划分

在领域层里面我们新建一个BaseModel,里面有三个类

这三个类IEntity、IAggregateRoot、AggregateRoot分别定义了实体的接口、聚合根的接口、聚合根的抽象实现类。

//用作泛型约束,表示继承自该接口的为领域实体
public interface IEntity
{

}

/// <summary>
/// 聚合根接口,用作泛型约束,约束领域实体为聚合根,表示实现了该接口的为聚合根实例,由于聚合根也是领域实体的一种,所以也要实现IEntity接口 /// </summary>
public interface IAggregateRoot:IEntity
{

}

///


/// 聚合根的抽象实现类,定义聚合根的公共属性和行为 ///

public abstract class AggregateRoot:IAggregateRoot
{

}

这里定义接口的作用是定义实体和聚合根的泛型约束,抽象类用来定义聚合根的公共行为,目前为止,这些接口和类里面都是空的,后面会根据项目的需求一步一步往里面加入逻辑。

在EF里面由edmx文件会生成实体的属性,前面说到领域模型主张充血模式,所以要在EF的实体model里面加入实体的行为,为了不改变EF生成实体的代码,我们使用partial类来定义实体的行为。我们来看Model文件夹下面的代码

public partial class TB\_DEPARTMENT: AggregateRoot
{ public override string ToString()
    { return base.ToString();
    }
}

public partial class TB\_MENU : AggregateRoot
{

}

/// <summary>
/// 由于不会直接操作此表,所以TB\_MENUROLE实体不必作为聚合根,只是作为领域实体即可 /// </summary>
public partial class TB\_MENUROLE:IEntity
{
}

public partial class TB\_ROLE:AggregateRoot
{
}

public partial class TB\_USERROLE:IEntity
{
}

public partial class TB\_USERS:AggregateRoot
{
}

我们看到,这些实体,只有TB_MENUROLE和TB_USERROLE不是聚合根,其他实体都是聚合根。我这里大概划分为4个聚合:

聚合1:TB_DEPARTMENT实体

聚合2:TB_MENU、TB_MENUROLE、TB_ROLE这3个为一个聚合,聚合根是TB_MENU。

聚合3:TB_USERS、TB_USERROLE、TB_DEPARTMENT、TB_ROLE这4个为一个聚合,聚合根是TB_USERS。

聚合4:TB_ROLE、TB_USERS、TB_USERROLE、TB_MENUROLE、TB_MENU这5个表为一个聚合,聚合根是TB_ROLE。

可能这样分会有一定的问题,后续出现再慢慢纠正。

到这里,聚合的划分基本完了,至于为什么要做这么一些约束和设计,因为仓储只能对聚合根做操作,下篇讲仓储的时候会详细说明。

DDD博大精深,文中很多观点为博主个人理解,可能不太成熟或者有误,欢迎拍砖~~

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的。今天看到一篇博文里面写道:越是忙人越有时间写博客。呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西。前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper、仓储模式、WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前看过这篇 博客园的大牛们,被你们害惨了,Entity Framework从来都不需要去写Repository设计模式 文章的朋友应该还记得,不止是该文章的作者,很多园友在评论里面也提到了使用它的不爽。博主的项目中也遇到类似的问题,虽然引入了仓储模式,但是由于没有架构好,把仓储的接口和实现统一放在了数据访问层,导致到后面代码越写越难维护,完全感觉不到仓储带来的好处。所以博主觉得单纯分享仓储模式很容易使读者陷入“为了模式而模式”的误区,再加上最近一段时间在看《领域驱动设计:软件核心复杂性应对之道.Eric.Eva》这本书和博客园大牛dax.net的DDD系列文章,所以打算分享一个Demo来说明仓储模式、Automapper、WCF等知识点。

DDD领域驱动设计初探系列文章:

一、领域驱动设计基本概念

根据《领域驱动设计:软件核心复杂性应对之道.Eric.Eva》书中的观点,领域模型是软件项目的公共语言的核心,是领域专家和开发人员共同遵守的通用语言规则,那么在DDD里面,建模的重要性不用多说,所以要想更好理解领域驱动设计,理解领域模型的划分和建立就变得相当必要。首先来看看DDD里面几个比较重要的概念:

1、领域模型:领域模型与数据模型不同,它表述的是领域中各个类及其之间的关系。从领域驱动设计的角度看,数据库只不过是存储实体的一个外部机制,是属于技术层面的东西。数据模型主要用于描述领域模型对象的持久化方式,应该是先有领域模型,才有数据模型,领域模型需要通过某种映射而产生相应的数据模型,从这点来说,最新的EF的Code First就是一个很好的体现。领域模型对象分为实体、值对象和服务。

2、实体:在领域驱动设计里面,实体是模型中需要区分个体的对象。这里的实体和EntityFramework里面的实体不是一个概念,EF的实体为数据实体,不包含对象的行为。就博主的理解,DDD概念里面的实体就是包括实体数据(EF的Model)和行为的结合体

3、值对象:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。相比实体而言,值对象仅仅是比实体少了一个标识。值对象的设计比较存在争议,我们暂且记住值对象和实体的区别:(1)实体拥有唯一标识,而值对象没有;(2)实体允许变化,而值对象不允许变化;(3)判断两个实体相等的方法是判断实体的标识相等,而判断两个值对象相等的标准是值对象内部所有属性值相等;

4、聚合(以及聚合根):聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。博主觉得关于聚合的划分学问还是挺大的,需要在实践中慢慢积累。同一个实体,在不同的聚合中,它可能是聚合根,也可能不是,需要根据实际的业务决定。聚合根是聚合所表述的领域概念的主体,外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问

5、领域服务:博主的理解,领域模型主张富领域模式,也就是说把领域逻辑尽量写在领域实体里面,也就是常说的“充血模式”,而对于业务逻辑,最好是以服务的形式提供。至于领域逻辑和业务逻辑的界定,这个要根据实际情况来定。总之,领域服务是用来处理那些领域模型里面不好定义或者某些可变逻辑的的时候才会用到。待验证!

6、工厂、仓储等概念留在Demo里面说明。

二、领域驱动设计开始之旅

1、项目分层

领域驱动设计将软件系统分为四层:基础结构层、领域层、应用层和表现层。来看看书中的分层:

其实在dax.net的系列中这张图更能说明这种架构

2、项目架构

博主打算用权限系统的案例说明的领域驱动设计的项目架构。项目严格按照表现层、应用层、领域层、基础设施层来划分。

表现层:MVC的Web项目,负责UI呈现。

应用层:WCF服务,负责协调领域层的调用,向UI层提供需要的接口。

领域层:定义领域实体和领域逻辑。

基础设施层:一些通用的技术,比如AOP、MEF注入、通用的工具类、DTO模型层,这里为什么要有一个DTO模型层,DTO是用于UI展现用的纯数据Model,它不包含实体行为,是一种贫血的模型。

整个项目的调用方式严格按照DDD设计来进行,UI层通过WCF服务调用应用层的WCF接口,WCF服务通过仓储调用领域层里面的接口,基础设施层贯穿其他各层,在需要的项目中都可以引用基础设施层里面的内库。

3、代码示例

接下来,博主就根据自己的理解,从零开始使用这种架构写一个简单的权限管理系统。由于是领域驱动设计,所以,文章的重点会放在领域层,项目使用了EF的Model First来进行,先设计实体,后生成数据库。

3.1 首先来看看表结构

 

根据博友要求,这里说明一下表之间的映射关系:

1表示TB_DEPARTMENT表的主键DEPARTMENT_ID作为TB_USERS表的外键;

2表示TB_USERS表的主键USER_ID作为TB_USERROLE表的外键;

3表示TB_ROLE表的主键ROLE_ID作为TB_USERROLE表的外键;

4表示TB_ROLE表的主键ROLE_ID作为TB_MENUROLE表的外键

5表示TB_MENU表的主键MENU_ID作为TB_MENUROLE表的外键

首先建好对应的表实体,然后根据模型生成数据库

将生成的sql语句执行后就可以得到对应的表结构。

3.2 聚合的划分

在领域层里面我们新建一个BaseModel,里面有三个类

这三个类IEntity、IAggregateRoot、AggregateRoot分别定义了实体的接口、聚合根的接口、聚合根的抽象实现类。

//用作泛型约束,表示继承自该接口的为领域实体
public interface IEntity
{

}

复制代码

/// <summary>
/// 聚合根接口,用作泛型约束,约束领域实体为聚合根,表示实现了该接口的为聚合根实例,由于聚合根也是领域实体的一种,所以也要实现IEntity接口 /// </summary>
public interface IAggregateRoot:IEntity
{

}

复制代码

复制代码

///


/// 聚合根的抽象实现类,定义聚合根的公共属性和行为 ///

public abstract class AggregateRoot:IAggregateRoot
{

}

复制代码

这里定义接口的作用是定义实体和聚合根的泛型约束,抽象类用来定义聚合根的公共行为,目前为止,这些接口和类里面都是空的,后面会根据项目的需求一步一步往里面加入逻辑。

在EF里面由edmx文件会生成实体的属性,前面说到领域模型主张充血模式,所以要在EF的实体model里面加入实体的行为,为了不改变EF生成实体的代码,我们使用partial类来定义实体的行为。我们来看Model文件夹下面的代码

复制代码

public partial class TB\_DEPARTMENT: AggregateRoot
{ public override string ToString()
    { return base.ToString();
    }
}

复制代码

public partial class TB\_MENU : AggregateRoot
{

}

/// <summary>
/// 由于不会直接操作此表,所以TB\_MENUROLE实体不必作为聚合根,只是作为领域实体即可 /// </summary>
public partial class TB\_MENUROLE:IEntity
{
}

public partial class TB\_ROLE:AggregateRoot
{
}

public partial class TB\_USERROLE:IEntity
{
}

public partial class TB\_USERS:AggregateRoot
{
}

我们看到,这些实体,只有TB_MENUROLE和TB_USERROLE不是聚合根,其他实体都是聚合根。我这里大概划分为4个聚合:

聚合1:TB_DEPARTMENT实体

聚合2:TB_MENU、TB_MENUROLE、TB_ROLE这3个为一个聚合,聚合根是TB_MENU。

聚合3:TB_USERS、TB_USERROLE、TB_DEPARTMENT、TB_ROLE这4个为一个聚合,聚合根是TB_USERS。

聚合4:TB_ROLE、TB_USERS、TB_USERROLE、TB_MENUROLE、TB_MENU这5个表为一个聚合,聚合根是TB_ROLE。

可能这样分会有一定的问题,后续出现再慢慢纠正。

到这里,聚合的划分基本完了,至于为什么要做这么一些约束和设计,因为仓储只能对聚合根做操作,下篇讲仓储的时候会详细说明。

DDD博大精深,文中很多观点为博主个人理解,可能不太成熟或者有误,欢迎拍砖~~


正文

前言:上篇C#进阶系列——WebApi接口传参不再困惑:传参详解介绍了WebApi参数的传递,这篇来看看WebApi里面异常的处理。关于异常处理,作为程序员的我们肯定不陌生,记得在介绍 AOP 的时候,我们讲过通过AOP可以统一截获异常。那么在我们的WebApi里面一般是怎么处理异常的呢,今天这一篇,博主带着大家一起来实践下WebApi的异常处理。

WebApi系列文章

为什么说是实践?因为在http://www.asp.net里面已经明确给出WebApi的异常处理机制。光有理论还不够,今天我们还是来试一把。通过实践,我们可能发现一些更详尽的用法。

我们知道,一般情况下,WebApi作为服务使用,每次客户端发送http请求到我们的WebApi服务里面,服务端得到结果输出response到客户端。这个过程中,一旦服务端发生异常,会统一向客户端返回500的错误。

[HttpGet] public string GetAllChargingData([FromUri]TB_CHARGING obj)
{ throw new NotImplementedException(“方法不被支持”);
}

我们来看看http请求

而有些时候,我们客户端需要得到更加精确的错误码来判断异常类型,怎么办呢?

记得在介绍AOP的时候,我们介绍过MVC里面的IExceptionFilter接口,这个接口用于定义异常筛选器所需的方法,在WebApi里面,也有这么一个异常筛选器,下面我们通过一个实例来看看具体如何实现。

首先在App_Start里面新建一个类WebApiExceptionFilterAttribute.cs,继承ExceptionFilterAttribute,重写OnException方法

复制代码

public class WebApiExceptionFilterAttribute : ExceptionFilterAttribute 
{ //重写基类的异常处理方法
    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    { //1.异常日志记录(正式项目里面一般是用log4net记录异常日志)
        Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "——" + actionExecutedContext.Exception.GetType().ToString() \+ ":" + actionExecutedContext.Exception.Message + "——堆栈信息:" + actionExecutedContext.Exception.StackTrace); //2.返回调用方具体的异常信息
        if (actionExecutedContext.Exception is NotImplementedException)
        {
            actionExecutedContext.Response \= new HttpResponseMessage(HttpStatusCode.NotImplemented);
        } else if (actionExecutedContext.Exception is TimeoutException)
        {
            actionExecutedContext.Response \= new HttpResponseMessage(HttpStatusCode.RequestTimeout);
        } //.....这里可以根据项目需要返回到客户端特定的状态码。如果找不到相应的异常,统一返回服务端错误500
        else {
            actionExecutedContext.Response \= new HttpResponseMessage(HttpStatusCode.InternalServerError);
        } base.OnException(actionExecutedContext);
    }
}

复制代码

 代码解析:通过判断异常的具体类型,向客户端返回不同的http状态码,示例里面写了两个,可以根据项目的实际情况加一些特定的我们想要捕获的异常,然后将对应的状态码写入http请求的response里面,对于一些我们无法判断类型的异常,统一返回服务端错误500。关于http的状态码,framework里面定义了一些常见的类型,我们大概看看:

#region 程序集 System.dll, v4.0.0.0
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.dll
#endregion

using System; namespace System.Net
{ // 摘要: // 包含为 HTTP 定义的状态代码的值。
public enum HttpStatusCode
{ // 摘要: // 等效于 HTTP 状态 100。 System.Net.HttpStatusCode.Continue 指示客户端可能继续其请求。
Continue = 100, //
// 摘要: // 等效于 HTTP 状态 101。 System.Net.HttpStatusCode.SwitchingProtocols 指示正在更改协议版本或协议。
SwitchingProtocols = 101, //
// 摘要: // 等效于 HTTP 状态 200。 System.Net.HttpStatusCode.OK 指示请求成功,且请求的信息包含在响应中。 这是最常接收的状态代码。
OK = 200, //
// 摘要: // 等效于 HTTP 状态 201。 System.Net.HttpStatusCode.Created 指示请求导致在响应被发送前创建新资源。
Created = 201, //
// 摘要: // 等效于 HTTP 状态 202。 System.Net.HttpStatusCode.Accepted 指示请求已被接受做进一步处理。
Accepted = 202, //
// 摘要: // 等效于 HTTP 状态 203。 System.Net.HttpStatusCode.NonAuthoritativeInformation 指示返回的元信息来自缓存副本而不是原始服务器,因此可能不正确。
NonAuthoritativeInformation = 203, //
// 摘要: // 等效于 HTTP 状态 204。 System.Net.HttpStatusCode.NoContent 指示已成功处理请求并且响应已被设定为无内容。
NoContent = 204, //
// 摘要: // 等效于 HTTP 状态 205。 System.Net.HttpStatusCode.ResetContent 指示客户端应重置(或重新加载)当前资源。
ResetContent = 205, //
// 摘要: // 等效于 HTTP 状态 206。 System.Net.HttpStatusCode.PartialContent 指示响应是包括字节范围的 GET // 请求所请求的部分响应。
PartialContent = 206, //
// 摘要: // 等效于 HTTP 状态 300。 System.Net.HttpStatusCode.MultipleChoices 指示请求的信息有多种表示形式。 // 默认操作是将此状态视为重定向,并遵循与此响应关联的 Location 标头的内容。
MultipleChoices = 300, //
// 摘要: // 等效于 HTTP 状态 300。 System.Net.HttpStatusCode.Ambiguous 指示请求的信息有多种表示形式。 默认操作是将此状态视为重定向,并遵循与此响应关联的 // Location 标头的内容。
Ambiguous = 300, //
// 摘要: // 等效于 HTTP 状态 301。 System.Net.HttpStatusCode.MovedPermanently 指示请求的信息已移到 Location // 头中指定的 URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。
MovedPermanently = 301, //
// 摘要: // 等效于 HTTP 状态 301。 System.Net.HttpStatusCode.Moved 指示请求的信息已移到 Location 头中指定的 // URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。 原始请求方法为 POST 时,重定向的请求将使用 GET 方法。
Moved = 301, //
// 摘要: // 等效于 HTTP 状态 302。 System.Net.HttpStatusCode.Found 指示请求的信息位于 Location 头中指定的 // URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。 原始请求方法为 POST 时,重定向的请求将使用 GET 方法。
Found = 302, //
// 摘要: // 等效于 HTTP 状态 302。 System.Net.HttpStatusCode.Redirect 指示请求的信息位于 Location 头中指定的 // URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。 原始请求方法为 POST 时,重定向的请求将使用 GET 方法。
Redirect = 302, //
// 摘要: // 等效于 HTTP 状态 303。 作为 POST 的结果,System.Net.HttpStatusCode.SeeOther 将客户端自动重定向到 // Location 头中指定的 URI。 用 GET 生成对 Location 标头所指定的资源的请求。
SeeOther = 303, //
// 摘要: // 等效于 HTTP 状态 303。 作为 POST 的结果,System.Net.HttpStatusCode.RedirectMethod 将客户端自动重定向到 // Location 头中指定的 URI。 用 GET 生成对 Location 标头所指定的资源的请求。
RedirectMethod = 303, //
// 摘要: // 等效于 HTTP 状态 304。 System.Net.HttpStatusCode.NotModified 指示客户端的缓存副本是最新的。 未传输此资源的内容。
NotModified = 304, //
// 摘要: // 等效于 HTTP 状态 305。 System.Net.HttpStatusCode.UseProxy 指示请求应使用位于 Location 头中指定的 // URI 的代理服务器。
UseProxy = 305, //
// 摘要: // 等效于 HTTP 状态 306。 System.Net.HttpStatusCode.Unused 是未完全指定的 HTTP/1.1 规范的建议扩展。
Unused = 306, //
// 摘要: // 等效于 HTTP 状态 307。 System.Net.HttpStatusCode.RedirectKeepVerb 指示请求信息位于 Location // 头中指定的 URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。 原始请求方法为 POST 时,重定向的请求还将使用 // POST 方法。
RedirectKeepVerb = 307, //
// 摘要: // 等效于 HTTP 状态 307。 System.Net.HttpStatusCode.TemporaryRedirect 指示请求信息位于 Location // 头中指定的 URI 处。 接收到此状态时的默认操作为遵循与响应关联的 Location 头。 原始请求方法为 POST 时,重定向的请求还将使用 // POST 方法。
TemporaryRedirect = 307, //
// 摘要: // 等效于 HTTP 状态 400。 System.Net.HttpStatusCode.BadRequest 指示服务器未能识别请求。 如果没有其他适用的错误,或者不知道准确的错误或错误没有自己的错误代码,则发送 // System.Net.HttpStatusCode.BadRequest。
BadRequest = 400, //
// 摘要: // 等效于 HTTP 状态 401。 System.Net.HttpStatusCode.Unauthorized 指示请求的资源要求身份验证。 WWW-Authenticate // 头包含如何执行身份验证的详细信息。
Unauthorized = 401, //
// 摘要: // 等效于 HTTP 状态 402。 保留 System.Net.HttpStatusCode.PaymentRequired 以供将来使用。
PaymentRequired = 402, //
// 摘要: // 等效于 HTTP 状态 403。 System.Net.HttpStatusCode.Forbidden 指示服务器拒绝满足请求。
Forbidden = 403, //
// 摘要: // 等效于 HTTP 状态 404。 System.Net.HttpStatusCode.NotFound 指示请求的资源不在服务器上。
NotFound = 404, //
// 摘要: // 等效于 HTTP 状态 405。 System.Net.HttpStatusCode.MethodNotAllowed 指示请求的资源上不允许请求方法(POST // 或 GET)。
MethodNotAllowed = 405, //
// 摘要: // 等效于 HTTP 状态 406。 System.Net.HttpStatusCode.NotAcceptable 指示客户端已用 Accept 头指示将不接受资源的任何可用表示形式。
NotAcceptable = 406, //
// 摘要: // 等效于 HTTP 状态 407。 System.Net.HttpStatusCode.ProxyAuthenticationRequired 指示请求的代理要求身份验证。 // Proxy-authenticate 头包含如何执行身份验证的详细信息。
ProxyAuthenticationRequired = 407, //
// 摘要: // 等效于 HTTP 状态 408。 System.Net.HttpStatusCode.RequestTimeout 指示客户端没有在服务器期望请求的时间内发送请求。
RequestTimeout = 408, //
// 摘要: // 等效于 HTTP 状态 409。 System.Net.HttpStatusCode.Conflict 指示由于服务器上的冲突而未能执行请求。
Conflict = 409, //
// 摘要: // 等效于 HTTP 状态 410。 System.Net.HttpStatusCode.Gone 指示请求的资源不再可用。
Gone = 410, //
// 摘要: // 等效于 HTTP 状态 411。 System.Net.HttpStatusCode.LengthRequired 指示缺少必需的 Content-length // 头。
LengthRequired = 411, //
// 摘要: // 等效于 HTTP 状态 412。 System.Net.HttpStatusCode.PreconditionFailed 指示为此请求设置的条件失败,且无法执行此请求。 // 条件是用条件请求标头(如 If-Match、If-None-Match 或 If-Unmodified-Since)设置的。
PreconditionFailed = 412, //
// 摘要: // 等效于 HTTP 状态 413。 System.Net.HttpStatusCode.RequestEntityTooLarge 指示请求太大,服务器无法处理。
RequestEntityTooLarge = 413, //
// 摘要: // 等效于 HTTP 状态 414。 System.Net.HttpStatusCode.RequestUriTooLong 指示 URI 太长。
RequestUriTooLong = 414, //
// 摘要: // 等效于 HTTP 状态 415。 System.Net.HttpStatusCode.UnsupportedMediaType 指示请求是不支持的类型。
UnsupportedMediaType = 415, //
// 摘要: // 等效于 HTTP 状态 416。 System.Net.HttpStatusCode.RequestedRangeNotSatisfiable 指示无法返回从资源请求的数据范围,因为范围的开头在资源的开头之前,或因为范围的结尾在资源的结尾之后。
RequestedRangeNotSatisfiable = 416, //
// 摘要: // 等效于 HTTP 状态 417。 System.Net.HttpStatusCode.ExpectationFailed 指示服务器未能符合 Expect // 头中给定的预期值。
ExpectationFailed = 417, //
UpgradeRequired = 426, //
// 摘要: // 等效于 HTTP 状态 500。 System.Net.HttpStatusCode.InternalServerError 指示服务器上发生了一般错误。
InternalServerError = 500, //
// 摘要: // 等效于 HTTP 状态 501。 System.Net.HttpStatusCode.NotImplemented 指示服务器不支持请求的函数。
NotImplemented = 501, //
// 摘要: // 等效于 HTTP 状态 502。 System.Net.HttpStatusCode.BadGateway 指示中间代理服务器从另一代理或原始服务器接收到错误响应。
BadGateway = 502, //
// 摘要: // 等效于 HTTP 状态 503。 System.Net.HttpStatusCode.ServiceUnavailable 指示服务器暂时不可用,通常是由于过多加载或维护。
ServiceUnavailable = 503, //
// 摘要: // 等效于 HTTP 状态 504。 System.Net.HttpStatusCode.GatewayTimeout 指示中间代理服务器在等待来自另一个代理或原始服务器的响应时已超时。
GatewayTimeout = 504, //
// 摘要: // 等效于 HTTP 状态 505。 System.Net.HttpStatusCode.HttpVersionNotSupported 指示服务器不支持请求的 // HTTP 版本。
HttpVersionNotSupported = 505,
}
}

Http状态码

定义好了异常处理方法,剩下的就是如何使用了。可以根据实际情况,在不同级别使用统一的异常处理机制。

1、接口级别

[WebApiExceptionFilter]
[HttpGet] public string GetAllChargingData([FromUri]TB_CHARGING obj)
{ throw new NotImplementedException(“方法不被支持”);
}

执行到异常后,会先进到OnException方法:

执行完成之后浏览器查看:

如果需要,甚至可以向Status Code里面写入自定义的描述信息,并且还可以向我们的Response的Content里面写入我们想要的信息。我们稍微改下OnException方法:

复制代码

if (actionExecutedContext.Exception is NotImplementedException)
{ var oResponse = new HttpResponseMessage(HttpStatusCode.NotImplemented);
oResponse.Content = new StringContent(“方法不被支持”);
oResponse.ReasonPhrase = “This Func is Not Supported”;
actionExecutedContext.Response = oResponse;
}

复制代码

看看ReasonPhrase描述信息

看看Response的描述信息

2、控制器级别

如果想要某一个或者多个控制器里面的所有接口都使用异常过滤,直接在控制器上面标注特性即可。

  • 某一个控制器上面启用异常过滤

复制代码

[WebApiExceptionFilter] public class ChargingController : BaseApiController
{ #region Get [HttpGet] public string GetAllChargingData([FromUri]TB_CHARGING obj)
{ throw new NotImplementedException(“方法不被支持”); }
}

复制代码

  • 多个控制器上面同时启用异常过滤

[WebApiExceptionFilter] public class BaseApiController : ApiController
{
}

复制代码

public class ChargingController : BaseApiController
{ #region Get \[HttpGet\] public string GetAllChargingData(\[FromUri\]TB\_CHARGING obj)
    { throw new NotImplementedException("方法不被支持");
    }
}

复制代码

这样,所有继承BaseApiController的子类都会启用异常过滤。

3、全局配置

如果需要对整个应用程序都启用异常过滤,则需要做如下两步:

  1、在Global.asax全局配置里面添加 GlobalConfiguration.Configuration.Filters.Add(new WebApiExceptionFilterAttribute()); 这一句,如下:

复制代码

void Application_Start(object sender, EventArgs e)
{ // 在应用程序启动时运行的代码
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
GlobalConfiguration.Configuration.Filters.Add(new WebApiExceptionFilterAttribute());
}

复制代码

  2、在WebApiConfig.cs文件的Register方法里面添加  config.Filters.Add(new WebApiExceptionFilterAttribute()); 这一句,如下:

复制代码

    public static void Register(HttpConfiguration config)
    { //跨域配置
        config.EnableCors(new EnableCorsAttribute("\*", "\*", "\*")); // Web API 路由

config.MapHttpAttributeRoutes();

        RouteTable.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        ).RouteHandler \= new SessionControllerRouteHandler();

        config.Filters.Add(new WebApiExceptionFilterAttribute());
    }

复制代码

上面说的是全局的异常捕获以及处理方式,在某些情况下,我们希望以异常的方式向客户端发送相关信息,可能就需要用到我们的HttpResponseException。比如:

复制代码

[HttpGet] public TB_CHARGING GetById(string id)
{ //从后台查询实体
var oModel = server.Find(id); if (oModel == null)
{ var resp = new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent(string.Format(“没有找到id={0}的对象”, id)),
ReasonPhrase = “object is not found” }; throw new HttpResponseException(resp);
} return oModel;
}

复制代码

执行之后浏览器里面查看结果:

代码释疑:细心的朋友可能,发现了,这里既使用了HttpResponseMessage,又使用了HttpResponseException,那么,像这种可控的异常,我们是否可以直接以HttpResponseMessage的形式直接返回到客户端而不用抛出异常呢?这里就要谈谈这两个对象的区别了,博主的理解是HttpResonseMessage对象用来响应讯息并包含状态码及数据内容,HttpResponseException对象用来向客户端返回包含错误讯息的异常。****

在网上看到一篇 文章 这样描述两者的区别:当呼叫 Web API 服务时发生了与预期上不同的错误时,理当应该中止程序返回错误讯息,这时对于错误的返回就该使用 HttpResponseException,而使用 HttpResponseMessage 则是代表着当客户端发送了一个工作请求而 Web API 正确的完成了这个工作,就能够使用 HttpResponseMessage 返回一个 201 的讯息,所以 HttpResponseMessage 与 HttpResponseException 在使用上根本的目标就是不同的,用 HttpResponseMessage 去返回一个例外错误也会让程序结构难以辨别且不够清晰。

HttpError对象提供一致的方法来响应正文中返回错误的信息。准确来说,HttpError并不是一个异常,只是用来包装错误信息的一个对象。其实在某一定的程度上,HttpError和HttpResponseMessage使用比较相似,二者都可以向客户端返回http状态码和错误讯息,并且都可以包含在HttpResponseException对象中发回到客户端。但是,一般情况下,HttpError只有在向客户端返回错误讯息的时候才会使用,而HttpResponseMessage对象既可以返回错误讯息,也可返回请求正确的消息。其实关于HttpError没什么特别好讲的,我们来看一个例子就能明白:

复制代码

public HttpResponseMessage Update(dynamic obj)
{
TB_Product oModel = null; try { var id = Convert.ToString(obj.id);
oModel = Newtonsoft.Json.JsonConvert.DeserializeObject<TB_Product>(Convert.ToString(obj.dataModel)); //…复杂的业务逻辑
} catch(Exception ex)
{ return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex.Message);
} return Request.CreateResponse<TB_Product>(HttpStatusCode.OK, oModel);

    }

复制代码

假如现在在执行try里面复杂业务逻辑的时候发生了异常,我们捕获到了异常然后向客户端返回HttpError对象,这个对象里面包含我们自定义的错误讯息,如果正常则返回HttpResponseMessage对象。

如果请求异常:

如果请求正常

 以上三种异常的处理方法,可以根据不同的场景选择使用。

  • 如果项目对异常处理要求并不高,只需要记录好异常日志即可,那么使用异常筛选器就能够搞定
  • 如果项目需要对不同的异常,客户端做不同的处理。而这个时候使用异常筛选器不能详尽所有的异常,可能使用HttpResponseException对象是更好的选择,定义更加精细的异常和异常描述。
  • 对于何时使用HttpError,又何时使用HttpResponseMessage,可以参考上文三里面用法。
  • 当然实际项目中很可能以上两种或者三种同时使用。

上文通过一些简单的示例介绍了下WebApi里面异常的处理机制,可能不够深入,但对于一般项目的异常处理基本够用。其实有一点博主还没有想明白,对于构造函数里面的异常该如何统一捕获呢?通过异常筛选器是捕获不到的,不知道园友们有没有什么更好的办法,不吝赐教,感谢感谢!如果本文能帮到你,不妨推荐下,您的推荐是博主继续总结的动力!


正文

前言:已经有一个月没写点什么了,感觉心里空落落的。今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧。之前分享过一篇 C#进阶系列——WebApi接口传参不再困惑:传参详解 ,这篇博文内容本身很基础,没想到引起很多园友关注,感谢大家的支持。作为程序猿,我们都知道参数和返回值是编程领域不可分割的两大块,此前分享了下WebApi的传参机制,今天再来看看WebApi里面另一个重要而又基础的知识点:返回值。还是那句话:本篇针对初初使用WebApi的同学们,比较基础,有兴趣的且看看。

WebApi系列文章

使用过Webapi的园友应该都知道,Webapi的接口返回值主要有四种类型

  • void无返回值
  • IHttpActionResult
  • HttpResponseMessage
  • 自定义类型

此篇就围绕这四块分别来看看它们的使用。

void关键字我们都不陌生,它申明方法没有返回值。它的使用也很简单,我们来看一个示例就能明白。

复制代码

public class ORDER
{ public string ID { get; set; } public string NO { get; set; } public string NAME { get; set; } public string DESC { get; set; }
}

复制代码

复制代码

public class OrderController : ApiController
{
[HttpPost] public void SaveOrder(ORDER name)
{ //处理业务逻辑
} }

复制代码

在Web里面调用

复制代码

$(function () {
$.ajax({
type: ‘post’,
url: ‘http://localhost:21528/api/Order/SaveOrder‘,
data: { ID: “aaa”, NAME: “test” },
success: function (data, status) {
alert(data);
}
});
});

复制代码

得到结果

 

 可以看到,使用void申明的方法,在success方法里面得不到返回值,并且会返回http状态码204,告诉客户端此请求没有返回值。

IHttpActionResult类型是WebApi里面非常重要的一种返回值类型。下面博主就根据平时在项目里面使用最多的几种方式来讲解下这种类型的返回值的一些用法。

1、Json(T content)

使用MVC开发过的朋友一定记得,在MVC里面,请求数据的接口的返回值类型大部分使用的是JsonResult,在MVC里面你一定也写过类似这样的接口:

public JsonResult GetResult()
{ return Json(new { }, JsonRequestBehavior.AllowGet);
}

那么,在WebAPI里面是否也存在类似的用法呢。呵呵,在这点上面,微软总是贴心的。在WebApi的ApiController这个抽象类里面,为我们封装了Json(T content)这个方法,它的用法和MVC里面的JsonResult基本类似。我们通过一个例子来说明它的用法:

复制代码

[HttpGet] public IHttpActionResult GetOrder()
{ var lstRes = new List(); //实际项目中,通过后台取到集合赋值给lstRes变量。这里只是测试。
lstRes.Add(new ORDER() { ID = “aaaa”, NO = “111”, NAME = “111”, DESC = “1111” });
lstRes.Add(new ORDER() { ID = “bbbb”, NO = “222”, NAME = “222”, DESC = “2222” }); return Json<List>(lstRes);
}

复制代码

看到这个代码,有人就疑惑了,我们定义的返回值类型是IHttpActionResult类型,直接返回Json(T content)这样可行么?我们将Json转到定义看看:

    protected internal JsonResult<T> Json<T>(T content); 

我们继续将JsonResult转到定义

原来JsonResult是实现了IHttpActionResult接口的,难怪可以直接返回呢。

知道了这个,我们直接在Web里面通过ajax请求来调用:

复制代码

$(function () {
$.ajax({
type: ‘get’,
url: ‘http://localhost:21528/api/Order/GetOrder‘,
data: {},
success: function (data, status) {
alert(data);
}
});
});

复制代码

来看结果:

 既然实体类可以直接这样传递,那么如果我们想要传递一些匿名类型呢,因为很多情况下,我们需要返回到前端的对象都没有对应的实体来对应,如果我们想要返回匿名对象怎么办呢?我们知道,这里的Json(T content)必须要传一个对应的泛型类型,如果是匿名类型这里肯定不好传。还好有我们的object类型,当然你可以使用dynamic,我们来试一把。

[HttpGet] public IHttpActionResult GetOrder()
{
return Json<dynamic>(new { AA = “”, BB = “cc” }); }

同样的来看测试结果:

2、Ok()、 Ok(T content)

除了Json(T content),在ApiController里面还有另外一个比较常用的方法:Ok()。同样,我们将Ok()转到定义

protected internal virtual OkResult Ok();

OkResult转到定义

有了这个作为基础,我们就可以放心大胆的使用了。

[HttpGet] public IHttpActionResult GetOKResult()
{ return Ok();
}

得到结果

如果返回Ok(),就表示不向客户端返回任何信息,只告诉客户端请求成功。

除了Ok()之外,还有另外一个重载Ok(T content)。

[HttpGet] public IHttpActionResult GetOKResult(string name)
{ return Ok<string>(name);
}

这种用法和Json(T content)比较类似,如果你非要问这两者有什么区别,或者说怎么选择两者。那么我的理解是如果是返回实体或者实体集合,建议使用Json(T content),如果是返回基础类型(如int、string等),使用Ok(T content)。

3、NotFound()

当需要向客户端返回找不到记录时,有时需要用到NotFound()方法。

protected internal virtual NotFoundResult NotFound();

来看看它的使用场景

复制代码

[HttpGet] public IHttpActionResult GetNotFoundResult(string id)
{ var lstRes = new List(); //实际项目中,通过后台取到集合赋值给lstRes变量。这里只是测试。
lstRes.Add(new ORDER() { ID = “aaaa”, NO = “111”, NAME = “111”, DESC = “1111” });
lstRes.Add(new ORDER() { ID = “bbbb”, NO = “222”, NAME = “222”, DESC = “2222” }); var oFind = lstRes.FirstOrDefault(x => x.ID == id) ; if (oFind == null)
{ return NotFound();
} else { return Json(oFind);
}
}

复制代码

复制代码

$(function () {
$.ajax({
type: ‘get’,
url: ‘http://localhost:21528/api/Order/GetNotFoundResult‘,
data: { id :”cccc” },
success: function (data, status) {
alert(data);
}
});
});

复制代码

得到结果

 

NotFound()方法会返回一个404的错误到客户端。

4、其他

其他还有一些方法,都有它特定的用途。在此贴出来。

4.1、Content(HttpStatusCode statusCode, T value)

[HttpGet] public IHttpActionResult GetContentResult()
{ return Content<string>(HttpStatusCode.OK, “OK”);
}

向客户端返回值和http状态码。

4.2、BadRequest()

复制代码

[HttpGet] public IHttpActionResult GetBadRequest(ORDER order)
{ if (string.IsNullOrEmpty(order.ID)) return BadRequest(); return Ok();
}

复制代码

向客户端返回400的http错误。

4.3、Redirect(string location)

[HttpGet] public IHttpActionResult RedirectResult()
{ return Redirect(“http://localhost:21528/api/Order/GetContentResult“);
}

将请求重定向到其他地方。

5、自定义IHttpActionResult接口的实现

上面介绍了一些系统内置的常用的实现IHttpActionResult接口的方法。如果我们需要自定义IHttpActionResult的返回呢?

在介绍之前,我们有必要先来看看IHttpActionResult类型的定义,将IHttpActionResult转到定义可以看到:

复制代码

namespace System.Web.Http
{ // 摘要: // Defines a command that asynchronously creates an System.Net.Http.HttpResponseMessage.
public interface IHttpActionResult
{ // 摘要: // Creates an System.Net.Http.HttpResponseMessage asynchronously. //
// 参数: // cancellationToken: // The token to monitor for cancellation requests. //
// 返回结果: // A task that, when completed, contains the System.Net.Http.HttpResponseMessage.
Task<System.Net.Http.HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken);
}
}

复制代码

这个接口包含唯一的一个方法ExecuteAsync(),此方法将以异步方式创建一个HttpResponseMessage实例返回给客户端。

有了这个作为基础,下面,我们自定义一个bootstrapTable服务端分页的子类去展示自定义IHttpActionResult的用法。

首先,自定义一个实现类

复制代码

  public class PageResult : IHttpActionResult
{ object _value;
HttpRequestMessage _request; public PageResult(object value, HttpRequestMessage request)
{
_value = value;
_request = request;
} public Task ExecuteAsync(System.Threading.CancellationToken cancellationToken)
{ var response = new HttpResponseMessage()
{
Content = new ObjectContent(typeof(object), _value, new JsonMediaTypeFormatter()),
RequestMessage = _request
}; return Task.FromResult(response);
}
}

复制代码

然后,在API接口里面返回PageResult对象

复制代码

[HttpGet] public IHttpActionResult GetPageRow(int limit, int offset)
{ var lstRes = new List(); //实际项目中,通过后台取到集合赋值给lstRes变量。这里只是测试。
lstRes.Add(new ORDER() { ID = “aaaa”, NO = “111”, NAME = “111”, DESC = “1111” });
lstRes.Add(new ORDER() { ID = “bbbb”, NO = “222”, NAME = “222”, DESC = “2222” }); var oData = new { total = lstRes.Count, rows = lstRes.Skip(offset).Take(limit).ToList() }; return new PageResult(oData, Request);
}

复制代码

最好,ajax调用

复制代码

$(function () {
$.ajax({
type: ‘get’,
url: ‘http://localhost:21528/api/Order/GetPageRow‘,
data: { limit:1,offset:1},
success: function (data, status) {
alert(data);
}
});
});

复制代码

得到结果

在上文自定义IHttpActionResult返回类型的时候,提到过HttpResponseMessage这个对象。它表示向客户端返回一个http响应的消息对象(包含http状态码和需要返回客户端的消息)。这个对象也有它独特的使用场景:需要向客户端返回HttpResponse时就要用到这个对象。以导出为例,由于需要将导出的Excel文件输出到客户端浏览器,Webapi的服务端需要向Web的客户端输出文件流,这个时候一般的IHttpActionResult对象不方便解决这个问题,于是HttpReponseMessage派上了用场。我们来看看它的使用示例。

复制代码

public HttpResponseMessage Export()
{ //取数据
var lstRes = OrderBLL.Export(); //向Excel里面填充数据
HSSFWorkbook workbook = new HSSFWorkbook();
CreateAndFillSheet(workbook, lstRes); //保存到服务
var fileName = “Excel” + DateTime.Now.ToString(“yyyyMMddHHmmss”) + “.xls”; var strPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @”Data\“ + fileName); using (FileStream fs = new FileStream(strPath, FileMode.Create))
{
workbook.Write(fs); using (MemoryStream ms = new MemoryStream())
{
workbook.Write(ms);
}
} //输出到浏览器
try { var stream = new FileStream(strPath, FileMode.Open);
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent(stream);
response.Content.Headers.ContentType = new MediaTypeHeaderValue(“application/octet-stream”);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(“attachment”)
{
FileName = fileName
}; return response;
} catch { return new HttpResponseMessage(HttpStatusCode.NoContent);
}
}

复制代码

将文件流保存在StreamContent对象里面,然后输出到浏览器。在浏览器端即可将Excel输出。

以上几种返回值类型能解决我们大部分返回值的问题,当然,你也可以将webapi的接口和普通方法一样,返回任意的类型,WebApi会自动序列化你自定义任何返回类型,然后将序列化的值写到响应正文里,状态码统一返回200。比如:

复制代码

[HttpGet] public object GetOther()
{ var lstRes = new List(); //实际项目中,通过后台取到集合赋值给lstRes变量。这里只是测试。
lstRes.Add(new ORDER() { ID = “aaaa”, NO = “111”, NAME = “111”, DESC = “1111” });
lstRes.Add(new ORDER() { ID = “bbbb”, NO = “222”, NAME = “222”, DESC = “2222” }); return lstRes;
}

复制代码

得到结果

和上面的Json、Ok等用法在效果上面没有太大区别。

以上通过四个方面详细分享了下WebApi里面返回值的常见用法,不能说哪种方式最好,因为每种方式都有其特定的使用场景。博主觉得为了规范WebApi接口,对于一般接口的返回值,尽量使用IHttpActionResult类型作为返回值,毕竟是微软内置的东西,可能为我们考虑了很多我们考虑不到的东西。当然,你可能会觉得麻烦,你可能会说直接和普通方法一样来使用不是更爽,博主当初也有这种想法,可是学习微软的东西多了之后发现很多东西还是遵守一定的标准比较好,至少维护起来方便。这就像博主最近正在努力学习的WebApi+oData一样,为什么要搞这么一套标准性的东西,还不是为了更加方便地规范Restful风格。如果本文能帮到你,不妨推荐下,您的推荐是博主继续总结的动力!


一、引言

  在软件系统中,有时需要创建一个复杂对象,并且这个复杂对象由其各部分子对象通过一定的步骤组合而成。例如一个采购系统中,如果需要采购员去采购一批电脑时,在这个实际需求中,电脑就是一个复杂的对象,它是由CPU、主板、硬盘、显卡、机箱等组装而成的,如果此时让采购员一台一台电脑去组装的话真是要累死采购员了,这里就可以采用建造者模式来解决这个问题,我们可以把电脑的各个组件的组装过程封装到一个建造者类对象里,建造者只要负责返还给客户端全部组件都建造完毕的产品对象就可以了。然而现实生活中也是如此的,如果公司要采购一批电脑,此时采购员不可能自己去买各个组件并把它们组织起来,此时采购员只需要像电脑城的老板说自己要采购什么样的电脑就可以了,电脑城老板自然会把组装好的电脑送到公司。下面就以这个例子来展开建造者模式的介绍。

二、建造者模式的详细介绍

2.1 建筑者模式的具体实现

  在这个例子中,电脑城的老板是直接与客户(也就是指采购员)联系的,然而电脑的组装是由老板指挥装机人员去把电脑的各个部件组装起来,真真负责创建产品(这里产品指的就是电脑)的人就是电脑城的装机人员。理清了这个逻辑过程之后,下面就具体看下如何用代码来表示这种现实生活中的逻辑过程:

复制代码

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5
6
7 ///


8 /// 以组装电脑为例子 9 /// 每台电脑的组成过程都是一致的,但是使用同样的构建过程可以创建不同的表示(即可以组装成不一样的电脑,配置不一样) 10 /// 组装电脑的这个场景就可以应用建造者模式来设计 11 ///

12 namespace 设计模式之建造者模式 13 {
14 ///
15 /// 客户类 16 ///

17 class Customer 18 {
19 static void Main(string[] args)
20 {
21 // 客户找到电脑城老板说要买电脑,这里要装两台电脑 22 // 创建指挥者和构造者
23 Director director = new Director(); 24 Builder b1 = new ConcreteBuilder1(); 25 Builder b2 = new ConcreteBuilder2(); 26
27 // 老板叫员工去组装第一台电脑
28 director.Construct(b1);
29
30 // 组装完,组装人员搬来组装好的电脑
31 Computer computer1 = b1.GetComputer(); 32 computer1.Show();
33
34 // 老板叫员工去组装第二台电脑
35 director.Construct(b2);
36 Computer computer2 = b2.GetComputer(); 37 computer2.Show();
38
39 Console.Read();
40 }
41 }
42
43 ///
44 /// 小王和小李难道会自愿地去组装嘛,谁不想休息的,这必须有一个人叫他们去组装才会去的 45 /// 这个人当然就是老板了,也就是建造者模式中的指挥者 46 /// 指挥创建过程类 47 ///

48 public class Director 49 {
50 // 组装电脑
51 public void Construct(Builder builder) 52 {
53 builder.BuildPartCPU();
54 builder.BuildPartMainBoard();
55 }
56 }
57
58 ///
59 /// 电脑类 60 ///

61 public class Computer 62 {
63 // 电脑组件集合
64 private IList<string> parts = new List<string>();
65
66 // 把单个组件添加到电脑组件集合中
67 public void Add(string part) 68 {
69 parts.Add(part);
70 }
71
72 public void Show() 73 {
74 Console.WriteLine(“电脑开始在组装…….”);
75 foreach (string part in parts) 76 {
77 Console.WriteLine(“组件”+part+”已装好”);
78 }
79
80 Console.WriteLine(“电脑组装好了”);
81 }
82 }
83
84 ///
85 /// 抽象建造者,这个场景下为 “组装人” ,这里也可以定义为接口 86 ///

87 public abstract class Builder 88 {
89 // 装CPU
90 public abstract void BuildPartCPU(); 91 // 装主板
92 public abstract void BuildPartMainBoard(); 93
94 // 当然还有装硬盘,电源等组件,这里省略 95
96 // 获得组装好的电脑
97 public abstract Computer GetComputer(); 98 }
99
100 ///
101 /// 具体创建者,具体的某个人为具体创建者,例如:装机小王啊 102 ///

103 public class ConcreteBuilder1 : Builder 104 { 105 Computer computer = new Computer(); 106 public override void BuildPartCPU() 107 { 108 computer.Add(“CPU1”); 109 } 110
111 public override void BuildPartMainBoard() 112 { 113 computer.Add(“Main board1”); 114 } 115
116 public override Computer GetComputer() 117 { 118 return computer; 119 } 120 } 121
122 ///
123 /// 具体创建者,具体的某个人为具体创建者,例如:装机小李啊 124 /// 又装另一台电脑了 125 ///

126 public class ConcreteBuilder2 : Builder 127 { 128 Computer computer = new Computer(); 129 public override void BuildPartCPU() 130 { 131 computer.Add(“CPU2”); 132 } 133
134 public override void BuildPartMainBoard() 135 { 136 computer.Add(“Main board2”); 137 } 138
139 public override Computer GetComputer() 140 { 141 return computer; 142 } 143 } 144 }

复制代码

上面代码中都有详细的注释代码,这里就不过多解释,大家可以参考代码和注释来与现实生活中的例子做对比,下图展示了上面代码的运行结果:

2.2 建造者模式的定义和类图

  介绍完了建造者模式的具体实现之后,下面具体看下建造者模式的具体定义是怎样的。

建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式使得建造代码与表示代码的分离,可以使客户端不必知道产品内部组成的细节,从而降低了客户端与具体产品之间的耦合度,下面通过类图来帮助大家更好地理清建造者模式中类之间的关系。

三、建造者模式的分析

介绍完了建造者模式的具体实现之后,让我们总结下建造模式的实现要点:

  1. 在建造者模式中,指挥者是直接与客户端打交道的,指挥者将客户端创建产品的请求划分为对各个部件的建造请求,再将这些请求委派到具体建造者角色,具体建造者角色是完成具体产品的构建工作的,却不为客户所知道。
  2. 建造者模式主要用于“分步骤来构建一个复杂的对象”,其中“分步骤”是一个固定的组合过程,而复杂对象的各个部分是经常变化的(也就是说电脑的内部组件是经常变化的,这里指的的变化如硬盘的大小变了,CPU由单核变双核等)。
  3. 产品不需要抽象类,由于建造模式的创建出来的最终产品可能差异很大,所以不大可能提炼出一个抽象产品类。
  4. 在前面文章中介绍的抽象工厂模式解决了“系列产品”的需求变化,而建造者模式解决的是 “产品部分” 的需要变化。
  5. 由于建造者隐藏了具体产品的组装过程,所以要改变一个产品的内部表示,只需要再实现一个具体的建造者就可以了,从而能很好地应对产品组成组件的需求变化。

四、.NET 中建造者模式的实现

  前面的设计模式在.NET类库中都有相应的实现,那在.NET 类库中,是否也存在建造者模式的实现呢? 然而对于疑问的答案是肯定的,在.NET 类库中,System.Text.StringBuilder(存在mscorlib.dll程序集中)就是一个建造者模式的实现。不过它的实现属于建造者模式的演化,此时的建造者模式没有指挥者角色和抽象建造者角色,StringBuilder类即扮演着具体建造者的角色,也同时扮演了指挥者和抽象建造者的角色,此时建造模式的实现如下:

复制代码

///


/// 建造者模式的演变 /// 省略了指挥者角色和抽象建造者角色 /// 此时具体建造者角色扮演了指挥者和建造者两个角色 ///

public class Builder
{ // 具体建造者角色的代码
private Product product = new Product(); public void BuildPartA()
{
product.Add(“PartA”);
} public void BuildPartB()
{
product.Add(“PartB”);
} public Product GetProduct()
{ return product;
} // 指挥者角色的代码
public void Construct()
{
BuildPartA();
BuildPartB();
}
} ///
/// 产品类 ///

public class Product
{ // 产品组件集合
private IList<string> parts = new List<string>(); // 把单个组件添加到产品组件集合中
public void Add(string part)
{
parts.Add(part);
} public void Show()
{
Console.WriteLine(“产品开始在组装…….”); foreach (string part in parts)
{
Console.WriteLine(“组件” + part + “已装好”);
}

        Console.WriteLine("产品组装完成");
    }
} // 此时客户端也要做相应调整
class Client 
{ private static Builder builder; static void Main(string\[\] args)
    {
        builder \= new Builder();
        builder.Construct();
        Product product \= builder.GetProduct();
        product.Show();
        Console.Read();
    }
}

复制代码

  StringBuilder类扮演着建造string对象的具体建造者角色,其中的ToString()方法用来返回具体产品给客户端(相当于上面代码中GetProduct方法)。其中Append方法用来创建产品的组件(相当于上面代码中BuildPartA和BuildPartB方法),因为string对象中每个组件都是字符,所以也就不需要指挥者的角色的代码(指的是Construct方法,用来调用创建每个组件的方法来完成整个产品的组装),因为string字符串对象中每个组件都是一样的,都是字符,所以Append方法也充当了指挥者Construct方法的作用。

五、总结

  到这里,建造者模式的介绍就结束了,建造者模式(Builder Pattern),将一个复杂对象的构建与它的表示分离,使的同样的构建过程可以创建不同的表示。建造者模式的本质是使组装过程(用指挥者类进行封装,从而达到解耦的目的)和创建具体产品解耦,使我们不用去关心每个组件是如何组装的。

本专题中所有源码: 建造者模式源码

一、引言

在软件系统中,当创建一个类的实例的过程很昂贵或很复杂,并且我们需要创建多个这样类的实例时,如果我们用new操作符去创建这样的类实例,这未免会增加创建类的复杂度和耗费更多的内存空间,因为这样在内存中分配了多个一样的类实例对象,然后如果采用工厂模式来创建这样的系统的话,随着产品类的不断增加,导致子类的数量不断增多,反而增加了系统复杂程度,所以在这里使用工厂模式来封装类创建过程并不合适,然而原型模式可以很好地解决这个问题,因为每个类实例都是相同的,当我们需要多个相同的类实例时,没必要每次都使用new运算符去创建相同的类实例对象,此时我们一般思路就是想——只创建一个类实例对象,如果后面需要更多这样的实例,可以通过对原来对象拷贝一份来完成创建,这样在内存中不需要创建多个相同的类实例,从而减少内存的消耗和达到类实例的复用。 然而这个思路正是原型模式的实现方式。下面就具体介绍下设计模式中的原型设计模式。

二、原型模式的详细介绍

在现实生活中,也有很多原型设计模式的例子,例如,细胞分裂的过程,一个细胞的有丝分裂产生两个相同的细胞;还有西游记中孙悟空变出后孙的本领和火影忍者中鸣人的隐分身忍术等。下面就以孙悟空为例子来演示下原型模式的实现。具体的实现代码如下:

复制代码

///火影忍者中鸣人的影分身和孙悟空的的变都是原型模式
class Client
{ static void Main(string[] args)
{ // 孙悟空 原型
MonkeyKingPrototype prototypeMonkeyKing = new ConcretePrototype(“MonkeyKing”); // 变一个
MonkeyKingPrototype cloneMonkeyKing = prototypeMonkeyKing.Clone() as ConcretePrototype;
Console.WriteLine(“Cloned1:\t”+cloneMonkeyKing.Id); // 变两个
MonkeyKingPrototype cloneMonkeyKing2 = prototypeMonkeyKing.Clone() as ConcretePrototype;
Console.WriteLine(“Cloned2:\t” + cloneMonkeyKing2.Id);
Console.ReadLine();
}
} ///


/// 孙悟空原型 ///

public abstract class MonkeyKingPrototype
{ public string Id { get; set; } public MonkeyKingPrototype(string id)
{ this.Id = id;
} // 克隆方法,即孙大圣说“变”
public abstract MonkeyKingPrototype Clone();
} ///
/// 创建具体原型 ///

public class ConcretePrototype : MonkeyKingPrototype
{ public ConcretePrototype(string id)
: base(id)
{ } ///
/// 浅拷贝 ///

///
public override MonkeyKingPrototype Clone()
{ // 调用MemberwiseClone方法实现的是浅拷贝,另外还有深拷贝
return (MonkeyKingPrototype)this.MemberwiseClone();
}
}

复制代码

上面原型模式的运行结果为(从运行结果可以看出,创建的两个拷贝对象的ID属性都是与原型对象ID属性一样的):

上面代码实现的浅拷贝的方式,浅拷贝是指当对象的字段值被拷贝时,字段引用的对象不会被拷贝。例如,如果一个对象有一个指向字符串的字段,并且我们对该对象做了一个浅拷贝,那么这两个对象将引用同一个字符串,而深拷贝是对对象实例中字段引用的对象也进行拷贝,如果一个对象有一个指向字符串的字段,并且我们对该对象进行了深拷贝的话,那么我们将创建一个对象和一个新的字符串,新的对象将引用新的字符串。也就是说,执行深拷贝创建的新对象和原来对象不会共享任何东西,改变一个对象对另外一个对象没有任何影响,而执行浅拷贝创建的新对象与原来对象共享成员,改变一个对象,另外一个对象的成员也会改变。

介绍完原型模式的实现代码之后,下面看下原型模式的类图,通过类图来理清原型模式实现中类之间的关系。具体类图如下:

三、原型模式的优缺点

原型模式的优点有:

  1. 原型模式向客户隐藏了创建新实例的复杂性
  2. 原型模式允许动态增加或较少产品类。
  3. 原型模式简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样。
  4. 产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构

原型模式的缺点有:

  1. 每个类必须配备一个克隆方法
  2. 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。

四、.NET中原型模式的实现

在.NET中可以很容易地通过实现ICloneable接口(这个接口就是原型,提供克隆方法,相当于与上面代码中MonkeyKingPrototype抽象类)中Clone()方法来实现原型模式,如果我们想我们自定义的类具有克隆的功能,首先定义类继承与ICloneable接口并实现Clone方法。在.NET中实现了原型模式的类如下图所示(图中只截取了部分,可以用Reflector反编译工具进行查看):

五、总结

到这里关于原型模式的介绍就结束了,原型模式用一个原型对象来指明所要创建的对象类型,然后用复制这个原型对象的方法来创建出更多的同类型对象,它与工厂方法模式的实现非常相似,其中原型模式中的Clone方法就类似工厂方法模式中的工厂方法,只是工厂方法模式的工厂方法是通过new运算符重新创建一个新的对象(相当于原型模式的深拷贝实现),而原型模式是通过调用MemberwiseClone方法来对原来对象进行拷贝,也就是复制,同时在原型模式优点中也介绍了与工厂方法的区别(第三点)。

本专题中所有源码:设计模式之原型模式

一、引言

在实际的开发过程中,由于应用环境的变化(例如使用语言的变化),我们需要的实现在新的环境中没有现存对象可以满足,但是其他环境却存在这样现存的对象。那么如果将“将现存的对象”在新的环境中进行调用呢?解决这个问题的办法就是我们本文要介绍的适配器模式——使得新环境中不需要去重复实现已经存在了的实现而很好地把现有对象(指原来环境中的现有对象)加入到新环境来使用

二、适配器模式的详细介绍

2.1 定义

下面让我们看看适配器的定义,适配器模式——把一个类的接口变换成客户端所期待的另一种接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作。适配器模式有类的适配器模式和对象的适配器模式两种形式,下面我们分别讨论这两种形式的实现和给出对应的类图来帮助大家理清类之间的关系。

2.2  类的适配器模式实现

在这里以生活中的一个例子来进行演示适配器模式的实现,具体场景是: 在生活中,我们买的电器插头是2个孔的,但是我们买的插座只有三个孔的,此时我们就希望电器的插头可以转换为三个孔的就好,这样我们就可以直接把它插在插座上,此时三个孔插头就是客户端期待的另一种接口,自然两个孔的插头就是现有的接口,适配器模式就是用来完成这种转换的,具体实现代码如下:

复制代码

using System; /// 这里以插座和插头的例子来诠释适配器模式 /// 现在我们买的电器插头是2个孔,但是我们买的插座只有3个孔的 /// 这是我们想把电器插在插座上的话就需要一个电适配器
namespace 设计模式之适配器模式
{ ///


/// 客户端,客户想要把2个孔的插头 转变成三个孔的插头,这个转变交给适配器就好 /// 既然适配器需要完成这个功能,所以它必须同时具体2个孔插头和三个孔插头的特征 ///

class Client
{ static void Main(string[] args)
{ // 现在客户端可以通过电适配要使用2个孔的插头了
IThreeHole threehole = new PowerAdapter();
threehole.Request();
Console.ReadLine();
}
} ///
/// 三个孔的插头,也就是适配器模式中的目标角色 ///

public interface IThreeHole
{ void Request();
} ///
/// 两个孔的插头,源角色——需要适配的类 ///

public abstract class TwoHole
{ public void SpecificRequest()
{
Console.WriteLine(“我是两个孔的插头”);
}
} ///
/// 适配器类,接口要放在类的后面 /// 适配器类提供了三个孔插头的行为,但其本质是调用两个孔插头的方法 ///

public class PowerAdapter:TwoHole,IThreeHole
{ ///
/// 实现三个孔插头接口方法 ///

public void Request()
{ // 调用两个孔插头方法
this.SpecificRequest();
}
}
}

复制代码

从上面代码中可以看出,客户端希望调用Request方法(即三个孔插头),但是我们现有的类(即2个孔的插头)并没有Request方法,它只有SpecificRequest方法(即两个孔插头本身的方法),然而适配器类(适配器必须实现三个孔插头接口和继承两个孔插头类)可以提供这种转换,它提供了Request方法的实现(其内部调用的是两个孔插头,因为适配器只是一个外壳罢了,包装着两个孔插头(因为只有这样,电器才能使用),并向外界提供三个孔插头的外观,)以供客户端使用。

2.3 类图

上面实现中,因为适配器(PowerAdapter类)与源角色(TwoHole类)是继承关系,所以该适配器模式是类的适配器模式,具体对应的类图为:

2.4 对象的适配器模式

上面都是类的适配器模式的介绍,然而适配器模式还有另外一种形式——对象的适配器模式,这里就具体讲解下它的实现,实现的分析思路:既然现在适配器类不能继承TwoHole抽象类了(因为用继承就属于类的适配器了),但是适配器类无论如何都要实现客户端期待的方法的,即Request方法,所以一定是要继承ThreeHole抽象类或IThreeHole接口的,然而适配器类的Request方法又必须调用TwoHole的SpecificRequest方法,又不能用继承,这时候就想,不能继承,但是我们可以在适配器类中创建TwoHole对象,然后在Requst中使用TwoHole的方法了。正如我们分析的那样,对象的适配器模式的实现正式如此。下面就让我看看具体实现代码:

复制代码

namespace 对象的适配器模式
{ class Client
{ static void Main(string[] args)
{ // 现在客户端可以通过电适配要使用2个孔的插头了
ThreeHole threehole = new PowerAdapter();
threehole.Request();
Console.ReadLine();
}
} ///


/// 三个孔的插头,也就是适配器模式中的目标(Target)角色 ///

public class ThreeHole
{ // 客户端需要的方法
public virtual void Request()
{ // 可以把一般实现放在这里
}
} ///
/// 两个孔的插头,源角色——需要适配的类 ///

public class TwoHole
{ public void SpecificRequest()
{
Console.WriteLine(“我是两个孔的插头”);
}
} ///
/// 适配器类,这里适配器类没有TwoHole类, /// 而是引用了TwoHole对象,所以是对象的适配器模式的实现 ///

public class PowerAdapter : ThreeHole
{ // 引用两个孔插头的实例,从而将客户端与TwoHole联系起来
public TwoHole twoholeAdaptee = new TwoHole(); ///
/// 实现三个孔插头接口方法 ///

public override void Request()
{
twoholeAdaptee.SpecificRequest();
}
}
}

复制代码

从上面代码可以看出,对象的适配器模式正如我们开始分析的思路去实现的, 其中客户端调用代码和类的适配器实现基本相同,下面让我们看看对象的适配器模式的类图,具体类图如下:

三、适配器模式的优缺点

在引言部分已经提出,适配器模式用来解决现有对象与客户端期待接口不一致的问题,下面详细总结下适配器两种形式的优缺点。

类的适配器模式:

优点:

  • 可以在不修改原有代码的基础上来复用现有类,很好地符合 “开闭原则”
  • 可以重新定义Adaptee(被适配的类)的部分行为,因为在类适配器模式中,Adapter是Adaptee的子类
  • 仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例(这个即是优点也是缺点)。

缺点:

  • 用一个具体的Adapter类对Adaptee和Target进行匹配,当如果想要匹配一个类以及所有它的子类时,类的适配器模式就不能胜任了。因为类的适配器模式中没有引入Adaptee的实例,光调用this.SpecificRequest方法并不能去调用它对应子类的SpecificRequest方法。
  • 采用了 “多继承”的实现方式,带来了不良的高耦合。

对象的适配器模式

优点:

  • 可以在不修改原有代码的基础上来复用现有类,很好地符合 “开闭原则”(这点是两种实现方式都具有的)
  • 采用 “对象组合”的方式,更符合松耦合。

缺点:

  • 使得重定义Adaptee的行为较困难,这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。

四、使用场景

在以下情况下可以考虑使用适配器模式:

  1. 系统需要复用现有类,而该类的接口不符合系统的需求
  2. 想要建立一个可重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  3. 对于对象适配器模式,在设计里需要改变多个已有子类的接口,如果使用类的适配器模式,就要针对每一个子类做一个适配器,而这不太实际。

五、.NET中适配器模式的实现

1.适配器模式在.NET Framework中的一个最大的应用就是COM Interop。COM Interop就好像是COM和.NET之间的一座桥梁(关于COM互操作更多内容可以参考我的互操作系列)。COM组件对象与.NET类对象是完全不同的,但为了使.NET程序

象使用.NET对象一样使用COM组件,微软在处理方式上采用了Adapter模式,对COM对象进行包装,这个包装类就是RCW(Runtime Callable Wrapper)。RCW实际上是runtime生成的一个.NET类,它包装了COM组件的方法,并内部实现对COM组件的调用。如下图所示:

 

2..NET中的另外一个适配器模式的应用就是DataAdapter。ADO.NET为统一的数据访问提供了多个接口和基类,其中最重要的接口之一是IdataAdapter。DataAdpter起到了数据库到DataSet桥接器的作用,使应用程序的数据操作统一到DataSet上,而与具体的数据库类型无关。甚至可以针对特殊的数据源编制自己的DataAdpter,从而使我们的应用程序与这些特殊的数据源相兼容。

六、总结

到这里适配器模式的介绍就结束了,本文主要介绍了适配器模式的两种实现、分析它们的优缺点以及使用场景的介绍,在适配器模式中,适配器可以是抽象类,并适配器模式的实现是非常灵活的,我们完全可以将Adapter****模式中的“现存对象”作为新的接口方法参数,适配器类可以根据参数参数可以返回一个合适的实例给客户端。

本专题的所有源码:设计模式之适配器模式

一、引言

这里以电视遥控器的一个例子来引出桥接模式解决的问题,首先,我们每个牌子的电视机都有一个遥控器,此时我们能想到的一个设计是——把遥控器做为一个抽象类,抽象类中提供遥控器的所有实现,其他具体电视品牌的遥控器都继承这个抽象类,具体设计类图如下:

这样的实现使得每部不同型号的电视都有自己遥控器实现,这样的设计对于电视机的改变可以很好地应对,只需要添加一个派生类就搞定了,但随着时间的推移,用户需要改变遥控器的功能,如:用户可能后面需要对遥控器添加返回上一个台等功能时,此时上面的设计就需要修改抽象类RemoteControl的提供的接口了,此时可能只需要向抽象类中添加一个方法就可以解决了,但是这样带来的问题是我们改变了抽象的实现,如果用户需要同时改变电视机品型号和遥控器功能时,上面的设计就会导致相当大的修改,显然这样的设计并不是好的设计。然而使用桥接模式可以很好地解决这个问题,下面让我具体看看桥接模式是如何实现的。

二、桥接模式的详细介绍

2.1 定义

桥接模式即将抽象部分与实现部分脱耦,使它们可以独立变化。对于上面的问题中,抽象化也就是RemoteControl类,实现部分也就是On()、Off()、NextChannel()等这样的方法(即遥控器的实现),上面的设计中,抽象化和实现部分在一起,桥接模式的目的就是使两者分离,根据面向对象的封装变化的原则,我们可以把实现部分的变化(也就是遥控器功能的变化)封装到另外一个类中,这样的一个思路也就是桥接模式的实现,大家可以对照桥接模式的实现代码来解决我们的分析思路。

2.2 桥接模式实现

上面定义部分已经给出了我们桥接模式的目的以及实现思路了,下面让我们具体看看桥接模式是如何解决引言部分设计的不足。

抽象化部分的代码:

复制代码

///


/// 抽象概念中的遥控器,扮演抽象化角色 ///

public class RemoteControl
{ // 字段
private TV implementor; // 属性
public TV Implementor
{ get { return implementor; } set { implementor = value; }
} ///
/// 开电视机,这里抽象类中不再提供实现了,而是调用实现类中的实现 ///

public virtual void On()
{
implementor.On();
} ///
/// 关电视机 ///

public virtual void Off()
{
implementor.Off();
} ///
/// 换频道 ///

public virtual void SetChannel()
{
implementor.tuneChannel();
}
} ///
/// 具体遥控器 ///

public class ConcreteRemote : RemoteControl
{ public override void SetChannel()
{
Console.WriteLine(“-——————–”); base.SetChannel();
Console.WriteLine(“-——————–”);
}
}

复制代码

遥控器的实现方法部分代码,即实现化部分代码,此时我们用另外一个抽象类TV封装了遥控器功能的变化,具体实现交给具体型号电视机去完成:

复制代码

///


/// 电视机,提供抽象方法 ///

public abstract class TV
{ public abstract void On(); public abstract void Off(); public abstract void tuneChannel();
} ///
/// 长虹牌电视机,重写基类的抽象方法 /// 提供具体的实现 ///

public class ChangHong : TV
{ public override void On()
{
Console.WriteLine(“长虹牌电视机已经打开了”);
} public override void Off()
{
Console.WriteLine(“长虹牌电视机已经关掉了”);
} public override void tuneChannel()
{
Console.WriteLine(“长虹牌电视机换频道”);
}
} ///
/// 三星牌电视机,重写基类的抽象方法 ///

public class Samsung : TV
{ public override void On()
{
Console.WriteLine(“三星牌电视机已经打开了”);
} public override void Off()
{
Console.WriteLine(“三星牌电视机已经关掉了”);
} public override void tuneChannel()
{
Console.WriteLine(“三星牌电视机换频道”);
}
}

复制代码

采用桥接模式的客户端调用代码:

复制代码

///


/// 以电视机遥控器的例子来演示桥接模式 ///

class Client
{ static void Main(string[] args)
{ // 创建一个遥控器
RemoteControl remoteControl = new ConcreteRemote(); // 长虹电视机
remoteControl.Implementor = new ChangHong();
remoteControl.On();
remoteControl.SetChannel();
remoteControl.Off();
Console.WriteLine(); // 三星牌电视机
remoteControl.Implementor = new Samsung();
remoteControl.On();
remoteControl.SetChannel();
remoteControl.Off();
Console.Read();
}
}

复制代码

上面桥接模式的实现中,遥控器的功能实现方法不在遥控器抽象类中去实现了,而是把实现部分用来另一个电视机类去封装它,然而遥控器中只包含电视机类的一个引用,同时这样的设计也非常符合现实生活中的情况(我认为的现实生活中遥控器的实现——遥控器中并不包含换台,打开电视机这样的功能的实现,遥控器只是包含了电视机上这些功能的引用,然后红外线去找到电视机上对应功能的的实现)。通过桥接模式,我们把抽象化和实现化部分分离开了,这样就可以很好应对这两方面的变化了。

2.3 桥接模式的类图

看完桥接模式的实现后,为了帮助大家理清对桥接模式中类之间关系,这里给出桥接模式的类图结构:

三、桥接模式的优缺点

介绍完桥接模式,让我们看看桥接模式具体哪些优缺点。

优点:

把抽象接口与其实现解耦。

抽象和实现可以独立扩展,不会影响到对方。

实现细节对客户透明,对用于隐藏了具体实现细节。

缺点: 增加了系统的复杂度

四、使用场景

我们再来看看桥接模式的使用场景,在以下情况下应当使用桥接模式:

  1. 如果一个系统需要在构件的抽象化角色和具体化角色之间添加更多的灵活性,避免在两个层次之间建立静态的联系。
  2. 设计要求实现化角色的任何改变不应当影响客户端,或者实现化角色的改变对客户端是完全透明的。
  3. 需要跨越多个平台的图形和窗口系统上。
  4. 一个类存在两个独立变化的维度,且两个维度都需要进行扩展。

五、一个实际应用桥接模式的例子

桥接模式也经常用于具体的系统开发中,对于三层架构中就应用了桥接模式,三层架构中的业务逻辑层BLL中通过桥接模式与数据操作层解耦(DAL),其实现方式就是在BLL层中引用了DAL层中一个引用。这样数据操作的实现可以在不改变客户端代码的情况下动态进行更换,下面看一个简单的示例代码:

复制代码

// 客户端调用 // 类似Web应用程序
class Client
{ static void Main(string[] args)
{
BusinessObject customers = new CustomersBusinessObject(“ShangHai”);
customers.Dataacces = new CustomersDataAccess();

        customers.Add("小六");
        Console.WriteLine("增加了一位成员的结果:");
        customers.ShowAll();
        customers.Delete("王五");
        Console.WriteLine("删除了一位成员的结果:");
        customers.ShowAll();
        Console.WriteLine("更新了一位成员的结果:");
        customers.Update("Learning\_Hard");
        customers.ShowAll();

        Console.Read();
    }
} // BLL 层
public class BusinessObject
{ // 字段
    private DataAccess dataacess; private string city; public BusinessObject(string city)
    { this.city = city;
    } // 属性
    public DataAccess Dataacces
    { get { return dataacess; } set { dataacess = value; }
    } // 方法
    public virtual void Add(string name)
    {
        Dataacces.AddRecord(name);
    } public virtual void Delete(string name)
    {
        Dataacces.DeleteRecord(name);
    } public virtual void Update(string name)
    {
        Dataacces.UpdateRecord(name);
    } public virtual string Get(int index)
    { return Dataacces.GetRecord(index);
    } public virtual void ShowAll()
    {
        Console.WriteLine();
        Console.WriteLine("{0}的顾客有:", city);
        Dataacces.ShowAllRecords();
    }
} public class CustomersBusinessObject : BusinessObject
{ public CustomersBusinessObject(string city) 
        : base(city) { } // 重写方法
    public override void ShowAll()
    {
        Console.WriteLine("\------------------------"); base.ShowAll();
        Console.WriteLine("\------------------------");
    }
} /// <summary>
/// 相当于三层架构中数据访问层(DAL) /// </summary>
public abstract class DataAccess
{ // 对记录的增删改查操作
    public abstract void AddRecord(string name); public abstract void DeleteRecord(string name); public abstract void UpdateRecord(string name); public abstract string GetRecord(int index); public abstract void ShowAllRecords();
} public class CustomersDataAccess:DataAccess
{ // 字段
    private List<string\> customers =new List<string\>(); public CustomersDataAccess()
    { // 实际业务中从数据库中读取数据再填充列表
        customers.Add("Learning Hard");
        customers.Add("张三");
        customers.Add("李四");
        customers.Add("王五");
    } // 重写方法
    public override void AddRecord(string name)
    {
        customers.Add(name);
    } public override void DeleteRecord(string name)
    {
        customers.Remove(name);
    } public override void UpdateRecord(string updatename)
    {
        customers\[0\] = updatename;
    } public override string GetRecord(int index)
    { return customers\[index\];
    } public override void ShowAllRecords()
    { foreach (string name in customers)
        {
            Console.WriteLine(" " + name);
        }
    }
    
}

复制代码

六、总结

到这里,桥接模式的介绍就介绍,桥接模式实现了抽象化与实现化的解耦,使它们相互独立互不影响到对方