设计原则与思想:设计原则


设计原则与思想:设计原则

理论一:单一职责原则

  • 1.如何理解单一职责原则?

    • What:一个类只负责完成一个职责或者功能
    • How:不要设计大而全的类->要设计粒度小、功能单一的类
    • Why:实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性
  • 2.如何判断类的职责是否足够单一?
    不同场景的判断标准不同,下面这些情况就有可能说明这类的设计不满足单一职责原则(不是绝对的):

    • 类中代码行数过多、函数或者属性过多
    • 类依赖或被依赖的其他类的过多
    • 私有(private)方法过多->考虑抽取私有方法到新的类中并设置为public方法
    • 类难命名(如只能命名为一些笼统的词语如Manager、Context等)
    • 类大量方法都是集中操作类中的某几个属性(如用户信息UserInfo表中大部分在操作地址address信息则可以考虑抽出来)
      public class UserInfo {
          //用户基础信息
          private long uid;//用户唯一id
          private String username;//用户昵称
          private int age;//用户年龄
          private Date createTime;//用户注册时间
          private Date lastLoginTime;//用户最后登陆时间
          private String avatarUrl;//用户头像
          
          //地址相关信息
          private String provinceOfAddress;//省
          private String cityOfAdddress;//市
          private String regionOfAddress;//区
          private String detailedAddress;//详细地址
      }
  • 3.类的职责是否设计得越单一越好?
    如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性

理论二:开闭原则(对扩展开放、修改关闭)

  • 1.如何理解”对扩展开放、修改关闭”?
    添加一个新的功能,
    应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等), //对扩展开发
    而非修改已有代码(修改模块、类、方法、属性) //对修改关闭

  • 2.如何做到”对扩展开发、修改关闭”?

    • 时刻具备扩展意识、抽象意识、封装意识
    • 写代码时候思考未来可能有哪些需求变更
    • 思考如何设计代码结构,事先留好扩展点
    • 保证在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,插入新的代码
  • 3.常见的提高代码扩展性的方法

    • 多态
    • 依赖注入(DI)
    • 基础接口而非实现编程
    • 大部分的设计模式(如装饰、策略、模板、责任连、状态)

理论三:里式替换原则(LSP)

  • What里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。(design by contract,按照协议来设计)
  • How:父类定义了函数的”约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的”约定”。这里的约定包括:
    • 函数声明要实现的功能 //比如父类中提供了sortOrdersByAmount()订单排序函数是按照金额从小到大排序的,而如果子类重写该函数是按照日期(而非原本的金额)来排序,那么就是违背里式替换原则
    • 对输入、输出、异常的约定 //比如父类约定运行出错返回null,而子类运行出错抛异常,那么就是违背里式替换原则
    • 注释罗列的任何特殊说明 //比如父类withdraw()提醒函数写了注释”用户提现金额不得超过账户余额”,而子类重写该函数后针对VIP账号实现了透支提现的功能(也就是提醒金额可以大于账户金额),那么就是违背里式替换原则
  • Why里式替换原则是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

理论四:接口隔离原则(ISP)

  • What接口隔离原则即”Interface Segregation Principle”(缩写为ISP)。直白来说,就是客户端不应该强迫依赖它不需要的接口。
  • Why
    • 防止误操作(如误删用户)
    • 更细粒度,会更加灵活、易扩展、易复用
    • 调用方无需引用+理解没必要的接口
  • Example
    • 不使用ISP时
      public interface UserService {
        //注册
        boolean register(User user);
        //登陆
        boolean login(User user);
        //查询用户
        User getUserByUid(long uid);
        //删除(敏感操作,理论上只能在后台操作)
        boolean deleteUserByUid(long uid);
      }
      public class UserServiceImpl implements UserService {
        //...
      }
    • 使用ISP时
      public interface UserService {
        //注册
        boolean register(User user);
        //登陆
        boolean login(User user);
        //查询用户
        User getUserByUid(long uid);
      }
      //为了防止误操作,我们应该把用户操作的API和后台操作的API隔离开(为了防止调用UserService的误使用deleteUserByUid引起安全隐患)
      public interface AdminUserService {
        //删除(敏感操作,理论上只能在后台操作)
        boolean deleteUserByUid(long uid);
      }
      public class UserServiceImpl implements UserService,AdminUserService {
          //...
      }

