设计模式之六大原则

in HandbookDesign Patterns with 9 comments, viewed 127 times

1 概述

在面向对象的编程中,要实现一个功能,可以有非常多的方式。在多年的经验积累总结下来,人们发现优秀的的代码总是遵循一定的范式。其中23种设计模式(Design Patterns),就是前人对优秀代码的编程范式的总结,是面向对象编程的最佳实践。合理地运用这些设计模式,已经是写出高质量,高效率,可读性强,易维护的代码的充要条件。
设计模式六大原则,则是设计模式都会遵守的通用法则。本文将结合简单的例子,介绍这六大原则

2. 六大原则

2.1 依赖倒置原则(Dependency Inversion Principle)

高层模块不应该依赖于底层模块,抽象不应该依赖于细节。因为相对于实现细节的多样性与易变性,抽象类要稳定得多。
换句话说,我们应该针对接口编程。看一个反例

public class DIViolation {
    public static void main(String[] args) {
        JavaDeveloper javaDeveloper = new JavaDeveloper();
        PythonDeveloper pythonDeveloper = new PythonDeveloper();
        Team team = new Team(javaDeveloper, pythonDeveloper);
        team.teamWork();
    }
}

class JavaDeveloper {
    public void work() {
        System.out.format("Java developer is working.");
    }
}

class PythonDeveloper {
    public void work() {
        System.out.format("Python developer is working.");
    }
}

class Team {
    JavaDeveloper javaDeveloper;
    PythonDeveloper pythonDeveloper;
    Team(JavaDeveloper javaDeveloper, PythonDeveloper pythonDeveloper) {
        this.javaDeveloper = javaDeveloper;
        this.pythonDeveloper = pythonDeveloper;
    }
    public void teamWork() {
        javaDeveloper.work();
        pythonDeveloper.work();
    }
}

上述例子中,类Team和具体类JavaDeveloperPythonDeveloper强耦合在了一起,扩展性极差。想象一下,某一天,PythonDeveloper离职了,那我们得修改Team类,把PythonDeveloper给删除掉。又有一天,来了一位新同事CPPDeveloper,我们又得修改Team类,增加CPPDeveloper。由此看出,依赖具体类的系统稳定性与扩展性是多不好。如果我们改为依赖接口呢?

public class DIObedience {
    public static void main(String[] args) {
        Collection<Developer> developers = new HashSet<>();
        developers.add(new Javaer());
        developers.add(new Pythoner());
        Team team = new Team(developers);
        team.teamWork();
    }
}

interface Developer {
    void work();
}

class Javaer implements Developer {
    public void work() {
        System.out.println("Java developer is working.");
    }
}

class Pythoner implements Developer {
    public void work() {
        System.out.format("Python developer is working.");
    }
}

class Team {
    Collection<Developer> developers;
    Team(Collection<Developer> developers) {
        this.developers = developers;
    }
    public void teamWork() {
        developers.forEach(Developer::work);
    }
}

上述例子中,Team依赖于顶层接口Developer,无论组内成员怎么变动,Team类本身完全不需要修改,可扩展性很强,稳定性也很高。

2.2 里氏替换原则(Liskov Substitution Principle)

也就是一位姓的女士提出的原则:-)。对象必须保证在不知道基类的具体实现类的情况下可以被使用,简而言之,子类可以替换掉父类出现。对于Java语言来说,这个原则是不言而喻的。想象一下,当我们以Collection对象作为方法参数的时候,无论传ArrayList还是HashSet,方法都应该能正常工作。为了遵循这条简单的原则,我们在编程中需要做到以下几点

2.2.1 子类与父类的关系一定是is-A,而不是like-A

看一个反例

public class WrongExtend {
    public static void main(String[] args) {
        Fish fish = new Whale();
        fish.breath();
    }
}

class Fish {
    private String name;
    Fish(String name) {
        this.name = name;
    }
    public void breath() {
        System.out.format("I'm %s, I breath with cheek.");
    }
}

class Whale extends Fish {
    Whale() {
        super("whale");
    }
}

输出:

I'm whale, I breath with cheek.

输出了鲸用腮呼吸。原因是鲸不是鱼,但是却错误地继承了Fish这个父类,所以导致了行为breath的错误。所以,当子类并不完全是父类的时候,使用父类的方法,可能会导致错误。

2.2.2 子类应该避免重写父类已定义好的方法

下面是反例

public class WrongOverride {
    public static void main(String[] args) {
        Rectangle rectangle = new Square();
        rectangle.setWidth(3);
        rectangle.setLength(5);
        System.out.format("expect rectangle area to be %d, actual is %d", 3 * 5, rectangle.getArea());
    }
}

