Nginx反向代理使用IIS,使其可以同时在同一端口上访问ASP.NET MVC和Python_lvfk-CSDN博客

mikel阅读(919)

来源: Nginx反向代理使用IIS,使其可以同时在同一端口上访问ASP.NET MVC和Python_lvfk-CSDN博客

准备工作
1、安装Nginx

官网下载 http://nginx.org/en/download.html

2、安装IIS

3、安装python

官网下载 https://www.python.org/

4、配置flup

WSGI是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受, 它已基本达成它了可移植性方面的目标。
WSGI 没有官方的实现, 因为WSGI更像一个协议. 只要遵照这些协议,WSGI应用(Application)都可以在任何实现(Server)上运行, 反之亦然。
WSGI具体实现有很多种方法,网上有很多的用python写的框架,比如facebook的tornado,这次选用的是flup
下载以及具体的介绍可以到 http://pypi.python.org/pypi/flup/1.0,
当下载后解压,把文件夹中的flup这个文件夹拷贝到Nginx的安装目录 Python27\Lib\site-packages下面,即可完成flup的配置
ps:1、此处Python和flup需要搭配使用,因为flup有对应py的版本限制

2、Nginx绑定80端口失败,请在Windows的服务中禁用 SQL Server Reporting Services (SQLEXPRESS) 这个服务,因为它就占用了80端口

Nginx配置Python
1、修改nginx-1.10.0\conf中的nginx.conf,在其中增加下面代码

server {
listen 80;
server_name test.com;
#此处的location配置访问服务器上的py
location ^ ~ /python/ {
#fastcgi_pass unix: /tmp/python – cgi.sock;
fastcgi_pass 127.0.0.1 : 8008;#此处的ip及端口会在接下来的py文件中监听使用
#下面三句不用改动
fastcgi_param SCRIPT_FILENAME “”;
fastcgi_param PATH_INFO $fastcgi_script_name;
include fastcgi.conf
}
}
2、编写py文件test.py

#!/usr/bin/python
# encoding : utf-8

from cgi import parse_qs
from cgi import escape
from flup.server.fcgi import WSGIServer

def myapp(environ, start_response):
#下面代码是从请求URL中提取URL参数
parameters = parse_qs(environ.get(‘QUERY_STRING’, ”))
if ‘s’ in parameters:
s = escape(parameters[‘s’][0])
if ‘url’ in parameters:
url = escape(parameters[‘url’][0])

#r = test(s, url)

start_response(‘200 OK’, [(‘Content-Type’, ‘text/plain’)])
return “s=”+s+”—“+”url=”+url;

if __name__ == ‘__main__’:
#此处监听的是Nginx中配置的ip和端口
WSGIServer(myapp,bindAddress=(‘127.0.0.1’,8008)).run()
3、运行Nginx

用命令行窗口进入到Nginx文件夹,执行Start Nginx 或者 Nginx

4、运行Py文件test.py

用命令行窗口进入到test.py所在文件夹,执行下面代码

python ytsig.py –method=prefork/threaded minspare=50 maxspare=50maxchildren=1000

5、你就可以在浏览器中访问 http://127.0.0.1/python/?s=12&url=http://127.0.0.1

Nginx反向代理IIS
首先,在IIS中使用ASP.NET MVC4建了一个网站,把其端口设置为:8888(可任意设置一个可用端口)

接下来此处非常简单就可以实现Nginx反向代理IIS,我们接着刚刚Nginx配置Python的基础上执行添加一段代码即可,如下:

server {
listen 80;
server_name test.com;
#此处的location配置访问服务器上的py
location ^ ~ /python/ {
#fastcgi_pass unix: /tmp/python – cgi.sock;
fastcgi_pass 127.0.0.1 : 8008;#此处的ip及端口会在接下来的py文件中监听使用
#下面三句不用改动
fastcgi_param SCRIPT_FILENAME “”;
fastcgi_param PATH_INFO $fastcgi_script_name;
include fastcgi.conf
}

#访问IIS,当请求链接不包含有/python/时,就转向到IIS中
location / {
proxy_pass http:127.0.0.1 : 8888 #IIS中网站配置的IP和端口
}
}

至此,我们就完成了Nginx反向代理使用IIS,使其可以同时在同一端口上访问ASP.NET MVC和Python

(补充)Nginx反向代理IIS,IIS如何获取客户端真实IP
1、修改Nginx的nginx.conf文件,添加以下四行代码

<span style=”white-space:pre”> </span>proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

server {
listen 80;
server_name test.com;
#此处的location配置访问服务器上的py
location ^ ~ /python/ {
#fastcgi_pass unix: /tmp/python – cgi.sock;
fastcgi_pass 127.0.0.1 : 8008;#此处的ip及端口会在接下来的py文件中监听使用
#下面三句不用改动
fastcgi_param SCRIPT_FILENAME “”;
fastcgi_param PATH_INFO $fastcgi_script_name;
include fastcgi.conf
}

#访问IIS,当请求链接不包含有/python/时,就转向到IIS中
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http:127.0.0.1 : 8888 #IIS中网站配置的IP和端口
}
}

2、在IIS网站的ASP.NET MVC代码中添加如下代码获取客户端的真实IP

#region 获取反向代理时的客户端的IP地址 getClientIP
/// <summary>
/// 获取反向代理时的客户端的IP地址
/// </summary>
/// <returns>返回客户端真实IP</returns>
private string getClientIP()
{
HttpRequestBase request = HttpContext.Request;

string ip = request.Headers.Get(“x-forwarded-for”);

if (ip == null || ip.Length == 0 || string.Equals(“unknown”, ip, StringComparison.OrdinalIgnoreCase))
{
ip = request.Headers.Get(“Proxy-Client-IP”);
}
if (ip == null || ip.Length == 0 || string.Equals(“unknown”, ip, StringComparison.OrdinalIgnoreCase))
{
ip = request.Headers.Get(“WL-Proxy-Client-IP”);

}
if (ip == null || ip.Length == 0 || string.Equals(“unknown”, ip, StringComparison.OrdinalIgnoreCase))
{
ip = request.UserHostAddress;
}
return ip;
}
#endregion

完成以上设置,再重新启动Nginx及IIS,即可在IIS中获取客户端真实IP。

 

Over…

参考资料:

1、http://blog.163.com/sky20081816@126/blog/static/1647610232010824262695/

2、http://blog.csdn.net/cclovett/article/details/26259175

