耦合关系直接决定着软件面对变化时的行为
软件的对象经常要发生巨大的变化,但它却拥有比较稳定的接口
我们继续来说”new”的问题,我们在简单工厂模式中,将实例化对象的工作推迟到了专门负责创建对象的工厂类中,这样,在我们事先预知的情况下,可以根据我们的需要动态创建产品类。但是,我们的预知是有限的,客户的变化可能是无限的。所以,就出现了问题,一旦客户的变化超越了我们的预知,我们就必须修改我们的源代码了。这是设计模式所不允许的,怎么办呢?工厂方法模式正是解决此类问题的。 问题:具体工厂类的创建工作不能满足我们的要求了,创建的工作变化了 解决思路:哪里变化,封装哪里。把具体工厂封装起来。
工厂方法模式又称为工厂模式,在工厂方法模式中,父类负责定义创建对象的公共接口,而子类则负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成,即由子类来决定究竟应该实例化(创建)哪一个类。 工厂方式法模式(Factory Method),定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟到子类。
抽象产品类(Product):定义产品的接口
具体产品类(ConcreteProduct) :实现接口Product的具体产品类
抽象工厂类(Creator) :声明工厂方法(FactoryMethod),返回一个产品
真实的工厂(ConcreteCreator):实现FactoryMethod工厂方法,由客户调用,返回一个产品的实例
据说清朝有个皇帝穿衣非常的奢侈,每种衣服(具体产品类)由一宫女(具体工厂类)专门负责,这样一来,每增加一种衣服(具体产品类),就要多出一个宫女(具体工厂类),但是他们各负其责,互不影响。皇帝之所以这样做,是因为针对穿衣服这件事来说,可扩展性是非常强的。
抽象产品类:
public abstract class Procuct {
//产品类的公共方法
public void method1() {}
//抽象方法
public abstract void method2();
}
具体产品类:
public class ConcreteProduct1 extends Product {
public void method2(){}
}
public class ConcreteProduct2 extends Product {
public void method2(){}
}
抽象工厂类:
public abstract class Creator {
/*
* 创建一个产品对象,其输入参数类型可以自行设置
* 通常为String、Enum、Class等,也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);
}
具体工厂类:
public class ConcreteCreator {
public <T extends Product> T createProduct(Class<T> c) {
Product product = null;
try {
product = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T)product;
}
}
场景类:
public class Client {
public static void main(String[] args) {
Creator = new ConcreteCreator();
Product product = creator.createProduct(ConcreteProduct1.class);
/*
*继续业务处理
*/
}
}
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?
将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
Director使用者/客户程序 Builder生成器
配置文件在一些情况下可以防止代码的修改,程序通过反射得到类型,需要修改时只需要修改配置文件里的内容,而不修改代码,不需要重新编译。
Product产品类
Builder抽象生成器
ConcreteBuilder具体生成器
Director导演类
相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用生成器模式。
多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用生成器模式非常适合。
产品类:
public class Product{
public void doSomething(){}
}
抽象生成器:
public abstract class Builder{
//设置产品的不同部分,以获得不同的产品
public abstract void setPart();
//建造产品
public abstract Product buildProduct();
}
具体生成器:
public class ConcreteBuilder extends Builder{
private Product product = new Product();
//设置产品零件
public void setPart(){
//产品类内的逻辑处理
}
//组建一个产品
public Product buildProduct(){
return product;
}
}
导演类:
public class Director{
private Builder builder = new ConcreteBuilder();
//构建不同的产品
public Product getProduct(){
builder.setPart();
/*
* 设置不同的零件、产生不同的产品
*/
return builder.buildProduct();
}
}
常规的对象创建方法:Road road = new Road();
new的问题:实现依赖,不能应对“具体实例化类型”的变化。
解决思路:
封装变化点——哪里变化,封装哪里。
潜台词:如果没有变化,当然不需要额外的封装!
//一点点帮助,以后修改,只需要在工厂方法里修改,而不在客户程序中逐个修改
class RoadFactory{
public static Road createRoad(){
return new Road();
}
}
//创建一个Road对象,客户程序
Road road = roadFactroy.createRoad();
简单工厂的问题:
不能应对“不同系列对象”的变化。比如有不同风格的游戏场景——对应不同风格的道路、房屋、地道…
如何解决:
使用面向对象的技术来“封装”变化点
在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合?
提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。
例如游戏开发,有房屋、道路、地道、沙漠…我们利用简单工厂进行封装创建,但当需要改变风格,如现代房屋、现代道路、现代地道…的时候我们就可以利用抽象工厂,让简单工厂继承抽象工厂,如上图所示,Abstra ctFactroy为抽象工厂,ConcreateFactroy1是简单工厂,CreateProductA()创建对象(道路、房屋),右边为客户程序,更换场景风格,更新一系列对象的创建。即抽象工厂适应于系列对象的整体变动。
如果没有应对“多系列对象构建”的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的静态工厂完全可以。
“系列对象”指的是这些对象之间有相互依赖、或作用的关系,例如游戏开发从场景中的“道路”与“房屋”的依赖,“道路”与“地道”的依赖。
Abstract Factory模式主要在于应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动。
Abstract Factory模式经常和Factroy Method模式共同组合来应对”对象创建的需求变化”。
//抽象产品类
public abstract class AbstractProductA{
//每个产品共有的方法
public void shareMethod(){}
//每个产品相同方法,不同实现
public abstract void doSomething();
}
//两个产品实现类
public class ProductA1 extends AbstractProductA{
public void doSomething(){
System.out.println("产品A1的实现方法");
}
}
public class ProductA2 extends AbstractProductA{
public void doSomething(){
System.out.println("产品A2的实现方法");
}
}
//抽象工厂类
public abstract class AbstractFactory{
//创建产品A家族
public abstract AbstractProductA createProductA();
//创建产品B家族
public abstract AbstractProductA createProductB();
}
//只生产产品等级为1的工厂
public class Factory1 extends AbstractFactory{
public AbstractProductA createProductA(){
return new productA1();
}
public AbstractProductB createProductB(){
return new productB1();
}
}
//只生产产品等级为2的工厂
public class Factory1 extends AbstractFactory{
public AbstractProductA createProductA(){
return new productA2();
}
public AbstractProductB createProductB(){
return new productB2();
}
}
//场景类
public class Client{
public static void main(String[] args){
//定义出两个工厂
AbstractFactory factory1 = new Factory1();
AbstractFactory factory2 = new Factory2();
//产生A1、B1对象
AbstractProductA a1 = new factory1.createProductA();
AbstractProductB b1 = new factory1.createProductB();
//产生A2、B2对象
AbstractProductA a2 = new factory2.createProductA();
AbstractProductB b2 = new factory2.createProductB();
......
}
}
该模式下,产品等级是非常容易扩展的,增加一个产品等级或系列,只要增加一个工厂类负责新增加出来的产品生产任务即可。但是要是扩展产品族是比较难的,比如增加一个新的产品,那么每个工厂都要修改,添加相应的方法。
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
Facade门面角色
subsystem子系统角色
为一个复杂的模块或子系统提供一个供外界访问的接口
子系统相对独立——外界对子系统的访问只要黑箱操作即可
预防低水平人员带来的风险扩散
从客户程序的角度看,Facade模式不仅简化了整个组件系统的接口,同时对于组件内部与外部客户程序来说,从某种程度上也达到了一种“解耦”的效果——内部子系统的任何变化不会影响到Facade接口的变化。
Facade设计模式更注重从架构的从层次去看整个系统,而不是单个类的层次。Facade很多时候更是一种架构设计模式。
门面不参与子系统内部的业务逻辑,子系统不能依赖门面才能被访问。
注意区分Facade模式、Adapter模式、Bridge模式与Decorator模式。
Facade模式更注重简化接口,Adapter模式注重转换接口,Bridge模式注重分离接口(抽象)与其实现,Decorator模式更注重稳定接口的前提下为对象扩展功能。
子系统:
public class ClassA{
public void doSomethingA();
}
public class ClassB{
public void doSomethingB();
}
public class ClassC{
public void doSomethingC();
}
门面对象:
public class Facade{
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
a.doSomethingA();
}
public void methodB(){
b.doSomethingB();
}
public void methodC(){
c.doSomethingC();
}
}
从目的来看:
从范围来看:
在软件系统之中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保他们的逻辑正确性、以及良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
这应该是类设计者的责任,而不是使用者的责任。
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
Singleton模式中的实例构造器可以设置为protected以允许子类派生。
Singleton模式一般不要支持ICloneable接口,因为这可能会导致多个对象实例,与Singleton模式的初衷违背。
Singleton模式一般不要支持序列化,因为这也有可能导致多个对象实例,同样与Singleton模式的初衷违背。
Singleton模式只考虑到了对象创建的管理,没有考虑对象销毁的管理。就支持垃圾回收的平台和对象的开销来讲,我们一般没有必要对其销毁进行特殊的管理。
不能应对多线程环境:在多线程环境下,使用Singleton模式仍然有可能得到Singleton类的多个实例对象。
将一个实例扩展到n个实例,例如对象池的实现。
将new构造器的调用转移到其他类中,例如多个类协同工作环境中,某个局部环境只需要拥有某个类的一个实例。
理解和扩展Singleton模式的核心是“如何控制用户使用new对一个类的实例构造器的任意调用”。
//只支持单线程,多线程下有可能创建多个对象
class Singleton{
private static Singleton instance;
//设置成私有的构造器,防止外界new它
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) {
instance = new Singleton();
}
return instance;
}
}
//支持多线程
class Singleton1{
//把变量声明为volatile类型,编译器与运行时都会注意到这个变量时共享的,不会将该变量上的操作与其他内存操作一起重排序
private static volatile Singleton1 instance;
private Singleton1() {}
public static Singleton1 getInstance() {
synchronized(instance) {
if(instance==null) {
instance = new Singleton1();
}
}
return instance;
}
}
什么是shell?
Shell(外壳)是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。
Shell是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
什么是脚本?
脚本简单地说就是一条条的文字命令,这些文字命令是可以看到的(如可以用记事本打开查看、编辑)。
常见的脚本:JavaScript、VBScript、ASP、JSP、PHP、SQL、Perl、Shell、python、Ruby、JavaFX、Lua等
为什么要学习和使用shell?
shell是内置的脚本。
程序开发的效率非常高,依赖于功能强大的命令可以迅速地完成开发任务。(批处理)
语法简单,代码写起来比较轻松,简单易学
常见的shell种类?
在linux中有很多类型的shell,不同的shell具备不同的功能,shell还决定了脚本中函数的语法,Linux中默认的shell是/bin/bash(重点),流行的shell有ash、bash、ksh、csh、zsh等,不同的shell都有自己的特点以及用途。
csh
C shell使用的是“类C”语法,csh是具有C语言风格的一种shell,其内部命令有53个,较为庞大。目前使用的并不多,已经被/bin/tcsh所取代。
ksh
Korn shell 的语法与 Bourne shell 相同,同时具备了C shell 的易用特点。许多安装脚本都使用 ksh,ksh有42条内部命令,与bash相比有一定的限制性。
tcsh
tcsh是csh的增强版,与 C shell 完全兼容。
sh
是一个快捷方式,已经被/bin/bash所取代。
nologin
指用户不能登录。 操作命令:usermod -s /sbin/nologin dalian
zsh
目前Linux里最庞大的一种shell:zsh。它有84个内部命令,使用起来也比较复杂。一般情况下不会使用该shell。
bash
大多数Linux系统默认使用的shell,base shell是Bourne shell的一个免费版本,它是最早的Unix shell,bash还有一个特点,可以通过help命令来查看帮助。包含的功能几乎可以涵盖shell所具有的功能,所以一般的shell脚本都会指定它为执行路径。
编写规范:
代码规范:
#!/bin/bash 【指定告知系统当前这个脚本使用的shell解释器】
Shell相关指令
文件命名规范:
文件名.sh .sh是linux下 bash shell 的默认后缀
使用流程:
①创建.sh文件 touch/vim
②编写shell代码
③执行shell脚本 脚本必须得有执行权限
案例1:创建test.sh实现第一个shell脚本程序,输出hello world。
输出命令:echo 123
注意:输出的内容如果包含字母和符号(不包含变量),则需要用引号包括起来。如果是纯数字则可以包也可以不包。
在这里运在运行时一定要写成./test.sh,而不是test.sh,运行其他二进制的程序也一样,直接写test.sh,Linux系统会去PATH(环境变量)里寻找有没有叫 test.sh的,而只有/bin,/sbin,/usr/bin,/usr/sbin等在PATH里,你的当前目录通常不在PATH里,所以写成 test.sh 是会找不到命令的,要用 /test.sh 告诉系统说,就在当前目录找。
案例2:使用root用户账号创建并执行test2.sh,实现创建一个shelltest用户,并且在其家目录中新建文件 try.html
touch test2.sh
vim test2.sh
#!/bin/bash
useradd shelltest;
touch /home/shelltest/try.html;
chmod +x test2.sh
脚本执行的另一个方式:/bin/bash 脚本的路径(了解) /bin/bash test2.sh
Shell脚本分为简单的写法(简单命令的堆积)和复杂写法(程序的设计)
a. 什么是量:量就是数据
b. 什么是变量:数据可以发生改变就是变量。
在一个脚本周期内,其值可以发生改变的量就是变量。
c. 什么叫做一个脚本周期
一个脚本周期我们可以简单的理解为当前的shell文件,变量时shell中不可或缺的一部分,也是最基础。最终的组成部分。
定义形如:class name=”yunwei”
使用形如:echo $class_name
变量就是由2部分组成,一个是变量名(左边),另一部分是变量的值(右边)
在使用变量的时候一定需要在变量名前面添加一个$符号,该要求在其他语言中也存在的(例如php)。
在定义变量的时候不能随便加空格!!!
关于单双引号的问题:
双引号能够识别变量,双引号能够实现转义。(类似于”*“)
单引号不能识别变量,只能原样输出,单引号不能转义。
案例1:使用变量改写入门脚本中第1个shell脚本。
案例2:定义一个变量,输出当前时间,要求格式为“年-月-日 时:分:秒”。
date +”%F %T” dt = date +'%F %T'
注意:反引号 `,当在脚本找那个需要执行一些指令并且将执行的结果赋给变量的时候需要使用“反引号”。
语法:readonly 变量名
语法:read -p 提示信息 变量名
案例:编写一个脚本 test6.sh,要求执行之后提示用户输入文件的名称(路径),然后自动为用户创建该文件,
语法:unset 变量名
语法:
if [ condition1 ] //注意空格
then
command1
command2
...
elif [ condition2 ]
then
command1
else
command1
...
fi
单行写法(一般在命令行中):if[condition];then command;fi