重构一个功能块的总结
因为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?总结一句话 —— 就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。