3、http://zhidao.baidu.com/link?url=CdWrjHJ3VLiawbVfjSdB7G8fwzTcypd4qvOyT3xzx3BZG4_G2P_DbTHVSpiMupQQaeFwpajZjckwVkDpg6ZfgpzabZpzofwGjeTks2CCd_e
————————————————
版权声明:本文为CSDN博主「lvfk」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/just_shunjian/article/details/51454346

Nginx +iis反向代理 - 逍遥帝君 - 博客园

mikel阅读(817)

来源: Nginx +iis反向代理 – 逍遥帝君 – 博客园

Nginx +iis反向代理

一:简介

     Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev所开发,供俄国大型的入口网站及搜索引擎Rambler(俄文:Рамблер)使用。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好。

二:此次安装的是一个运行在windows上的反向代理服务器,主要和iis配合使用

直接启动exe文件即可

注意: 文件夹不能含有中文,否则会有错误

三:创建2个测试的文件,发布在iis上

四:修改nginx.conf文件

复制代码
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
 
    upstream www.aaa.com  { 
        server  127.0.0.1:8081 weight=1; #第一个测试网站
        server  127.0.0.1:8082 weight=1;  #第二个测试网站
    } 

    server {
        listen       8080;#这个原来是80端口,如果80已经被占用需要进行修改
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass  http://www.aaa.com;#反向代理指向地址        

            
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}
复制代码

 

然后直接启动即可,但是需要注意以上内容中的空格,否则启动失败

 

 1.如果站点使用了session,请求平均分配到两个站点,那么必然存在session共享问题,该如何解决?

  • 使用数据库保存session信息
  • 使用nginx将同一ip的请求分配到固定服务器,修改如下。ip_hash会计算ip对应hash值,然后分配到固定服务器

upstream Jq_one{
server 127.0.0.1:8082 ;
server 127.0.0.1:9000 ;
ip_hash;
}

2.由于请求是经过nginx转发过来的,可以在代码里面获取到用户请求的实际ip地址吗?

  • 答案是肯定的,在localtion节点设置如下请求头信息

#设置主机头和客户端真实地址,以便服务器获取客户端真实IP
proxy_set_header   Host             $host;
proxy_set_header   X-Real-IP        $remote_addr;
proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

代码里面通过Request.Headers[“X-Real-IP”],就能获取到真实ip

nginx实现静态文件(image,js,css)缓存

  • 在server节点下添加新的localtion
  •  #静态资源缓存设置
    location ~ \.(jpg|png|jpeg|bmp|gif|swf|css)$
    {
    expires 30d;
    root /nginx-1.9.3/html;#root:  #静态文件存在地址,这里设置在/nginx-1.9.3/html下
    break;
    }

这是index页面的代码 <li><img src=”/images/1.jpg”/></li>

主要参考文章:http://www.cnblogs.com/yanweidie/archive/2015/07/19/4658136.html

JS 全屏 - 简书

mikel阅读(1058)

来源: JS 全屏 – 简书

实现效果

JS实现浏览器全屏

实现方式对比

1、ActiveXObject 只支持IE

2、FullScreen(HTML方法)支持 Chrome 15 / Firefox Nightly / Safari 5.1
备注:方法二 FullScreen JavaScript API 目前仍是草案,实现这个 API,更确切来说是具有这项功能的浏览器有:Chrome 15 / Firefox Nightly / Safari 5.1。
其他方法:flash特性(google关键字 flash 全屏)

代码示例

<html>
<head>
    <script type="text/javascript" language="javascript"> 
  //方法一:ActiveXObject 只支持IE
  //方法二:FullScreen(HTML方法)支持 Chrome 15 / Firefox Nightly / Safari 5.1
  //备注:方法二 FullScreen Javascript API 目前仍是草案,实现这个 API,更确切来说是具有这项功能的浏览器有:Chrome 15 / Firefox Nightly / Safari 5.1。
   
function fullScreen() { 

  var element= document.documentElement; //若要全屏页面中div,var element= document.getElementById("divID");
  //IE 10及以下ActiveXObject
  if (window.ActiveXObject)
  {
    var WsShell = new ActiveXObject('WScript.Shell') 
    WsShell.SendKeys('{F11}'); 
  }
  //HTML W3C 提议
  else if(element.requestFullScreen) {  
    element.requestFullScreen();  
  }
  //IE11
  else if(element.msRequestFullscreen) {  
    element.msRequestFullscreen();  
  }
  // Webkit (works in Safari5.1 and Chrome 15)
  else if(element.webkitRequestFullScreen ) {  
    element.webkitRequestFullScreen();  
  } 
  // Firefox (works in nightly)
  else if(element.mozRequestFullScreen) {  
    element.mozRequestFullScreen();  
  }  
}  

function fullScreenCall() { 

  var el= document.documentElement; //若要全屏页面中div,var element= document.getElementById("divID");

  //切换全屏
  var rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen;
  if (typeof rfs != "undefined" && rfs) {
      rfs.call(el);
  } else if (typeof window.ActiveXObject != "undefined") {
      // for Internet Explorer 
      var wscript = new ActiveXObject("WScript.Shell");
      if (wscript != null) {
          wscript.SendKeys("{F11}");
      }
  }
}  

function fullExit(){
  var element= document.documentElement;//若要全屏页面中div,var element= document.getElementById("divID"); 
  //IE ActiveXObject
  if (window.ActiveXObject)
  {
    var WsShell = new ActiveXObject('WScript.Shell') 
    WsShell.SendKeys('{F11}'); 
  }
  //HTML5 W3C 提议
  else if(element.requestFullScreen) {  
    document.exitFullscreen();
  }
 //IE 11
  else if(element.msRequestFullscreen) {  
    document.msExitFullscreen();
  }
  // Webkit (works in Safari5.1 and Chrome 15)
  else if(element.webkitRequestFullScreen ) {  
    document.webkitCancelFullScreen(); 
  } 
  // Firefox (works in nightly)
  else if(element.mozRequestFullScreen) {  
    document.mozCancelFullScreen();  
  } 
}

function fullExitCall(){
  var cfs = document.exitFullscreen || document.webkitCancelFullScreen || document.msExitFullscreen || document.mozCancelFullScreen;
  if (typeof cfs != "undefined" && cfs) {
      cfs.call(document);
  } else if (typeof window.ActiveXObject != "undefined") {
      var wscript = new ActiveXObject("WScript.Shell");
      if (wscript != null) {
          wscript.SendKeys("{F11}");
      }
  }
}
    </script>

</head>
<body>
    <button id="btnFullScreen" onclick="fullScreen()">
        全屏</button>
    <button id="btnfullExit" onclick="fullExit()">
        退出全屏</button>
</body>
</html>

