设计模式六大原则

2019年2月26日19:41:21html

设计模式六大原则

为何会有六大原则

有言曰,“无规矩不成方圆”,有“规”才能画“圆”,那设计模式要遵循的六大原则要画一个什么的“圆”呢?前端

这里要从面向对象编程提及,从面向过程编程到面向对象编程是软件设计的一大步,封装、继承、多态是面向对象的三大特征,原本这些都是面向对象的好处,可是一旦有人滥用了,就有了坏味道。java

好比,封装是隐藏对象的属性和实现细节的,我想到了还没提倡MVC的时候,一个servlet里的doGet、doPost方法就完成了全部事情,业务逻辑、数据持久化、页面渲染等,这样一来咱们须要修改业务逻辑的时候是修改这个servlet,须要修改数据持久化的是修改这个servlet,甚至页面修改也是修改这个servlet。这样可维护性就不好了。数据库

由于滥用或者不正确的时候致使代码的坏味道,致使系统的可维护性和复用性等变低,因此面向对象须要遵循一些原则make the code better。如:一个servlet干全部事情能够改成MVC,每一层的类作本身负责的事情,遵循单一职责原则。编程

为了提升系统的可维护性、复用性和高内聚低耦合等,因此有了六大原则。由于设计模式是面向对象实践出来的经验,因此这六大原则既是面向对象的六大原则,也是设计模式的六大原则。设计模式

六大原则

设计模式六大原则

先来个图,整体感觉一下,其实说简单也简单,死记硬背这六个名词不用十分钟,可是要使用得游刃有余,仍是要下一点功夫的。本文也只是纸上谈兵,聊聊六大原则的定义、用法、好处等。ide

单一职责原则(Single Responsibility Principle,SRP)

定义:不要存在多于一个致使类变化的缘由(There should never be more than one reason for a class to change.)。函数

就像我前面说到的那个例子,一个servlet干完了全部事情,这样致使servlet变化的缘由就不止一个了,因此要将这些事情分给不一样的类。测试

好比我如今要实现一个登陆的功能,servlet代码是这样的:this

public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 一、获取前端传过来的数据
        // 二、链接数据库,查询数据
        // 三、比较数据,得出结果
        // 四、封装结果,返回给前端
    }
}

应用MVC后,代码修改以下:

public class LoginController {

    private LoginService loginService;

    public ModelAndView Login(HttpServletRequest req, HttpServletResponse resp) {
        // 一、获取前端传过来的数据
        loginService.login();
        // 四、封装结果,返回给前端
        return null;
    }
}

public class LoginService {

    private UserDao userDao;

    public boolean login() {
        userDao.findOne();
        // 三、比较数据,得出结果
        return false;
    }
}

public class UserDao {

    public User findOne(){
        // 二、链接数据库,查询数据
        return null;
    }
}

有图以下:
单一职责原则

这样职责分明,有变动需求只须要找到职责相关的那部分修改就好。好比要修改比较逻辑,就修改Service层代码;要修改链接数据库,就修改Dao层就能够了;要修改返回页面的内容,就修改Controller层就能够了。

应用场景:在项目开始阶段要明确类的职责,若是发现一个类有两个或以上的职责,那就拆成多个类吧。若是是项目后期,要评估好修改的代价以后在重构。别让一个类作的事情太多。

好处:实现高内聚、低耦合,增长代码的复用性。

开闭原则(Open Closed Principle, OCP)

定义:软件实体,如:类、模块与函数,对于扩展开放,对修改关闭(Software entities like classes, modules and functions should be open for extension but closed for modifications.)。

从简单工厂模式到工厂方法模式,完美诠释了开闭原则的应用场景。有兴趣能够查看本人所写的《简单工厂模式》《工厂方法模式》

用类对象实现操做符运算:

简单工厂模式实现:

public static IOperation createOperation(String op) {
    IOperation operation = null;

    if ("+".equals(op)) {
        operation = new AddOperationImpl();
    } else if ("-".equals(op)) {
        operation = new SubOperationImpl();
    } else if ("*".equals(op)) {
        operation = new MulOperationImpl();
    } else if ("/".equals(op)) {
        operation = new DivOperationImpl();
    }
    
    return operation;
}

这是简单工厂模式中的工厂角色实现建立全部实例的内部逻辑的方法,调用方法时,根据传进来的操做符选择不一样的实现类,可是若是我要添加一个乘方的话,就须要添加else if结构,没有对修改关闭,这样就不符合开闭原则了。

工厂方法模式实现:

// 加
// 建立具体工厂
IOperationFactory operationFactory = new AddOperationFactoryImpl();
// 建立具体产品
IOperation operation = operationFactory.createOperation();
// 调用具体产品的功能
int result = operation.getResult(a, b);

