480 likes | 611 Views
Принципы. проектирования классов. Качество архитектуры. Что такое «хороший» дизайн? Дизайн, который (как минимум) не обладает признаками «плохого». Что такое «плохой» дизайн? Признаки плохого дизайна:. . Жесткость Хрупкость Монолитность Вязкость Неоправданная сложность
E N D
Принципы проектированияклассов
Качествоархитектуры Чтотакое«хороший»дизайн? Дизайн,который(какминимум)необладает признаками«плохого». Чтотакое«плохой»дизайн? Признакиплохогодизайна: Жесткость Хрупкость Монолитность Вязкость Неоправданнаясложность Дублирование Непонятность
Жесткость Дизайнявляетсяжестким,еслиодно изменениевлечѐтзасобойкаскад последующихизмененийвзависимых модулях. Чембольшемодулейподвергается изменениям,темболеежесткимявляется проект.
Хрупкость Проектявляетсяхрупким,еслипри внесенииодногоизмененияпрограмма «разрушается»вомногихместах. Новыепроблемыоченьчастовозникаютв местах,которыенесвязанысизменѐнным компонентом. Впроцессеисправленияошибоквозникают новыеошибки.
Монолитность Проектявляетсямонолитным,еслион содержиткомпоненты,которыемогутбыть повторноиспользованывдругихсистемах, однакоусилия,связанныесизоляцией компонента,слишкомвелики. Врезультатевозрастаетдублирование исходногокода.
Вязкость Проектявляетсявязким,когдареализовать какую-либофункциональностьнамногопроще «неправильными»методами(фактически применениемантипаттернов). Приреализациифункциональности «неправильными»методамивысока вероятностьдопуститьошибку,ареализация «правильными»методамислишкомсложна. Врезультатезатратывременинареализацию функциональностинеоправданновозрастают.
Неоправданнаясложность Проектимеетнеоправданнуюсложность, еслисодержитэлементы,которыене используютсявнастоящиймоментвремени (и,возможно,небудутиспользоваться никогда). Возникает,когдаразработчикипытаются предвидетьвозможныеизменениявкодеи проводятмероприятия,связанныесэтими потенциальнымиизменениями.
Архитектурныепринципы РобертМартинсоставилсписокизпятипринципов, способствующихулучшениюдизайна: принципединственностиответственности (SRP,TheSingleResponsibilityPrinciple), принципоткрытости/закрытости (OCP,TheOpenClosedPrinciple), принципподстановкиЛисков (LSP,TheLiskovSubstitutionPrinciple), принципразделенияинтерфейса (ISP,TheInterfaceSegregationPrinciple), принципинверсиизависимости (DIP,TheDependencyInversionPrinciple).
Связанность Связанность(coupling)—этомера,определяющая, насколькожесткоодинэлементсвязансдругими элементами,либокакимколичествомданныходругих элементахонобладает. Припроектированиинеобходимообеспечивать наименьшуюсвязанность. Элементснизкойстепеньюсвязанностизависитотне оченьбольшогочисладругихэлементовиимеет следующиесвойства: Малоечислозависимостеймеждуклассами (подсистемами). Слабаязависимостьодногокласса(подсистемы)от измененийвдругомклассе(подсистеме). Высокаястепеньповторногоиспользованияподсистем.
Сцепление Сцепление(cohesion,связность,функциональное сцепление)—этомерасфокусированностиобязанностей класса. Объектобладаетвысокойстепеньюсцепления,еслиего обязанноститесносвязанымеждусобой. Объектснизкойстепеньюсцеплениявыполняетмного разнородныхфункцийилинесвязанныхмеждусобой обязанностей. Слабоесцеплениеприводиткследующимпроблемам: Трудностьпонимания. Сложностьповторногоиспользования. Сложностьподдержки. Ненадежность,постояннаяподверженность изменениям.
ПринципединственностиответственностиПринципединственностиответственности Формулировка:классилимодульдолжениметь однуитолькооднупричинудляизменений. Последствиянарушения: Монолитность. Потенциальноедублирование. Большиеклассы. Следствияприменения: Компактныеклассы. Уменьшениесвязанности. Увеличениесцепления.
ПринципединственностиответственностиПринципединственностиответственности Пустьестькласс,представляющийсобойнекоторый продуктсовстроеннойвалидациейданных: publicclassProduct{ publicintPrice{get;set;} publicProduct(intprice){ Price=price; } publicboolIsValid(){ returnPrice>0; } } Productproduct=newProduct(100); boolisValid=product.IsValid();
ПринципединственностиответственностиПринципединственностиответственности Предположим,чтообъектпродуктастал использоватьсянекоторымобъектомCustomerService, длякоторогоиспользуетсядругойалгоритмпроверки корректности: publicclassProduct { publicboolIsValid(boolisCustomerService){ if(isCustomerService) returnPrice>100000; returnPrice>0; } } //CustomerServiceusage Productproduct=newProduct(100); boolisValid=product.IsValid(true);
ПринципединственностиответственностиПринципединственностиответственности Оченьвероятно,чтовскоребудутдобавленыновые алгоритмывалидации.Становитсяпонятно,чтоза валидациюданныхдолженотвечатьотдельныйобъект: publicinterfaceIProductValidator{ boolIsValid(Productproduct); } publicclassDefaultProductValidator:IProductValidator{ publicboolIsValid(Productproduct){ returnproduct.Price>0; } } publicclassCustomerServiceProductValidator:IProductValidator{ publicboolIsValid(Productproduct){ returnproduct.Price>100000; } }
ПринципединственностиответственностиПринципединственностиответственности Реализацияклассапродуктаизменитсяследующим образом: publicclassProduct{ privatereadonlyIProductValidatorvalidator; publicProduct(intprice):this(price,newDefaultProductValidator()) {} publicProduct(intprice,IProductValidatorvalidator){ Price=price; this.validator=validator; } publicintPrice{get;set;} publicboolIsValid(){ returnvalidator.IsValid(this); } }
ПринципединственностиответственностиПринципединственностиответственности Теперьобъектбудетиспользоватьсятак: //Defaultusage Productproduct=newProduct(100); //CustomerServiceusage Productproduct=newProduct(100, newCustomerServiceProductValidator());
Принципоткрытости/закрытости Формулировка:программныесущности должныбытьоткрытыдлярасширения,но закрытыдляизменения Последствиянарушения: Хрупкость. Следствиявыполнения: Увеличениегибкостисистемы. Упрощениетестирования.
Принципоткрытости/закрытости Рассмотримиерархиюклассов,используемуюдля представленияобъектовбазыданных(т.н. метаобъектов).Корневойсущностьютакойиерархии будетявлятьсяследующийкласс: publicclassMetaObject { publicstringName{get;set;} publicMetaObject(stringname) { Name=name; } }
Принципоткрытости/закрытости Обозначимклассытаблицыипредставления: publicclassTable:MetaObject { privatereadonlyList<Field>fields=newList<Field>(); publicTable(stringname):base(name){} publicIEnumerable<Field>Fields{get{returnfields;}} } publicclassView:MetaObject { privatereadonlyList<Field>fields=newList<Field>(); publicstringBody{get;set;} publicIEnumerable<Field>Fields{get{returnfields;}} publicView(stringname):base(name){} }
Принципоткрытости/закрытости КлассFieldможетиметьследующийвид: publicclassField:MetaObject { publicstringDataTypeName{get;set;} publicField(stringname) :base(name) { DataTypeName="INT"; } }
Принципоткрытости/закрытости РассмотримтеперьклассSqlDumpGenerator,которыйпозволяет получитьSQL-скрипт,создающийуказанныеобъектыБД: publicclassSqlDumpGenerator{ publicstringGenerate(IEnumerable<MetaObject>metaObjects){ StringBuilderresult=newStringBuilder(); foreach(MetaObjectmetaObjectinmetaObjects){ StringBuildercreateScript=newStringBuilder(); if(metaObjectisTable){ createScript.Append("CREATETABLE") .AppendLine(metaObject.Name); //... } elseif(metaObjectisView){ createScript.Append("CREATEVIEW") .AppendLine(metaObject.Name); //... } result.AppendLine(createScript.ToString()); } returnresult.ToString(); } }
Принципоткрытости/закрытости Данныйкласснарушаетпринцип открытости/закрытости,т.к.длядобавленияновой функциональности(например,новогонаследника классаMetaObject)необходимомодифицироватькод функцииGenerate. Совершенноочевидно,чтодлясоблюденияпринципа следуетсоздатьметодGetCreateScriptвклассе MetaObject: publicclassMetaObject { publicvirtualstringGetCreateScript() { //... } }
Принципоткрытости/закрытости Теперьпридобавленияновыхобъектов,нет необходимостивмодификацииметодаGenerate, достаточнотолькодобавитькласснаследник MetaObject. Заметим,однако,чтоеслифункцияGenerateдолжна добавлятьскриптысозданиявразномпорядке(например, сначаладобавитьвсетаблицы,апотомвсе представления),товозникаетнеобходимостьвеѐ модификации. Вообщеговоря,всегданайдѐтсятакоеизменение, котороепотребуетмодификации(анедобавления) исходногокода.Разработчикдолженсамопределять самыевероятныеизмененияисоздаватьабстракции, которыепозволяютзащититьсяотэтихизменений.
ПринципподстановкиЛисков Формулировка: Пустьq(x)являетсясвойством,вернымотносительно объектовxнекотороготипаT.Тогдаq(y)такжедолжнобыть вернымдляобъектовyтипаS,гдеSявляетсяподтипомтипаT. или Функции,которыеиспользуютссылкинабазовыеклассы, должныиметьвозможностьиспользоватьобъекты производныхклассов,незнаяобэтом. Последствиянарушения: Нарушениеабстракций. Затруднениетестирования. Следствиявыполнения: Чѐткоеопределениеабстракций. Упрощениетестирования. Упрощениепониманиякода.
ПринципподстановкиЛисков Рассмотримклассическийпримернарушенияданногопринципа. Определимкласспрямоугольника: publicclassRectangle{ privatedoublewidth; privatedoubleheight; publicdoubleWidth{ get{returnwidth;} set{width=value;} } publicdoubleHeight{ get{returnheight;} set{height=value;} } publicdoubleGetArea(){ returnWidth*Height; } }
ПринципподстановкиЛисков Пустьтеперьнашейсистеме потребовалсяклассквадрата.Как известно,квадрат—эточастный случайпрямоугольника,иными словами,квадратявляется прямоугольником(выполняется отношениеisa).
ПринципподстановкиЛисков Однаковсестороныквадратаравнымеждусобой,поэтомумы вынужденыобъявитьметодыустановкишириныивысоты виртуальнымивклассепрямоугольникаиперекрытьихвклассе квадратасоответствующимобразом: publicclassSquare:Rectangle{ publicoverridedoubleWidth{ get{returnbase.Width;} set{ base.Width=value; base.Height=value; } } publicoverridedoubleHeight{ get{returnbase.Height;} set{ base.Width=value; base.Height=value; } } }
ПринципподстановкиЛисков Очевиднойпроблемойвидитсяявнаяизбыточностьданныхв классеквадрата(достаточнохранитьтолькооднусторону).Но предположим,чтонасневолнуютпроблемыэкономиипамяти. Рассмотримследующийпростойтест: [TestFixture] publicclassTests{ privatevoidInternalTestRectangleArea(Rectanglerectangle){ rectangle.Width=3; rectangle.Height=2; Assert.That(rectangle.GetArea(),Is.EqualTo(6)); } [Test] publicvoidTestArea(){ InternalTestRectangleArea(newRectangle()); InternalTestRectangleArea(newSquare()); } }
ПринципподстановкиЛисков Заметим,чтосамипосебеклассыквадратаи прямоугольникакорректны. Однако,указанныевышетестыпредполагали, чтоширинаивысотапрямоугольника независимы,тоестьклассSquareнарушил инвариантклассаRectangle. Этоозначает,чтоотношениенаследования относитсятакжеикповедениюобъектов.С этойточкизрения«квадрат»неявляется «прямоугольником».
Принципразделенияинтерфейса Формулировка:Клиентынедолжнызависеть отинтерфейсов,вкоторыхненуждаются. Последствиянарушения: Усложнениекода. Возникновениененужныхзависимостей междумодулями. Следствиявыполнения: Упрощениепониманиякода. Уменьшениесвязанности.
Принципразделенияинтерфейса Вкачественагляднойдемонстрациинарушения данногопринципарассмотримследующийпример. Рассмотримсистемуклассов,представляющуюдва типаработниковсметодамиWorkиEat: publicinterfaceIWorker { voidWork(); voidEat(); }
Принципразделенияинтерфейса classWorker:IWorker { publicvoidWork() { //....working } publicvoidEat() { //......eatinginlaunchbreak } } classSuperWorker:IWorker { publicvoidWork() { //....workingmuchmore } publicvoidEat() { //....eatinginlaunchbreak } }
Принципразделенияинтерфейса Заметим,чтоесликуказаннойсистемеклассовмы захотимдобавитьклассробота,окажется,что реализацияметодаEatвданномклассебудетпустой: classRobot:IWorker { publicvoidWork() { //....working } publicvoidEat() {} }
Принципразделенияинтерфейса Другойнедостатокуказанногоинтерфейса демонстрируетклассменеджера,которыйуправляет работникомследующимобразом: classManager { publicIWorkerWorker{get;set;} publicvoidManage() { Worker.Work(); } } Данныйклассиспользуетлишьчастьинтерфейса IWorker.
Принципразделенияинтерфейса Указанныевышепримерыдемонстрируютнарушение рассматриваемогопринципа. РешениемявляетсяразбиениеинтерфейсаIWorker: publicinterfaceIWorkable{ voidWork(); } publicinterfaceIFeedable{ voidEat(); } classWorker:IWorkable,IFeedable{ publicvoidWork(){ //....working } publicvoidEat(){ //......eatinginlaunchbreak } }
Принципразделенияинтерфейса ТеперьвклассеRobotнетнеобходимостисоздавать фиктивныереализацииметодов: publicclassRobot:IWorkable { publicvoidWork() { //....working } } Классменеджеразависиттолькооттойчасти интерфейса,котораяемутребуется: classManager{ publicIWorkableWorker{get;set;} publicvoidManage(){ Worker.Work(); } }
Принципинверсиизависимости Формулировка: Модуливерхнегоуровнянедолжнызависетьотмодулей нижнегоуровня.Обадолжнызависетьотабстракций. Абстракциинедолжнызависетьотдеталей.Деталидолжны зависетьотабстракций. Абстракции—этоабстрактныеклассы(интерфейсы), описывающиеосновныесущностисистемы. Следствиянарушения: Монолитность. Хрупкость. Следствиявыполнения: Увеличениегибкостисистемы. Возможностьповторногоиспользованиякода. Упрощениетестирования.
Принципинверсиизависимости Рассмотримпростоеприложение,котороевыводитна консольSQL-дампсодержимогонекоторойтаблицы. Центральнымобъектомнашегоприложениябудет объектDataScripter,аклиентскийкод,которыйего использует,будетследующим: publicstaticclassProgram{ publicstaticvoidMain(string[]args){ IDbConnectionconnection=CreateConnection(); DataScripterscripter=newDataScripter(); using(IDbCommandcommand=connection.CreateCommand()) { command.CommandText=string.Format("SELECT*FROM{0}",args[0]); using(IDataReaderreader=command.ExecuteReader()) { Console.WriteLine(scripter.GetSqlDump(reader)); } } } }
Принципинверсиизависимости КлассDataScripterвыглядитследующимобразом: publicclassDataScripter { publicstringGetSqlDump(IDataReaderdataReader) { StringBuilderresult=newStringBuilder(); InsertDataDumperdataDumper=newInsertDataDumper(); while(dataReader.Read()) { stringrowSqlDump=dataDumper.GetRowDump(dataReader); result.AppendLine(rowSqlDump); } returnresult.ToString(); } }
Принципинверсиизависимости КлассInsertDataDumperможетиметьследующийвид: publicclassInsertDataDumper { publicstringGetRowDump(IDataRecordrecord) { StringBuilderresult=newStringBuilder(); result.Append("INSERTINTO"); //... returnresult.ToString(); } }
Принципинверсиизависимости ФункцияGetSqlDumpможетоказаться несколькоперегруженнойзнаниямии обязанностями: знает,чтоименноInsertDataDumper генерируетSQL-дампстроки; умеетсоздаватьобъектInsertDataDumper.
Принципинверсиизависимости Рассмотримтеперьприменениепринципаинверсии зависимостей. ДляэтоговыделиминтерфейсIDataDumper: publicinterfaceIDataDumper { stringGetRowDump(IDataRecordrecord); } publicclassInsertDataDumper:IDataDumper { publicstringGetRowDump(IDataRecordrecord) { StringBuilderresult=newStringBuilder(); result.Append("INSERTINTO"); //... returnresult.ToString(); } }
Принципинверсиизависимости Теперьвместотого,чтобысоздаватьобъектInsertDataDumper внутриметодаGetSqlDump,передадимобъектуDataScripter соответствующийпараметр: publicclassDataScripter{ privatereadonlyIDataDumperdataDumper; publicDataScripter(IDataDumperdataDumper){ this.dataDumper=dataDumper; } publicstringGetSqlDump(IDataReaderdataReader){ StringBuilderresult=newStringBuilder(); while(dataReader.Read()){ stringrowSqlDump=dataDumper.GetRowDump(dataReader); result.AppendLine(rowSqlDump); } returnresult.ToString(); } }
Принципинверсиизависимости ТеперькодиспользованияобъектаDataScripterбудет выглядетьследующимобразом: IDbConnectionconnection=CreateConnection(); DataScripterscripter=newDataScripter(newInsertDataDumper()); using(IDbCommandcommand=connection.CreateCommand()) { command.CommandText=string.Format("SELECT*FROM{0}",args[0]); using(IDataReaderreader=command.ExecuteReader()) { Console.WriteLine(scripter.GetSqlDump(reader)); } }
Принципинверсиизависимости Применениепринципаинверсиизависимости уменьшаетсвязанность,чтопозволяетнамлегко изменитьметодгенерациидампастрокисINSERTна UPSERT: publicclassUpsertDataDumper:IDataDumper { publicstringGetRowDump(IDataRecordrecord) { StringBuilderresult=newStringBuilder(); result.Append("REPLACEINTO"); //... returnresult.ToString(); } } DataScripterscripter=newDataScripter(newUpsertDataDumper());
Принципинверсиизависимости Обратимвнимание,нато,чтодампданныхвсегдаполностью сохраняетсявстроке.Такойподходможетпривестикперерасходу памяти.Измениммеханизмзаписиданныхиприменимпринцип инверсиизависимости.Дляэтоговоспользуемсяимеющейсяв библиотеке.NETабстракциейTextWriter: publicclassDataScripter{ privatereadonlyIDataDumperdataDumper; privatereadonlyTextWriterwriter; publicDataScripter(IDataDumperdataDumper,TextWriterwriter){ this.dataDumper=dataDumper; this.writer=writer; } publicvoidGetSqlDump(IDataReaderdataReader){ while(dataReader.Read()){ stringrowSqlDump=dataDumper.GetRowDump(dataReader); writer.WriteLine(rowSqlDump); } } }
Принципинверсиизависимости Теперькод,которыйвыводитдампданныхвконсоль, будетвыглядетьследующимобразом: IDbConnectionconnection=CreateConnection(); DataScripterscripter= newDataScripter(new InsertDataDumper(),Console.Out); using(IDbCommandcommand=connection.CreateCommand()) { command.CommandText=string.Format("SELECT*FROM{0}",args[0]); using(IDataReaderreader=command.ExecuteReader()) { scripter.GetSqlDump(reader); } }
Принципинверсиизависимости Длявыводаданныхвфайлможноиспользовать следующийкод: //... DataScripterscripter=newDataScripter( newInsertDataDumper(), newStreamWriter("output.sql")); //...