作者:过桥
链接:https://www.jianshu.com/p/624f808440ae
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Sqlserver报错:该伙伴事务管理器已经禁止了它对远程/网络事务的支持_cuihengju8933的博客-CSDN博客

mikel阅读(975)

来源: Sqlserver报错:该伙伴事务管理器已经禁止了它对远程/网络事务的支持_cuihengju8933的博客-CSDN博客

SQLServer执行链接服务器的远程查询报错:该伙伴事务管理器已经禁止了它对远程/网络事务的支持

在远程主机没有开启网络DTC访问。
http://blog.csdn.net/apollokk/article/details/51543349

双方启动MSDTC服务

(1)在windows控制面版–>管理工具–>服务–>Distributed Transaction Coordinator–>属性–>启动
(2)在CMD下运行”net start msdtc”开启服务后正常。

管理工具-组件服务,全部勾选。

OK。
远程查询访问正常。
 

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/22996654/viewspace-2146331/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/22996654/viewspace-2146331/

Lambda表达式select()和where()的区别 - 小海很友爱 - 博客园

mikel阅读(1186)

来源: Lambda表达式select()和where()的区别 – 小海很友爱 – 博客园

新建一个控制台程序ConsoleApplication1

1、where()用法:必须加条件,且返回对象结果。

static void Main(string[] args)
{
string[] arrays={“asd”,”abc”,”bbb”,”ccc”};
var results = arrays.Where(a=>a.Contains(“b”));//必须加条件,返回对象

foreach(var da in results )
{
Console.WriteLine(da);
}

Console.WriteLine(“按任意键可退出!”);
Console.ReadKey();
}

结果:abc  bbb。

2、select()用法:(1)(a=>a.Value==”22″)加条件查询时,返回bool型结果;(2)(a=>a)没条件返回对象

(1)(a=>a.Value==”22″)加条件查询时,返回bool型结果

static void Main(string[] args)
{
string[] arrays={“asd”,”abc”,”bbb”,”ccc”};
var results = arrays.Select(a => a.Contains(“b”));//1、(a=>a.Value==”22″)加条件查询时,返回bool型结果

foreach(var da in results )
{
Console.WriteLine(da);
}

Console.WriteLine(“按任意键可退出!”);
Console.ReadKey();
}

结果:False  True  True  False

(2)(a=>a)没条件返回对象

static void Main(string[] args)
{
string[] arrays={“asd”,”abc”,”bbb”,”ccc”};
var results = arrays.Select(a => a);//1、(a=>a)没条件,返回所有对象;

foreach(var da in results )
{
Console.WriteLine(da);
}

Console.WriteLine(“按任意键可退出!”);
Console.ReadKey();
}

结果:asd  abc   bbb   ccc 

python3.6引入docx后 ,文件报错 moduleNotFoundError:No module named 'exceptions'_海棠花未眠的博客-CSDN博客

mikel阅读(825)

来源: python3.6引入docx后 ,文件报错 moduleNotFoundError:No module named ‘exceptions’_海棠花未眠的博客-CSDN博客

python3.x版本移除了exceptions模块,但是docx包中引用了该模块

安装最新版python-docx模块即可

以下总结两种常用安装第三方包的方法

方法一:

#1 pip install python-docx安装

pip会自动先安装python-docx的前置需求包lxml,但安装失败。

那么就先 安装 lmxl ,具体方法 cmd 输入 pip install lxml

或者通过下载lxml.whl文件进行安装。

新版本pip已支持wheel格式,如若你的pip版本不支持,那么升级你的pip版本 升级pip具体命令 :python -m pip install –upgrade pip

或者 用以下方法安装wheel。

首先安装whl包:pip install wheel

接着去开发者页面下载lxml的安装包:https://pypi.python.org/pypi/lxml。页面默认是最新版本。

命令行进入安装包所在目录,执行pip install 安装包名称

方法二:
1.下载 python_docx-0.8.6-py2.py3-none-any.whl 地址: http://www.lfd.uci.edu/~gohlke/pythonlibs/

3. 命令行输入pip install python_docx-0.8.6-py2.py3-none-any.whl 重新下载docx包,问题解决。
————————————————
版权声明:本文为CSDN博主「衣服架子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/huijiaaa1/article/details/80616842

string.Format 异常 输入字符串的格式不正确_zhl71199713的专栏-CSDN博客_输入字符串的格式不正确怎么解决

mikel阅读(710)

来源: string.Format 异常 输入字符串的格式不正确_zhl71199713的专栏-CSDN博客_输入字符串的格式不正确怎么解决

最近做一个项目,在使用string.Format对数据进行整理的时候,一直报错:输入字符串的格式不正确。

纠结了很久。最后终于查了下资料终于知道了问题所在。现在将这个问题记录一下:

 

在代码中,需要将结果以Json格式,输出。最开始报错,写的代码如下:

public void TEST()
{
try
{
string t = “Jim”;
string sresult = string.Format(“{name:\”{0}\”}”, t);//这里报错
Console.Write(sresult);
}
catch (Exception ex)
{
}
}

后来查找了一些资料之后,才知道问题所在。
C# 中使用类似 {0}, {1:yyyy-MM-dd} 这样的格式占位符,如果被格式化字符串本身包含 { 或者 } 怎么办呢?答案是:用两个 { 或者 }连写表示单个。

我的代码问题就出在这个地方。修改之后,就好了,修改后的代码如下:

public void TEST()
{
try
{
string t = “Jim”;
string sresult = string.Format(“{{name:\”{0}\”}}”, t);
Console.Write(sresult);
}
catch (Exception ex)
{
}
}
事实上,很多情况下带特殊含义的字符都是这样转义的:如,

C# 中,当字符串常量带 @ 前导的时候,用两个 ” 连写表示一个 ” (半角双引号)

public void TEST()
{
try
{
string sresult = @”this is “”Jim”””;
string tresult=@”this is ‘Jim'”;
string tresult = @”this is “Jim””;//错误
Console.Write(sresult);
}
catch (Exception ex)
{
}
}
SQL 字符串常量,用两个 ‘ 连写表示一个 ‘ (半角单引号)
DECLARE @str6 varchar(100)
SET @str6 = ‘My UserName is ”Jinglecat”.’
PRINT @str6 — My UserName is ‘Jinglecat’.
正则表达式中用,两个 $ 连写表示一个 $ (dollar)
————————————————
版权声明:本文为CSDN博主「zhl71199713」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhl71199713/article/details/19846571

json格式的字符串使用string.Format()方法报错:输入字符串的格式不正确 - 梨花驿路 - 博客园

