PHP开发第一步,PHP5.6 + MySQL5.5 + Apache2.4环境搭建
最后更新于:2022-04-01 09:57:26
前言
在web开发这个领域对php 可是早有耳闻,大家对php赞不绝口,称赞有加,这让php火了一阵子,现在也依旧流行,所以我实在是忍不住想搞搞php了,以前以为学了jsp就没必要学php了,因为用j2ee这个平台也照样做web开发,但是,听到php这么多优点,这么火,所以打算学学php,而且据说有c和java基础的人学习起来特别快,因为php善于吸收其他语言的优点,如c的语法和指针(虽说没有真正意义上的指针),java的面向对象和异常处理等等,还有perl语言的优点。那我就要看看php的神秘面纱。纵观php的历史,从95年php的诞生,全称是“Personal Home Page”,到97年php发展到php3,全称是“Hypertext Preprocessor”,再到2000年php4的正式发布,再到2004年php5发布,这个时候的php面向对象的功能被强化,再到刚过去的2015年,在不久前的2015年6月php7发布,据说php7性能比php5.6提升了两倍,还提供全面一致的64位的支持。简单地了解了一下php的发展历史,可见php一直在不断地升级完善,所以非常值得研究一下,下面开始万里长征第一步php开发环境搭建,仿佛我看到了前面的曙光,哈哈。
资源准备
由于php是嵌套在html中的运行于服务器端的脚本语言, 运行于服务器端说明在前台网页的源代码中只能看到html,js,css等前端代码而看不到php的源码,因为php的源码被服务器解析了。那么和jsp开发一样需要一款服务器,jsp通常用的是tomcat服务器,那么php通常用的是什么服务器呢?php通常用的是Apache,当然还可以是IIS服务器,因为Apache服务器最大的优点是免费开源,所以更多的是选择apache服务器。php做web开发除了需要有自身的语音包支持和apache服务器外还需要有一个后台数据库,通常是用MySQL,也可以是sql server或者Oracle,而php做web开发最佳组合是PHP+MySQL+Apache,所以开发学习php web开发之前我们需要去网上下载一些资源,php web开发需要的资源如下:
(1) PHP语言包
官网下载地址:[http://windows.php.net/download#php-7.0](http://windows.php.net/download#php-7.0),在这里选择电脑处理器位数和相应的版本的php下载(最好是线程安全的)。这篇文章基于php5.6。
(2) Apache服务器
官网有很多种类和版本,这里的文章是基于Apache 2.4,去官网下载apache教程:
1、进入apache[下载官网](http://httpd.apache.org/download.cgi),如下图,点击红色部分
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3821d1b.jpg "")
2、进入第二个页面,选择红色部分
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3839420.jpg "")
3、进入第三个页面,选择相应VC版本的apache下载(注意红色部分)
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b384d6f5.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b388a1a3.jpg "")
下载好Apache后,下载安装MySQL.
(3) MySQL下载安装
可以去官网下载,这里我直接给上MySQL5.5的安装包,各个版本差异不大。[点击下载MySQL5.5]。([http://download.csdn.net/detail/ydxlt/9407169](http://download.csdn.net/detail/ydxlt/9407169))
MySQL的安装都好简单,但需要注意安装之前,先把之前安装的MySQL清理干净,否则可能会导致安装失败。如果之前的可以用,就不要在安装MySQL了。安装过程需要注意的选项如下(没有附图的代表选择默认就可以了,红色部分代表我们选择的部分):
选择第二个选择自定义安装后
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b38b405e.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b38de763.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3912b2c.jpg "")
用户名和密码需要记住,这里都设为root,以后我们需要这个用户名和密码连接数据库。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3934280.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3952dca.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3974ec2.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b399ab9a.jpg "")
MySQL安装好后,资源就准备完毕了,下面开始配置开发环境。
Apache安装配置
第一步: 找到下载的apache,解压放到一个目录下面,我这里选择的目录是D盘的AMP目录:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b39bfe1e.jpg "")
第二步:配置和安装apache服务,需要先修改配置文件的错误,因为apache解压版,默认apache是安装在c盘的Apache。可以通过命令行httpd -t指令检测配置文件是否有语法错误,如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b39d72fa.jpg "")
这里的意思是说apache配置文件httpd.conf配置文件语法有错误,那我们就修改过来呗。打开apache解压目录conf文件夹下的httpd.conf文件:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b39ee281.jpg "")
打开这个配置文件:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a14692.jpg "")
这里我用得是notopad++,这款编辑器不错推荐一下,还有sublime text编辑器也不错。这里点击全部替换。
替换后再打开cmd命令行,输入http -t再检查一下语法,如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a364a8.jpg "")
可以看到我们替换后,配置文件就有语法错误了,下面开始安装apache服务。
第三步:安装apache服务,用管理员身份打开cmd,进入apache的bin目录下执行`httpd -k install`命令安装apache服务:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a47f27.jpg "")
接下来启动apache服务,测试一下是否安装成功,打开apache bin目录下的ApacheMonitor.exe(如果打开提示缺少dll文件,那么就需要安装上面说的VC了,安装后就可以打开这个monitor了),打开后如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a56fac.jpg "")
OK,在浏览器中输入localhost回车,看到如下图说明apache安装成功了。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a821fc.jpg "")
其实浏览器显示的it works是apache默认站点htdocs(其实就位于apache目录下的htdocs文件夹)下的默认主页index.html的内容。自此,apache安装成功,接下来需要配置php,让apache和php一起工作(将php作为apache的一个模块)。
第四步:配置php模块到apache服务器,以使得apache可以解析php。
先在apache 的htdocs目录建一个php文件,这里叫index.php,在这个文件中写入如下内容:
~~~
<?php
echo "hello php world!";
?>
~~~
启动apache服务,在浏览器中输入localhost/index.php可以看到:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3a945b1.jpg "")
原封不动地显示了php的内容,并没有解析php,那么我们在httpd.conf中做如下配置:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3aa6de3.jpg "")
完了后重启apache服务器(修改了配置文件都要重启),再次在浏览器中访问index.php,可以看到php被解析了:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3ac7d92.jpg "")
说明:apache配置php成功!接下来,我们需要配置做一些php的相关配置及将MySQL配置到php中。
php配置(时区和MySQL)
(1) 配置php时区
打开php解压目录,可以看到:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3adabd1.jpg "")
复制开发阶段的文件到当前目录(也可以直接修改后缀),改名为php.ini,打开这个php.int,配置时区:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3af3118.jpg "")
PRC代表中华人民共和国,即这里是中国时区,测试时区是否配置成功
在index.php中写如下语句:
~~~
<?php
echo "hello php world!<br>";
echo "currentTime:" . date("Y:m:d H:i:s") ."<br>";
?>
~~~
打开浏览器访问localhost/index.php:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b1ad9c.jpg "")
这表明,时区配置成功,如果没有做上面的时区配置,那么将会显示UTC(全国统一时间),并显示警告。接下来要配置mysql了。
(2) 配置mysql
配置之前我们先在index.php中添加如下代码(前面的root是你mysql的账号,后面的root是你mysql的密码,自己改过来):
~~~
$link = mysql_connect("localhost","root","root");
if($link){
echo "连接失败!";
}else{
echo "连接成功!";
}
~~~
访问index.php:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b294ff.jpg "")
发现我们调用了没有定义的函数,那我们需要将mysql模块添加到php中。
开始配置:
1、打开php.ini文件,查找extension_dir关键字
可以看到这行语句:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b3aa54.jpg "")
去掉前面的注释并将ext文件路径改成我们自己的ext路径,即修改为:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b4c628.jpg "")
2、在php.int中继续查找php_mysql关键字
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b61fe5.jpg "")
重新启动apache,再次访问index.php:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3b82aaa.jpg "")
至此php web开发环境就搭建好了。
总结:
php web开发环境搭建的整个过程主要是为了完成三个任务,即:
1. 配置apache以运行php,即输入第一行语句;
1. 配置时区,即输入第二行语句;
1. 配置mysql,即输入第三行的结果。
所以我们在测试页面index.php中写了如下测试代码:
~~~
<?php
echo "hello php world!<br>"; // 能解析输出hello php world!说明php模块成功添加到了apache中了
echo "currentTime:" . date("Y:m:d H:i:s") ."<br>"; // 能输出我们现在的时间,说明时区改过来了
$link = mysqli_connect("localhost","root","28b21c1cfd"); // mysql连接成功,说明mysql模块成功添加到了php中了
if($link){
echo "连接成功!";
}else{
echo "连接失败!";
}
?>
~~~
这些都能成功输出说明我们的环境搭建完成了,这里说明一下,apache安装后有个默认的站点是安装目录的htdocs文件夹,这个我们也可以修改,还可以配置多站点及访问权限和分布式权限控制,这个将在以后的文章中会记录,其实也很简单。
(十三),解释器模式
最后更新于:2022-04-01 09:57:24
解释器模式,从字面上解释来说就是为一个文法(具有特定语法的形式的语句或表达式)构造解释器,这个解释器用来解释这个文法,使得这种具有某种书写规则的文法能够表示特定的功能,这种特定书写规则也就是通常所说的语法,如C/C++,Java,Python等计算机语言有自己的语法。还有,一些解释型语言如Python,它在运行的时候需要Python解释器,这也就是一种解释器。
定义:解释器模式为一些具有特定书写规则的文法编写解释器,从而使得它具有某种意义。
使用场景:
1.
作为解释器。解释具有特定语法的语句的功能或者意义。如解释具有特定书写规则的语句,在下面的这个例子中就是作为解释器来解释运算表达式;
1.
作为翻译机或者译码器。如高级语言中的常量,编译的时候编译器系统会将常量替换成它代表的数一样,类似的我们也可以定义一些特定的具有某种意义的语句,然后用相应的解释器来解释器它。
假设又这么个表达式或语句a_+_b_-_c,我们想要知道它的意思,假如我们规定_符号用来分隔数字和运算符,那么上面的这个语句表达的意思就是计算a+b-c,这和学生考试的时候,它们规定1表示A,2表示B,3表示C,4表示D来传选择题答案一样的,类似于编码译码过程,其实译码器也就是解释器,那么我们通过代码来解释上面表达式的意思。
代码实现:
抽象解释器(基类)
~~~
/**
* 基本的解释器,是所有解释器的基类
* @author lt
*
*/
public abstract class BaseInterpreter<T> {
/**
* 抽象解释方法
* @return
*/
public abstract T interpret();
}
~~~
抽取了解释器的共性
整数解释器(解释整数)
~~~
/**
* 整数解释器
* @author lt
*
*/
public class NumberInterpreter extends BaseInterpreter<Integer>{
private int num;
public NumberInterpreter(int num){
this.num = num;
}
@Override
public Integer interpret() {
return this.num; // 自动装箱拆箱
}
}
~~~
解释整数,直接返回这个整数,这里的整数也就是终结符
运算符解释器(基类)
~~~
/**
* 二目运算符操作解释器,也是一个基类,因为有好多的二目运算符
* @author lt
*
*/
public abstract class OperatorInterpreter extends BaseInterpreter<Integer>{
protected BaseInterpreter<Integer> exp1;
protected BaseInterpreter<Integer> exp2;
public OperatorInterpreter(BaseInterpreter<Integer> exp1,BaseInterpreter<Integer> exp2){
this.exp1 = exp1;
this.exp2 = exp2;
}
}
~~~
解释二目运算符,需要两个整数,为非终结符解释器。
加法解释器(计算加法)
~~~
/**
* 加法解释器,计算加法
* @author lt
*
*/
public class AdditionInterpreter extends OperatorInterpreter{
public AdditionInterpreter(BaseInterpreter<Integer> exp1,
BaseInterpreter<Integer> exp2) {
super(exp1, exp2);
}
/**
* 用来计算加法
*/
@Override
public Integer interpret() {
return exp1.interpret() + exp2.interpret();
}
}
~~~
减法解释器(计算减法)
~~~
/**
* 减法计算器
* @author lt
*
*/
public class SubtractionInterpreter extends OperatorInterpreter{
public SubtractionInterpreter(BaseInterpreter<Integer> exp1,
BaseInterpreter<Integer> exp2) {
super(exp1, exp2);
}
@Override
public Integer interpret() {
return exp1.interpret() - exp2.interpret();
}
}
~~~
计算器,翻译a_+_b_-_c(计算表达式)
~~~
import java.util.Stack;
/**
* 计算器
* @author lt
*
*/
public class Calculator {
private Stack<BaseInterpreter<Integer>> mExpStack = new Stack<BaseInterpreter<Integer>>();
public Calculator(String expression){
// 声明两个BaseInterpreter<Integer>的临时变量,因为计算必须要记录两个数
BaseInterpreter<Integer> exp1,exp2;
// 以符号_分隔,这是我们自己规定的
String[] exps = expression.split("_");
for(int i=0;i<exps.length;i++){
switch (exps[i].charAt(0)) {
case '+': // 加法
exp1 = mExpStack.pop();
exp2 = new NumberInterpreter(Integer.valueOf(exps[++i]));
mExpStack.push(new AdditionInterpreter(exp1, exp2));
break;
case '-':
exp1 = mExpStack.pop();
exp2 = new NumberInterpreter(Integer.valueOf(exps[++i]));
mExpStack.push(new SubtractionInterpreter(exp1, exp2));
break;
default: // 数字
mExpStack.push(new NumberInterpreter(Integer.valueOf(exps[i])));
break;
}
}
}
/**
* 计算
* @return
*/
public int calculate(){
return mExpStack.pop().interpret();
}
}
~~~
这个类用来翻译a_+_b_-_c等形式结构的语句的意思,这里的符号_是我规定用来分隔数字的,当然你也可以规定其他符号作为分隔符。这个类的方法也很简单,构造方法中先是将表达式按符号_分隔,得到一些运算符和数字,然后在根据分隔出来的字符串的第一个字符的类型判断是+运算符还是-运算符还是数字,如果是+运算符,那么就将存储的上一个数字弹出栈并记录到exp1,然后得到运算符后面的那个字符串(肯定是数字)并记录到变量exp2中,最后用加法解释器解释这两个变量记录的数字并压入栈,等下一次循环的时候弹出和下一个数字进行计算;如果是-运算符,那么和+运算符是一样的,只不过用的是减法解释器;如果是数字,直接压入栈,整个过程是逐步计算的过程。*calculate*方法用来输出计算结果。
测试:
~~~
public class Test {
public static void main(String[] args) {
String testStr = "1_+_34_-_10_+_50";
Calculator calculator = new Calculator(testStr);
System.out.println("result="+calculator.calculate());
}
}
~~~
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b38086b7.jpg "")
可以看到我们定义的解释器成功解释了a_+_b_-_c这种形式的语句,其实这个解释器实现的功能类似译码的功能或者翻译的功能。不过话说回来,虽然是成功解释了那种形式的语句,但是也只能计算整数的加减法,如果想要做其他运算或者其他类型的数字,如乘除法和浮点型,那么需要添加相应的解释器,但如果涉及到混合运算的时候,那复杂多了,还得考虑优先级,这个时候这种模式可能就不适合了,也就是说解释器模式适合于简单的语句。
总结:
优点:
- **灵活的扩展性**。当我们对语法规则扩展延伸适合,只需要添加相应的非终结符解释器(如上面的加减法解释器),并在构建抽象语法树时,使用到新增加的解释器对象(如添加减法解释器)进行具体的解释(计算减法)即可,非常方便。非终结符也就是还没有结束的符号,如下面例子中的加法和减法解释器分别解释的加好和减号一样,它们是二目运算符,其两边肯定要两个数。
缺点:
- **类数量膨胀,后期维护困难**。因为对于每一条文法都对应至少一个解释器类,会产生大量的类,导致后期维护困难;同时,对于复杂的文法,构建其抽象的语法树会显得比较繁琐,甚至需要构建多颗语法树,因此,对于复杂的文法并不推荐使用解释器模式。
(十二),外观模式
最后更新于:2022-04-01 09:57:22
终于考试完了,瞬间感觉轻松了许多,又可以安心地写代码了,下面进入今天的正题–外观模式。
外观模式,也称门面模式,顾名思义,就是一个对象封装了一系列相关的操作(行为),使得这些操作仅对外提供(暴露)方法(接口),客户端根据这些外观(暴露的接口)就可以简单地完成一系列操作,达到了客户端无需知道内部实现细节,只需知道对象的外观就可以实现一系列行为,简单来说就是面向对象的封装。这一系列行为也就是一个系统的功能。
定义:通过一个统一的对象实现一个系统的外部与内部的通讯,提供了一个高层次的接口,使得系统功能更加透明,更加容易使用。
使用场景:
1.
为一个复杂系统提供一个简单的接口。为一个复杂系统提供一个简单的接口,对外部隐藏系统的内部实现,隔离变化,使得当这个系统因为不断演化而不断的修改,定制也可以更加容易地扩展使用,即对外部的使用是一样的,客户端无需知道内部发生了什么变化,隐藏了系统的内部实现,这也就是封装的好处了。
1.
简化子系统之间的依赖,降低它们之前的耦合。当不同的子系统需要使用其他系统的功能的时候,那么我们就需要构建一个层次结构的系统,这时我们通过外观模式为这些子系统提供一个通讯接口,即每层的入口点。
优点:
1.
因为对客户端隐藏了系统的细节,减少了客户端对于系统的耦合,能够拥抱变化。
1.
对系统一系列功能进行了整合,封装,使得系统更加容易使用。
缺点:
1.
外观类接口膨胀。因为我们外观类需要封装一系列相关的功能,这一系列相关的功能可能需要不同的类实现,那么我们不是简单地给这个外观类提供实现不同功能类,而是为每个实现不同功能的类提供一个接口,然后再使用的时候给这些接口提供实现类,这样可以便于扩展,反之,外观类接口必然膨胀,也增加了程序员的一定的负担。
1.
违背了开闭原则,当业务出现变更的时候,可能需要直接修改外观类(通常是修改外观类中的接口的实现类)。
下面以现代智能机模拟实现外观模式
代码实现:
虚拟手机(接口)—-接打电话功能接口
~~~
public interface Phone {
/**
* 打电话
*/
public void call();
/**
* 挂断
*/
public void handup();
}
~~~
虚拟相机(接口)—-拍照功能接口
~~~
/**
* 照相机
* @author lt
*
*/
public interface Camera {
public void open();
public void takePicture();
public void close();
}
~~~
真实的手机(实现类)
~~~
/**
* 以前的旧手机,非智能,只能打电话和挂电话
* @author lt
*
*/
public class PhoneImpl implements Phone{
@Override
public void call() {
System.out.println("打电话");
}
@Override
public void handup() {
System.out.println("挂断电话");
}
}
~~~
真实的相机(实现类)
~~~
/**
* 三星相机
* @author lt
*
*/
public class SamsungCamera implements Camera{
@Override
public void open() {
System.out.println("打开相机");
}
@Override
public void takePicture() {
System.out.println("拍到了一个美女");
}
@Override
public void close() {
System.out.println("相机关闭了");
}
}
~~~
现代智能机(Android/Iphone)—-外观类
~~~
/**
* 现代智能机 --- 集照相,视频聊天,打电话于一身
* @author lt
*
*/
public class SmartPhone {
// 对应实现完成手机功能接口的实现类
public Phone phone = new PhoneImpl();
// 对应实现相机功能接口的实现类
public Camera camera = new SamsungCamera();
public void call() {
phone.call();
}
public void hangup() {
phone.handup();
}
public void takePicture() {
openCamera();
camera.takePicture();
closeCamera();
}
public void openCamera() {
camera.open();
}
public void closeCamera() {
camera.close();
}
/**
* 视频通话
*/
public void videoChat() {
openCamera();
System.out.println("和妹子视频聊天");
closeCamera();
}
}
~~~
SmartPhone是外观类,这里具体是现代智能机,具有接打电话,拍照等功能,是这个模式核心类;Phone和Camera是两个特定功能的接口,分别是接打电话的功能,拍照功能,相应的实现类是PhoneIml和SamsungCamera。这里给外观类整了两个接口,即Phone和Camera,这样做的目的是以后要修改接打电话和拍照功能的实现时(如新的硬件)只需要将外观类SmartPhone中的相应功能的接口实现类改变一下就可以了,其他的都不用改,这也就是面向接口编程的好处,和J2EE中的Service,Dao层都提供一个接口和相应实现类一样,目的是一样的,即便于修改扩展。
测试:
~~~
public class Test {
public static void main(String[] args) {
SmartPhone iphone7s = new SmartPhone();
// 用7s拍照
iphone7s.takePicture();
// 用7s视频聊天
iphone7s.videoChat();
}
}
~~~
运行结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b37e63ed.jpg "")
可以看到,现代的智能机既有一般手机的功能,也有相机的功能,使得我们只需要一部智能机就不需要专门为打电话买一个手机,专门为拍照买一个相机了,一部手机统统搞定,而且避免了许多麻烦,如我们点击屏幕的相机(系统内置App)系统自动打开了相机,拍照完了以后系统自动将相机关闭,视频聊天也一样,这使得我们使用更加简单,更加方便。
总结:
外观模式使用也非常多,面向对象中的封装也通常是使用了外观模式,封装不一定使用了外观模式,但外观模式一定需要封装。通过外观模式,使得复杂系统功能更加丰富,使用更加简单。通过一个外观类就可以操作整个系统,减少了用户的使用成本,同时因为内部面向接口编程,使得使扩展维护更加简单,从而使得系统可以容易地面对多变的场景,提升了系统的扩展性灵活性。
(十一),装饰模式
最后更新于:2022-04-01 09:57:20
装饰模式(包装模式),也是结构型设计模式之一。它主要用于动态地对一个类或者对象添加新的功能,使得原有的类得到扩展,与代理模式不同的是该模式是直接对一个原有的对象进行扩展功能,而代理模式代理原对象的行为,而不是对原来的对象进行扩展。同时也不同于适配器模式,适配器模式是对一个接口或者类进行适配,使得在新的场合下也可以使用。而装饰模式则是对对象进行扩展包装,是继承关系的代替方案。但比继承的子类更加灵活,因为在关系上包装的类与被包装的类不是继承关系。
定义:动态地对一个对象进行装饰(包装),以使得该对象具有更多的功能。
使用场景:
- 需要透明或者动态地扩展类的功能,即需要对类进行包装。
与代理模式的区别:
- 可以将这种模式说成是代理模式的特殊情况,但装饰模式是对类行为或者说功能进行扩展,增强类的功能。而代理模式强调通过一个代理对象对原对象的行为施加控制,而不对类本身扩展功能。这两种模式很相似,因此要细心加以区分。
装饰模式的例子在生活中也随处可见,生活中的万物可以都需要进行包装,如水果,店铺,教室等等,甚至我们人也要进行包装,如穿衣服,带帽子等等以便我们自己的形象更加好。所以,下面以生活中的一个小例子来用代码实现装饰模式,以便我们对装饰模式有一个清晰的认识。
假设我们开了一个淘宝店铺,那么我们在卖商品前要对我们的淘宝店进行装饰。什么logo,公告,背景音乐等等。
代码实现:
店铺(接口)
~~~
/**
* 店铺(接口),淘宝给商家开放的一个接口
* @author lt
*
*/
public interface ShopI {
/**
* 展示
*/
public void show();
}
~~~
淘宝店(被装饰的对象)
~~~
public class TaobaoShop implements ShopI{
private String name;
public TaobaoShop(String name){
this.name = name;
}
/**
* 展示
*/
@Override
public void show() {
System.out.println("名字:"+name);
}
}
~~~
装饰后的淘宝店(装饰对象)
~~~
public class DecoratedShop implements ShopI{
private ShopI shop; // 持有一个要装饰对象的引用,通过该引用对对象进行装饰
public DecoratedShop(ShopI shop){
this.shop = shop;
}
/**
* logo,我们扩展的功能
*/
private void logo(){
System.out.println("logo:特卖Logo");
}
/**
* 背景音乐,我们扩展的功能
*/
private void bgMusic(){
System.out.println("背景音乐:你是我的小啊小苹果,怎么爱你都不嫌多...");
}
/**
* 公告,我们扩展的功能
*/
private void notice(){
System.out.println("公告:这是一家专门做特卖的店铺,原装正品,童叟无欺!");
}
@Override
public void show() {
shop.show();
logo(); // 上传logo
notice(); // 发布公告
bgMusic(); // 添加背景音乐
}
}
~~~
说明:这里装饰对象和被装饰对象实现相同的接口是为了使得装饰对象对原对象功能装饰包装得对客户端更加能透明,也可以不用实现相同的接口,如果装饰对象通过继承被装饰对象以扩展它的功能的话,那就是继承关系了。
测试:
~~~
public class Test {
public static void main(String[] args) {
// 申请一个淘宝店,叫微真会
TaobaoShop newShop = new TaobaoShop("微真会");
System.out.println("刚申请的店铺:");
newShop.show();
// 对这个店铺进行装饰
DecoratedShop decoratedShop = new DecoratedShop(newShop);
System.out.println("装饰后的店铺:");
decoratedShop.show();
}
}
~~~
Run AS:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b37d14ac.jpg "")
如果我们在对原对象(newShop)进行*show()*方法调用,那么还是会显示只有一个名字,因为我们进仅仅是对原对象进行了一层包装,而并没有改变原对象的功能,原对象脱离了装饰它的对象那么就只有原来的功能了,就像我们人一样,卸掉身上的装饰品后还是我们自己,但就是更不那么好看了。
总结:
装饰模式特别容易理解,因为这样的例子在我们生活中随处可见,完全可以通过字面进行理解,即装饰。但我们要注意细心区分装饰模式和代理模式的区别,因为这两种模式特别像,区别在于代理原对象的行为还是要扩展原对象的行为以装饰包装原对象。
(十),代理模式
最后更新于:2022-04-01 09:57:17
代理模式也称委托模式,是结构型设计模式之一,在实际应用中使用非常广泛,因此我们必须掌握这个设计模式。
定义:为一个对象提供一个代理对象,通过这个代理对象控制这个对象的行为。
使用场景:
-
直接访问或者控制对象的行为困难的时候,可以通过对象的代理对象间接控制对象的行为,为了使用简单透明,委托对象和代理对象需要实现相同的接口,即同类型,使用方法一样。
-
可以一定程度上保证对象的安全性。即不必直接将对象暴露给客户端,而是暴露该对象的代理对象给客户端,使得将该对象更加安全。但这不是主要的,不然就直接用原型模式了。
和适配器模式的区别:
-
适配器模式是为了一个接口不兼容而导致该接口不能直接使用而用适配器去为该接口适配以到达可以使用的目的,可能需要添加接口的行为(扩展)。
-
代理模式为了使得直接控制一个类的行为困难而使用和该类具有相同行为(实现了相同的接口)的代理对象代替原对象的行为(间接控制),不会添加接口的行为(只是代替而已)。
今年过年小明嫌收到的压岁钱太少了,生气了,马上就要开学了,自己不愿自己去学校报道了,那怎么办,小明的父母可不能让小明不报道啊,但又拿小明没办法,那只好自己代替小明去给小明报道了呗,于是带着小明的寒假作业去学校给小明报道了…
以上面的这个例子为例实现代码模式:
代码实现:
报道的行为(接口)
~~~
/**
* 开学报道行为(接口)
* @author lt
*
*/
public interface Report {
// 去学校
void goSchool();
// 交学费
void pay();
// 领书本
void getBook();
// 回家
void goHome();
}
~~~
学生(被代理的对象)
~~~
/**
* 学生 (被代理对象)
* @author lt
*
*/
public class Student implements Report{
@Override
public void goSchool() {
System.out.println("开开心心地去学校报道了");
}
@Override
public void pay() {
System.out.println("交了4000学费!");
}
@Override
public void getBook() {
System.out.println("领取了6本书!");
}
@Override
public void goHome() {
System.out.println("总算报道完了,回家了!");
}
}
~~~
家长(代理对象)
~~~
/**
* 家长 (代理对象)
* @author lt
*
*/
public class Parent implements Report{
private Report student; // 持有被代理对象的引用
public Parent(Report student){
this.student = student;
}
@Override
public void goSchool() {
student.goSchool();
}
@Override
public void pay() {
student.pay();
}
@Override
public void getBook() {
student.getBook();
}
@Override
public void goHome() {
student.goHome();
}
}
~~~
测试:(具体的一次报道行为)
~~~
public class Test {
public static void main(String[] args) {
// 小明15岁了,现在在上初中
Report children = new Student();
// 小明今年过年没收到去年那么多的压岁钱,不高兴了,去年都是自己去报道,今年不去了
Parent parent = new Parent(children);
parent.goSchool();
parent.pay();
parent.getBook();
parent.goHome();
}
}
~~~
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b37bc3df.jpg "")
通常开学报道的时候都好开心的,因为毕竟刚完年,收到不少压岁钱,还等着快点去学校花呢。但小明太侨情了,嫌压岁钱太少了,宁死也不去报道,要小明自己去学校报道太难了,小明的父母只好代替小明去报名了。代替小明去报名也不只有父母才可以哦,还可以是自己的亲戚或者其他和关系好的朋友也行。当然,小明的父母也可以代替别人家的孩子报道,比如亲戚或者朋友的孩子报道。这说明了,当控制一个对象的行为难的时,可以通过代理对象代替具有一组实现了相同接口的对象的行为,而一个对象也可以有其他的代理对象,前提是两者实现了相同的接口,即具有相同的行为。
总结:
代理模式的例子无论是在生活中还是在代码世界里都很常见,使用广泛,具有解决某些需求的优点,同时也具有设计模式的通病,那就是类的增加。掌握设计模式关键在于理解,而不是死记模板,当我在写代码的时候在某些特定场合可以自然而然地将我们的设计模式融入应用到我们的程序中,使得我们的程序结构更加清晰,灵活性高,易于扩展等等。
(九),适配器模式
最后更新于:2022-04-01 09:57:15
记得刚学Java SE的AWT(新版Swing)编程的时候,那个时候自己特别兴奋,因为学了那么久的Java了,没看到一点实在点的东西,觉得很没有成就感。后来学到Swing的时候,用它编写图形化界面,于是写了一个小小的计算器和其他简单的图形化程序。然而,在知道了Java的在图形化界面方面做的不是很好以后,心里有点失望,于是对Swing的热情也慢慢消退了。但是,我并不后悔花了点时间学Swing编程,因为在学习的过程中收获了喜悦,产生了一点小小的成就感,这些就已经够了。呵呵,话又说回来,Java的Swing编程还是值得了解或者学习一下的,因为用它来做一些演示还是挺好的。Java SE的那个Java的Applet(Java应用小程序)就简单了解一下知道Java有这么个小东西就行了,因为现在都基本宣布死亡了。
上面又说了一些废话,不过,学习Swing也是我第一次接触适配器模式,知道有Adapter这个东西。后来,学了用Java作为开发语言的Android开发后,我才更加知道了Java的Swing没有白学,因为不仅可以用Swing编程学一些小的演示程序,而且使得我特别快就上手了Android开发,对Android中的ListView的Adapter模式也特别理解,不仅如此,不知道是不是之前学了Swing还是怎么的,对Android开发非常容易就理解了。到现在,自己可以独立开发Android了。但是,知识学不完,新技术不断,对Android还需要进阶,达到更高的一个层次,让我们一起努力,一起进步。下面,直接进入正题。
定义:把一个类的接口扩展成客户端所期待的另外一个接口,使得原来不能一起使用的两个类(一个接口和一个使用该接口的类)一起工作。
使用场景:
1.
需要一个统一的输入接口,而输入端的类型不可知。这个时候,我们可以暴露一个接口适配器给客户端,让客户端自己定制。如Android的ListView的Adapter;
1.
解决接口不兼容。使得原来的一些接口在新的场景下依旧可以用,而不必写新的接口。
优点:
1.
更好的复用性。系统通过需要使用现有的类,而此类的接口不符合系统的需要,那么通过适配器模式就可以让这些功能得到更好的复用,而避免了写新的接口;
1.
更好的扩展性。通过暴露给客户端一个适配器,让客户端自己定制功能,从而更好地扩展了系统的功能,如Android中既提供了SimpleAdapter,ArrayAdapter还提供了一个抽象适配器BaseAdapter让用户自己定制。
缺点:
- 过多使用适配器,让系统杂乱不堪,不易于整体把握。如我们每次都为一个接口写一个是适配器(因为我们不需要实现该接口的所有方法)。
以生活中的裁缝铺为例
代码实现
衣服(接口)
~~~
/**
* 衣服(接口)
* @author lt
*
*/
public interface Clothes {
public int getSize();
}
~~~
具体的衣服(客户端)
~~~
/**
* 具体的衣服(客户端)
* @author lt
*
*/
public class Clothes1 implements Clothes{
/**
* 假设是180尺码
*/
@Override
public int getSize() {
return 180;
}
}
~~~
裁缝铺(适配器)
~~~
/**
* 裁缝铺(Adapter)
* @author lt
*
*/
public class TailorShop implements Clothes{
private Clothes clothes;
public TailorShop(Clothes clothes){
this.clothes = clothes;
}
/**
* 缩短五厘米
*/
@Override
public int getSize() {
return clothes.getSize()-5;
}
/**
* 将衣服拉长5厘米
* @return
*/
public int largen(){
return clothes.getSize()+5;
}
}
~~~
这里的裁缝铺是把衣服缩短5厘米,当然,裁缝铺也可以将衣服变长,也不是固定就是5厘米哦,我们可以用一个参数来让客户端指定修剪长度,如我们将裁缝铺角色改造。
~~~
/**
* 裁缝铺(Adapter)
* @author lt
*
*/
public class TailorShop implements Clothes{
private Clothes clothes;
/**
* 要修剪的长度,正数为拉长,负数缩短
*/
private int ds;
public TailorShop(Clothes clothes,int ds){
this.clothes = clothes;
this.ds = ds;
}
/**
* 缩短五厘米
*/
@Override
public int getSize() {
return clothes.getSize()+ds;
}
}
~~~
当然,裁缝铺的功能不仅是改变衣服的尺寸哦,还有缝补衣服。这里就不扩展了,要扩展也就添加方法的事了。
测试(有图有真相):
~~~
public class Test {
public static void main(String[] args) {
// 小明最近长了5里面,原来180,现在185了,但衣服还是180的
Clothes clothes = new Clothes1();
System.out.println("原来的衣服尺码:"+clothes.getSize());
// 小明说拉长5厘米
TailorShop tailorShop = new TailorShop(clothes,5);
System.out.println("经过裁缝捕裁剪后衣服的尺码:"+tailorShop.getSize());
}
}
~~~
结果(Run As):
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b37a8806.jpg "")
裁缝铺修剪完后,小明顿时非常开心地穿起了原来自己喜欢但穿不了的衣服高兴地…
这个例子向我们演示了两个类(小明和衣服)原本不能一起工作,后来经过适配器(裁缝铺)修剪后又可以一起工作了(小明又穿上那件自己喜欢的衣服了)。其实,这个是对象适配器模式,当然还可以有类适配器模式,这两种模式的区别就是前者为类进行包裹封装,后者直接扩展类(继承)。下面再以一个例子结束本文的内容。
对于下面的这个接口:
~~~
public interface DemoInterface {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
public void method6();
// ...
}
~~~
我们在使用这个接口的时候,我们每次都要实现6个或者更多的方法,如果我们不需要实现所有的方法,那么我们可以写一个这样的适配器
~~~
public abstract class DemoAdapter implements DemoInterface{
@Override
public abstract void method1();
@Override
public void method2() {
}
@Override
public void method3() {
}
@Override
public void method4() {
}
@Override
public void method5() {
}
@Override
public void method6() {
}
}
~~~
除了我们必须要实现的method1外,其它不必实现的给个空的实现,不管他。下次,我们使用个适配器代替原来的接口,通常系统也会为我们提供这样的一个适配器,如果没有,我们可以自己写。
总结:
任何的模式都有优缺点,适配器模式也不例外,如上面的抽象适配器就有一个明显的缺点,就是将一个接口替换成了抽象类,优点就是可以不用实现我们不需要实现的方法,而只关心我们必须要实现的那些方法,缺点就是由接口变成了类,使用可能会受限,因为java不支持多重继承,但可以实现多个接口。
(八),责任链模式
最后更新于:2022-04-01 09:57:13
废话不多说,最近要期末考试了,还没预习呢,下面直接进入正题。
定义:定义多个可以处理请求(承担责任)的类,并将它们连成一条链,沿着该链向下传递请求(责任),直到有能力解决问题(承担责任)的类处理之。这使得它们都有机会(看能力大小咯)处理请求。
使用场景:
1.
一个请求有多个类可以处理,但具体由那个类处理要在运行时刻确定;
1.
在请求不知道由谁来处理的时候,可以沿着责任链传递请求;
1.
需要动态指定一组对象处理请求。
优点:
- 使得请求者和处理者之前关系解耦,提高了代码灵活性。
缺点:
- 影响程序的性能,尤其在递归调用中更为明显,因为势必要对链中的处理者遍历,直到找到合适的处理者。
以游戏BOSS悬赏为例,发布一个悬赏,招能人义士消灭BOSS。
代码实现:
悬赏 (请求)
~~~
/**
* 悬赏 ----->请求
* @author lt
*
*/
public class Reward {
/**
* BOSS战斗力
*/
public int bossFightingCapacity;
}
~~~
游戏角色(处理者Handler)
~~~
/**
* 游戏角色 ---> 可以处理请求的处理者Handler
* @author lt
*
*/
public abstract class Role {
public Role nextPlayer; // 下一个领了悬赏的玩家
protected String roleName;
public Role(String roleName){
this.roleName = roleName;
}
/**
* 得到玩家角色的战斗力
* @return 战斗力
*/
public abstract int getFightingCapacity();
/**
* 处理请求或者传递 -- 这里为领取悬赏任务,完成不了(战斗力不够)就下个领了悬赏的玩家完成(传递)
* @param bossFightingCapacity,悬赏榜的BOSS的战斗力
*/
public final void handleRequest(int bossFightingCapacity){
if(bossFightingCapacity<=getFightingCapacity()){
// BOSS战斗力不高于玩家,玩家就可以杀死BOSS
killBOSS();
}else{
// BOSS太厉害了,由高手解决吧,自己泪闪了
if(nextPlayer!=null){
// 让下个玩家领取悬赏
nextPlayer.handleRequest(bossFightingCapacity);
}else{
// 没人领悬赏
System.out.println("这个BOSS为无敌BOSS,屌炸天!");
}
}
}
/**
* 具体的处理方法 -- 这里为每个玩家怎么杀死BOSS的
*/
public abstract void killBOSS();
}
~~~
说明:每个具体的角色为一个玩家。
骑士小明
~~~
/**
* 骑士小明
* @author lt
*
*/
public class XiaoMing extends Role {
public XiaoMing(String roleName) {
super(roleName);
}
@Override
public int getFightingCapacity() {
return 2000;
}
@Override
public void killBOSS() {
System.out.println("我是"+roleName+",我的大刀早已饥渴难耐了!BOSS被大刀砍杀了");
}
}
~~~
法师小丽
~~~
/**
* 法师小丽
* @author lt
*
*/
public class XiaoLi extends Role {
public XiaoLi(String roleName) {
super(roleName);
}
@Override
public int getFightingCapacity() {
return 1000;
}
@Override
public void killBOSS() {
System.out.println("我是"+roleName+",我的新魔杖,代表月亮消灭了BOSS!");
}
}
~~~
猎手小华
~~~
/**
* 猎手小华
* @author lt
*
*/
public class XiaoHua extends Role{
public XiaoHua(String roleName) {
super(roleName);
}
@Override
public int getFightingCapacity() {
return 3000;
}
@Override
public void killBOSS() {
System.out.println("我是"+roleName+",我的弓箭早已暗藏不住了!BOSS被射杀了");
}
}
~~~
测试:
悬赏一个2900战力的BOSS
~~~
public class Test {
public static void main(String[] args) {
Role xm = new XiaoMing("血魂骑士");
Role xh = new XiaoHua("狙魔猎手");
Role xl = new XiaoLi("秘法游侠");
xl.nextPlayer = xm;
// 通常高手都是最后一个出现的
xm.nextPlayer = xh;
// 今有一个2900战斗力的BOSS,需要能人义士来消灭
Reward reward = new Reward();
reward.bossFightingCapacity = 2900;
// 小丽太激动了,第一个领了悬赏任务
xl.handleRequest(reward.bossFightingCapacity);
}
}
~~~
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3770c4a.jpg "")
悬赏一个1900战力的BOSS
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3785522.jpg "")
悬赏一个900战力的BOSS
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3795cc4.jpg "")
总结:
通过上面的BOSS悬赏测试,我们看到了当领取了悬赏任务的所有玩家中,当最先领取的玩家完成不了时会交给下面的玩家来完成否则自己来完成,直到有玩家可以杀死BOSS。这些领取了悬赏的所有玩家都在一条责任链中(杀死BOSS),使得他们都有机会去杀死BOSS,这就是责任链模式的简单实现。当然,责任链中的处理顺序也不是固定的,可以改变,如小丽完成不了可以直接交给小华,而不用先交给小明。
责任链模式是为了处理某种请求而生的,当我们有这样的请求的时候可以考虑使用该模式,具体可以参考使用场景。好吧,该预习了,不能挂科了。
(七),观察者模式
最后更新于:2022-04-01 09:57:10
似乎所有的设计模式都是为了使得程序具有低耦合,灵活性高,可扩展性好,程序结构清晰等等。今天的这个设计模式—观察者模式自然也不例外,但程序结构清晰可能就不是重点了。好吧,废话少说,模式这种简单粗暴的东西还是要快点学习,下面直接进入正题。
定义:观察者模式是让对象与对象之前建立一种一对多(不是Bean之前的一对多)的关系,这种关系使得当一的一方的状态改变时,所有多的一方自动根据一的一方的改变做出相应的改变。
使用场景:
1.
事件多级触发。如一个公司,CEO是最顶层的被观察者(一的一方),观察CEO的有多个经理,如:多个总经理(多的一方)同时观察CEO,而每个总经理有可以被多个其他经理观察,如:项目经理,产品经理等,那么这里的总经理即是观察者也是二级被观察者,那么只要CEO发话,相关的总经理就要做出改变,而总经理可能又要让底下的项目经理和业务经理做出改变,这就是事件多级触发;
1.
关联绑定行为,关联行为可以拆分,即解除绑定,不是“组合”,即两个行为是相互独立的,不是一个整体;
1.
跨系统消息交换场景,如消息队列,事件总线处理机制。
优点:
1.
增强程序的灵活性,可扩展性;
1.
降低程序耦合性。
缺点:
- 由于这种模式实际上是通过被观察者用一个集合存放所有的观察者的引用,然后依次遍历集合调用每个观察者的*update*方法。所以,这就产生了开发效率和运行效率的问题。还有,通常遍历是顺序的,那么所有的观察者实际上不是同时做出更新的。更有甚者,如果其中一个观察者更新出现卡顿,那么后面的观察者就要延迟做出更新了,在这种情况下,通常采用多线程异步方式,然而,带来了新的问题-并发。
下面先通过JDK自带的*Observer*类和*Observable*实现观察者模式
代码实现(利用JDK内置对象):
被观察者:
~~~
import java.util.Observable;
/**
* 被观察者
* @author lt
*
*/
public class BeObservered extends Observable{
/**
* 提交改变,调用Observable的setChanged()和notifyObservers();
* 注意:必须调用setChanged(),然后notifyObservers()才有效
*/
public void postChanged(Object news){
// 标识被被观察者有新的改变
setChanged();
// 通知所有的观察者
notifyObservers(news);
}
}
~~~
注意:
- postChanged()是我们自己写的方法;
- 必须在这个方法里面同时调用*setChanged()*和*notifyObservers()*观察者才会接受到变化,即回调*update*方法。
观察者:
~~~
import java.util.Observable;
import java.util.Observer;
/**
* 观察者
* @author lt
*
*/
public class MyObserver implements Observer{
private String name;
public MyObserver(String name){
this.name = name;
}
/**
* @param observable 该观察者观察的被观察者 即BeObservered
* @param BeObservered调用notifyObservers(Object arg)时传入的arg参数
*/
@Override
public void update(Observable observable, Object news) {
System.out.println(name+"接受到变化"+",更新的内容为:"+news);
}
@Override
public String toString() {
return "MyObserverName=" + name + "]";
}
}
~~~
这里在*update*方法里面做了点简单的反馈。
测试:
~~~
public class Test {
public static void main(String[] args) {
MyObserver observer1 = new MyObserver("小明");
MyObserver observer2 = new MyObserver("小丽");
MyObserver observer3 = new MyObserver("小强");
BeObservered beObervered = new BeObservered();
// 添加到观察者队列
beObervered.addObserver(observer1);
beObervered.addObserver(observer2);
beObervered.addObserver(observer3);
// 提交改变
beObervered.postChanged("2016,新年快乐!");
}
}
~~~
今天是新年2016的第一天,祝福大家新年快乐,心想事成,万事如意!
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b375b1cc.jpg "")
结果不用说,大家都给点反馈了(哈哈,文章点个赞,评论一番也是挺爽的)。
上面使用了JDK内置对象来实现观察者模式,其实我们也可以自己来实现观察者模式。下面我们自己来实现观察者模式吧。
自定义实现观察者模式:
被观察者:
~~~
import java.util.HashSet;
import java.util.Set;
/**
*自定义被观察者
* @author lt
*
*/
public class Observable {
public Set<Observer> observers = new HashSet<Observer>();
public boolean isChanged; // 默认为false
public void addObserver(Observer o){
// 防止多线程
synchronized (observers) {
observers.add(o);
}
}
public void setChanged(){
this.isChanged = true;
}
public void notifyObservers(Object arg){
if(isChanged){
// 遍历集合,依次通知所有的观察者
for(Observer observer : observers){
observer.update(this, arg);
}
isChanged = false; // 重置
}
}
public void notifyObservers(){
notifyObservers(null);
}
}
~~~
观察者:
~~~
public interface Observer {
public void update(Observable observable, Object news);
}
~~~
OK,自定义实现观察者模式完成了,将上面的MyObserver和BeObservered导入的JDK的Observer及Observable改成我们自己写的,运行Test测试。
结果:
和用JDK的Observer及Observable是一样的,再次祝大家新年快乐,心想事成,万事如意!。
OK,到这里我们自己定义实现观察者模式就完成了,是不是感觉好简单,就是一个回调。其实,这里也并不简单,里面肯定涉及到好多东西,什么优化啊,安全啊什么的。我们上面自己写的类就做了一个同步处理,其他的都没做,大家也可以去看看JDK源码,看看它是怎么做的。
总结:
观察者模式主要的作用是用于对象解耦,基于抽象接口(易扩展)Observer和Observable,将观察者和被观察者完全分离(低耦合),实现当被观察者发生改变时相应的所有的观察者选择做出改变。这点有很多的用途,如我们Android开发一个软件下载市场等需要不同的Activity可以同时需要看到同一个软件下载的进度,还有ListView中也用到了这种模式等等。同时,知道了这种模式的原理后,我们自己也可以定义我们的观察者和被观察者。
(六),状态模式
最后更新于:2022-04-01 09:57:08
我们是否在写程序的过程中有过在一个类中写了很多状态,是否需要根据该对象的不同状态决定该对象的不同行为。如:我们Android中自定义一个上拉加载更多,下拉刷新的RefreshListView呢?,我们在RefreshListView中定义了三中状态,即:下拉刷新状态,正在刷新,松开刷新,而且这三中不同的状态决定了该自定义控件是否可以下拉等行为,假如你没定义过该控件或者没有写个那种一个对象有多种状态对应控制多种行为也不要紧,因为状态模式很简单,我们只需要一个例子就知道怎么实现状态模式了。
定义:当一个对象内部状态改变时允许改变其行为,而不用根据不同的状态做过多的if-else判断,而是用不同的类代表不同状态下的行为,这不同的类可以之间不用相互依赖,各自代表不同状态下的行为。
状态模式使用场景:
1.
一个对象的行为取决于它的状态,并且需要在运行的时候根据不同的状态改变行为。如:我们的RefreshListView,如果当前它的状态是正在刷新,那么它就不可下拉
1.
代码中包含大量与对象状态相关的if-else语句,一个操作中含有大量的多分支语句if-else或者switch-case语句依赖于该对象的状态的情况,如果是这种情况,那么代码的可读性不好,且不易修改维护。
状态模式的优点:
- 使程序的可扩展提高,易于维护。状态模式用一个状态行为类代表一种状态下个行为,摆脱了繁琐的状态判断行为,将繁琐的状态判断转换成结构清晰的状态类集。
状态模式的缺点:
- 必然使得系统类和对象的个数增多。
代码实现:
电视机行为接口:
~~~
/**
* 电视机行为
* @author lt
*
*/
public interface TvAction {
public void startUp(); // 开机
public void startOff(); // 关机
public void nextChannel(); // 下一个频道
public void prevChannel(); // 上一个频道
}
~~~
开机状态行为:
~~~
/**
* 电视机开机状态下的行为
* @author lt
*
*/
public class TvStartUpState implements TvAction{
/**
* 开机,无效
*/
@Override
public void startUp() {
}
/**
* 关机,有效
*/
@Override
public void startOff() {
System.out.println("关机啦!");
}
/**
* 下一个频道,无效
*/
@Override
public void nextChannel() {
System.out.println("切换到下一个频道");
}
/**
* 上一个频道,无效
*/
@Override
public void prevChannel() {
System.out.println("切换到上一个频道");
}
}
~~~
关机状态行为:
~~~
/**
* 电视机关机状态下的行为
* @author lt
*
*/
public class TvStartOffState implements TvAction{
/**
* 开机,有效
*/
@Override
public void startUp() {
System.out.println("开机啦!");
}
/**
* 关机,无效
*/
@Override
public void startOff() {
}
/**
* 下一个频道,无效
*/
@Override
public void nextChannel() {
}
/**
* 上一个频道,无效
*/
@Override
public void prevChannel() {
}
}
~~~
遥控器:
~~~
/**
* 遥控器
* @author lt
*
*/
public class TvController implements TvAction{
private TvAction tvStartUpState = new TvStartUpState();
private TvAction tvStartOffState = new TvStartOffState();
/**
* 电视机的状态行为,状态即行为,默认为关机状态
*/
private TvAction tvState = tvStartOffState;
@Override
public void startUp() {
tvState.startUp();
this.tvState = tvStartUpState;
}
@Override
public void startOff() {
tvState.startOff();
this.tvState = tvStartOffState;
}
@Override
public void nextChannel() {
tvState.nextChannel();
}
@Override
public void prevChannel() {
tvState.prevChannel();
}
}
~~~
测试:
~~~
public class Test {
public static void main(String[] args) {
TvController tvController = new TvController();
// 开机
tvController.startUp();
tvController.nextChannel();
tvController.prevChannel();
// 关机
tvController.startOff();
tvController.nextChannel();
tvController.prevChannel();
// 开机
tvController.startUp();
// 关机
tvController.startOff();
}
}
~~~
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3747eb9.jpg "")
状态模式模拟结束,上面我们测试的时候,我们对电视机的状态频繁切换,并操作了对应状态下的行为,达到了不同状态下对象的行为改变了。但你是否在上面所有的代码你有看到if-else或者switch-case?没有吧,事实上,状态模式就是为了避免过多的if-else和switch-case语句。上面模拟过程中我们看到了电视机的状态行为切换如此简单。
总结:
状态模式的出现就是为了解决一个对象多种状态下不同行为表现导致出现过多的if-else或者switch-case语句判断切换不同状态下的行为, 通常这种做法需要设置一个变量记录该对象当前的状态,然后根据变量执行不同的代码。而状态模式根本就不需要if-else和switch-case,一个对象类代表一种状态下的行为表现,当状态切换时切换不同的对象类即可,达到状态即行为的效果。状态模式的使用和没有使用状态模式的if-else/switch-case控制行为的关系有点像c是面向过程而java面向对象的关系一样。
(五),策略模式
最后更新于:2022-04-01 09:57:06
策略模式,顾名思义,就是在一个问题有多种解决方法的时候应用哪种方法,哪种策略来完成而设计的一种模式,以达到高解耦,灵活性高,代码更加清晰明了。策略模式又是一种面向对象的设计模式,说白了,所有的这些模式的出现都是为了使程序或者代码结构更加清晰,易于理解,降低耦合性,提高灵活性以使代码更加容易扩展等等。
定义:策略模式定义了一系列算法,并将每个算法封装起来,而且使得它们可以相互替换,策略模式让算法和使用它的客户端而独立变化。
策略模式的使用场景:
1.
针对一个问题有多种解决方法的时候,用来选择具体的哪种方法。
1.
将多种同类型的操作独立封装起来, 保证多种同类型的操作安全性。当我们的程序有多种同类型的方法时我们不应该因为添加了新类型的方法而要改变原来的方法,不然就违背了OCP(开闭)原则和单一职责原则。
1.
摆脱臃肿。当出现同一个类有多个子类时选择哪个子类而不需要if-else或者switch-else。
策略模式的优点:
1.
结构清晰明了,使用简单直观。使用时根据需要选择不同的策略;
1.
耦合度较低,灵活性高,易扩展;
1.
操作(策略)封装比较彻底,数据更加安全。
策略模式的缺点:
- 随着策略数量增加的增加,需要的策略子类越来越多,程序体积变大。
策略模式的实现:
路线情况:
~~~
/**
* 乘车的路线情况
* @author lt
*
*/
public class Route {
public String type; // 路的类型,如:水路
public int money; // 消耗的路费
public String time; // 花费的时间
@Override
public String toString() {
return "Route [type=" + type + ", money=" + money + ", time=" + time
+ "]";
}
}
~~~
抽象乘车策略类:
~~~
/**
* 乘车策略
* @author lt
*
*/
public abstract class RideStrategy {
/**
* 计算乘车价格
* @return
*/
public abstract Route CalcuRidePrice();
}
~~~
走水路:
~~~
/**
* 水路,这是一种策略
* @author Administrator
*
*/
public class Waterway extends RideStrategy {
@Override
public Route CalcuRidePrice() {
Route route = new Route();
route.type = "水路";
route.money = 100;
route.time = "2个小时";
return route;
}
}
~~~
走陆路:
~~~
/**
* 陆路,这是一种策略
* @author lt
*
*/
public class Randway extends RideStrategy {
@Override
public Route CalcuRidePrice() {
Route route = new Route();
route.type = "陆路";
route.money = 180;
route.time = "1个半小时";
return route;
}
}
~~~
走空路:
~~~
/**
* 空路,乘飞机,这是一种策略
* @author lt
*
*/
public class Airway extends RideStrategy {
@Override
public Route CalcuRidePrice() {
Route route = new Route();
route.type = "空路";
route.money = 400;
route.time = "20分钟";
return route;
}
}
~~~
乘车:
~~~
/**
* 乘车
* @author lt
*
*/
public class Ride {
private RideStrategy rideStrategy;
/**
* 获取路线情况
* @return
*/
public Route getRoute(){
return rideStrategy.CalcuRidePrice();
}
public RideStrategy getRideStrategy() {
return rideStrategy;
}
public void setRideStrategy(RideStrategy rideStrategy) {
this.rideStrategy = rideStrategy;
}
}
~~~
测试使用这三种策略:
~~~
public class Test {
public static void main(String[] args) {
Ride ride = new Ride();
// 1. 走水路
ride.setRideStrategy(new Waterway());
System.out.println(ride.getRoute());
// 2. 走陆路
ride.setRideStrategy(new Randway());
System.out.println(ride.getRoute());
// 3. 走空路
ride.setRideStrategy(new Airway());
System.out.println(ride.getRoute());
}
}
~~~
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b372c8f6.jpg "")
可以看到,上面我们测试的方法里面这三种策略只是换了一个实现而已,源码什么了都没修改,仅仅改变了策略,这就实现了这三种策略的使用。而且以后如果各路线的情况改变了,比如水路涨价了,那么我们只需要修改水路那种策略,即:只要修改Waterway.java这个类而不用修改其他策略(陆路和空路),保证了其他策略(陆路和空路)的安全,并且以后如果有其他种类的路线(除了海陆空),那么相应的只需要增加一个策略类而已,极易扩展和使用。
总结:
策略模式的引入使得我们的程序结构更加清晰明了,耦合性降低,灵活性提升,在面对实际的一个操作有多种算法或者策略实现的情况下应多使用这种模式。
(四),工厂方法模式
最后更新于:2022-04-01 09:57:04
相关概念:
抽象工厂方法模式是工厂方法模式的一个特例。
**定义:**工厂方法模式(FACTORY METHOD)是一种常用的对象创建型设计模式,此模式的核心精神是封装类中不变的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。它的核心结构有四个角色,分别是抽象工厂;具体工厂;抽象产品;具体产品。
**抽象工厂(Creator):**是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。
**具体工厂(Concrete Creator):**这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建产品对象。
**抽象产品(Product):**工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。在上图中,这个角色是Light。
**具体产品(Concrete Product):**这个角色实现了抽象产品角色所定义的接口。某具体产品有专门的具体工厂创建,它们之间往往一一对应。
**抽象工厂方法模式:**为创建一组相关或者相互依赖的对象提供一个接口,而不需要指定它们的具体类。每个抽象产品都对应一个或多个具体的产品,每个抽象工厂都对应一个或多个具体工厂,包含四个基本角色。
适用场景:
1.
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
1.
工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。
1.
由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。
工厂方法模式和Builder模式的异同:
1.
这两种模式都是为了创建复杂对象而设计的模式
1.
工厂方法模式用来生成一系列同类型的复杂对象(如:汽车生产工厂生产汽车),复杂对象的创建过程在工厂方法中,并且可以管理一系列的生成的产品等,当一个工厂只生产一种类型的产品,生产出来的产品类型相同。
1.
Builder模式是用来生成一个更为复杂对象(这种类型的对象具有不同的表现),将复杂对象的创建与展示相分离,将对象的创建过程封装以向外界隐藏起来,使得同样的构建过程可以创建不同的表示。
实现代码:
抽象产品
~~~
/**
* 抽象产品
* @author lt
*
*/
public abstract class Product {
/**
* 每个产品都应该有它的用途
*/
public abstract void use();
}
~~~
抽象工厂
~~~
/**
* 抽象工厂
* @author lt
*
*/
public abstract class Factory {
/**
* Product是要生成的产品的一个抽象类,即抽象产品
* @return Product
*/
public abstract Product createProduct();
}
~~~
具体产品
~~~
/**
* 具体产品
* @author lt
*
*/
public class Car extends Product{
protected void run(){
System.out.println("汽车跑起来了");
}
protected void stop(){
System.out.println("汽车停下来了");
}
@Override
public void use() {
System.out.println("一种交通工具");
}
}
~~~
具体工厂
~~~
/**
* 具体工厂,这里是汽车生产工厂
* @author lt
*
*/
public class CarFactory extends Factory{
/**
* Car是具体产品,是产品的一个子类
* @return
*/
@Override
public Car createProduct() {
return new Car();
}
}
~~~
具体工厂里面可以封装一些产品创建过程中需要做的操作,如:将生产的产品添加到一个集合中,以便后期管理。
**测试:**
~~~
public static void main(String[] args) {
Factory factory = new CarFactory();
Car car = (Car) factory.createProduct();
car.use();
car.run();
car.stop();
}
~~~
可以看到这里的工厂是抽象工厂(接口),但实际上是具体的工厂(实例),那么它实际生产出来的产品也是具体产品(让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类),这里的具体产品是汽车。
**结果:**
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3705968.jpg "")
上面的这些代码是工厂方法模式的完整的形式,工厂方法的完整描述:定义一个创建对象的接口(即抽象工厂类)实例化抽象类(抽象产品),让其子类(具体工厂类)决定实例化哪一个类(具体产品类)。“一对一”的关系。,由四个角色组成。
除了上面的四个角色组成的工厂方法模式外,还有简单工厂方法模式或者说是静态工厂方法模式。
我们对上面的代码进行一些改造
在具体工厂CarFactory中添加一个方法:
~~~
public <T extends Object> T createCar(Class<T> clazz){
T obj = null;
try {
obj = (T) Class.forName(clazz.getName()).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
~~~
-
也可以为每一种具体的汽车写一个生产方法,这里为了简单起见,利用反射根据传递进来的类字节码生成类(具体产品)的实例。
-
如果这个方法写成静态`static`的,那么这个工厂就是简单工厂或者静态工厂了,相应的模式为简单工厂方法模式或静态工厂方法模式。
具体产品A,宝马汽车
~~~
/**
* 具体产品,宝马汽车
* @author lt
*
*/
public class BaoMa extends Car{
@Override
protected void run() {
System.out.println("宝马跑起来了!");
}
@Override
protected void stop() {
System.out.println("宝马停下来了!");
}
}
~~~
具体产品B,奔驰汽车
~~~
/**
* 具体产品,大奔
* @author lt
*
*/
public class BenChi extends Car{
@Override
protected void run() {
System.out.println("奔驰跑起来了!");
}
@Override
protected void stop() {
System.out.println("奔驰停下来了!");
}
}
~~~
**测试:**
~~~
public static void main(String[] args) {
CarFactory factory = new CarFactory();
BaoMa baoma = factory.createCar(BaoMa.class);
baoma.run();
baoma.stop();
BenChi benchi = factory.createCar(BenChi.class);
benchi.run();
benchi.stop();
}
~~~
**结果:**
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b3718489.jpg "")
最简单的一个工厂方法模式是静态工厂方法模式,即不含有任何抽象,相应的方法为static的,这个模式在Android开发中用一个工厂管理多个Fragment的时候用到过。
### 总结:
工厂方法模式中的抽象工厂方法模式具有如下优缺点:
抽象工厂方法模式优点:
1.
分离接口与实现,耦合性低。要客户端使用抽象工厂来创建需要的对象,如客户端不需要知道具体的实现是谁,客户端只是面向接口编程,使其在具体的产品实现中解耦。
1.
灵活性高,极易扩展。由于是基于接口与实现的分分离,使得抽象工厂方法模式在切换同类型的产品类的时候更加灵活,更加简单。
抽象工厂方法模式缺点:
1.
类文件爆炸性增加。每当有一个具体产品与其他具体产品不同时都需要对应的一个具体工厂。
1.
不太容易扩展新类型的产品。因为我们增加一个新类型的产品类时候就需要修改抽象工厂,抽象工厂更改后,那么对应的具体的工厂类均要被修改,甚至当新类型的产品类和原来的产品类截然不同时,就需要一个添加一个新的抽象工厂。
(三),原型模式
最后更新于:2022-04-01 09:57:01
### 前言
面向对象中的另外一个设计模式是原型(prototype)模式,这种模式使用非常简单,使用的场合不是很多,所以不是很常用。
定义:将一个对象实例设为原型,通过clone该原型实例得到新的一个实例,而不是通过new得到原型对象实例,这点java和C++的对象拷贝是一样的。
适用场景:
1、提升对象创建的效率。如果一个对象的创建比较复杂,需要消耗很长的时间才能完成该对象的创建,那么这个时候使用原型模式比较好,即通过一个已有该对象的实例克隆出一个新的实例而不是通过new。
2、保护原型实例的安全。对于一个已经有的原型对象的实例,如果要操作该原型实例(读取数据),又要保证安全性,那么我们可以将该实例复制出一个新的实例给要读取该实例的客户端使用,而不是直接将该实例返回。这样客户端就无法更改原来那个对象的实例的数据了。
### 原型模式的实现
以java语言为例,原型模式的实现非常简单。下面模拟一个用户登录的过程来实现原型模式。
1、原型对象,User.java
~~~
public class User implements Cloneable{
private String username;
private String password;
private Role role;
public User(String username, String password) {
this.username = username;
this.password = password;
}
//.. -> setter
@Override
protected User clone(){
User user = null;
try {
user = (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return user;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password
+ ", role=" + role + "]";
}
}
~~~
需要注意的是clone方法是重写Object对象的,这里重写clone方法没有处理引用类型的复制,为浅复制。
> java对象的复制(需要实现Cloneable接口)
> 1. 浅复制:没有对引用类型进行处理,这样会导致复制出来的对象,如果存在引用类型,那么复制出来的对象和原来的对象里面的引用类型对象指向的是同一个内存空间。
> 1. 深复制:对引用类型也进行复制,这样可以使得克隆对象和原对象完全脱离关系,即怎么操作克隆对象都不会影响原对象。
2、登录的Session,LoginSession.java
~~~
public class LoginSession {
private static User currentUser;
public static void login(User user) {
if (currentUser == null) {
currentUser = user;
System.out.println("登录用户:"+currentUser);
}
}
public static User getCurrentUser() {
return currentUser.clone();
}
}
~~~
注意:
1. login()方法模拟用户实际登录的过程,用户实际登录则会在Session中添加一个登录的用户(原型实例)。
1. getCurrentUser()不是返回原型实例,而是返回原型实例的克隆实例。这样保证了当前Session中登录的用户信息只能由登录的时候决定,登录后不能修改用户名或者密码了。而这个克隆实例则给程序的其他地方使用(即使改变了里面的数据,原型实例中的数据也不变)。
3、模拟用户从浏览器登录网站和其它地方操作登录的用户信息,Test.java
~~~
public class Test {
public static void main(String[] args) {
User currentUser = new User("lt","123");
currentUser.setRole(new Role("程序员"));
// 模拟登陆
LoginSession.Login(currentUser);
// 模拟其他地方操作当前登录用户的情况
User clone = LoginSession.getCurrentUser();
clone.setUsername("ydx");
Role role = clone.getRole();
role.setName("老板");
System.out.println("cloneUser:"+clone);
System.out.println("登录用户:"+currentUser);
}
}
~~~
这是一个模拟用户登录和其它地方操作登录的用户的例子,下面我们运行看输出:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b36d0a4b.jpg "")
其它地方得到当前登录的用户实例(实际上是一个克隆实例),并修改了克隆实例的用户名和角色信息,发现原型实例用户名没变而角色却变了。这里我们明明改的是克隆实例的信息,为什么原型实例角色信息却变了!这里涉及到了浅复制和深复制。我们对User的克隆方法改成下面这样并修重写Role的clone(注意需要实现CloneAble方法,否则克隆的时候会报CloneNotSupportedException异常)。
修改后的User的clone()
~~~
@Override
protected User clone(){
User user = null;
try {
user = (User) super.clone();
user.setRole((Role) user.getRole().clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return user;
}
~~~
重写了Role的clone方法,需要实现Cloneable接口(告诉外界这个对象可以被复制),Role无引用类型,浅复制和深复制一样,所以直接给个空的实现即可。
~~~
public class Role implements Cloneable{
private String name;
public Role(String name) {
super();
this.name = name;
}
//.. -> setter
@Override
public String toString() {
return "Role [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
~~~
再次运行Test.java:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b36e6d19.jpg "")
可以看到我们修改了克隆实例的信息后,原型实例什么信息也没有改变,包括引用类型的对象信息也没变,这就是深复制和浅复制的区别。到此这个模拟过程结束,这个模拟过程保证了其它地方可以读取到原型实例的信息但无法影响原型实例信息,满足了我们安全性要求和逻辑需要。
总结:原型模型的实现很简单,就是对象的拷贝(克隆或复制),但我们要注意对象的克隆有深和浅之分。
(二),Builder模式
最后更新于:2022-04-01 09:56:59
### 前言
Builder(构造者)模式也是一种常用的设计模式,它是用来一步一步构建复杂事物而设计的模式,使用这种模式可以使得**复杂事物的构建与展示相分离**,具有降低耦合,向外部隐藏复杂事物的构建过程等优点。
Builder模式的使用场景:
(1)相同的方法,不同的执行顺序,产生不同的结果;
(2)多个部件或零件,属性都可以装配到一个对象中,但是产生的运行又不相同;
(3)产品类非常复杂,或者产品类中的调用顺序不同产生不同的作用,这个时候用Builder模式非常合适;
(4)当初始化一个对象特别复杂,如:参数属性多,且很多参数都有默认值。
### Builder模式的实现
以生产汽车为例展示Builder模式的过程。我们知道汽车有很多零件,什么方向盘,发动机,轮子等等,同时它还有一个属性,如:名称,价格,级别等。这些都要汽车生产商来完成,那么汽车生产商就是Builder。汽车生产好了以后要投入市场出售,这时汽车会在汽车商城展示。
1、汽车 –>要构建的复杂的事物
~~~
public class Car {
private String name; // 名称:宝马,奔驰,奥迪...
private String price; // 价格,3万,10万...
private String grade; // 级别,小型车,中型车,大型车
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
@Override
public String toString() {
return "Car [name=" + name + ", price=" + price + ", grade=" + grade
+ "]";
}
}
~~~
这里只为了展示Builder模式,所以不涉及零件或者属性等具体的实现。
2、汽车生产商–>CarBuilder.java
~~~
public class CarBuilder {
private String name;
private String price;
private String grade;
/**
* 构造名称
*/
public CarBuilder buildName(String name){
this.name = name;
return this;
}
/**
* 构造价格
*/
public CarBuilder buildPrice(String price){
this.price = price;
return this;
}
/**
* 构造级别
*/
public CarBuilder buildGrade(String grade){
this.grade = grade;
return this;
}
/**
* 生产汽车
*/
public Car create(){
Car car= new Car();
car.setName(name);
car.setPrice(price);
car.setGrade(grade);
return car;
}
}
~~~
通常来说,Builder的setter会返回自身this,这样可以实现链表式操作,这里的CarBuilder代表汽车生产商,模拟汽车的生产过程。
3、汽车商城(展示汽车)–>CarShow.java
~~~
public class CarShow {
private CarBuilder builder;
public CarShow(CarBuilder builder){
this.builder = builder;
}
public Car getCar(){
return builder.create();
}
}
~~~
这一步有时候也往往会省略,即只有Builder,这只实现汽车的一个展示的过程,客户也可以直接去汽车生产商那买汽车,不用去汽车商城买汽车。
4、客户挑选汽车–>Test.java
~~~
public class Test {
public static void main(String[] args) {
CarBuilder builder = new CarBuilder();
builder.buildName("宝马");
builder.buildPrice("100万");
builder.buildGrade("小型车");
Car car = builder.create();
System.out.println(new CarShow(builder).getCar());
}
}
~~~
结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-17_56ea5b36b8610.jpg "")
即:客户实际看到的汽车是100万的小型车宝马
总结:上面的这些就是Builder的过程,通常就是两步,即:构建和展示。有时候会省略展示的过程,直接由构建者Builder完成构建和展示。上面的例子不是为了初始化一个Car对象而是为了通过这种模式模拟实现汽车的构造和展示相分离。如果我们将Builder设置成一个接口或者抽象类,那么传入不同的Buidler就可以得到不同的汽车,这样一来这种模式的灵活性和可扩展性就大大提高了,而且耦合性非常低。
(一),单例模式
最后更新于:2022-04-01 09:56:57
### 前言
面向对象语言具有封装,继承,多态等三个特性,同时,面向对象语言通常有好多种设计模式,这些设计模式在面向对象语言中是相通的,java是一种面向对象的语言,使用java语言作为开发语言的Android的源码中大量运用了设计模式。单例模式是应用最为广泛而且最为简单的一种设计模式。
单例模式:在任何时刻保证单例对象只存在一个实例,这个实例对象服务于整个系统,如:一个公司只有一个CEO,一个国家只有一个主席或者总统,一个宇宙只有一个地球。同时J2EE中的Application,Session及安卓中的Application都是单例。
### 实现单例模式的几种方式
实现单例模式需要注意一下几个关键点即可:
1. 构造函数私有化,即权限设置为private
1. 通过一个静态方法或者枚举返回单例类对象
1. 确保单例类的实例只有一个,尤其是在多线程的情况下
1. 确保单例类对象在反序列化是不会重新创建对象
通过将单例类的构造函数私有化,使得客户端代码不同通过new产生该类的实例。该单例类通过暴露一个方法给客户端,使得客户端通过该方法得到该单例类的唯一实例,通过还需要在获取该类的实例的过程中保证线程安全,即在多线程的情况下也不能出现单例类多个实例的情况,即单例类的实例在任何情况下都有且只有一个,并向整个系统提供使用。
以java语言为例,实现单例的方式有好多种:
1、懒汉模式(简单但不建议使用)
~~~
public class Singleton {
public static Singleton mInstance;
/**
* 构造函数的私有化
*/
private Singleton (){
}
/**
* 提供一个静态方法获取单例类的实例
* @return
*/
public static synchronized Singleton getInstance(){
if(mInstance == null){
mInstance = new Singleton();
}
return mInstance;
}
}
~~~
这种方式简单,但是每次得到实例对象的时候都要同步,同步的过程是需要开销的,所以效率低,不推荐使用。
2、Double Check Lock(DCL)(一般可以考虑使用) –>改进的懒汉模式
~~~
public class Singleton {
public static Singleton mInstance;
/**
* 构造函数的私有化
*/
private Singleton(){
}
/**
* 提供一个静态方法获取单例类的实例
* @return
*/
public static Singleton getInstance(){
if(mInstance == null){ // 第一次检查
synchronized (Singleton.class) { // lock
if(mInstance == null){ // 第二次检查
mInstance = new Singleton();
}
}
}
return mInstance;
}
}
~~~
这个方式对懒汉模式做了一些改进,synchronized没有加在方法上,而是加在了方法体上。调用该方法的时候先检查实例对象是否为空,不为空直接放返回,为空就加同步(防止多线程出现多个实例的情况),并判断实例对象是否为空,为空就创建。这种方式只会在实例对象第一次为空的情况下同步。克服了懒汉模式每次请求得到实例都有同步,造成不必要的开销的缺点。
注意:该方法由于java内存模型的原因偶尔会失败,但是在绝大多数情况下可以保证单例对象的唯一性。
> java的内存模型,在通过new新建一个实例的时候,这个新建语句最终会编译成多条汇编指令:
(1) 给Singleton的实例分配内存
(2) 调用Singleton()构造函数,初始化成员属性
(3) 将该实例的引用mInstance 指向分配的内存空间
3、静态内部类实现单例(推荐)
~~~
public class Singleton {
/**
* 构造方法私有化
*/
private Singleton(){
}
/**
* 私有的内部静态类,使用了加载外部类的时候内部类不会立即加载的特性
* @author lt
*
*/
private static class SingletonHolder{
public static Singleton mInstance = new Singleton();
}
/**
* 暴露一个方法取得单例对象的实例
* @return
*/
public static Singleton getInstance(){
return SingletonHolder.mInstance;
}
}
~~~
当第一次加载Singleton类的时候,并不会初始化mInstance,只有在调用了Singleton的getInstance()方法时候才会初始化mInstance,第一次调用getInstance()方法会导致虚拟机加载其内部类,这种方式不仅可以确保线程安全,也能够保证单例对象的唯一性,同时也延迟单例对象的初始化。推荐使用
4、枚举单例(超级简单)
~~~
public enum Singleton {
HELLO;
public void println(String text){
System.out.println(text);
}
}
~~~
枚举与java中的普通类一样,不仅可以有字段,也可以有自己的方法,如果我们需要一个对象来实现一个简单的方法能完成的事件,如:Android中显示土司,那么我们就可以考虑使用枚举,并且枚举实例的创建默认是**线程安全**的,其他的类尽管将类的构造方法私有化了,但反序列化是会重新生成实例,而枚举不会。
5、使用容器实现单例模式
~~~
public class Singleton {
private static Map<String,Object> instances = new HashMap<String,Object>();
public static void putObj(String key,Object instance){
if(!instances.containsKey(key)){
instances.put(key, instance);
}
}
public static Object getInstance(String key){
return instances.get(key);
}
}
~~~
该方法就是将已经产生的对象保存到HashMap中暂存起来,每次根据相同的key从这里取到同一个实例。通常在程序开始的时候会将多种类的实例保存到该集合中,这样可以管理这些类的实例,下次直接取出这个实例。
总结:单例模式作为面向对象设计模式中应用最简单最广泛的一种设计模式。这种设计模式带来的好处是:
(1)提高了系统的性能,如:一个包含线程池,缓存系统,网络请求的ImageLoader对象,因为这种类型的对象占用的系统资源大,单例模式解决了这个缺点,整个系统中只有一个该对象的实例,减小了系统性能开销。
(2)减小了系统内存开销,单例模式使得某类对象在内存中只存在一个实例,从而减少了系统内存的占用,
(3)单例模式避免对资源的多重占用保证系统整体的协调,试想一下,如果一个对文件的写操作,如果只有一个对象存在内存中,那么避免了对文件同时写的过程
(4)单例对象可以设置全局的通讯站点,优化和共享资源访问,如:Android中的Application,我们可以在Application中放置一些数据,从而在其他的地方都可以取出该数据,实现数据全局共享。
单例模式的缺点:
(1)单例对象由一般没有接口,也不是抽象的不易扩展,如要扩展,就需要修改类的源代码,这点和开闭原则相违背。
(2)单例对象的使用容易产生一些问题,如:我们在单例对象放置了某些因生命周期或者其它原因我们人为无法控制而失效,那么我们在其它地方使用就会出现内存泄漏等错误。
前言
最后更新于:2022-04-01 09:56:54
> 原文出处:[面向对象设计模式](http://blog.csdn.net/column/details/oodm.html)
>作者:[ydxlt](http://blog.csdn.net/ydxlt)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 面向对象设计模式
> 面向对象语言的设计模式,所有面向对象语言都可以有的设计模式,其思想是相通的。但不同的语言实现不一样,这里参照了【Android源码设计模式解析】一书基于Java实现丰富的设计模式,就像数据结构通常基于C一样。