Что относится к принципам объектно ориентированного программирования
Перейти к содержимому

Что относится к принципам объектно ориентированного программирования

  • автор:

Шпаргалка по принципам ООП

Обложка поста Шпаргалка по принципам ООП

Чтобы стать программистом, нужно знать принципы ООП как Отче наш. Держите структурированную шпаргалку по объектно-ориентированному программированию.

Главное

  • Инкапсулируйте все, что может изменяться;
  • Уделяйте больше внимания интерфейсам, а не их реализациям;
  • Каждый класс в вашем приложении должен иметь только одно назначение;
  • Классы — это их поведение и функциональность.

Базовые принципы ООП

  • Абстракция — отделение концепции от ее экземпляра;
  • Полиморфизм — реализация задач одной и той же идеи разными способами;
  • Наследование — способность объекта или класса базироваться на другом объекте или классе. Это главный механизм для повторного использования кода. Наследственное отношение классов четко определяет их иерархию;
  • Инкапсуляция — размещение одного объекта или класса внутри другого для разграничения доступа к ним.

Используйте следующее вместе с наследованием

  • Делегация — перепоручение задачи от внешнего объекта внутреннему;
  • Композиция — включение объектом-контейнером объекта-содержимого и управление его поведением; последний не может существовать вне первого;
  • Агрегация — включение объектом-контейнером ссылки на объект-содержимое; при уничтожении первого последний продолжает существование.

Не повторяйся (Don’t repeat yourself — DRY)

Избегайте повторного написания кода, вынося в абстракции часто используемые задачи и данные. Каждая часть вашего кода или информации должна находиться в единственном числе в единственном доступном месте. Это один из принципов читаемого кода.

Принцип единственной обязанности

Для каждого класса должно быть определено единственное назначение. Все ресурсы, необходимые для его осуществления, должны быть инкапсулированы в этот класс и подчинены только этой задаче.

Принцип открытости/закрытости

Программные сущности должны быть открыты для расширения, но закрыты для изменений.

Принцип подстановки Барбары Лисков

Методы, использующие некий тип, должны иметь возможность использовать его подтипы, не зная об этом.

Принцип разделения интерфейсов

Предпочтительнее разделять интерфейсы на более мелкие тематические, чтобы реализующие их классы не были вынуждены определять методы, которые непосредственно в них не используются.

Принцип инверсии зависимостей

Система должна конструироваться на основе абстракций “сверху вниз”: не абстракции должны формироваться на основе деталей, а детали должны формироваться на основе абстракций.

Перевод статьи «Object-Orientated Design Principles»

Что относится к принципам объектно ориентированного программирования