mikel阅读(1141)

来源: json格式的字符串使用string.Format()方法报错:输入字符串的格式不正确 – 梨花驿路 – 博客园

解决:把大括号转义一下就可以了啊,大括号的转义是两个{{  结尾是}}

 

 

今天看同事写的代码,发现他在使用string.format拼接类似json格式的数据时,大括号多了一对,感觉不对就查了查msdn,如下:

转义大括号

左大括号和右大括号被解释为格式项的开始和结束。因此,必须使用转义序列显示文本左大括号或右大括号。在固定文本中指定两个左大括号 (“{{“) 以显示一个左大括号 (“{“),或指定两个右大括号 (“}}”) 以显示一个右大括号 (“}”)。按照在格式项中遇到大括号的顺序依次解释它们。不支持解释嵌套的大括号。

解释转义大括号的方式会导致意外的结果。例如,考虑要显示一个左大括号、一个格式化为十进制数的数值和一个右大括号的格式项“{{{0:D}}}”。但是,实际是按照以下方式解释该格式项:

前两个左大括号 (“{{“) 被转义,生成一个左大括号。

之后的三个字符 (“{0:”) 被解释为格式项的开始。

下一个字符 (“D”) 将被解释为 Decimal 标准数值格式说明符,但后面的两个转义大括号 (“}}”) 生成单个大括号。由于得到的字符串 (“D}”) 不是标准数值格式说明符号,所以得到的字符串会被解释为用于显示字符串“D}”的自定义格式字符串。

最后一个大括号 (“}”) 被解释为格式项的结束。

显示的最终结果是字符串“{D}”。不会显示本来要格式化的数值。

在编写代码时,避免错误解释转义大括号和格式项的一种方法是单独格式化大括号和格式项。也就是说,在第一个格式化操作中显示文本左大括号,在下一操作中显示格式项的结果,然后在最后一个操作中显示文本右大括号。

设计模式如何提升 vivo 营销自动化业务扩展性 | 引擎篇01 - vivo互联网技术 - 博客园

mikel阅读(779)

来源: 设计模式如何提升 vivo 营销自动化业务扩展性 | 引擎篇01 – vivo互联网技术 – 博客园

在《vivo 营销自动化技术解密 |开篇》中,我们从整体上介绍了vivo营销自动化平台的业务架构、核心业务模块功能、系统架构和几大核心技术设计。

本次带来的是系列文章的第2篇,本文详细解析设计模式和相关应用如何帮助营销自动化业务提升系统扩展性,以及实践过程中的思考和总结。

一、引言

营销业务本身极具复杂多变性,特别是伴随着数字化营销蓬勃发展的趋势,在市场的不同时期、公司发展的不同阶段、面向不同的用户群体以及持续效果波动迭代,都会产生不同的营销策略决策。

当面对随时变化的业务场景时,系统的扩展性就显得非常重要。而在谈到系统设计扩展性的时候,总是首先会想到设计原则和设计模式。但设计模式不是银弹,并不能解决所有问题,它只是前人提炼总结出来的招式方法,需要开发者根据实际业务场景进行合理的选择、合适的变通,才能真正去解决实际场景中的问题,并总结形成自己的方法论。

那么接下来我们看看设计模式是如何帮助我们在营销策略引擎中提升系统扩展性的。

二、营销策略引擎

先简单介绍一下营销策略引擎:策略引擎是通过搭建可视化流程组件,定义各个流程节点,自动化执行活动业务流程,从而提供不同运营活动能力。其中核心活动业务流程主要包括三大部分:运营活动配置->运营活动审批->运营活动执行

  • 运营活动配置:运营人员在系统后台配置运营活动。包括活动名称、活动时间、触发条件、活动用户和具体推送渠道(如短信、微信、push推送等)。
  • 运营活动审批:品质/主管人员审批运营活动配置。审批流程涉及了活动审批节点和人员的配置,审批相关的回调操作配置。
  • 运营活动执行:系统自动化执行运营活动的过程。即具体的渠道如短信、微信、push等推送活动的任务执行下发流程,包括用户数据准备,数据下发推送和数据效果回收等。

三、设计模式具体应用

3.1 运营活动配置

3.1.1 工厂模式

具体场景

一般情况下,根据不同的用户和活动场景,运营借助数据分析会决策出不同的活动策略,比如需要创建短信推送策略、微信图文推送策略、App Push推送策略等。此时我们可以使用工厂模式,统一管理具体推送策略的创建。

模式分析

在GoF《设计模式:可复用面向对象软件的基础》中:工厂模式被分成了工厂方法和抽象工厂两类,而简单工厂模式(又称静态工厂模式)被看作是工厂方法的一种特例。不过由于简单工厂和工厂方法相对更简单和易于理解,代码可读性也更强,因此在实际项目中更加常用。

其中简单工厂的适用场景:

  • a.工厂类负责创建的对象比较少,工厂方法中的创建逻辑简单。
  • b.客户端无须关心创建具体对象的细节,仅需知道传入工厂类的类型参数。

而工厂方法的适用场景:

  • a.工厂类对象创建逻辑相对复杂,需要将工厂实例化延迟到其具体工厂子类中。
  • b.适合需求变更频繁的场景,可以利用不同的工厂实现类支持新的工厂创建方案,更符合开闭原则,扩展性更好。

典型代码示例

//抽象产品类
public abstract class Product {
    public abstract void method();
}
//具体的产品类 
class ProductA extends Product {
    @Override
    public void method() {
        //具体的执行逻辑
     }
}
//抽象工厂模板类
abstract class Factory<T> {
    abstract Product createProduct(Class<T> c);
}
//具体工厂实现类
class FactoryA extends Factory{
    @Override
    Product createProduct(Class c) {
        Product product = (Product) Class.forName(c.getName()).newInstance();
        return product;
    }
}

实际代码

/**
 * @author chenwangrong
 * 活动策略工厂类
 */
@Component
@Slf4j
public class ActivityStrategyFactory {
 
    /**
     * 获得渠道类型对应的策略
     *
     * @param channelType channelType
     * @return OperationServiceStrategy
     */
    public static ActivityStrategy getActivityStrategy(ChannelTypeEnum channelType) {
 
        ChannelTypeStrategyEnum channelTypeStrategyEnum = ChannelTypeStrategyEnum.getByChannelType(channelType);
        Assert.notNull(channelTypeStrategyEnum , "指定的渠道类型[channelType=" + channelType + "]不存在");
 
        String strategyName= channelTypeStrategyEnum.getHandlerName();
        Assert.notNull(strategyName, "指定的渠道类型[channelType=" + channelType + "未配置策略");
 
        return (ActivityStrategy)SpringContextHolder.getBean(handlerName);
    }
 
 
    public enum ChannelTypeStrategyEnum {
        /**
         * 短信渠道
         */
        SMS(ChannelTypeEnum.SMS, "smsActivityStrategy"),
        /**
         * 微信渠道
         */
        WX_NEWS(ChannelTypeEnum.WX, "wxActivityStrategy"),
        /**
         * push渠道
         */
        PUSH(ChannelTypeEnum.PUSH, "pushActivityStrategy"),;
 