理论五:依赖反转原则(DIP)

1.控制反转(IOC)

  • 控制:指的是对程序执行流程的控制

  • 反转

    • 没使用框架前:程序员自己控制整个程序的执行

      public class UserServiceTest {
        public static boolean doTest() {
          //...
        }
      
        public static void main(String[] args) {
          if(doTest()){
            System.out.println("success");
          } else {
            System.out.println("fail");
          }
        }
      }
    • 使用框架后:整个程序的执行流程通过框架来控制(流程的控制权从程序员反转给了框架)

      public abstract class TestCase {
        public void run() {
          if (doTest()) {
            System.out.println("success");
          } else {
            System.out.println("fail");
          }
        }
      
        public abstract boolean doTest();
      }
      
      //我们只需要在框架预留的扩展点(也就是TestCase类中的doTest()抽象函数中填充具体测试代码就能模拟单元测试了)
      public class UserServiceTest extends TestCase {
        @Override
        public boolean doTest() {
          //...
        }
      }
      
      //抽象出来的框架
      public class JUnitApplication {
        private static final List<TestCase> testCases = new ArrayList<>();
      
        public static void register(TestCase testCase) {
          testCases.add(testCase);
        }
      
        public static void main(String[] args) {
          //添加需要测试的类
          JUnitApplication.register(new UserServiceTest());
      
          //控制反转式"测试"
          for (TestCase testCase : testCases) {
            testCase.run();
          }
        }
      }

2.依赖注入(DI)

  • What:不通过new的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好,通过构造函数等方式传递(或注入)给类使用

  • How:通过一个邮箱系统来讲解

    • 不使用DI

      //邮箱
      public class MailBox {
        //发送邮件的工具
        MailSender mailSender;
        public MailBox() {
          this.mailSender = new MailSender();
        }
      }
      public class TestApplication {
        //构造MailBox的时候内部会自己new一个MailSender
        MailBox mailBox = new MailBox();
      }
    • 使用DI

      //邮箱
      public class MailBox {
        //发送邮件的工具
        MailSender mailSender;
        public MailBox(MailSender mailSender) {
          this.mailSender = mailSender;
        }
      }
      
      public class TestApplication {
        //先初始化内部依赖的对象
        MailSender mailSender = new MailSender();
        //再通过构造方法传递
        MailBox mailBox = new MailBox(mailSender);
      }

3.依赖注入框架(DI Framework)

  • 简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现如下内容(原本需要程序员做的事情)
    • 由框架来自动创建对象
    • 由框架来管理对象的生命周期
    • 由框架来管理对象的依赖注入

4.依赖反转原则(DIP)

  • What
    • 依赖反转原则也叫做依赖倒置原则
    • 用来指导框架层面的设计
  • How
    • 高层模块不依赖低层模块,它们共同依赖同一个抽象
    • 抽象不要依赖具体实现细节,具体实现细节依赖对象
  • Example
    • Tomcat是运行Java Web应用程序的容器
    • 应用程序可以部署在Tomcat容器上,便可被Tomcat容器调用执行
    • Tomcat是高层模块,应用程序是低层模块
    • Tomcat和应用程序代码之间没有直接的依赖关系,两者都依赖同一个”抽象”(也就是Servlet规范)
    • Servlet规范不依赖具体的Tomcat容器和应用程序的实现细节,而Tomcat容器和应用程序依赖Servlet规范

先跳过

理论六:KISS、YAGNI原则

理论七:DRY原则

理论八:LOD原则


文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录