须要什么运算,就继承IOperationFactory实现对应的实现类,使用时只须要在须要的地方new这个实现类便可。不用修改工厂类,增长运算就增长抽象工厂类的实现类,符合开闭原则。

应用场景:在系统的任何地方

好处:使得系统在拥有适应性和灵活性的同时具有较好的稳定性和延续性

里氏替换原则(Liskov Substitution Principle,LSP)

定义:使用基类的指针或引用的函数,必须是在不知情的状况下,可以使用派生类的对象(Functions that use pointers or references to base classes must be able to use objects of derived classes whithout knowing it.)。

为何叫里氏替换原则? 里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。

里氏替换原则告诉咱们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,若是一个软件实体使用的是一个子类对象的话,那么它不必定可以使用基类对象。例如,我喜欢动物,那我必定喜欢狗,由于狗是动物的子类;可是我喜欢狗,不能据此判定我喜欢动物,由于我并不喜欢老鼠,虽然它也是动物。

上面叙述转为代码以下:

// 动物
public interface Animal {
    public String getName();
}
// 狗
public class Dog implements Animal{
    private String name = "狗";
    @Override
    public String getName() {
        return this.name;
    }
}
// 老鼠
public class Mouse implements Animal{
    private String name = "老鼠";
    @Override
    public String getName() {
        return this.name;
    }
}
// 测试类
public class ISPTest {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal mouse = new Mouse();
        iLoveAnimal(dog);
        iLoveAnimal(mouse);
//        iLoveDog(dog);
//        iLoveDog(mouse);
    }

    public static void iLoveAnimal(Animal animal) {
        System.out.println(animal.getName());
    }

    public static void iLoveDog(Dog dog) {
        System.out.println(dog.getName());
    }

}

其中iLoveAnimal(Animal animal)能够传递的对象参数有Dog和Mouse,由于Dog和Mouse是Animal的子类,因此编译经过。iLoveDog(Dog dog)不能Mouse为参数,虽然他们同属Animal的子类,编译不能经过。在编译阶段,Java编译器会检查一个程序是否符合里氏替换原则,可是Java编译器的检查是有局限的,这只是一个与实现无关、纯语法意义上的检查,在设计上咱们要注意遵循里氏替换原则。

理论上,里氏替换原则是实现开闭原则的重要方式之一,使用基类对象的地方均可以使用子类对象,因此在程序中尽可能使用基类类型来对对象进行定义,而在运行时在肯定其子类类型,当子类类型改变时,就能实现扩展,并无对现有的代码结构进行修改

实践中,咱们能作的是:

  • 尽可能将基类设计为抽象类和接口

  • 子类必须实现父类中声明的全部方法,且子类的全部方法必须在基类中声明

最少知识原则(Least Knowledge Principle,LKP)又名迪米特法则(Law of Demeter)

定义:只与你最直接的朋友交流(Only talk to you immediate friends.)。

又名迪米特法则的缘由是:迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。