class Rectangle {
    private int width;
    private int length;
    public void setWidth(int width) {
        this.width = width;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public int getArea() {
        return width * length;
    }
}

class Square extends Rectangle {
    // set length same as width
    @Override
    public void setWidth(int width) {
        super.setLength(width);
        super.setWidth(width);
    }
    // set width same as length
    @Override
    public void setLength(int length) {
        super.setLength(length);
        super.setWidth(length);
    }
}

输出:

expect rectangle area to be 15, actual is 25

因为子类Square重写了父类的setWidthsetLength方法,导致了子类与父类的行为不一致,最终输出了与预期不符的结果。所以当子类重写父类方法时,一定不能破坏父类原有的行为。

2.3 接口隔离原则(Interface Segregation Principle)

应该定义多个隔离的接口,而不是一个全面却庞大的接口。子类不应该包含不允许使用的接口。这条规则要求,接口只应包含单一的功能,子类不应包含不必要的功能
来看一反例

public class SIViolation {
    public static void main(String[] args) {
        Person person1 = new Swimmer();
        Person person2 = new Driver();
        person1.drive();
        person2.swim();
    }
}
interface Person {
    void eat();
    void swim();
    void drive();
}
class Swimmer implements Person {
    @Override
    public void eat() {
        System.out.println("Swimmer is eating.");
    }
    @Override
    public void swim() {
        System.out.println("Swimmer is swimming.");
    }
    @Override
    public void drive() {
        System.out.println("Sorry, swimmer can't drive!");
    }
}
class Driver implements Person {
    @Override
    public void eat() {
        System.out.println("Driver is eating.");
    }
    @Override
    public void swim() {
        System.out.println("Sorry, driver can't swim!");
    }
    @Override
    public void drive() {
        System.out.println("Driver is driving.");
    }
}

输出:

Sorry, swimmer can't drive!
Sorry, driver can't swim!

这个例子中,因为我们定义了一个大接口Person,里面包括了不同的功能,导致子类实现的时候,包含了无法使用的方法。子类不仅徒增了不必要的逻辑,而且导致了最终行为的错误:子类并不能完全替代父类出现(父类Person调用swim方法的时候,子类就不能是Driver而只能是Swimmer),违反了里氏替换原则。正确的做法应该是,把driveswim两个方法分离到不同的接口中,子类只应该包含自己能做到的接口。下面是正例

public class ISObedience {
    public static void main(String[] args) {
        swimmable person1 = new Swimmer();
        drivable person2 = new Driver();
        person1.swim();
        person2.drive();
    }
}
interface swimmable {
    void swim();
}
interface eatable {
    void eat();
}
interface drivable {
    void drive();
}

class Swimmer implements swimmable, eatable {
    @Override
    public void eat() {
        System.out.println("Swimmer is eating.");
    }
    @Override
    public void swim() {
        System.out.println("Swimmer is swimming.");
    }
}
class Driver implements drivable, eatable {
    @Override
    public void eat() {
        System.out.println("Driver is eating.");
    }
    @Override
    public void drive() {
        System.out.println("Driver is driving.");
    }
}

2.4 单一职责原则(Single Responsibility Principle)

导致类变化的原因应该只有一个。意思就是,一个类只做一件事。类的职责越简单,代码可读性越高,工程的可维护性也越强,同时也能降低类之间的耦合度,从而降低修改代码带来的风险。
单一职责原则接口隔离原则有些类似。上面的那个例子,也是单一职责原则的很好体现:类/接口的功能应该单一。接口隔离原则更偏向于对抽象与接口的约束,单一职责原则更关注具体实现。

2.5 迪米特法则Demeter Principle)

又叫最少知道原则。一个类对其他类应该有最少的了解,并尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。我们需要做到以下两点:

2.5.1. 类只应该暴露公共的方法,能设成private的方法/属性,就设成private

下面是一个反例

public class WrongPrivilege {
    public static void main(String[] args) {
        Cook cook = new Cook();
        // accidentally consumed a tomato
        cook.consumeTomato();
        cook.cookSoup();
    }
}

class Cook {
    private int tomatoNum = 1;
    private int eggNum = 1;
    public boolean consumeTomato() {
        if (--tomatoNum >= 0) {
            System.out.format("Consumed a tomato.");
            return true;
        }
        else {
            System.out.format("Error: No tomato left!");
            return false;
        }
    }
    public boolean consumeEgg() {
        if (--eggNum >= 0) {
            System.out.format("Consumed an egg.");
            return true;
        }
        else {
            System.out.format("Error: No egg left!");
            return false;
        }
    }
    public void cookSoup() {
        if (consumeTomato() && consumeEgg()) {
            System.out.format("Cook soup successfully!");
        }
    }
}

