2022-01-03 设计原则-依赖倒置与KISS、YAGNI原则总结

依赖倒置(DIP)

  1. 控制反转实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

    public class UserServiceTest {
        public static boolean doTest(){
            return false;
        }
    
        //代码流程都由程序员来控制
        public static void main(String[] args) {//注意:这部分逻辑可以放到框架中
            if(doTest()){
                System.err.println("Test successed!");
            }else {
                System.err.println("Test failed!");
            }
        }
    }
    
    /**
     * JunitApplication类
     *
     * 框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,
     * 只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
     *
     * 这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个
     * 程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。
     *
     */
    public class JunitApplication {
    
        private static final List testCases = new ArrayList<>();
    
        public static void register(TestCase testCase){
            testCases.add(testCase);
        }
    
        //程序启动入口
        public static final void main(String[] args) {
            for (TestCase testCase : testCases) {
                testCase.run();
            }
        }
    }
    
    public abstract class TestCase {
    
        public void run() {
            if (doTest()) {
                System.out.println("Test succeed.");
            }else{
                System.out.println("Test failed.");
            }
        }
        //框架预留的扩展点
        public abstract boolean doTest();
    }
    
    public class IocUserServiceTest extends TestCase{
    
        @Override
        public boolean doTest() {
            return false;
        }
    
        public static void main(String[] args) {
            // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
            JunitApplication.register(new IocUserServiceTest());
        }
    }
    
  2. 依赖注入依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用

    public class Notification {
    
        private MessageSender messageSender;
    
        // 非依赖注入实现方式
        public Notification() {
            this.messageSender = new MessageSender();
        }
    
        public void sendMessage(String cellphone, String message){
            this.messageSender.send(cellphone,message);
        }
    }
    
    public class Notification {
    
        private MessageSender messageSender;
    
        //依赖注入实现方式,注入对象可以是抽象类也可以是接口
        //通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类,此处使用
        // 接口+组合方式替代继承可以显著提高代码的扩展性
        public Notification(MessageSender messageSender) {
            this.messageSender = messageSender;
        }
    
        public void sendMessage(String cellphone, String message) {
            this.messageSender.send(cellphone, message);
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            //Notification notification = new Notification();
            //notification.sendMessage("cellphone","非依赖注入实现方式");
    
            MessageSender messageSender = new MessageSender();
            Notification notification = new Notification(messageSender);
    
            notification.sendMessage("cellphone","依赖注入实现方式");
        }
    }
    
  3. 依赖注入框架我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情

  4. 依赖反转原则也叫作依赖倒置原则,英文翻译是 Dependency Inversion Principle,缩写为 DIP。它的英文描述:High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.翻译成中文,大概意思就是:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

    所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象

    我们拿 Tomcat 这个 Servlet 容器作为例子来解释一下。Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

KISS原则

KISS 原则是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。

对于如何写出满足 KISS 原则的代码,我还总结了下面几条指导原则:

不要使用同事可能不懂的技术来实现代码;

不要重复造轮子,

要善于使用已经有的工具类库;不要过度优化。

/**
 * IpValidater类
 *
 */
public class IpValidater {

    // 第一种实现方式: 使用正则表达式;不满足KISS原则
    //1. 正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;
    // 2.并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。
    // 这种实现方式会导致代码的可读性和可维护性变差
    public boolean isValidIpAddressV1(String ipAddress) {
        if (StringUtils.isBlank(ipAddress)) return false;
        String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
            + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
            + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
            + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
        return ipAddress.matches(regex);
    }

    // 第二种实现方式: 使用现成的工具类;满足KISS原则
    public boolean isValidIpAddressV2(String ipAddress) {
        if (StringUtils.isBlank(ipAddress)) return false;
        String[] ipUnits = StringUtils.split(ipAddress, '.');
        if (ipUnits.length != 4) {
            return false;
        }
        for (int i = 0; i < 4; ++i) {
            int ipUnitIntValue;
            try {
                ipUnitIntValue = Integer.parseInt(ipUnits[i]);
            } catch (NumberFormatException e) {
                return false;
            }
            if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
                return false;
            }
            if (i == 0 && ipUnitIntValue == 0) {
                return false;
            }
        }
        return true;
    }

    // 第三种实现方式: 不使用任何工具类;不满足KISS原则
    // 第三种要比第二种更加有难度,更容易写出 bug。从可读性上来说,第二种实现方式的代码逻辑更清晰、更好理解。
    public boolean isValidIpAddressV3(String ipAddress) {
        char[] ipChars = ipAddress.toCharArray();
        int length = ipChars.length;
        int ipUnitIntValue = -1;
        boolean isFirstUnit = true;
        int unitsCount = 0;
        for (int i = 0; i < length; ++i) {
            char c = ipChars[i];
            if (c == '.') {
                if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
                if (isFirstUnit && ipUnitIntValue == 0) return false;
                if (isFirstUnit) isFirstUnit = false;
                ipUnitIntValue = -1;
                unitsCount++;
                continue;
            }
            if (c < '0' || c > '9') {
                return false;
            }
            if (ipUnitIntValue == -1) ipUnitIntValue = 0;
            ipUnitIntValue = ipUnitIntValue * 10 + (c - '0');
        }
        if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
        if (unitsCount != 3) return false;
        return true;
    }
}

/**
 * ContentValidater类
 * 
 * KMP 算法以快速高效著称。当我们需要处理长文本字符串匹配问题(几百 MB 大小文本内容的匹配),
 * 或者字符串匹配是某个产品的核心功能(比如 Vim、Word 等文本编辑器),又或者字符串匹配算法是
 * 系统性能瓶颈的时候,我们就应该选择尽可能高效的 KMP 算法。而 KMP 算法本身具有逻辑复杂、实现
 * 难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。
 *
 * 不过,平时的项目开发中涉及的字符串匹配问题,大部分都是针对比较小的文本。在这种情况下,直接调
 * 用编程语言提供的现成的字符串匹配函数就足够了。如果非得用 KMP 算法、BM 算法来实现字符串匹配,
 * 那就真的违背 KISS 原则了。
 *
 * 也就是说,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。
 */
public class ContentValidater {


    // KMP algorithm: a, b分别是主串和模式串;n, m分别是主串和模式串的长度。
    public static int kmp(char[] a, int n, char[] b, int m) {
        int[] next = getNexts(b, m);
        int j = 0;
        for (int i = 0; i < n; ++i) {
            while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
                j = next[j - 1] + 1;
            }
            if (a[i] == b[j]) {
                ++j;
            }
            if (j == m) { // 找到匹配模式串的了
                return i - m + 1;
            }
        }
        return -1;
    }

    // b表示模式串,m表示模式串的长度
    private static int[] getNexts(char[] b, int m) {
        int[] next = new int[m];
        next[0] = -1;
        int k = -1;
        for (int i = 1; i < m; ++i) {
            while (k != -1 && b[k + 1] != b[i]) {
                k = next[k];
            }
            if (b[k + 1] == b[i]) {
                ++k;
            }
            next[i] = k;
        }
        return next;
    }
}

YAGNI原则

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。

当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现;我们不要在项目中提前引入不需要依赖的开发包。

总结:YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)

你可能感兴趣的:(2022-01-03 设计原则-依赖倒置与KISS、YAGNI原则总结)