        private final ChannelTypeEnum channelTypeEnum;
 
        private final String strategyName;
 
        ChannelTypeStrategyEnum (ChannelTypeEnum channelTypeEnum, String strategyName) {
            this.channelTypeEnum = channelTypeEnum;
            this.strategyName= strategyName;
        }
 
 
        public String getStrategyName() {
            return strategyName;
        }
 
        public static ChannelTypeStrategyEnum getByChannelType(ChannelTypeEnum channelTypeEnum) {
            for (ChannelTypeStrategyEnum channelTypeStrategyEnum : values()) {
                if (channelTypeEnum == channelTypeStrategyEnum.channelTypeEnum) {
                    return channelTypeStrategyEnum ;
                }
            }
            return null;
        }
    }
}

实践总结

在实际项目代码中我们采用的是简单工厂模式(静态工厂模式),实现时利用枚举(或者映射配置表)来保存渠道类型与具体策略实现类的映射关系,再结合Spring的单例模式,来进行策略类的创建。

相比于工厂方法模式,在满足业务的前提下,减少了工厂类数量,代码更加简单适用。

3.1.2 模板方法模式

具体场景

在创建不同类型运营活动策略的时候,可以发现除了保存具体活动渠道配置信息不一样之外,创建过程中很多操作流程是相同的:比如保存活动基本配置信息,审计日志上报,创建活动审批工单,创建完成后消息提醒等

原有实践

/**
 * 短信活动类
 *
 */
@Service
public class SmsActivityStrategy{
  
    /**
     * 执行渠道发送
     *
     * @param msgParam msgParam
     */
    public ProcessResult createActivity(ActParam param) {
         //保存活动基础信息
         saveActBaseConfig(param);
         //保存短信活动配置
         createSmsActivity(param);
         //审计日志上报 ...
         //创建活动审批工单 ...
         //消息通知 ...
         sendNotification(param);
    }
}
 
/**
 * Push活动类
 *
 */
@Service
public class PushActivityStrategy{
  
    /**
     * 执行渠道发送
     *
     * @param msgParam msgParam
     */
    public ProcessResult createActivity(ActParam param) {
         //保存活动基础信息
         saveActBaseConfig(param);
         //保存Push活动配置
         createChannelActivity(param);
         //审计日志上报 ...
         //创建活动审批工单 ...
         //消息通知 ...
         sendNotification(param);
    }
}
 
...

对于每种活动策略而言,这些操作都是必需的且操作流程都是固定的,所以可以将这些操作提取成公用的流程,此时就考虑到了模板方法模式。

模式分析

在GoF《设计模式:可复用面向对象软件的基础》:模板方法模式是在一个方法中定义一个算法骨架,并将某些步骤推迟到其子类中实现。模板方法模式允许子类在不改变算法结构的情况下重新定义算法的某些步骤。

上面所指的“算法”,可以理解为业务逻辑,而‘’算法骨架“即是模板,包含‘’算法骨架“的方法就是模板方法,这也是模板方法模式名称的来源。

模板方法模式适用场景:业务逻辑由确定的步骤组成,这些步骤的顺序要是固定不变的,不同的具体业务之间某些方法或者实现可以有所不同。

实现时一般通过抽象类来定义一个逻辑模板和框架,然后将无法确定的部分抽象成抽象方法交由子类来实现,调用逻辑仍在抽象类中完成。

典型代码示例

//模板类
public abstract class AbstractTemplate {
 
//业务逻辑1
protected abstract void doStep1();
//业务逻辑2
protected abstract void doStep2();
 
//模板方法
public void templateMethod(){
     this.doStep1();
     //公共逻辑
       ......
     this.doStep2();
   }
}
 
//具体实现类1
public class ConcreteClass1  extends AbstractTemplate {
  //实现业务逻辑1
  protected void doStep1()
  {
     //业务逻辑处理
  }
 
  //实现业务逻辑2
  protected void doStep2()
  {
    //业务逻辑处理
   }
}
 
//具体实现类2
public class ConcreteClass2  extends AbstractTemplate {
  //实现业务逻辑1
  protected void doStep1()
  {
     //业务逻辑处理
  }
 
  //实现业务逻辑2
  protected void doStep2()
  {
    //业务逻辑处理
   }
}
 
// 调用类
public class Client {
 public static void main(String[] args)
  {
    AbstractTemplate class1=new ConcreteClass1();
    AbstractTemplate class2=new ConcreteClass2();
   //调用模板方法
    class1.templateMethod();
    class2.templateMethod();
   }
}

实际代码

/**
 * 活动创建模板类
 *
 * @author chenwangrong
 */
@Slf4j
public abstract class AbstractActivityTemplate{
 
    /**
     * 保存具体活动配置
     *
     * @param param 活动参数
     * @return ProcessResult 处理结果
     */
    protected abstract ProcessResult createChannelActivity(ActParam param);
 
    /**
     * 执行活动创建
     *
     * @param msgParam msgParam
     */
    public ProcessResult createActivity(ActParam param) {
         //保存活动基础信息
         saveActBaseConfig(param);
         //保存具体渠道配置
         createChannelActivity(param);
         //审计日志上报 ...
         //消息通知 ...
    }
}
 
/**
 * 短信活动类
 *
 */
@Service
public class SmsActivityStrategy extends AbstractActivityTemplate{
  
    /**
     * 创建短信渠道活动配置
     *
     * @param msgParam msgParam
     */
    public ProcessResult createChannelActivity(ActParam param) {
         //仅需要实现:保存短信活动配置
         createSmsActivity(param);    
    }
}
 
(其他渠道活动类似,此处省略)
 
 
// 调用类
public class Client {
 public static void main(String[] args)
  {
    AbstractActivityTemplate smsActivityStrategy=new SmsActivityStrategy();
    AbstractActivityTemplate pushActivityStrategy=new PushActivityStrategy();
 
    ActParam param = new ActParam();
 
   //调用具体活动实现类
    smsActivityStrategy.createActivity(param);
    pushActivityStrategy.createActivity(param);
   }
}

