ООП в F#

Привет, Хабр!
Объектно-ориентированное программирование представляет собой подход к разработке, где основой являются объекты — экземпляры классов, объединяющие в себе и данные, и поведение. В F#, который вырос на функциональных концепциях, ООП представлен как дополнение к уже существующему функционалу. F# позволяет использовать классы, наследование и интерфейсы, при этом так, что ООП элементы просто идеально вписываются в функциональный контекст, не создавая ощущения чужеродности.
Основы ООП в F#
Классы и поля
Классы в F# — это структуры, которые позволяют объединить поля и методы, определяющие состояние и поведение объекта соответственно. Создание класса в F# начинается с ключевого слова type, за которым следует имя класса и его конструктор:
type MyClass(param1 : int, param2 : string) =
// поля
let mutable field1 = param1
let field2 = param2
// свойство
member this.Property1 = field1
member this.Property2 with get() = field2 and set(value) = field1 <- value
// метод
member this.Method1() =
printfn "Method1 called with field1: %d and field2: %s" field1 field2
MyClass с двумя полями: field1 и field2, они инициализируются через параметры конструктора. Также присутствуют два свойства Property1 и Property2, где Property1 является только для чтения, а Property2 — для чтения и записи, что демонстрирует использование свойств в F# для контроля доступа к данным класса. Метод Method1 печатает значения полей.
let привязки в классе используются для объявления полей или функций, доступных только внутри класса.
do привязки выполняют код инициализации при создании экземпляра класса.
type MyClass(x: int, y: int) =
let sum = x + y
do printfn "Сумма x и y равна %d" sum
member this.Sum = sum
Конструктор инициализирует поля и выполняет дополнительное действие
Можно использовать самоидентификаторы для обращения к текущему экземпляру класса:
type MyClass(x: int, y: int) as self =
member self.Sum = x + y
member self.Multiply = x * y
as self позволяет обратиться к текущему экземпляру класса внутри его методов.
Интерфейсы
Интерфейсы определяются с использованием ключевого слова type с указанием ключевого слова interface и перечислением методов и свойств без их реализации. Каждый метод или свойство в интерфейсе является абстрактным, определяя форму без конкретной реализации:
type IExampleInterface =
abstract member ExampleMethod : string -> string
abstract member ExampleProperty : int with get
IExampleInterface определяет интерфейс с одним методом ExampleMethod, принимающим строку и возвращающим строку, и свойством ExampleProperty только для чтения типа int.
Реализация интерфейса указывается в теле класса с использованием ключевого слова interface с последующим ключевым словом with, за которым следуют реализации методов и свойств интерфейса:
type ExampleClass() =
interface IExampleInterface with
member this.ExampleMethod(input) = "Processed: " + input
member this.ExampleProperty with get() = 42
ExampleClass реализует IExampleInterface, предоставляя конкретные реализации для ExampleMethod и ExampleProperty. Это позволяет объектам ExampleClass быть использованными там, где ожидается IExampleInterface.
Наследование и абстрактные классы
Наследование позволяет создать новый класс на основе существующего, наследуя его свойства и методы:
try
let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)
DerivedClass наследует BaseClass, добавляя новый параметр param2 и сохраняя параметр param1 от базового класса.
Абстрактный класс в F# — это класс, который не может быть инстанцирован сам по себе и служит основой для других классов. Абстрактные классы могут содержать абстрактные методы (без реализации) и методы с реализацией:
[]
type Shape(x0: float, y0: float) =
let mutable x, y = x0, y0
abstract member Area: float
abstract member Perimeter: float
member this.Move(dx: float, dy: float) =
x <- x + dx
y <- y + dy
Shape определяет общие характеристики для фигур, такие как площадь и периметр, но не предоставляет их конкретные реализации, делая класс абстрактным.
Пример наследования от абстрактного класса:
type Circle(x: float, y: float, radius: float) =
inherit Shape(x, y)
override this.Area =
Math.PI * radius * radius
override this.Perimeter =
2.0 * Math.PI * radius
Circle наследует абстрактный класс Shape, предоставляя конкретные реализации для Area и Perimeter.
Управление исключениями и ошибками
Блок try...with используется для перехвата и обработки исключений. Код внутри блока try выполняется, и если в процессе его выполнения возникает исключение, исполнение передается в блок with, где исключение может быть обработано:
try
let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)
Исключение перехватывается в блоке with, и программа выводит соответствующее сообщение, вместо того чтобы аварийно завершиться
Блок try...finally используется для гарантии выполнения определенного кода после блока try, независимо от того, произошло исключение или нет:
try
let result = 10 / 2
finally
// код в этом блоке выполнится в любом случае
printfn "Этот код выполнится независимо от исключений."
Можно создавать собственные типы исключений для обработки специфических ошибочных ситуаций. Это делается путем наследования от класса System.Exception или любого другого встроенного исключения:
type MyCustomException(message: string) =
inherit System.Exception(message)
MyCustomException — это пользовательское исключение, которое принимает сообщение об ошибке в качестве параметра и передает его конструктору базового класса System.Exception:
let riskyOperation x =
if x < 0 then
raise (MyCustomException("Число не должно быть отрицательным"))
else
printfn "%d - это положительное число" x
riskyOperation генерирует MyCustomException, если ей передано отрицательное число, что позволяет точно указать тип ошибки и облегчить ее обработку.
Так же можно подключить логирование ошибок с помощью Serilog. Добавляем зависимость Serilog к проекту через NuGet:
open Serilog
Log.Logger <- LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/myapp.txt", rollingInterval = RollingInterval.Day)
.CreateLogger()
Log.Information("Запуск приложения")
try
let result = 10 / 0
with
| ex ->
Log.Error(ex, "Произошло исключение при выполнении операции")
В блоке try...with исключение перехватывается и логируется с использованием Serilog, предоставляя подробную информацию об ошибке.
Оптимизация
Инкапсуляция — это ООП-принцип, заключающийся в ограничении прямого доступа к некоторым компонентам объекта и управлении доступом к этим данным через методы:
type BankAccount() =
let mutable balance = 0.0
member this.Deposit(amount: float) =
if amount > 0.0 then
balance <- balance + amount
else
raise (ArgumentException("Сумма должна быть больше нуля"))
member this.Withdraw(amount: float) =
if amount <= balance && amount > 0.0 then
balance <- balance - amount
else
raise (InvalidOperationException("Недостаточно средств или сумма меньше нуля"))
member this.GetBalance() = balance
Здесь юзаем инкапсуляцию для контроля изменений баланса банковского счета через методы Deposit и Withdraw, предотвращая прямой доступ к полю balance
Наследование позволяет создать новый класс на основе существующего, переиспользуя его код и расширяя функциональность:
type Vehicle() =
abstract member Move: unit -> string
type Car() =
inherit Vehicle()
override this.Move() = "Едет на четырех колесах"
type Bicycle() =
inherit Vehicle()
override this.Move() = "Едет на двух колесах"
Car и Bicycle наследуют от Vehicle и реализуют абстрактный метод Move
Полиморфизм используется для создания интерфейсов и абстрактных классов, которые могут быть реализованы множеством различных классов:
type IDrawable =
abstract member Draw: unit -> unit
type Circle() =
interface IDrawable with
member this.Draw() = printfn "Рисуется круг"
type Square() =
interface IDrawable with
member this.Draw() = printfn "Рисуется квадрат"
let drawShapes shapes =
shapes |> List.iter (fun shape -> shape.Draw())
Circle и Square реализуют интерфейс IDrawable, и так можно обрабатывать их одинаково в функции drawShapes.
ООП позволяет создавать структурированные и легко поддерживаемые системы. А больше про ООП и ЯП в целом можно узнать в рамках экспертных курсов по программированию от моих друзей из OTUS.