Объектно-ориентированное программирование (ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов (либо, в менее известном варианте языков с прототипированием, — прототипов).

Класс — это тип, описывающий устройство объектов. Понятие «класс» подразумевает некоторое поведение и способ представления. Понятие «объект» подразумевает нечто, что обладает определённым поведением и способом представления. Говорят, что объект — это экземпляр класса. Класс можно сравнить с чертежом, согласно которому создаются объекты. Обычно классы разрабатывают таким образом, чтобы их объекты соответствовали объектам предметной области.

Объект — сущность в адресном пространстве вычислительной системы, появляющаяся при создании экземпляра класса (например, после запуска результатов компиляции (и линковки) исходного кода на выполнение).

Прототип — это объект-образец, по образу и подобию которого создаются другие объекты.

История

Объектное и объектно-ориентированное программирование (ООП) возникло в результате развития идеологии процедурного программирования, где данные и подпрограммы (процедуры, функции) их обработки формально не связаны. Кроме того, в современном объектно-ориентированном программировании часто большое значение имеют понятия события (так называемое событийно-ориентированное программирование) и компонента ( компонентное программирование).

Первым языком программирования, в котором были предложены принципы объектной ориентированности, была Симула. В момент своего появления (в 1967 году), этот язык программирования предложил поистине революционные идеи: объекты, классы, виртуальные методы и др., однако это всё не было воспринято современниками как нечто грандиозное. Тем не менее, большинство концепций были развиты Аланом Кэйем и Дэном Ингаллсом в языке Smalltalk. Именно он стал первым широко распространённым объектно-ориентированным языком программирования.

В настоящее время количество прикладных языков программирования ( список языков), реализующих объектно-ориентированную парадигму, является наибольшим по отношению к другим парадигмам. В области системного программирования до сих пор применяется парадигма процедурного программирования, и общепринятым языком программирования является язык C. Хотя при взаимодействии системного и прикладного уровней операционных систем заметное влияние стали оказывать языки объектно-ориентированного программирования. Например, одной из наиболее распространенных библиотек мультиплатформенного программирования является объектно-ориентированная библиотека Qt, написанная на языке C++.

Главные понятия и разновидности

Структура данных « класс», представляющая собой объектный тип данных, внешне похожа на типы данных процедурно-ориентированных языков, такие как структура в языке Си или запись в Паскале или QuickBasic. При этом элементы такой структуры (члены класса) могут сами быть не только данными, но и методами (то есть процедурами или функциями). Такое объединение называется инкапсуляцией.

Основные понятия

Абстракция данных Объекты представляют собой упрощенное, идеализированное описание реальных сущностей предметной области. Если соответствующие модели адекватны решаемой задаче, то работать с ними оказывается намного удобнее, чем с низкоуровневым описанием всех возможных свойств и реакций объекта. Инкапсуляция Инкапсуляция — это принцип, согласно которому любой класс должен рассматриваться как чёрный ящик — пользователь класса должен видеть и использовать только интерфейсную часть класса (т. е. список декларируемых свойств и методов класса) и не вникать в его внутреннюю реализацию. Поэтому данные принято инкапсулировать в классе таким образом, чтобы доступ к ним по чтению или записи осуществлялся не напрямую, а с помощью методов. Принцип инкапсуляции (теоретически) позволяет минимизировать число связей между классами и, соответственно, упростить независимую реализацию и модификацию классов. Сокрытие данных Сокрытие данных — неотделимая часть ООП, управляющая областями видимости. Является логическим продолжением инкапсуляции. Целью сокрытия является невозможность для пользователя узнать или испортить внутреннее состояние объекта. Наследование Наследованием называется возможность порождать один класс от другого с сохранением всех свойств и методов класса-предка (прародителя, иногда его называют суперклассом) и добавляя, при необходимости, новые свойства и методы. Набор классов, связанных отношением наследования, называют иерархией. Наследование призвано отобразить такое свойство реального мира, как иерархичность. Полиморфизм Полиморфизмом называют явление, при котором функции (методу) с одним и тем же именем соответствует разный программный код (полиморфный код) в зависимости от того, объект какого класса используется при вызове данного метода. Полиморфизм обеспечивается тем, что в классе-потомке изменяют реализацию метода класса-предка с обязательным сохранением сигнатуры метода. Это обеспечивает сохранение неизменным интерфейса класса-предка и позволяет осуществить связывание имени метода в коде с разными классами — из объекта какого класса осуществляется вызов, из того класса и берётся метод с данным именем. Такой механизм называется динамическим (или поздним) связыванием — в отличие от статического (раннего) связывания, осуществляемого на этапе компиляции.

  • Пользователь может взаимодействовать с объектом только через этот интерфейс. Реализуется с помощью ключевого слова: public.
  • Пользователь не может использовать закрытые данные и методы. Реализуется с помощью ключевых слов: private, protected, internal.

Предостережение: Одна из наиболее распространенных ошибок — делать сокрытие реализации только ради сокрытия. Целями, достойными усилий, являются:

  • предельная локализация изменений при необходимости таких изменений,
  • прогнозируемость изменений (какие изменения в коде надо сделать для заданного изменения функциональности) и прогнозируемость последствий изменений.

Образ в пример: ложка, опущенная в стакан, не меняет его свойств и не становится частью стакана, хотя и помогает пить из него чай; в то же время, сахар, растворенный в чае с помощью ложки, делает его сладким.

Часто инкапсуляция может быть достигнута простейшими организационными мерами: знание того, что «вот так-то делать нельзя» иногда является самым эффективным средством инкапсуляции!

C++

class A 

public:
int a, b; //данные открытого интерфейса
int ReturnSomething(); //метод открытого интерфейса
private:
int Aa = a, Ab = b; //скрытые данные и использование ими интерфейса
void DoSomething(); //скрытый метод
>;

Класс А инкапсулирует свойства Aa, Ab и метод DoSomething, представляя внешний интерфейс ReturnSomething, a, b.

Полиморфизм (в языках программирования) — возможность объектов с одинаковой спецификацией иметь различную реализацию

Язык программирования поддерживает полиморфизм, если классы с одинаковой спецификацией могут иметь различную реализацию — например, реализация класса может быть изменена в процессе наследования [1] .

Полиморфизм позволяет писать более абстрактные программы и повысить коэффициент повторного использования кода. Общие свойства объектов объединяются в систему, которую могут называть по-разному — интерфейс, класс. Общность имеет внешнее и внутреннее выражение:

  • внешняя общность проявляется как одинаковый набор методов с одинаковыми именами и сигнатурами (именем методов и типами аргументов и их количеством);
  • внутренняя общность — одинаковая функциональность методов. Её можно описать интуитивно или выразить в виде строгих законов, правил, которым должны подчиняться методы. Возможность приписывать разную функциональность одному методу (функции, операции) называется перегрузкой метода (перегрузкой функций, перегрузкой операций).

Примеры

Класс геометрических фигур ( эллипс, многоугольник) может иметь методы для геометрических трансформаций ( смещение, поворот, масштабирование).

Класс потоков имеет методы для последовательной передачи данных. Потоком может быть информация, вводимая пользователем с терминала, обмен данными по компьютерной сети, файл (если требуется последовательная обработка данных, например, при разборе исходных текстов программ).

В объектно-ориентированных языках

В объектно-ориентированных языках класс является абстрактным типом данных. [Прим. 1] Полиморфизм реализуется с помощью наследования классов и виртуальных функций. Класс-потомок наследует сигнатуры методов класса-родителя, а реализация, в результате переопределения метода, этих методов может быть другой, соответствующей специфике класса-потомка. Другие функции могут работать с объектом класса-родителя, но при этом вместо него во время исполнения будет подставляться один из классов-потомков. Это называется поздним связыванием.

Абстрактные (или чисто виртуальные) методы не имеют реализации вообще (на самом деле некоторые языки, например C++, допускают реализацию абстрактных методов в родительском классе). Они специально предназначены для наследования. Их реализация должна быть определена в классах-потомках.

Класс может наследовать функциональность от нескольких классов. Это называется множественным наследованием. Множественное наследование создаёт известную проблему (в C++), когда класс наследуется от нескольких классов-посредников, которые в свою очередь наследуются от одного класса (так называемая « Проблема ромба»): если метод общего предка был переопределён в посредниках, неизвестно, какую реализацию метода должен наследовать общий потомок. Решается эта проблема путём отказа от множественного наследования для классов и разрешением множественного наследования для полностью абстрактных классов (т. е. интерфейсов) ( C#, Delphi, Java), либо через виртуальное наследование ( C++).

Наследование — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.

Типы наследования

Простое наследование

Класс, от которого произошло наследование, называется базовым или родительским ( англ. base class ). Классы, которые произошли от базового, называются потомками, наследниками или производными классами ( англ. derived class ).

В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».

Множественное наследование

При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ ( Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживает возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Единый базовый класс

В ряде языков программирования все классы явно или неявно наследуются от некого базового класса. Smalltalk был одним из первых языков, в которых использовалась эта концепция. К таким языкам относятся Objective-C ( NSObject ), Java ( java.lang.Object ), C# ( System.Object ), Delphi ( TObject ).

Наследование в языке C++

class A //базовый класс
>;

class B : public A //public наследование
>

class C : protected A //protected наследование
>

class Z : private A //private наследование
>

В C++ существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:

  • при public-наследовании все спецификаторы остаются без изменения.
  • при protected-наследовании все спецификаторы остаются без изменения, кроме спецификатора public, который меняется на спецификатор protected (то есть public-члены базового класса в потомках становятся protected).
  • при private-наследовании все спецификаторы меняются на private.

Одним из основных преимуществ public-наследования является то, что указатель на классы—наследники может быть неявно преобразован в указатель на базовый класс, то есть для примера выше можно написать:

A* a = new B; 

Принципы объектно-ориентированного программирования

Когда речь заходит о классических паттернах проектирования, нельзя не вспомнить о самом объектно-ориентированном программировании. Ведь паттерны GoF являются паттернами именно объектно-ориентированного программирования. В функциональном же программировании есть свои собственные паттерны.

Вообще устроено все следующим образом: есть само объектно-ориентированное программирование. У него есть принципы. Из принципов объектно-ориентированного программирования следуют разобранные нам шаблоны GRASP (как вариант — SOLID принципы), из которых, в свою очередь, следуют шаблоны GoF. Из них же следует ряд интересных вещей, например, enterprise паттерны.

Объектно-ориентированная парадигма

Определение гласит, что «Объектно-ориентированное программирование – это парадигма программирования, в которой основной концепцией является понятие объекта, который отождествляется с предметной областью.»

Таким образом, система представляется в виде набора объектов предметной области, которые взаимодействуют между собой некоторым образом. Каждый объект обладает тремя cоставляющими: идентичность (identity), состояние (state) и поведение (behaviour).

Состояние объекта — это набор всех его полей и их значений.

Поведение объекта — это набор всех методов класса объекта.

Идентичность объекта — это то, что отличает один объект класса от другого объекта класса. С точки зрения Java, именно по идентичности определяется метод equals.

Принципы объектно-ориентированного программирования

Объектно-ориентированное программирование обладает рядом принципов. Представление об их количестве расходится. Кто-то утверждает, что их три (старая школа программистов), кто-то, что их четыре (новая школа программистов):

  1. Абстрация
  2. Инкапсуляция
  3. Наследование
  4. Полиморфизм

Инкапсуляция

Вопреки мнению многих собеседующихся (а иногда и собеседуемых), инкапсуляция это не «когда все поля приватные». Инкапсуляция является фундаментальнейшим принципом проектирования ПО, ее следы наблюдаются на только на уровне микро-, но и на уровне макропроектирования.

Научное определение гласит, что «Инкапсуляция – это принцип, согласно которому любой класс и в более широком смысле – любая часть системы должны рассматриваться как «черный ящик»: пользователь класса или подсистемы должен видеть только интерфейс (т.е. список декларируемых свойств и методов) и не вникать во внутреннюю реализацию.»

Таким образом, получается, что если класс A обращается к полям класса B напрямую, это приводит не к тому, что «нарушается информационная безопасность», а к тому, что класс A завязывается на внутренне устройство класса B, и попытка изменить внутреннее устройство класса B приведет к изменению класса А. Более того, класс A не просто так работает с полями класса B, он работает по некоторой бизнес-логике. То есть логика по работе с состоянием класса В лежит в классе А, и когда мы захотим переиспользовать класс В, это не удастся сделать, ведь без кусочка класса А класс В может быть бесполезным, что приведет к тому, что класс В придется отдавать вместе с классом А. Экстраполируя это на всю систему, получается, что переиспользовать можно будет только всю систему целиком.

Инкапсуляция является самым недооцененным принципом, который, к сожалению, мало кем интерпретируется правильно. Она позволяет минимизировать число связей между классами и подсистемами и, соответственно, упростить независимую реализацию и модификацию классов и подсистем.

Наследование

Наследование — это возможность порождать один класс от другого с сохранением всех свойств и методов класса-предка (суперкласса), добавляя при необходимости новые свойства и
методы.

Наследование является самым переоцененным принципом. Когда-то считалось, что «У идеального программиста дерево наследования уходит в бесконечность и заканчивается абсолютно пустым объектом», потому как когда-то люди не очень хорошо понимали то, что наследование — это способ выразить такое свойство реального мира как иерархичность, а не способ переиспользовать код, отнаследовав машину от холодильника, потому что у обоих предметов есть ручка. Наследования желательно по возможности избегать, потому что наследование является очень сильной связью. Для уменьшения количества уровней наследования рекомендуется строить дерево «снизу-вверх».

Полиморфизм

Полиморфизм — это возможность использовать классы – потомки в контексте, который был предназначен для класса – предка.

За самым садистским определением кроется возможность языка программирования для декомпозиции задачи и рефакторинга if’ов и switch’ей.

Объектно-ориентированное программирование (C#)

C# — это объектно-ориентированный язык программирования. Четыре основных принципа объектно-ориентированного программирования следующие.

  • Абстракция. Моделирование требуемых атрибутов и взаимодействий сущностей в виде классов для определения абстрактного представления системы.
  • Инкапсуляция. Скрытие внутреннего состояния и функций объекта и предоставление доступа только через открытый набор функций.
  • Наследование. Возможность создания новых абстракций на основе существующих.
  • Полиморфизм. Возможность реализации наследуемых свойств или методов отличающимися способами в рамках множества абстракций.

Из предыдущего руководства с общими сведениями о классах вы узнали об абстракции и инкапсуляции. Абстракция банковского счета реализована с помощью класса BankAccount . Эту реализацию можно изменить, не влияя на код, в котором использовался класс BankAccount . Классы BankAccount и Transaction позволяют реализовать инкапсуляцию компонентов, требуемых для описания этих концепций в коде.

В этом руководстве показано, как расширить приложение, чтобы использовать наследование и полиморфизм для добавления новых функций. Вы также добавите компоненты в класс BankAccount , чтобы использовать преимущества абстракции и инкапсуляции, о которых вы узнали при работе с предыдущим руководством.

Создание разных типов счетов

После создания этой программы вы получите запросы на добавление в нее функций. Это прекрасно работает в ситуации, когда у вас есть только один тип банковского счета. С течением времени потребуется внести изменения, так как будут запрашиваться связанные типы счетов:

  • Счет для начисления процентов в конце каждого месяца.
  • Кредитная линия, которая может иметь отрицательный баланс и с которой в этом случае ежемесячно взимаются проценты.
  • Счет для предоплаченной подарочной карты с депозитом, используемый только для оплаты. Его можно пополнять один раз в начале каждого месяца.

Все эти счета можно реализовать с помощью класса BankAccount , определенного в предыдущем руководстве. Можно скопировать этот код, переименовать классы и внести изменения. Этот метод сработает в краткосрочной перспективе, но со временем ситуация усложнится. Все изменения нужно будет копировать во все затронутые классы.

Вместо этого можно создать новые типы банковских счетов, которые наследуют методы и данные из класса BankAccount , созданного при работе с предыдущим руководством. Эти новые классы могут расширить класс BankAccount за счет конкретного поведения, требуемого для каждого типа:

public class InterestEarningAccount : BankAccount < >public class LineOfCreditAccount : BankAccount < >public class GiftCardAccount : BankAccount

Каждый из этих классов наследует общее поведение общего базового класса BankAccount . Напишите реализации новых отличающихся функций в каждом из производных классов. Эти производные классы уже имеют поведение, определенное в классе BankAccount .

Рекомендуется создавать новый класс в другом исходном файле. В Visual Studio можно щелкнуть правой кнопкой мыши проект и выбрать Добавить класс, чтобы добавить новый класс в новый файл. В Visual Studio Code выберите Файл и Создать, чтобы создать исходный файл. В любом из этих средств присвойте файлу имя, соответствующее имени класса: InterestEarningAccount.cs, LineOfCreditAccount.cs и GiftCardAccount.cs.

При создании классов, как показано в примере выше, вы обнаружите, что ни один из производных классов не компилируется. За инициализацию объекта отвечает конструктор. Конструктор производного класса должен инициализировать производный класс и предоставить инструкции по инициализации объекта базового класса, включенного в производный класс. Правильная инициализация обычно выполняется без дополнительного кода. Класс BankAccount объявляет один открытый конструктор со следующей сигнатурой:

public BankAccount(string name, decimal initialBalance) 

Компилятор не создает конструктор по умолчанию при самостоятельном определении конструктора. Это означает, что каждый производный класс должен явно вызывать этот конструктор. Вы объявляете конструктор, который может передавать аргументы конструктору базового класса. В следующем коде показан конструктор для InterestEarningAccount :

public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)

Параметры для этого нового конструктора соответствуют типу и именам параметра конструктора базового класса. Для указания вызова конструктора базового класса используйте синтаксис : base() . Некоторые классы определяют несколько конструкторов, и этот синтаксис позволяет выбрать вызываемый конструктор базового класса. После обновления конструкторов можно разработать код для каждого производного класса. Требования к новым классам можно сформулировать следующим образом.

  • Счет для начисления процентов:
    • Будут начисляться 2 % от текущего баланса в конце месяца.
    • Может иметь отрицательный баланс, который не превышает абсолютное значение кредитного лимита.
    • Будут списываться проценты каждый месяц, в конце которого баланс не равен 0.
    • Будет взиматься комиссия за каждый вывод средств, превышающий кредитный лимит.
    • Может пополняться на указанную сумму в последний день каждого месяца.

    Как видите, для каждого из этих типов счетов предусмотрено действие, которое выполняется в конце каждого месяца, и эти действия отличаются. Для реализации этого кода используется полиморфизм. Перейдите к методу virtual в классе BankAccount :

    public virtual void PerformMonthEndTransactions()

    В приведенном выше коде показано, как использовать ключевое слово virtual для объявления метода в базовом классе, для которого производный класс может предоставить другую реализацию. virtual определяет метод, в котором любой производный класс может использовать повторную реализацию. Производные классы используют ключевое слово override для определения новой реализации. Обычно это называется переопределением реализации базового класса. Ключевое слово virtual указывает, что производные классы могут переопределить поведение. Вы также можете объявить методы abstract , где производные классы должны переопределять поведение. Базовый класс не предоставляет реализацию для метода abstract . Далее необходимо определить реализацию для двух новых классов, которые вы создали. Начните с InterestEarningAccount :

    public override void PerformMonthEndTransactions() < if (Balance >500m) < decimal interest = Balance * 0.02m; MakeDeposit(interest, DateTime.Now, "apply monthly interest"); >> 

    Добавьте следующий код в LineOfCreditAccount . Код обнуляет баланс для расчета положительной процентной ставки, снятой со счета:

    public override void PerformMonthEndTransactions() < if (Balance < 0) < // Negate the balance to get a positive interest charge: decimal interest = -Balance * 0.07m; MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest"); >> 

    Классу GiftCardAccount требуются два изменения, чтобы реализовать соответствующее действие, выполняемое в конце месяца. Во-первых, измените конструктор, включив в него дополнительную сумму, добавляемую ежемесячно:

    private readonly decimal _monthlyDeposit = 0m; public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance) => _monthlyDeposit = monthlyDeposit; 

    Конструктор предоставляет значение по умолчанию для значения monthlyDeposit , поэтому вызывающие объекты могут опускать 0 при отсутствии ежемесячного депозита. Во-вторых, переопределите метод PerformMonthEndTransactions , чтобы добавить месячный депозит, если в конструкторе было задано ненулевое значение:

    public override void PerformMonthEndTransactions() < if (_monthlyDeposit != 0) < MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit"); >> 

    Переопределение применяет набор месячных депозитов в конструкторе. Добавьте следующий код в метод Main , чтобы проверить эти изменения для GiftCardAccount и InterestEarningAccount :

    var giftCard = new GiftCardAccount("gift card", 100, 50); giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee"); giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries"); giftCard.PerformMonthEndTransactions(); // can make additional deposits: giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money"); Console.WriteLine(giftCard.GetAccountHistory()); var savings = new InterestEarningAccount("savings account", 10000); savings.MakeDeposit(750, DateTime.Now, "save some money"); savings.MakeDeposit(1250, DateTime.Now, "Add more savings"); savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills"); savings.PerformMonthEndTransactions(); Console.WriteLine(savings.GetAccountHistory()); 

    Проверьте результаты. Теперь добавьте аналогичный набор тестового кода для LineOfCreditAccount :

    var lineOfCredit = new LineOfCreditAccount("line of credit", 0); // How much is too much to borrow? lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance"); lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount"); lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs"); lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs"); lineOfCredit.PerformMonthEndTransactions(); Console.WriteLine(lineOfCredit.GetAccountHistory()); 

    Когда вы добавите приведенный выше код и запустите программу, вы увидите примерно такую ошибку:

    Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount') at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42 at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31 at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9 at OOProgramming.Program.Main(String[] args) in Program.cs:line 29 

    Фактический результат включает полный путь к папке с проектом. Имена папок опущены для краткости. Кроме того, в зависимости от формата кода номера строк могут отличаться.

    Этот код завершается ошибкой, так как в BankAccount предполагается, что исходный баланс должен быть больше 0. Еще одно допущение, реализованное в классе BankAccount , заключается в том, что баланс не может быть отрицательным. Вместо этого любой вывод средств, превышающий сумму на счете, отклоняется. Оба эти предположения необходимо изменить. Баланс кредитного счета равен 0 и, как правило, в дальнейшем будет иметь отрицательное значение. Кроме того, если клиент займет слишком много денег, с него будет списана комиссия. Транзакция принимается, она просто требует дополнительных затрат. Первое правило можно реализовать, добавив необязательный аргумент к конструктору BankAccount , который определяет минимальный баланс. Значение по умолчанию — 0 . Для второго правила требуется механизм, который позволяет производным классам изменять алгоритм по умолчанию. В некотором смысле базовый класс уточняет у производного типа, что должно произойти при перерасходе. Поведение по умолчанию — отклонить транзакцию, вызвав исключение.

    Начнем с добавления второго конструктора, который включает необязательный параметр minimumBalance . Этот новый конструктор выполняет все действия, выполняемые существующим конструктором. Кроме того, задается свойство, определяющее минимальный баланс. Вы можете скопировать текст существующего конструктора, но это означает, что в будущем изменится два расположения. Вместо этого можно использовать цепочки конструкторов, чтобы один конструктор вызывал другой. В следующем коде показаны два конструктора и новое дополнительное поле:

    private readonly decimal _minimumBalance; public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) < >public BankAccount(string name, decimal initialBalance, decimal minimumBalance) < Number = s_accountNumberSeed.ToString(); s_accountNumberSeed++; Owner = name; _minimumBalance = minimumBalance; if (initialBalance >0) MakeDeposit(initialBalance, DateTime.Now, "Initial balance"); > 

    В приведенном выше коде показаны два новых метода. Во-первых, поле minimumBalance помечается как readonly . Это означает, что значение нельзя изменить после создания объекта. После создания BankAccount minimumBalance не может измениться. Во вторых, конструктор, принимающий два параметра, использует : this(name, initialBalance, 0) < >в качестве реализации. Выражение : this() вызывает другой конструктор, который имеет три параметра. Этот метод позволяет применить одну реализацию для инициализации объекта, даже несмотря на то, что клиентский код может выбрать один из многих конструкторов.

    Эта реализация вызывает MakeDeposit , только если исходный баланс превышает 0 . Таким образом, мы придерживаемся правила, согласно которого депозиты должны быть положительными, а кредитный счет открывается с балансом, равным 0 .

    Теперь, когда класс BankAccount имеет доступное только для чтения поле для определения минимального баланса, последнее изменение заключается в изменении прописанного в коде значения 0 на minimumBalance в методе MakeWithdrawal :

    if (Balance - amount < _minimumBalance) 

    После расширения класса BankAccount можно изменить конструктор LineOfCreditAccount , чтобы вызвать новый базовый конструктор, как показано в следующем коде:

    public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)

    Обратите внимание, что конструктор LineOfCreditAccount изменяет знак параметра creditLimit , чтобы он соответствовал значению параметра minimumBalance .

    Разные правила, связанные с перерасходом

    Последняя добавляемая функция позволяет LineOfCreditAccount взимать комиссию за превышение кредитного лимита вместо того, чтобы отклонять транзакцию.

    Для этого можно, например, определить виртуальную функцию, в которой реализуется требуемое поведение. Класс BankAccount выполняет рефакторинг метода MakeWithdrawal , чтобы получить два метода. Новый метод выполняет указанное действие, когда при выводе средств баланс получает значение ниже определенного минимума. Существующий метод MakeWithdrawal имеет следующий код:

    public void MakeWithdrawal(decimal amount, DateTime date, string note) < if (amount if (Balance - amount < _minimumBalance) < throw new InvalidOperationException("Not sufficient funds for this withdrawal"); >var withdrawal = new Transaction(-amount, date, note); _allTransactions.Add(withdrawal); > 

    Замените его следующим кодом.

    public void MakeWithdrawal(decimal amount, DateTime date, string note) < if (amount Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance); Transaction? withdrawal = new(-amount, date, note); _allTransactions.Add(withdrawal); if (overdraftTransaction != null) _allTransactions.Add(overdraftTransaction); >protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn) < if (isOverdrawn) < throw new InvalidOperationException("Not sufficient funds for this withdrawal"); >else < return default; >> 

    Добавленный метод protected предполагает, что его можно вызывать только из производных классов. Это объявление предотвращает вызов метода другими клиентами. А ключевое слово virtual означает, что производные классы могут изменять поведение. Тип возвращаемого значения — Transaction? . Аннотация ? указывает, что метод может возвращать null . Добавьте следующую реализацию в LineOfCreditAccount , чтобы списывать комиссию при превышении лимита на вывод средств.

    protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) => isOverdrawn ? new Transaction(-20, DateTime.Now, "Apply overdraft fee") : default; 

    В этом случае переопределение возвращает транзакцию с комиссией. Если при выводе средств лимит не превышен, метод возвращает транзакцию null . Это означает, что комиссия не взимается. Чтобы проверить эти изменения, добавьте следующий код в метод Main в классе Program :

    var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000); // How much is too much to borrow? lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance"); lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount"); lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs"); lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs"); lineOfCredit.PerformMonthEndTransactions(); Console.WriteLine(lineOfCredit.GetAccountHistory()); 

    Запустите программу и проверьте результаты.

    Сводка

    Если у вас возникли проблемы, изучите исходный код для этого руководства, размещенный в репозитории GitHub.

    В этом руководстве показаны разные методы, используемые в объектно-ориентированном программировании:

    • Вы использовали абстракции, когда определяли классы для каждого из типов счетов. Эти классы описывали поведение для каждого типа счета.
    • Вы использовали инкапсуляцию, когда сохраняли в каждом классе много сведений ( private ).
    • Вы использовали наследование, когда применяли реализацию, уже созданную в классе BankAccount для сохранения кода.
    • Вы использовали полиморфизм, когда создавали методы virtual , которые производные классы могут переопределить для создания определенного поведения для этого типа счета.

    Совместная работа с нами на GitHub

    Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *