• 重构一个功能块的总结


    因为Leader不建议占用上班时间搞这个,基本上都是加班搞的

    在做这个事的过程中,对IntelliJ idea更加熟悉,回顾下Mocikitto中spy,mock,verify,when的使用,这次重构基本上是小步快走,层层分离

    IntelliiJ idea:
    重构手法:如果想把一个方法移动到另一个class中,可以通过method增加一个Class类型的参数,然后将光标放在方法名的任何字符上【若选中方法名,则F6会失效】,按F6,在弹出的对话框中选择预期的Class【因为这个方法的参数是其它类型的对象】

    更改方法的参数个数时,可以会调用处更改,从上往下进行,或按Ctrl+F6在调用的GUI界面操作。IntelliJ Idea的自动操作往往留一些尾巴,譬如,删除一个构造函数的一个参数,参数赋值的字段和构造中赋值语句不会自动删除。

    如果方法参数有多个,且参数类型差别不大,IntelliJ idea alt+Enter提供的修改可能是错误的,譬如你删除了第5个参数,Idea可能会把第4个参数删除了,然后更改实际函数中的参数名,造成参数错位,就会编译报错,这种情况建议使用Ctrl+F6的GUi界面来操作。

    把一个方法通过F6移动到另一个Class时,Move操作成功后,移过去的方法如果使用了原来Class中的字段,就会编译不过。这种情况不用担心,一般的步骤是先移动,然后解决编译报错。

    移动方法时,方法增加的Class一般不选择抽象方法,这样会把方法移动到抽象方法中,正确的做法是,增加的类型参数是预期的抽象类的子类,这样就可以了

    在建工厂类或接口时,可以先写Class Name,然后使用Idea的快捷键来创建Class,Interface,Method,Field

    去除if else 的常用方法是每个分支就是一个对象,然后实现相同的接口;把if else分支的判断逻辑放到Factory类,这样代码就清净了。
    扩展性和可维护性都增加了,扩展性:如果增加或减少一种场景,只修改工厂类或增加或删除新类即可,与此不相关的场景不会影响 ,不用知道其它情景的实现,也不用重新测试这些没有涉及的场景。
    并且 业务逻辑和每个场景都可以独立写测试用例。如果有重复代码,不仅看得见,更能去得掉。重复代码抽取到父类中即可。也可以定义一个接口,抽象类来实现,业务类再继承抽象类。重复的字段或方法放在抽象类中。

    Idea中的快捷键:
    Ctrl+Alt+F12:打开某个package;
    Ctrl+F12:打开File Structure
    Ctrl+Alt+B:接口的实现类,抽象类的子类
    Alt+F7:查找使用字段,方法
    Ctrl+E:最近使用的文件,在F6移动操作时很有用
    Ctrl+Alt+LeftArrow键:上一个操作代码位置。很遗憾,远程桌面中无法使用,只能使用鼠标点击工具栏上的按钮
    Ctrl+Shift:移动方法或字段或语句
    在equals方法的左或右边的对象上按Alt+Enter,会出现Flip,来互换
    Ctrl+B转到定义处,再按Ctrl+B,返回
    Alt+insert:构造函数,get,set,overrider
    Ctrl+Alt+T:surround with try catch
    在方法或类上按Ctrl+shift+T,用来创建测试用例
    Ctrl+P:查看方法参数
    Ctrl+Q:查看API
    Shift+F6:重命名
    Ctrl+Alt+n:内联,即独立的方法整合到调用中,被内联的方法就去掉了
    在进行重构操作的Class代码中按Ctrl+Z,即可undo刚才的操作
    compare with clipboard:先复制一段文本,然后在idea中选中一段文字,在选中文字上右键,选“Compare with clipboard”即可比较,identical:相同的,一致的
    Ctrl+Shift+F9:compile,要做中Module或Project,光标在一个Class文件中,就会只编译这个文件,如果引用其它非APIclass,则编译不过
    Idea中有在跑测试用例,编译Module或Project时,才会发现哪些Class中没有编译过。提交代码前一定要先编译下或先跑下测试用例

    git:
    git fetch//获取origin上的更新
    git merge//和本地代码合并,如果有clifict,还要resolve才能提交
    git add <some files>//暂存数据(标识要commit的本地所有文件),.代表所有本地文件(除了ignore文件中标识的)。也可以add指定文件名的单个文件
    git status -s//查看本地仓库的修改状态,如果没有输出,说明没有需要提交的。
    git commit -m "注释"//提交到本地仓库
    git push origin master//提交到git服务器origin的master分支
    git branch//查看
    git log --oneline -5

    测试用例中mock使用的回顾:
    spy,如果不想执行spy的方法,就使用doReturn,不然,被spy的方法还会被执行
    直接贴个例子吧,

    import com.download.util.Subnet;
    import com.file.utils.StmImManageImp;
    import com.file.utils.FileInfoUtil;
    import com.api.util.DebugPrn;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.runners.MockitoJUnitRunner;
    
    import java.io.File;
    import java.io.IOException;
    
    import static org.hamcrest.core.Is.is;
    import static org.junit.Assert.assertThat;
    import static org.mockito.Mockito.*;
    
    @RunWith(MockitoJUnitRunner.class)//这个必须有,不然@Mock就失效了
    public class StmFileExporterTest extends AbstractFileExportTest {//UT也需要去除重复
    
        private static final DebugPrn LOGGER = new DebugPrn(StmFileExporterTest.class.getName());
        private String targetPath;
        @Mock//使用Mock注解,代码更清晰
        private FileInfoUtil fileStyle;
        @Mock
        private StmImManageImp ImManageImp;
    
        @Before//准备工作
        public void init() throws IOException {
            targetPath = StmFileExporterTest.class.getResource("/com/file/fileexporter/").getPath();
            if (!new File(targetPath).mkdirs()) {
                LOGGER.info(String.format("path(%s) mkdirs failed!", targetPath));
            }
        }
    
        @Test
        public void should_doDataClearWork_deleteTempPath_when_getByDataType_is_true_and_saveFile_not_exists() {//方法名体现场景
            try {
                when(fileStyle.getTaskName()).thenReturn("task1");//mock掉方法的返回值
                String localTempPah = targetPath + fileStyle.getTaskName() + File.separator + "" + File.separator;
                Subnet spySubnet = spy(getGv3Subnet());//spy一个对象,因为这个对象中的
                File localTempFle = new File(localTempPah);
                if (!localTempFle.mkdirs()) {
                    LOGGER.info(String.format("path(%s) mkdirs failed!", localTempFle));
                }
                StmFileExporter StmFileExporter = new StmFileExporter(targetPath, fileStyle);
                StmFileExporter spy = spy(StmFileExporter);
                spy.setImManageImp(ImManageImp);
                 //Mock方法中要匹配某个参数,需使用eq();doReturn,不执行mock的相关方法
    doReturn(true).when(spy).getDataByDataType(eq(spySubnet), anyString(), anyString());
                spy.sync(spySubnet);
                verify(ImManageImp).doData();
                verify(ImManageImp).clearWork();//等价于verify(ImManageImp,times(1)).clearWork()
                
               //Mockito的assertThat方法,可以把不匹配的expect和actual数据都打印出来,预期的数据需要使用is(),is()的功能也很强大
    assertThat(localTempFle.exists(), is(false));
            } catch (Exception e) {
                throw new IllegalArgumentException("UT Fail", e);
            }
        }
    
        @Test
        public void should_doDataClearWork_deleteTempPath_when_Type_not_equals_gv3() {
            try {
                Subnet spySubnet = spy(getNotGv3Subnet());
                StmFileExporter StmFileExporter = new StmFileExporter(targetPath, fileStyle);
                StmFileExporter spy = spy(StmFileExporter);
    
                spy.sync(spySubnet);
    
                verify(spy, times(0)).doStmSync(spySubnet);
            } catch (Exception e) {
                throw new IllegalArgumentException("UT Fail", e);
            }
        }
    
    }
    

    累并充实着吧。
    就想起这么多了  

    再总结一下,上面去除if else的重构过程,是LSP的一个体现,即父类出来的地方,都可以用子类替代
    测试用例的书写原则,对象完成一个行为后,肯定要改变什么,通过IO操作改变了外设上的数据、改变了对象的状态,测试用例只需要覆盖变化即可。
    被测对象中的行为在执行时,使用了其它对象A,对象A中有些API的使用,导致无法正常写测试用例,这种场景,需要将对象A注入到被测对象,即给被测对象增加一个协作者,因为被测对象的行为不能单独完成
    多用组合,少用继承。组合时,注入需要协作的对象,即可方便写测试用例。
    新增对象时,一般会采用set方法而不在constructor中新增一个入参,这样改动最小,对已有对象影响最小。


    LSP:
    1.概述: 派生类(子类)对象能够替换其基类(父类)对象被调用
    2.概念:
    里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。(源自百度百科)
     
    3.子类为什么可以替换父类的位置?:
    当满足继承的时候,父类肯定存在非私有成员,子类肯定是得到了父类的这些非私有成员(假设,父类的的成员全部是私有的,那么子类没办法从父类继承任何成员,也就不存在继承的概念了)。既然子类继承了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员。所以,子类对象可以替换父类对象的位置。
     
    4.里氏代换原则优点:
    需求变化时,只须继承,而别的东西不会改变。由于里氏代换原则才使得开放封闭成为可能。这样使得子类在父类无需修改的话就可以扩展


    LSP的原定义比较复杂,我们一般对里氏替换原则 LSP的解释为:子类对象能够替换父类对象,而程序逻辑不变。

    里氏替换原则有至少以下两种含义:1. 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义

    子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

    2. 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例时逻辑不一致的可能。

    不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。

    如何符合LSP?总结一句话 —— 就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承




  • 相关阅读:
    让Windows Server 2008 + IIS 7+ ASP.NET 支持10万个同时请求
    C#多线程锁定全局变量
    ASP.NET 防止F5刷新页面按钮重复提交
    子线程调用窗体控件
    asp.net 转成XML后在SQL操作XML的值
    Sql中字符串的循环截取(用循环实现输入键串能输出值串)
    大数据量分页存储过程
    函数相关概念,及练习
    js循环(while循环,do while循环,for循环)相关知识点及练习
    parseInt()和Number()的应用区别
  • 原文地址:https://www.cnblogs.com/softidea/p/4893445.html
Copyright © 2020-2023  润新知