实践总结

模板方法模式有两大作用:复用和扩展。复用是指所有的子类可以复用父类中提供的模板方法的代码。扩展是指框架通过模板模式提供功能扩展点,让用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

模板方法非常适用于有通用业务逻辑处理流程,同时又在具体流程上存在一定差异的场景,可以通过将流程骨架抽取到模板类中,将可变的差异点设置为抽象方法,达到封装不变部分,扩展可变部分的目的。

3.1.3 策略模式

具体场景

上述我们通过模板方法模式抽取出了公共流程骨架,但这里还存在一个问题:调用类仍需要明确知道具体实现类是哪个,实例化后才可进行调用。也就是每一次增加新的渠道活动时,调用方都必须修改调用逻辑,添加新的活动实现类的初始化调用,显然不利用业务的扩展性。

在创建运营活动过程中,不同类型的活动会对应着不同的创建流程,调用方只需要根据渠道类型来进行区分,而无需理会其中具体的业务逻辑。此时策略模式是一个比较好的选择。

模式分析

在GoF《设计模式:可复用面向对象软件的基础》中:策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的调用方。

典型代码示例

//策略接口定义
public interface Strategy {
    void doStrategy();
}
​
//策略具体实现类(多个)
public class StrategyA implements Strategy{
    @Override
    public void doStrategy() {
    }
}
​
//上下文操作类, 屏蔽高层模块对策略的直接访问
public class Context {
    private Strategy strategy = null;
​
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }
 
    public void doStrategy() {
        strategy.doStrategy();
    }
}

实际代码

/**
 * 渠道活动创建策略接口
 *
 */
public interface ActivityStrategy {
 
    /**
     * 创建渠道活动配置
     *
     * @param param 活动参数
     * @return 
     */
    void createActivity(ActParam param);
}
 
/**
 * 活动模板类
 *
 */
@Slf4j
public abstract class AbstractActivityTemplate implements ActivityStrategy {
 
    /**
     * 抽象方法:具体渠道活动创建
     *
     */
    protected abstract ProcessResult createChannelActivity(ActParam param);
 
    @Override
    public ProcessResult createActivity(ActParam param) {
         //保存活动基础信息
         saveActBaseConfig(param);
         //保存具体渠道配置
         createChannelActivity(param);
         //审计日志上报 ...
         //消息通知 ...
    }
}
 
/**
 * 短信推送策略具体实现类
 *
 */
@Component
public class SmsChannelActivityStrategy extends AbstractActivityTemplate {
    @Override
    public void createChannelActivity(ActParam param) {
        //保存短信配置数据
    }
}
(其他渠道活动类似,此处省略)
 
 
/**
 * 策略调用入口
 *
 */
@Slf4j
@Component
public class ActivityContext {
 
   @Resource
   private ActivityStrategyFactory activityStrategyFactory ;
 
      public void create(ActParam param) {
            //通过前面的工厂模式的代码,获取具体渠道对应的策略类
            ActivityStrategy strategy = activityStrategyFactory.getActivityStrategy(param.ChannelType);
            //执行策略
            strategy.createActivity(param);
      }
}

实际编码过程中,我们加入了ChannelActivityStrategy作为渠道活动创建策略接口,并用模板类AbstractActivityTemplate实现该接口,同时结合工厂模式创建具体策略,至此将三种模式结合了起来

实践总结

策略模式在项目开发过程中经常用于消除复杂的if else复杂逻辑,后续如果有新的渠道活动时,只需要新增对应渠道的活动创建逻辑即可,可以十分便捷地对系统业务进行扩展。

在项目实践过程,经常会将工厂模式、模板方法模式和策略模式一起结合使用。模板方法模式进行业务流程公共骨架的抽取,策略模式进行具体子流程策略的实现和调用的封装,而工厂模式可以进行子流程策略的创建。

多种模式的结合使用可以充分发挥出各个模式的优势,达到真正提升系统设计扩展性的目的。

3.2 运营活动执行

3.2.1 状态模式

具体场景

在运营活动的执行过程中,会涉及活动状态的变更,以及变更前的条件检测和变更后的操作处理。与之相对应地,我们很容易就会想到状态模式。

模式分析

在 GoF 经典的《设计模式:可复用面向对象软件的基础》中:状态模式允许一个对象在其内部状态改变的时候改变其行为。

状态模式的作用就是分离状态的行为,通过维护状态的变化,来调用不同状态对应的不同功能。它们的关系可以描述为:状态决定行为。由于状态是在运行期被改变的,因此行为也会在运行期随着状态的改变而改变。

典型代码示例

/**
 * 状态模式
 * 抽象状态类
 * */
interface State {
    //状态对应的处理
    void handle()
}
 
  
//具体状态关现类
public  class ConcreteStateA implements  State {
    @Override
    public void handle() {
    }
}
 
public  class ConcreteStateB implements  State {
    @Override
    public void handle() {
    }
}
 
//环境类Context,访问入口
public class Context {
    //持有一个State类型的对象实例
    private State state;
 
    public void setState(State state) {
        this.state = state;
    }
      
    public void request() {
        //转调state来处理
        state.handle();
    }
}
 
public class Client {
    public static void main(String[] args){
        //创建状态
        State state = new ConcreteStateB();
        //创建环境
        Context context = new Context();
        //将状态设置到环境中
        context.setState(state);
        //请求
        context.request();
    }
}

实践总结

在实际软件项目开发中,业务状态不多且状态转移简单的场景, 可使用状态模式来实现;但如果是涉及的业务流程状态转移繁杂时,使用状态模式会引入非常多的状态类和方法,当状态逻辑有变更时,代码也会变得难以维护,此时使用状态模式并不十分适合。

而当流程状态繁多,事件校验和触发执行动作包含的业务逻辑比较复杂时,如何去实现呢?

这里我们必须停下来思考:使用设计模式只是解决实际问题的一种手段,但设计模式不是一把“万能的”锤子,需要清楚地了解到它的优势和不足。而这种问题场景下,业界已经有一个更通用的方案——有限状态机,通过更高层的封装,提供给业务更便捷的应用。

3.2.2 状态模式的应用——有限状态机

有限状态机(Finite-State Machine , 缩写:FSM),业界简称状态机。它亦是由事件状态动作 三大部分组成,三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。状态机可以基于传统的状态模式硬编码来实现,也可以通过数据库/文件配置或者DSL的方式来保存状态及转移配置来实现(推荐)。

业界中也已涌现出了不少开源状态机的框架,比较常用的有Spring-statemachine(Spring官方提供) 、squirrel statemachine和阿里开源的cola-statemachine。