根据迪米特法则有,对象O的一个方法M仅能访问如下类型的对象:

  • 一、当前对象O自身(this)

  • 二、M方法的参数对象(如,toString(Integer i)中对象i

  • 三、当前对象O成员对象(当前对象O直接依赖的对象)

  • 四、M方法中所建立的对象

重要的是,方法M不该该调用这些方法返回对象的方法,就是链式调用返回的但返回的并非自身对象的对象的方法。和你朋友说话,而不是和你朋友的朋友,对于你来讲是陌生人的人说话。

下面是一个例子:

public class LawOfDelimterDemo {

    /**
     * 这个方法有两个违反最少知识原则或迪米特法则的地方。
     */
    public void process(Order o) {

        // 这个方法调用符合迪米特法则,由于o是process方法的参数,是类型2的参数
        Message msg = o.getMessage();

        // 这个方法调用违反了迪米特法则,由于使用了msg对象,这个对象是从参数对象中获得的对象。
        // 咱们应该让Order去normalize这个Message,例如:o.normalizeMessage(),而不是使用msg对象的方法
        msg.normalize();

        // 这也是违反迪米特法则的,使用了方法链代替上面说的msg临时变量。
        o.getMessage().normalize();

        // 构造函数调用
        Instrument symbol = new Instrument();

        // 这个方法调用是符合迪米特法则的,由于Instrument实例是本地建立的,就是类型4的对象,process方法中所建立的对象
        symbol.populate(); 
    }
}

好处:下降系统的耦合性,增长系统的可维护性和适应性。由于较少依赖于其余对象的内部结构,其余对象的修改就从新修改它们的调用者。

坏处:可能会增长对象的方法,引起其余bug。

接口隔离原则(Interface Segregation Principle, ISP)

定义:一个类与另一个类之间的依赖性,应该依赖于尽量小的接口(The dependency of one class to another one should depend on the smallest possible interface.)。

例子:首先有一个经理,负责管理工人。其次,有两种类型的工人,一种是在平均水平的工人,一种是高效率的工人,这些工人都须要午休时间来吃饭。最后还有一种机器人在工做,可是机器人不须要午休。

设计实现代码:

interface IWorker {
    public void work();
    public void eat();
}

// 通常工人
class Worker implements IWorker {
    public void work() {
        // 正常工做
    }
    pubic void eat() {
        // 午休吃饭
    }
}

// 高效率工人
class SuperWorker implements IWorker {
    public void work() {
        // 高效率工做
    }
    public void eat() {
        // 午休吃饭
    }
}

// 机器人
class Rebot implements IWorker {
    public woid work() {
        // 工做
    }
    public void eat() {
        // (实现代码为空,什么也不作)
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker w) {
        worker = w;
    }
    public void manage() {
        worker.work();
        worker.eat();
    }

}

经理去管理工人的时候,调用接口eat方法的时候,机器人什么也不作。咱们应该让接口变小,把IWorker接口拆分。

// 工做接口
interface IWorkable {
    public void work();
}
// 吃饭接口
interface IFeedable {
    public void eat();
}

// 通常工人
class Worker implements IWorkable, IFeedable {
    public void work() {
        // 正常工做
    }
    pubic void eat() {
        // 午休吃饭
    }
}

// 高效率工人
class SuperWorker implements IWorkable, IFeedable {
    public void work() {
        // 高效率工做
    }
    public void eat() {
        // 午休吃饭
    }
}

// 机器人
class Rebot implements IWorkable {
    public woid work() {
        // 工做
    }
}

class Manager {
    IWorkable worker;
    IFeedable feed;

    public void setWorker(IWorkable w) {
        worker = w;
    }
    public void setfeed(IFeedable f) {
        feed = f;
    }
    public void manageWork() {
        worker.work();
    }
    public void manageFeed() {
        feed.eat();
    }

}

将IWorker接口拆分红IWorkable接口和IFeedable接口,将Manager类与工人类交互尽可能依赖与比较小的接口。

在使用接口隔离原则时,咱们须要注意控制接口的粒度,接口不能过小,若是过小会致使系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。通常而言,接口中仅包含为某一类用户定制的方法便可,不该该强迫客户依赖于那些它们不用的方法。

依赖倒置原则(Dependence Inversion Principle, DIP)

定义:高层模块不该该依赖于底层模块,它们都应该依赖于抽象。抽象不该该依赖于细节,细节应该依赖与抽象(High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.)。

尽可能面对接口编程,而不是面对实现编程。

例子:你如今是一个导演,你要拍一部电影,准备找刘德华作主角。在电影里,刘德华是一个警察,能够捉犯人。

// 刘德华
class LiuDeHua {
    public LiuDeHua(){}
    public void catchPrisoner(){}
}

// 剧本
class Play {
    LiuDeHua liuDeHua = new LiuDeHua();
    liuDeHua.catchPrisoner();
}

可是华仔由于档期来不了,因而找古天乐。

// 古天乐
class GuTianLe {
    public GuTianLe(){}
    public void catchPrisoner(){}
}

// 剧本
class Play {
    GuTianLe guTianLe = new GuTianLe();
    guTianLe.catchPrisoner();
}

古仔说要捐钱建学校,没空来。因而又说要找刘青云,编剧心好累……

若是编剧只是面对接口编程,就会变成这样:

// 警察
interface Police {
    public void catchPrisoner();
}

// 剧本
class Play {
    private Police police;
    public Play(Police p) {
        police = p;
    }
    police.catchPrisoner();
}

这样不管谁来,只须要实现Police接口就能够按剧本拍了。

在实现依赖倒置原则时,咱们须要针对抽象层编程,而将具体类的对象经过依赖注入(DependencyInjection, DI)的方式注入到其余对象中,依赖注入是指当一个对象要与其余对象发生依赖关系时,经过抽象来注入所依赖的对象。

经常使用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指经过构造函数来传入具体类的对象,设值注入是指经过Setter方法来传入具体类的对象,而接口注入是指经过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。

在大多数状况下,开闭原则、里氏替换原则和依赖倒置原则这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒置原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不一样而已。

参考

Law of Demeter in Java - Principle of least Knowledge

面向对象设计原则之依赖倒转原则

2019年3月22日14:31:03

相关文章
相关标签/搜索