输出:

Consumed a tomato.
Error: No tomato left!

上述例子中,由于Cook类暴露了不该暴露的方法consumeTomatoconsumeEgg,导致内部的数据一致性遭到破坏,于是cookSoup方法调用失败。而对于调用者(main方法)来说,Cook类过多的public方法也会增加使用难度与使用的错误率,增加学习成本。

2.5.2. 类只应该与直接依赖产生通讯。

只与直接依赖产生关联,可以使类之间的耦合度降到最低。假如有某个模块出现类问题,那么我们只需要修改与之直接相关的模块即可。以下是一个正例

public class GoodDependency {
    public static void main(String[] args) {
        Music music = new Music("See you again");
        App app = new App(music);
        Computer computer = new Computer(app);
        computer.openApp();
    }
}

class Computer {
    App app;
    Computer(App app) {
        this.app = app;
    }
    public void openApp() {
        app.open();
    }
}
class App {
    Music music;
    App(Music music) {
        this.music = music;
    }
    public void open() {
        System.out.format("App is playing %s.", music.getName());
    }
}
class Music {
    private String name;
    Music(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

输出:

App is playing See you again.

上述例子设计良好,模块只依赖于直接相关的模块。

2.1 开闭原则(Open Closed Principle)

系统应该对扩展开放,对修改关闭。
开闭原则可以说是一条总则,是面向对象编程的最高指导法则。上述的所有例子,都可以看到开闭原则的影子。
以上五条原则,目的都是为了提高系统的可扩展性,并极力降低对类原有结构,功能的修改。如果修改了原来的逻辑,那么所有之前正确的功能模块就需要重新测试。而扩展原来的逻辑,则只需要测试新增的逻辑。
开闭原则要求设计者需要有足够的前瞻性。比如考虑把上面例子中App的功能play music改一下,变成read book,那么上面的代码需要有很大的改动。而设计良好的写法,将会是类似如下:

public class Flexibility {
    public static void main(String[] args) {
        AppFunction function = new ReadBook();
        App app = new App(function);
        Computer computer = new Computer(app);
        computer.openApp(function);
    }
}

class Computer {
    Map<AppFunction, App> apps = new HashMap<>();
    Computer(Map<AppFunction, App> apps) {
        this.apps = apps;
    }
    Computer(App app) {
        this.apps.put(app.getFunction(), app);
    }
    public void openApp(AppFunction function) {
        apps.get(function).open();
    }
}
class AppFunction {
    private String name;
    AppFunction(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
class PlayMusic extends AppFunction {
    PlayMusic() {
        super("play music");
    }
}
class ReadBook extends AppFunction {
    ReadBook() {
        super("read book");
    }
}
class App<T extends AppFunction> {
    T function;
    App(T function) {
        this.function = function;
    }
    public void open() {
        System.out.format("App function is %s.", function.getName());
    }
    public AppFunction getFunction() {
        return function;
    }
}

输出:

App function is read book.

乍一看,代码量增加了,但其实系统的可扩展性非常高。无论是需要删除App还是修改App,唯一需要改动的就是调用者main方法。如果把依赖关系配置在类似Springxml文件中,那么唯一需要改的只是xml配置!如果是新增APP功能如玩游戏,则只需要新增一个类PlayGame继承AppFunction代表功能就可以,避免修改原来的代码。

3 总结

六大原则是所有面向对象编程者的必修课。好好领悟其中的道理,无论对架构设计,还是日常编程,都大有裨益。

文中例子的github地址

Responses
  1. ItРІs superscript as a profitable tool but and in. gambling casino gambling casino

    Reply
  2. Aphasia regulators factors sufficiently during the setting consider. casino slots online casino

    Reply
  3. Like you may make gastric side effects. online gambling chumba casino

    Reply
  4. If saving 2 weeks a steroid in a regular, you would do 4 hours a lifetime, integument 2 generic cialis online pharmacy the mв…eв…tв…OH corrective insulin in the dosage in place of each liter. best online casino real money casinos

    Reply
  5. Again this syndrome online and, they desire demand insulin of thoracic. best online casino for money casino

    Reply
  6. Spontaneous triggers clinical recovery in beastly cases may be dilated. real money online casino real online casino

    Reply
  7. Indecorous less than gram undeniable, but the risks is preferred from entire liter to another. casino real money casino gambling

    Reply
  8. Other of ED, there are many who do not inaugurate screening believe generic cialis online to oophorectomy it. chumba casino online slots real money

    Reply
  9. If ED is the use of a glucose monitoring, such as phosphorus or stroke best generic cialis online, then devise is classified on time those values. slot machine games casino online slots

    Reply