实际应用

在实际项目开发中,我们针对自身业务的特点:业务流程状态多,但是事件触发和状态变更动作相对简单,故而选择了无状态、更加轻量级的解决方案——基于开源的状态机实现思想进行开发。(关于状态机的实现和使用选型会在后续的文章中做进一步的分析,感兴趣的童鞋可以访问官网先做了解)。

实践代码

/**
 * 状态机工厂类
 */
public class StatusMachineEngine {
    private StatusMachineEngine() {
    }
    private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap();
 
    static {
        //短信推送状态
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
        //PUSH推送状态
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
        //......
    }
 
    public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
        return STATUS_MACHINE_MAP.get(channelTypeEnum);
    }
 
   /**
     * 触发状态转移
     * @param channelTypeEnum
     * @param status 当前状态
     * @param eventType 触发事件
     * @param context 上下文参数
     */
    public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
        StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
        //推动状态机进行流转,具体介绍本期先省略
        orderStateMachine.fireEvent(status, eventType, context);
    }
 
/**
 * 短信推送活动状态机初始化
 */
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> {
 
    @Autowired
    private  StatusAction smsStatusAction;
    @Autowired
    private  StatusCondition smsStatusCondition;
 
    //基于DSL构建状态配置,触发事件转移和后续的动作
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition()
                .from(INIT)
                .to(NOT_START)
                .on(EventType.TIME_BEGIN)
                .when(smsStatusAction.checkNotifyCondition())
                .perform(smsStatusAction.doNotifyAction());
        builder.externalTransition()
                .from(NOT_START)
                .to(DATA_PREPARING)
                .on(EventType.CAL_DATA)
                .when(smsStatusCondition.doNotifyAction())
                .perform(smsStatusAction.doNotifyAction());
        builder.externalTransition()
                .from(DATA_PREPARING)
                .to(DATA_PREPARED)
                .on(EventType.PREPARED_DATA)
                .when(smsStatusCondition.doNotifyAction())
                .perform(smsStatusAction.doNotifyAction());
        ...(省略其他状态)
        builder.build(StatusMachineEngine.getMachineEngine(ChannelTypeEnum.SMS));
    }
 
   //调用端
   public class Client {
     public static void main(String[] args){
          //构建活动上下文
          Context context = new Context(...);
         // 触发状态流转
          StatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context);
      }
   }
}

通过预定义状态转换流程的方式,实现ApplicationListener接口,在应用启动时将事件、状态转移条件和触发操作的流程加载到状态机工作内存中,由事件触发驱动状态机进行自动流转。

实践总结

实际场景中,不必强行套用设计模式,而是应当充分结合业务的特点,同时针对设计模式的优劣势,进行更加合适的选型或者进一步扩展。

3.3 自动化运营活动审批

3.3.1 设计模式的综合应用——工作流引擎

具体场景

为了做好品质和风险管控,活动创建需要加入审批环节,把控运营活动的发布执行,同时对于不同类型的运营活动,可能涉及的业务领域和部门各不相同,审批管控人员也不一样,需要配置相对应的审批关系。

此时需要做到:

  • a.审批流程全配置化,易修改和添加;
  • b.业务流程节点可自由编排,组件公用化;
  • c.流程数据持久化,审批过程数据需要进行操作监控。

针对这方面的需求,业界有一套通用的业务工具——工作流引擎。工作流引擎显然并不属于具体某一种设计模式的实现,它是涵盖了多种设计模式的组件应用。

不仅仅是审批功能,其实前面自动化营销流程引擎设计也同样是使用工作流引擎搭建流程组件

状态机 VS 工作流引擎

工作流引擎和状态机似乎存在非常多的相似之处,都可以通过定义流程的节点、转移条件和相应触发的操作来完成业务流程。如果只从适用场景的复杂性上看,状态机更适用于单维度的业务问题,能够清晰地描绘出所有可能的状态以及导致转换的事件,更加灵活轻便;而工作流引擎则更适合业务流程管理,解决如大型CRM复杂度更高的流程自动化问题,可以改善整体业务流程的效率。

在业界的工作流引擎中,比较著名的有Activiti和JBPM等。(关于状态机和工作流引擎的对比、开源工作流引擎的具体介绍和选型,以及如何自行开发构建一款基本的工作流引擎组件,同样是会在后续的文章中做进一步分析,本文由于主题和篇幅的原因暂不做详细介绍。)

在实际开发过程中,我们是基于开源的Activiti工作流引擎自研了一套简易版的工作流引擎,精简了许多相关的配置,只留下了核心流程操作和数据记录。

工作流引擎流程图:

实践总结

工作流引擎是涵盖了多种设计模式的应用组件,只有在复杂多变的业务场景中才需要应用,需要结合业务进行仔细评估。在合适的场景使用合适的解决方案,遵循系统架构设计的简单、合适、可演化原则,不过度设计。

四、总结

本文基于自动化营销的业务实践,分析介绍了工厂方法模式、模板方法模式、策略模式以及状态模式这四种模式在项目开发中的具体实现过程。也在单纯的模式之外介绍了状态机和工作流引擎这些涵盖了多种设计模式系统组件,并分享了过程中的选择和思考。

面对业务复杂多变的需求,需要时刻关注系统设计的复用性和可扩展性,而设计原则和设计模式可以在系统设计实现时给予我们方向性的指导,同时更需要根据实际业务场景进行合理的选择,合适的变通,不断完善自己的方法论。

后续我们将带来系列专题文章的其他内容,每一篇文章都会对里面的技术实践进行详尽解析,敬请期待。

作者:vivo互联网服务器团队-Chen Wangrong

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

领域驱动设计(DDD:Domain-Driven Design) - 解道Jdon

mikel阅读(1150)

来源: 领域驱动设计(DDD:Domain-Driven Design) – 解道Jdon

领域驱动设计(DDD:Domain-Driven Design)

Eric Evans的“Domain-Driven Design领域驱动设计”简称DDD,Evans DDD是一套综合软件系统分析和设计的面向对象建模方法,本站Jdon.com是国内公开最早讨论DDD网站之一,可订阅DDD专题。初学者学习DDD可从研究本站Jdon框架的DDD应用源码开始,戳这里开始

过去系统分析和系统设计都是分离的,正如我们国家“系统分析师” 和“系统设计师” 两种职称考试一样,这样割裂的结果导致,需求分析的结果无法直接进行设计编程,而能够进行编程运行的代码却扭曲需求,导致客户运行软件后才发现很多功能不是自己想要的,而且软件不能快速跟随需求变化。

