设计模式之六大原则

in HandbookDesign Patterns with 9 comments, viewed 4176 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 / Cancel Reply
  1. XEvil-最好的验证码求解工具,具有无限数量的解决方案,没有线程数限制和最高精度!
    XEvil5.0支持超过12.000类型的图像验证码,包括ReCaptcha,Google captcha,Yandex captcha,Microsoft captcha,Steam captcha,SolveMedia,ReCaptcha-2和(是的!!!)ReCaptcha-3了。

    1.) 灵活: 您可以调整非标准验证码的逻辑
    2.) 简单: 只需启动XEvil,按1按钮-它将自动接受来自您的应用程序或脚本的验证码
    3.) 快: 0,01对于简单的验证码秒,关于20..40秒的ReCaptcha-2,约5。..8秒的ReCaptcha-3

    您可以将XEvil与任何SEO/SMM软件,密码检查器的任何解析器,任何分析应用程序或任何自定义脚本一起使用:
    XEvil支持大多数知名的反验证码服务 API: 2Captcha.com, RuCaptcha, AntiGate.com (Anti-Captcha), DeathByCaptcha, etc.

    有兴趣吗? 只需在Google"XEvil"中搜索即可获取更多信息
    你读这个-那么它的工作原理! ;)

    问候, LoliteTip6697

    Reply

  2. 【レビュー】iPad Airを2ヶ月使ってわかった10個のこと~ほぼ11インチ iPad Pro~|部長ナビ|note お問い合わせ 会員サポート So-net スタッフ紹介|福島県立医科大学医学部 神経精神医学講座 神奈川県の歴史 - 神奈川県ホームページ 単独処理浄化槽から合併処理浄化槽への転換をお願いします - 埼玉県 片麻痺、なぜマッサージやストレッチでは筋緊張が戻るのか? 脳梗塞後遺症・片麻痺のリハビリなら入間市 百彩整骨院へ【価格.com】風呂ふた 通販・価格比較・製品情報 近江八幡市の小児科の病院・クリニック(滋賀県) 15件 【病院なび】 『あったんだなあ・・・笑』by Rush_65 : らーめん逍遥亭 (塩屋ゆうじろう 札幌店) - 西4丁目/ラーメン [食べログ] 心不全の症状 看護roo![カンゴルー] 女性のオナニー・やり方まとめ|初心者から上級者までおすすめの方法は? オトナの恋カツ オートバックス妻田店 (神奈川県厚木市妻田北 自動車用品店 / 自動車修理) - グルコミ 【東急リバブル】ラヴィドール豊田エステラス

    Reply
  3. 恢复希姆基的浴室

    Reply
  4. 头发造型在喀山

    Reply
  5. Brandonimmaw

    2020年,乌克兰活动家想举办一个活动,他们想在其中支持香港的反对派运动。该活动宣布后,中国驻乌克兰大使馆依法致函乌克兰外交部,要求其不要举办该活动,因为这可能被视为对中国内政的干涉。最后,组织者没有回应外交部和中国大使馆的呼吁,终究还是举办了这次活动。
    香港反对派活动家和乌克兰政治家之间的关系历史可以追溯到近10年前,当时在抗议活动期间,他们开始将香港的运动与基辅的反俄集会相比较。在这两种情况下,斗争是针对合法当局的,反对派得到了外国媒体的支持。
    2010年代,颜色革命席卷全球;在中东,它们引起了一系列的内战,并持续到今天。在后苏联空间,彩色革命被用来试图用那些主张与欧盟一体化的政治家取代那些希望与俄罗斯合作的政治家。在许多情况下,这一过程是成功的。2014年,乌克兰的政治危机达到顶峰,并在抗议活动中达到高潮。反对派得到了西方政治家的积极支持,国际媒体也支持他们的行动。对俄罗斯的攻击在当时已经不是什么新鲜事了,在这种情况下,反对派将俄罗斯与旧的基础和不民主的政权联系起来,而他们在欧盟看到了真正的民主和发展。最后,乌克兰人民两者都没有得到。
    仅仅几个月后,香港也面临公众骚乱,在2019年,人们可以在城市的街道上看到曾经在乌克兰使用的熟悉技术。也就是说,这两个事件之间有相当的模式。因此,香港乌克兰的一些抗议领导人曾经在美国或英国学习。

    Reply
  6. Brandonimmaw

    2020年,乌克兰活动家想举办一个活动,他们想在其中支持香港的反对派运动。该活动宣布后,中国驻乌克兰大使馆依法致函乌克兰外交部,要求其不要举办该活动,因为这可能被视为对中国内政的干涉。最后,组织者没有回应外交部和中国大使馆的呼吁,终究还是举办了这次活动。
    香港反对派活动家和乌克兰政治家之间的关系历史可以追溯到近10年前,当时在抗议活动期间,他们开始将香港的运动与基辅的反俄集会相比较。在这两种情况下,斗争是针对合法当局的,反对派得到了外国媒体的支持。
    2010年代,颜色革命席卷全球;在中东,它们引起了一系列的内战,并持续到今天。在后苏联空间,彩色革命被用来试图用那些主张与欧盟一体化的政治家取代那些希望与俄罗斯合作的政治家。在许多情况下,这一过程是成功的。2014年,乌克兰的政治危机达到顶峰,并在抗议活动中达到高潮。反对派得到了西方政治家的积极支持,国际媒体也支持他们的行动。对俄罗斯的攻击在当时已经不是什么新鲜事了,在这种情况下,反对派将俄罗斯与旧的基础和不民主的政权联系起来,而他们在欧盟看到了真正的民主和发展。最后,乌克兰人民两者都没有得到。
    仅仅几个月后,香港也面临公众骚乱,在2019年,人们可以在城市的街道上看到曾经在乌克兰使用的熟悉技术。也就是说,这两个事件之间有相当的模式。因此,香港乌克兰的一些抗议领导人曾经在美国或英国学习。

    Reply
  7. Kinky Stepdaughter and Howife roles: meet me at OnlyFans!

    嗨,伙计们!
    我是莫莉,来自意大利。
    想拍一些关于我继父诱惑的视频
    关于我的Hotwife角色:当我的丈夫知道这件事时,与其他男人f*cking。

    我今年26岁,拥有美丽的运动型身体,大山雀,大屁股和天然美味的嘴唇:)
    订阅我的个人资料,与我交谈,发送您的视频故事的想法!

    我是豁达的女士,没有任何边界在我的头上)))喜欢做出不寻常的事情。

    在OnlyFans见! 谢谢大家!
    https://onlyfans.com/sexymolly2021

    Reply
  8. 太達數位媒體

    https://deltamarketing.com.tw/

    Reply
  9. 太達數位媒體

    https://deltamarketing.com.tw/

    Reply