版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
本文提出了一种界面设计中的架构模式一界面组装器模式,它致力于分解界面,将界面和组装行为解耦,将界面逻辑处理与领域逻辑处理解耦,这样我们在开发GUI胖客户端界面应用时可以从众多的界面控制管理中解脱出来,而专注于我们的后台业务逻辑的开发。通过该模式,我们可以动态地组装我们的界面,我们甚全还可以在我们的界面中轻松地插入transaction事务或session会话管理。本文将通过分析设计一个架构的过程来讲解该模式,从一个简单的设计模型开始,一步步走向一个完整的架构。借此也向大家展示一个架构设计的思维历程。另外,本文给出了EclipseSWT(StandardWidgetToolkit)的示例。问题引出界面设计常常是模式产生的根源,无论是架构模式,还是设计模式,比如MVC模式,Observer,Facade等,也是整个软件行业向前发展的动力。遗憾的是,即使在软件技术发达的今天,界面设计仍是软件设计中的难以突破的瓶颈之一。我们用过Javaswing或EclipseSWT作过项目的都知道,要将界面进行分解是很困难的,它不像我们的业务逻辑,可以方便地按职责分解到不同的类中去实现,因为各个业务逻辑之间耦合度很低。但界面逻辑不一样,你不可能将一个文本框的读取操作委任到另一个类中去,而且各个界面元素之间相互依赖,无法去除耦合,一般的做法只能是在界面元素的事件触发(比如按钮点击事件)时,将输入数据封装成一个数据对象传给后台的逻辑处理类来处理。Eclipse的Wizard框架在界面分解上提供了一种很好的实践,它可以将按钮区和其他界面区分离出来,用类似MVC的方式实现了Wizard框架。但这个实现并非没有瑕疵,一个缺点是wizard是一个plug-in,这样的话就减少了可重用性,不能移植到eclipse以外的环境。另一个缺点就是它引入了很大的复杂性,而且在一些对界面元素的控制上丧失了一些精细控制的能力,这可能是它过度地强调了自动化和用户扩展的方便性的缘故。比如,用户不能将自己的逻辑插入按钮区的按钮事件控制中,而只能在自定义区的界面元素Listener中设定按钮区状态,如果用户自定义的界面元素很多,就需要很多个Listener来组合判断一个按钮状态(如是否进行“下一步”),这样的话就很影响性能,而且无端地多了一堆复杂的逻辑判断,也就是说本来只需在按钮Listener事件中处理的逻辑现在要分散在各个界面元素的Listener中去处理。这也正是设计上一个值得反复强调的普遍问题:当你要保持架构或设计的完美性时必然会以丧失其他特性为代价。世上永远没有完美的东西,我们只关注适合我们的。我下面要提出的这个架构模式的灵感来自于我的一个真实项目,一个用RSA(RationalSoftwareArchitect)/Eclipse建模的项目,在RSA环境中,读写模型都必须在一个特有的context下才能操作,这就意味着我在界面的启动之前必须封装好输入数据,关闭之后返回输出数据,而不是直接处理数据,必须对输入/输出数据对象进行封装。正如前面提到的,这种情况界面设计中很普遍。所以,在模式命名时我用了组装器-assembler这个词,有一层意思是输入/输出数据对象的组装,另一层意思就是界面部件(界面元素的集合)的组装,这里的组装还有更深层次的涵义就是指界面部件的可装配性,可以在运行时动态组装。而且这个模式可以用任何语言(Java,C++等)来实现。在这里我会从一个简单的设计模型开始,一步步走向一个完整的架构。借此也向大家展示一个架构设计的思维历程。本文中给出了EclipseSWT(StandardWidgetToolkit)的示例。界面分解在EclipseSWT中,有几个重要的界面部件,一个是Shell-界面的最外层容器,类似JavaSwing中的Frame,另一个就是Composite-界面元素的集合的容器,类似JavaSwing中的Panel。我们的界面分解将从Composite开始,(Shell本身是不需要分解的)。我们可以在Shell中装配上一个空的Composite,然后我们的具体界面元素都定义在这个Composite里。这样就把Composite逻辑从Shell中分离出来了,因此我们现在有了2个类(目前我们用概念类来表示):图1.把Composite逻辑从Shell中分离出来Editor:该类处理Shell的逻辑,如显示-show,关闭-close,它负责创建和销毁EditorComposite。EditorComposite:该类处理Composite的界面逻辑,如创建界面元素。有两点值得注意,第一,Editor负责EditorComposite的创建和销毁,也就是生命周期的管理。那么我们可以想到,如果我们的界面需要transaction-事务或session-会话的管理,那么我们完全可以让Editor来负责这项职责,而不是分散在各个EditorComposite中。怎么扩展界面的事务功能可能会很复杂,这已经超出本文的讨论范围,我只是从架构的层面来分析可能有的可扩展性。第二,一个Editor可以包括多个EditorComposite,比如我们的属性页,此时我们在Shell中定义的空的Composite将会是一个TabFolder.还有一种情况,就是我们可以根据某种逻辑来判断我们需要装配哪个EditorComposite。这就要求我们有一个装配的行为。界面部件装配当我们的装配逻辑很简单时,我们可以定义一个assemble()方法来负责装配行为。但是当我们的界面需要组装一系列EditorComposite时,就会牵涉到选择逻辑,选择逻辑不一定很复杂,但我们还是应该把这种行为从Editor中分离出来,这样Editor可以集中精力负责与用户交互方面的职责,而装配行为被分配到一个新的类EditorAssembler中,这样做还有一个好处,就是我们一旦有新的EditorComposite需要添加时,我们只需要改变EditorAssembler的代码,而不用修改Editor的代码,这就把变化隔离出来,对Editor的修改关闭,对装配行为的扩展开放。这正是面向对象设计领域反复强调的基本原则-开放-封闭原则(Open-ClosePrinciple)o经过重构后的架构如下图:图2.重构后的架构EditorAssembler:该类处理EditorComposite的创建,还包括多个EditorComposite的选择逻辑。这里的选择逻辑我们可以用if/else或switch/case来硬编码,如果逻辑不是很复杂而且今后的修改不会太频繁的话,用这种方法就足够了,当然可以考虑将多个EditorComposite的装载信息专门用一个资源/信息类来存储,这在EditorComposite比较多的情况下很有效,这样每次添加EditorComposite就只需要改变这个资源类,这是一个很有用的建模原则(为了简化我们的核心模型,我在这里不将这个资源类表示出来)。如果进一步考虑到我们的组装逻辑会比较复杂,或会比较容易改变,甚至在运行时动态改变,我们就可以将众多的EditorComposite和复杂的逻辑存储在一个元数据文件中,如XML或配置文件。这样,有新的EditorComposite需要支持,或修改装配逻辑时,不用修改EditorAssembler类,只要修改元数据文件即可。这样就可以很动态的配置我们的界面。这里会有一个架构权衡的问题,元数据由它的优点,也有它的缺点,其一,必须编写解析它的类,复杂性增加了,其二,不需要编译是它的优点也是它的缺点,对XML或配置文件我们可以随意修改,只有在运行时发现异常才知道改错了,而且也可能被人蓄意破坏掉。所以我们只在真的需要很频繁地修改EditorComposite的配置或经常需要增加EditorComposite时才采用元数据方案。在这里我倾向于采用资源类方案。IO数据装配模型设计进行到这里,我们似乎缺少了对数据流的建模,在一个标准的界面程序中,我们首先会有一组输出数据,比如按”OK”按钮之后,我们需要将界面元素上的输入信息输出到后台逻辑类来处理或直接调用好几个逻辑类分别处理不同的界面元素输入信息了。我们一般习惯上可能直接将这个数据传递到逻辑类来处理。这样做三个缺点:其一,如果我们的数据读写处理要求必须在特定的context中才能进行,这样的话我们不能在界面中直接调用后台逻辑处理类了。其实这种限制并不罕见,在一些涉及底层(比如协议层)的开发时,经常会碰到只能读不能写的情况。其二,UI的可替代性差,假如我们今后需要一种方案可以在运行时可以替换不同的UI但输出的数据是一样的,也就是说后台逻辑处理完全一致,那么这种情况我们就需要每一个UI自己去调用后台逻辑类,重复编码,而且可能由于程序员的失误每一个UI用了一个逻辑类,从而导致一个完全相同行为的类有了好几个不一致实现版本,这样不仅严重违反了面向对象设计,而且还可能产生难以预料的bug,难以维护。其三,UI的可重用性差,对于上面多个UI对应一种逻辑处理的例子,由于UI依赖了后台逻辑类,如果今后要修改逻辑类结构的话,我们就需要修改每一个UI。如果我们还有一种需求是要支持一个UI在不同的环境下需要不同的后台逻辑类时,我们可能要专门在一个UI中设置一个属性来标识后台将要使用的逻辑类。这会很复杂。解决上面几个缺点只有一种方法,就是将后台逻辑类与UI解耦。如果我们把要处理的输出数据打包成一个输出数据对象从界面统一输出,再由UI的调用者决定调用哪一个后台逻辑类来处理数据,而不是UI自己决定调用行为。还有一个输入数据对象就很好理解了,我们调用UI时,可能某些界面元素需要的从环境中动态装载数据,比如一个下列列表,还有一些我们上一次配置好的数据这次需要更新,也需要将已有数据导入。所以我们需要一个输入数据对象。这就得到下面的模型:图3.输入数据对象OEditorConipositeOEditorConipositeInputDataObject:该类封装了输入数据。由EditorComposite负责解析这些数据。OutputDataObject:该类封装了输出数据。由EditorComposite负责产生这些数据。Editor负责传输这两个数据对象。重构架构从上面的模型我们可以看出Editor类其实相当于一个Facade,所有的界面与用户的交互都由它负责集中调度管理,Editor会将装配行为分配给EditorAssembler类来处理,它还负责临时存储输入输出数据,当然如果我们有类似transaction或session之类的处理会由Editor委派到别的相关类去处理。应用Facade设计模式,我们可以给Editor改个名字叫EditorFacade,这样更能体现设计者的意图,千万不要忽视类的命名,设计是一门严肃的科学,每一个细节我们都不能苟且,对架构的设计更要严谨。命名可以起到沟通的作用,还能起到提醒的功能,EditorFacade提醒我们以后要给它添加新的行为是记住它是一个Facade,不能将不相干的职责分配进来。另外,我发现添加了InputDataObject类后,EditorComposite就有两个职责:装载界面元素初始化数据(一些需要从环境中动态获得的输入数据,从InputDataObject对象中获得)和显示上一次编辑的数据(也从InputDataObject对象中获得),我们定义两个方法来分别处理:loadDataInfo()-装载初始化数据;showPreInfo()-显示上一次编辑的数据。当然,一般来说这两个方法是私有的-private,因为这是EditorComposite自身的内部逻辑,但我们在这个架构中让它成为公有的-public,是因为我们可以在EditorAssembler类中集中控制它的调用,而且每一个EditorComposite都会有装载初始化数据和显示已有数据的行为,那么为什么不抽象出来呢,以便让EditorComposite的开发提供者更清楚自己的职责,虽然这么做有点破坏EditorComposite的封装性和其中方法的私密性,但从架构的角度来讲这种破坏是合适的,值得的。再看看前面的EditorAssembler类,它其实有两个职责,一个是创建EditorComposite,还有一个就是从几个EditorComposite选择出一个的判断逻辑。如果我们把这两个不相干的职责解耦,应用Factory设计模式,就可以将创建EditorComposite的工作委任给一个EditorCompositeFactory的新类。经过以上几项重构后得到以下概念类模型:图4.概念类模型实现架构经过上面的分析建模,我们可以开始实现架构了,从上面的概念模型我们可以很容易地抽象出相应的接口来。首先,我们看看EditorFacade类,基于我们上面的讨论,不同的界面可能有不同的需求,比如有的要支持transaction-事务,那么EditorFacade的实现就会不同,所以我们有必要提取出一个接口来表示,下面列出了这个接口IEditorFacade:清单1:IEditorFacade.javapublicinterfaceIEditorFacade(publicvoidshow();publicIlnputDataObjectgetInputData();publicvoidsetInputData(IInputDataObjectinputData);publicIOutputDataObjectgetOutputData();publicvoidsetOutputData(IOutputDataObjectoutputData);publicbooleanisFinishedOK();publicCompositegetRootComposite();publicvoidsetAssembler(IEditorAssemblerassembler);publicvoidclose(booleanstatus);}那么EditorFacade类的部分代码如下:清单2:EditorFacade.javapublicclassEditorFacadeimplementsIEditorFacade(privateShellshell;//validateifeditorisclosedwithOKorCancelprivatebooleanfinishedOK;//inputdataprivateIInputDataObjectinputData;//outputdataprivateIOutputDataObjectoutputData;privateCompositecomposite;privateIEditorAssemblerassembler;privatevoidcreateSShell()(shell=newShell();shell.setLayout(newGridLayout());createComponent();}privatevoidcreateComponent()(composite=newComposite(shell,SWT.NONE); assembler.create(this);}publicvoidshow()(this.shell.open();assembler.showPreInfo();}publicEditorFacade(IEditorAssemblerassembler,IlnputDataObjectinputData)(this.assembler=assembler;this.inputData=inputData;this.createSShell();}publicCompositegetRootComposite()(returncomposite;}publicvoidclose(booleanstatus)(finishedOK=status;this.shell.close();}}下一步,我们将两个IO数据类定义出来,很显然,不同的界面会有不同的输入输出数据,在这里我们只能定义出两个抽象的接口IInputDataObject和IOutputDataObject,它们继承了序列化java.io.Serializable接口,里面并无其它内容。这里注意一点,空的接口并非无意义,它可以起到标识的作用,另外,它隐藏了具体实现,在传递数据时传递者不用知道具体数据内容,这样传递者类具有更好的重用性,而且具体数据类也不用暴露给不该知道它的类-传递者类,这正是另一个面向对象的基本原则-迪米特法则(LoD):不要和陌生人说话。下面给出IInputDataObject的清单:清单3:IInputDataObject.javapublicinterfaceIInputDataObjectextendsSerializable(}接下来,我们看看EditorAssembler类的实现,根据前面的讨论,它封装了界面的装配逻辑,一定会被修改的,那么我们就需要一个接口IEditorAssembler来规范它的行为,在这里我还给出了一个抽象类AbstractEditorAssembler,实现了装载单个EditorComposite的方法,另外我还给出了一个具体的EditorAssembler类,这是一个每次只装载一个EditorComposite的例子,代码清单如下:清单4:lEditorAssembler.javapublicinterfacelEditorAssembler(/**createeditorbodyandinit@parameditor*/publicvoidcreate(IEditorFacadeeditor);/**createeditorcomposite@parameditor@paramcompositeClassID:compositeclassname,e.g.test.view.TestComposite@return*/publicIEditorCompositecreateComposite(IEditorFacadeeditor,StringcompositeClassID);/**showexistinfoinUIforupdate.*/publicvoidshowPreInfo();}清单5:AbstractEditorAssembler.javapublicabstractclassAbstractEditorAssemblerimplementsIEditorAssembler(publicIEditorCompositecreateComposite(IEditorFacadeeditor,StringcompositeClassID)(IEditorCompositebody;body=EditorCompositeFactory.createComposite(compositeClassID,editor);body.create(editor.getRootComposite());body.setEditor(editor);returnbody;}清单6:StandaloneEditorAssembler.javapublicclassStandaloneEditorAssemblerextendsAbstractEditorAssembler(privateStringcompositeClassID;privateIEditorCompositebodyComposite;/***@paramcompositeClassID:compositeclassqulifiedname,.ibm..XXComposite;*/publicStandaloneEditorAssembler(StringcompositeClassID)(positeClassID=compositeClassID;}publicvoidcreate(IEditorFacadeeditor)(bodyComposite=createComposite(editor,compositeClassID);if(bodyComposite!=null)bodyComposite.loadDataInfo();}publicvoidshowPreInfo()(bodyComposite.showPreInfo();}}接下来,是EditorCompositeFactory的实现,这个类的实现比较简单,只是根据类名产生类:清单7:EditorCompositeFactory.javapublicclassEditorCompositeFactory(/**createIEditorComposite@paramclsName@parameditor@return*/publicstaticIEditorCompositecreateComposite(StringclsName,lEditorFacadeeditor)(lEditorCompositecomposite=null;try(Classcls=Class.forName(clsName);if(cls!=null)composite=(IEditorComposite)cls.newInstance();}catch(Exceptione)(e.printStackTrace();}if(composite!=null)(composite.setEditor(editor);}returncomposite;}}最后,就是EditorComposite的实现了,很显然每个界面的EditorComposite都不一样,所以我们在这里只定义了一个接口来规范一下行为,具体的EditorComposite实现我会在代码附件中的测试包中给出。清单8:IEditorComposite.javapublicinterfaceIEditorComposite(/**setupcompositeUI*/publicvoidcreate(Compositeparent);/**setthecurrenteditorforshellcloseanddataset*/publicvoidsetEditor(IEditorFacadeeditor);/**showpreviousdatainformationinUI*/publicvoidshowPreInfo();publicvoidloadDataInfo();}下面,我们编写一些测试代码来测试它,这个测试应用是要编写一个电话簿,为了简单起见我只定义了一个EditorComposite-PhoneBookComposite,在编写组装逻辑时也只是示例性地改变了一下界面的标题和尺寸。(详细代码见代码下载)清单9:PhoneBookEditorAssembler.javapublicvoidcreate(IEditorFacadeeditor)(if(compositeType==0)(//itisaphonebook.bodyComposite=createComposite(editor,"test.PhoneBookComposite");| 10 20 30 40 50 60 70 80 9|| XMLerror:Thepreviouslineislongerthanthemaxof90characters 1editor.getShell().setText(〃PhoneBook");editor.getShell().setSize(400,300);editor.getShell().redraw();if(bodyComposite!=null)bodyComposite.loadDataInfo();}elseif(compositeType==1)(//itisamemobook.bodyComposite=createComposite(editor,"test.PhoneBookComposite");| 10 20 30 40 50 60 70 80 9|| XMLerror:Thepreviouslineislongerthanthemaxof90characters 1editor.getShell().setText("MemoBook");editor.getShell().setSize(500,300);editor.getShell().redraw();if(bodyComposite!=null)bodyComposite.loadDataInfo();}}清单10:Main.javapublicstaticvoidmain(String[]args)(〃定义PhoneBookEditorAssembler。IEditorAssemblerassembler=newPhoneBookEditorAssembler(0);〃定义PhoneBook输入数据IInputDataObjectinputData=newPhoneBookInputDO("LYL",);〃定义PhoneBookeditorEditorFacadeeditor=newEditorFacade(assembler,inputData);editor.show();if(editor.isFinishedOK())(〃取出PhoneBook输出数据。if(editor.getOutputData()instanceofPhoneBookOutputDO)(PhoneBookOutputDOoutputData=(PhoneBookOutputDO)editor.getOutputData();Stringname=outputData.getName();Stringphone=outputData.getPhone();System.out.println(〃name:〃+name+";phone:"+phone);}}}接下来,我们可以看一下架构的实现模型,注意,我在画下面的UML图时采用了分层的方式,所有的接口都会在上面一层,实现在下面一层,这种分层画UML图的方法有助于我们理清架构的思路,也便于与开发组的其他成员沟通。图5.架构的实现模型却I-17突€ ipriSJtftfl普口明tw{)炉shovxfrebifci却I-17突€ ipriSJtftfl普口明tw{)炉shovxfrebifci(j*creatsCompoErte0®shoM句i血{)加醐Intpi白口9*Offk/bD云加U0E曲"网咻MM心C¥y叱ajpupcoltiMClasslD;Strin。ntodvConipoyb?:lEdborOonipcwsite♦(XRM白()&ihcwPEeInfo()J'LKPS'■JjTaQJEd&Mf盐*&成匾I"邙W«composrtftType:ntnFnulEM垃:DhptltD^I:^Objectrautput&au;[{Xrtpul^ntnQbj^ct=:Lunipuiatu:Guiiiix治im曰济5丽山I斜:,・u*■■加占OES5■0Abjrra-ftEdjtoTjflssefTrfjfer MJ戏旧另ahoTT(j■&,一iiIL心;JLl坎G()■#rP-r^dti'c.itn.vt.-i(j♦salStputData(;■&阻WA.'l!!!、()奉必tF,56lL:E>r(j4tRM/Tl上向i1■#c.'i-it?■;!虬届至此,我们完成了界面组装器的核心架构的实现,注意,这只是一种实现,并不是界面组装模式的全部,作为一种模式,它必须有更广的外延,下面我们将要探讨它的模式本质。模式与价值观这个模式是一种架构模式,模式的定义有三个要素:问题,环境,解决方案,这在前面我们已经详细地论述过了,在这里我们讨论一下其他的参量。每个模式都有它自己独特的价值观,那么界面组装器模式给我们提供了什么样的价值观呢?首先,它的精髓在于这种分解界面,将界面和组装行为解耦的设计思想,这在拥有多个界面的应用中很有益处,当界面多的时候,如果没有一个比较集中的调度控制方式来对这些界面进行管理,就会形成界面行为无法规范,风格各异,更难以作transaction事务或session会话控制。这在小型应用开发中也许不很明显,但在一个大中型应用中对分散的不规范的界面行为进行控制将会是一场恶梦,到最后可能整个开发组都沉浸于bug的修复和界面修改中,而无暇顾及领域逻辑代码的编写。而通过将界面和组装行为解耦就可以让开发人员集中精力于界面逻辑和领域逻辑的开发,而不用每一个界面都去编写管理界面的代码。其实这也是模式化的一个优点,模式可以优化我们的架构,可以规范开发行为,因此也会节省开发成本。其二,它将界面逻辑处理与领域逻辑处理(也就是数据逻辑处理)解耦。我们将数据输入输出从界面模型中抽取出来,没有与界面耦合在一起,这就获得巨大的好处,第一,我们可以在界面之外来处理数据,在我们的领域类中处理这些数据,也就是说界面只是提供了一个定义数据的载体,而这些数据是被领域逻辑类使用的,而我们开发的主要精力也应该放在处理业务逻辑的领域类上。第二,现在我们将界面和领域类解耦,这样我们的界面和领域类都可以独立地变化,相互之间没有任何依赖,这就很方便于我们开发人员的分工,编写界面的开发组不用依赖于编写后台逻辑类的开发组。第三,在做单元测试-unittest时,开发后台逻辑类的人员可以单独测试领域类,而开发界面的人员也可以单独测试界面逻辑。第四,当我们有多套界面机制时,我们的后台逻辑类可以很方便地接插上去,比如我们要
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 答案详解(5星学霸数学中考册)
- 2026年客户关系管理系统项目可行性研究报告
- 2026年东南亚绿色电力证书项目公司成立分析报告
- 2026年二氧化碳罐车运输项目可行性研究报告
- 2026年量子纠错技术项目可行性研究报告
- 2026年下午茶微醺时光项目公司成立分析报告
- 2026年家庭光储融合项目公司成立分析报告
- 辽宁省普通高中2026年高中新课标高三第一次摸底测试化学试题含解析
- 山东省青州二中2026年高三下学期第三周测试生物试题含解析
- 陕西省安康市汉滨高中2026年高三全真化学试题模拟试卷(8)含解析
- 正念认知疗法实证研究-洞察及研究
- GJB2489A2023航空机载设备履历本及产品合格证编制要求
- 2025年云南省中考英语试卷真题(含标准答案及解析)
- 海运集货仓库管理制度
- 热点话题18 航天新征程:神舟二十号引领科技创新与传统突破-2025年高考语文作文主题预测+素材+模拟范文
- 2024年3月浙江省高中生物竞赛试卷 含解析
- DBJ50-T-274-2017 重庆市轨道交通客运服务标志标准
- 五年级数学(小数除法)计算题专项练习及答案汇编
- 人教版八年级下册物理期末考试试卷含答案
- 妊娠期乳腺癌护理
- 糖皮质激素在儿科疾病中的合理应用3
评论
0/150
提交评论