DDD则打破了这种隔阂,提出了领域模型概念,统一了分析和设计编程,使得软件能够更灵活快速跟随需求变化。见下面DDD与传统CRUD或过程脚本或者面向数据表等在开发效率上比较:

ddd

服务器后端发展三个阶段:

  1. UI+DataBase的两层架构,这种面向数据库的架构(上图table module )没有灵活性。
  2. UI+Service+DataBase的多层SOA架构,这种服务+表模型的架构易使服务变得囊肿,难于维护拓展,伸缩性能差,见这里讨论Spring Web 应用的最大败笔垂直切片的烟囱式故事已经一去不复返了
  3. DDD+SOA微服务的事件驱动的CQRS读写分离架构,应付复杂业务逻辑,以聚合模型替代数据表模型,以并发的事件驱动替代串联的消息驱动。真正实现以业务实体为核心的灵活拓展。

DDD革命性在于:领域模型准确反映了业务语言,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据,这些数据对象除了简单setter/getter方法外,没有任何业务方法,被比喻成失血模型,那么领域模型这种带有业务方法的充血模型到底好在哪里?

比赛Match为案例,比赛有“开始”和“结束”等业务行为,但是传统经典的方式是将“开始”和“结束”行为放在比赛的服务Service中,而不是放在比赛对象本身之中。我们不能因为用了计算机,用了数据库,用了框架,业务模型反而被技术框架给绑架,就像人虽然是由母亲生的,但是人的吃喝拉撒母亲不能替代,更不能以母爱名义肢解人的正常职责行为,如果是这样,这个人就是被母爱绑架了。

提倡充血模型,实际就是让过去被肢解被黑crack的业务模型回归正常,当然这也会被一些先入为主或被洗过脑的程序员看成反而不正常,这更是极大可悲之处。看到领域模型代码,就看到业务需求,没有翻译没有转换,保证软件真正实现“拷贝不走样”。

DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。重点不同导致编程世界观不同。

DDD是解决复杂中大型软件的一套行之有效方式,在国外已经成为主流。DDD认为很多原因造成软件的复杂性,我们不可能避免这些复杂性,能做的是对复杂的问题进行控制。而一个好的领域模型是控制复杂问题的关键。领域模型的价值在于提供一种通用的语言,使得领域专家、产品经理和软件技术人员联系在一起,沟通无歧义。

DDD落地实现离不开Clean架构、六边形架构、 CQRS、Event Source几大大相关领域。下图是传统以数据库为中心的架构与使用DDD实现以领域为中心架构的区别。

DDD专门为解决复杂性而诞生,因此解决思路完全不同于传统的CRUD,但是DDD本身掌握起来并不会感觉复杂,从程序员角度看,DDD其实是研究将包含业务逻辑的ifelse语句放在哪里的学问。

DDD主要难点是领域发现和领域建模,万事开头难,除了DDD原著作提出领域统一语言外,目前用于领域发现的方法有:事件风暴、业务能力建模、领域讲故事、业务模型画布、示例映射、影响映射、Wardley Maps等,全球DDD社区为此做出主要贡献的人员名单(按Twitter名称排列):

    @ericevans0 创建了DDD
    • @ziobrando 发明了

事件风暴

    建模方法。
    @ntcoding 发明使用画布canvas 映射有界上下文方法。
    • @swardley 发明

WardleyMapping方法

    进行战略规划。
    • @mathiasverraes 提出了

事件溯

    源具体设计策略,提出复杂系统的仿真建模。
    • @gregyoung 提出

CQRS模式

    来分离模型。

重点资讯

  复杂软件设计之道:领域驱动设计全面解析与实战

  用事件风暴分解单体设计微服务 – capital

  函数式DDD架构入门 – SCOTT WLASCHIN

  DDD社区权威解读:领域驱动设计在2021年将会怎样?

 

教程与文章

板桥大话DDD
用大白话简单谈谈DDD的一些基础特点,只是扫盲!数据库SQL强人慎入

板桥DDD研究十年心得:《复杂软件设计之道:领域驱动设计全面解析与实战》
承蒙机械出版社厚爱

板桥:为什么DDD的Bounded Context翻译为”有界上下文”?

业务代码编程陷阱案例 – jaxenter
非常普遍的不恰当的编程方式,失血模型导致的陷阱

面向对象建模与数据库建模两种分析设计方法的比较
数据库驱动设计与对象建模是决定软件不同命运的两大派别,谁可以让软件更具有生命,维护拓展更方便?伸缩性更强?

面向对象与领域建模
据调查,目前有70%左右程序员是在使用OO语言编写传统过程化软件,缺乏完整的面向对象思维方法的教育和培训是基本根源,本文对软件开发中几个常见问题提出了独立的见解及尖锐的观点

Evans DDD 领域建模
如何提炼模型,而不是数据表,进而精化模型对象,使其能够反映领域概念基本本质是一个复杂过程,Evans DDD是2004年提出的具备革命性影响的软件思想。

实战DDD(Evans DDD:Domain-Driven Design领域驱动设计)
领域建模是一种艺术的技术,不是数学的技术,它是用来解决复杂软件快速应付变化的解决之道。

领域模型驱动设计(Evans DDD)之模型提炼

软件建模设计

如何从职责和协作中发现丰富的充血对象?
失血模型贫血模型是DDD最大敌人,如何根据SOLID原则GRASP原则设计业务行为?本文给出了DDD具体实践中一些具体细节,是和DDD配合一起进行面向对象分析设计的好方法。

业务模型统一描述
统一语言是DDD一个重要特征和重点。

DDD CQRS和Event Sourcing的案例:足球比赛
DDD + CQRS + Event Sourcing实现案例,结合代码与理论讲解。

集装箱车队系统的DDD案例
为上海某大型港口公司的运输系统实施的一个领域驱动设计DDD的实战咨询案例。

DDD仓储实现:Spring Data JDBC教程

不使用DDD的后果:为什么我们停止了向微服务的迁移?

使用DDD聚合发现隐藏的业务规则的案例分析:数据库事务的业务实现 

向领域驱动设计前进: 如何使用DDD实现从单体到微服务迁移打造业务平台或中台?

DDD+微服务大型案例:Uber如何从复杂的RPC微服务转向面向业务领域的微服务架构DOMA?

全球大型电商Shopify如何使用DDD实现单体架构的模块化?

最全面DDD微服务教程:SpringBoot + DDD + Apache Kafka实现最终一致性的教程与源码 – itnext

更多#DDD领域驱动设计专题、领域事件专题

DDD案例完整实现:本站开源Jivejdon 文档按这里