Комментарии 45
Хороший разбор с первопричинами дает более ценный опыт.
Навскидку, конечно, со многим можно не согласиться, но как нибудь в другой раз)
От себя добавлю, что очень ценным в копилку понимания было то, что методы — это по сути тоже сахар, позволяющий связать класс с функциями, предназначенными исключительно для его обработки и модификации
То есть
class MyClassA{
private int a;
public void DoIt(int b, int c){...}
}
На самом деле
class MyClassA{
private int a;
private static void DoIt(MyClassA this, int b, int c){...}
}
А еще мое имхо — самые мощные средства связаны с полиморфизмом (хотя это и не свойство ООП) и с сахаром, предназначенным для сокрытия.
разве статический метод имеет доступ к нестатическим полям класса?
Я сам в свое время удивился. Думал, что приватные поля объекта могут употреблять только методы, из объекта вызываемые.
Оказывается, любой метод класса, неважно, откуда он был вызван (в т.ч. и статический) умеет менять поля любому попавшемуся объекту того же класса.
Оказывается, любой метод класса, неважно, откуда он был вызван (в т.ч. и статический) умеет менять поля любому попавшемуся объекту того же класса.
Статические методы имеют доступ только к статическим же полям, проперти и атрибутам.
Статический метод любого другого класса, принимающий этот же объект, в его приватные поля уже залезть не может.
Это псевдокод — иллюстрация.
В том и обяснение — нестатический метод первым параметром получает ссылку на сам класс — и именно поэтому имеет к ней доступ.
То есть ООП — это просто способ организации + сахар
Когда ты делаешь
var a = new MyClassA();
a.DoIt(1, 2);
Компилятор превращает в
var a = new MyClassA();
MyClassA.DoItStatic(a, 1, 2);
Где первый параметр автоматически именуется как this, который можно опускать.
В Python, кстати, self переменная задается явно
Абстрактно можно считать методы — простыми функциями, в которые вместе с параметрами передается ссылка на объект, от имени которого вызывается объект.
То есть вообще — речь о том, что ООП — это просто абстракция, сахар, нужная для организации. После компиляции по факту он превращается в обычный процедурный код. Соответственно ООП может быть сэмитирована через обычный процедурный код (привет С++ и линукс, написанный на С в объектном стиле)
Область видимости — такой же сахар, нужный чтобы ограничить программиста в использовании класса. Оно работает только до компиляции, чтобы сообщить «автор класса дал тебе публичный интерфейс — пользуйся им, чтобы работать с объектом безопасно». Приватные переменные — внутреннее состояние объекта, которое изолируется и защищается.
Это позволяет собственно делать самое полезное, что есть в ООП — разбивать код на понятные блоки, каждый из которых на своем уровне работает с простыми снаружи кусочками, сложными внутри.
Кстати статическому классу доступны приватные поля его класса через ссылку:
class Program
{
static void Main(string[] args)
{
var a = new A();
a.DoIt(1);
A.DoItStatic(a, 2);
}
}
public class A {
private int b = 1;
public void DoIt(int c) {
b = c;
Console.WriteLine(b);
}
public static void DoItStatic(A @this, int c) {
@this.b = c;
Console.WriteLine(@this.b);
}
}
Вот так работает
Правильно ли я понял, что научиться/понять ооп можно только делая относительно большой "бюрократический" проект?
У меня всегда были задачи скриптового уровня (хоть и при слиянии они частенько становились довольно многословными) поэтому всякий раз пытаясь приложить ооп к задаче я плевался и так и не понял зачем тратить столько сил.
"Научиться ООП" можно кучей разных способов.
Просто большинство из того что вам "предлагает" ООП начинает иметь смысл использовать только в определённых ситуациях.
И когда у вас большой проект, да ещё скажем над ним работают куча людей, да ещё и разбросанных территориально… Вот тогда начинаешь замечать что без всего этого "сахара" становится сложно.
Основное преимущество ООП перед процедурным программированием, пожалуй, — всё же полиморфизм, т.е. возможность функцией с одним и тем же именем делать разные вещи в зависимости от типа аргумента. Он нужен вовсе не только при строгой или статической типизации. В том же Матлабе (или в обычной математике) умножение — вполне себе полиморфная функция. В Common Lisp Object System, например, ООП понимается как возможность написания функций, поведение которых зависит от (рантайм) типов аргументов (и немножко есть наследование, но за… дцать лет все уже поняли, что наследование, кроме интерфейсов, почти нигде не нужно).
Инкапсуляцию тоже сейчас часто рассматривают не с точки зрения сокрытия полей/свойств/методов, а с точки зрения объединения комплекса взаимозависимых операций в рамках одного метода (в Python, например, сокрытия нет). В этом смысле это не совсем ООП-шная штука, но можно, с другой стороны, сказать, что даже когда в процедурном языке мы пишем структуры, определяем несколько интерфейсных процедур для них и дальше работаем только через этот интерфейс — это уже элемент ОО проектирования.
По моему разумению, это означает, что на языках, где есть указатели на функции, уже можно писать в ОО стиле.
А какой полиморфизм нельзя получить в процедурных языках программирования, можно полюбопытствовать?
Собственно части Линукса так написаны — на С, но в процедурной парадигме, с оформленными специально объектами (структуры + обрабатывающие их методы).
И С++ изначально был просто препроцессором для С, то есть по факту сахаром, который спокойно переводился в С код.
Инкапсуляция — сахар. Просто синтаксис для удобного связывания, и организации.
Области видимости — сахар. Аналог наследование может быть достигнут включениями или побайтовым совпадением структур (чтобы их подкладывать)
И только аналоги интерфейсов и виртуальных методов для полиморфизма реализуются сравнительно тяжело, с помощью ссылок на ссылки на функции и в этом духе.
ООП — реально про организацию кода и ООП языки — просто специально адаптированные под нужный сахар.
Так то мое ИМХО, что rust и go — вполне себе ООП френдли, потому что единственное, чего там нет, это наследования реализации (от которых и так рекомендуют отказываться все кому не лень), все остальные пункты — вполне.
Основное преимущество ООП перед процедурным программированием, пожалуй, — всё же полиморфизм
То, какие преимущества даёт или не даёт ООП лично вам, зависит в первую очередь от ваших задач и от ваших проблем. Кому-то полиморфизм вообще не интересен. А кому-то ООП создаст больше проблем чем решит.
В CLOS? Насколько мне известно, да, лишние проверки во время выполнения. Теоретически, стандарт не запрещает диспетчеризацию при компиляции, но на практике, кажется, нет таких реализаций. Но есть умельцы, которые не хотят с этим мириться.
Общего случая скорее нет. Важно то, что семантически появляется понятие "типа времени исполнения", в зависимости от которого выражение x.f()
будет приводить к выполнению разных наборов инструкций. Как это реализовано — второстепенный вопрос.
В языках со статической типизацией, как я понимаю, объект хранит указатель на таблицу виртуальных методов, из которой они вызываются уже по фиксированному смещению, которое определяется по имени метода на этапе компиляции. В каких-то случаях можно вывести, что виртуальный метод вызывается на типе, у которого нет подтипов, и подставить адрес статически или даже заинлайнить. Т.е. проверок типа в рантайме нет, просто безусловный переход на метод из таблицы.
В Python объекты — это словари, в них лежат как данные, так и ссылки на методы. Там, вроде бы, для поиска метода тип объекта не требуется, но поиск идёт уже в рантайме.
Задач не помню (но я и не фанат ООП, скорее тяготею к ФП). В "Алгоритмах" Седжвика примеры структур данных, куда ООП хорошо ложится.
Например, написать функцию общего вида для поиска корней на отрезке методом половинного деления.
Если язык не поддерживает first-class functions, то можно сделать интерфейс Callable
, который требует наличия метода call
, и бисекция пишется в стиле:
float find_root(Callable f, float a, float b) {
float fa = f.call(a);
float fb = f.call(b);
while (abs(b - a) < 1e-5) {
...
}
return answer;
}
Сами объекты, реализующие интерфейс Callable
, могут быть совершенно произвольными.
В коротких задачах ООП обычно не особо нужно, а вот для библиотечного кода упрощает жизнь.
А так, в не-ГУИ приложениях — например, решение нелинейных дифуравнений методом Рунге-Кутты.
Если уравнение приводимо в явном виде к такой форме
dy/dx=f(x,y),
то пишете базу данных EqSolver, куда пихаете все коэффициенты, входящие в функцию f(x,y).
Далее
EqSolver solver=new EqSolver()
solver.coeff1=…
solver.coeff2=…
… так делаете все коэффициенты
1) У вас будет метод f=solver.f(x,y), внутри он будет обращаться к coeff1, coeff2… и возвращать значение
2) У вас будет универсальный метод расчета 1й итерации который вычисляет значение функции f как f=this.f(x,y) вместо f=f(x,y,coeff1,coeff2,,,,)
3) У вас будет универсальный метод, который вызывает в цикле метод расчета одной итерации, в итоге получается массив y по заданному массиву х и начальному значению y_start
При этом во всех методах не нужно будет писать множество аргументов, только что-то типа y=solver.Solve(x, y_start), внутри он вызывает y_next=this.MakeStep(x,y_previous), а MakeStep внутри вызывает f=this.f(x,y).
Хотя может это тоже профдеформация матлабом.
будет как раз кошмар вида coeff=[ад на 10 строчек через запятую].
Нелинейные дифура внутри требуют вычисления подфункций, в них тоже передаются аргументы, так что перепаковывать вот этот массив coeff для скармливания нижестоящему коду будет очень болезненно.
solver.coeff1=k1;
solver.coeff2=k2;
чем запихать в массив coeff=[k1,k2...kn] и скормить функции этот coeff, и дальше его сверху вниз передавать f1(f2(f3(...?
1) имена коэффициентам присвоены. Это удобно, если они разные по физическому смыслу.
2) Внутри функций можно обратиться к коэффициентам по имени.
3) Если захочется добавить новые коэффициенты, то все функции, которые с ними (новыми коэффициентами) не работают, просто остаются, какими были. Дописать коэффициенты «в хвост» — представьте, что они у вас физически разные по смыслу и перемешаются в ходе «апдейтов с дописыванием к концу массива».
2) Ад через точку с запятой в самом начале будет по смыслу совершенно понятными, 1 раз написал и все.
Т.е. вы присваиваете нормальные имена всем переменным, пихаете их в базу данных, дальше внутри функции вытаскиваете по имени все переменные, какие надо. Если толком непонятно, как точно будет устроен код, вы называете по имени все переменные, какие вам кажутся нужными и пишете кусок. Потом еще приписываете кусок. Т.е. код пишется как бы по кускам…
'Вот кусок класса (делал на VBA в экселе)
'для расчета нелинейной электрической цепи
Public R_load, L_load, R2, L2 As Double 'Это сопротивление и индуктивность
Public Im, Iexp, omega, phi, Tp As Double 'это параметры источника тока
Public FLmax, k As Double 'это параметры, отвечающие за нелинейную характеристику
Public w1, w2 As Double 'это коэффициент трансформации (моделировался трансформатор)
Private this_curveCoeffs As CurveCoeffs 'это коэффициенты в относительных единицах, отвечающие за нелинейную характеристику
т.е. для себя делаю вывод, что это сахар для упрощения правки кода, чтоб не переписывать.
Да, ООП в какой-то степени это сахар для правки кода и как бы поблокового написания.
Удобно, когда толком не знаешь, как все будет работать целиком, но знаешь, как должны работать отдельные куски.
Тогда в ООП-стиле эти куски пишутся (при этом идет обращение к одной структуре с данными), далее додумывается код остальной части программы (в структуру с данными дописываются поля), при этом правка уже написанного минимальная.
mex-файлы — для симулинка, имеется в виду?
Я с симулинком раньше возился-возился, потом проклял его тормознутость, и стал моделировать почти все самописным кодом.
Я бы дальше пошёл здесь.
Сделал бы класс Solver
, в котором есть член rhp
, который отвечает за правую часть. У rhp
есть метод call(x, y)
, который вызывает собственно функцию, и сколько-то полей, отвечающих за параметры. Примерно так:
classdef Circuit
properties
...
end
methods
function dydx = call(self, x, y)
...
end
end
end
classdef ODESolver
properties
rhp
y_start
y_prev
y_next
x_start
x_prev
x_next
end
methods
function solver = ODESolver(rhp, x_start, y_start)
solver.rhp = rhp
solver.x_start = x_start
solver.y_start = y_start
solver.x_prev = x_start
solver.y_prev = y_start
end
function solution = solve(self, nsteps, dx)
...
end
...
end
end
Использование в стиле
circuit = Circuit
circuit.coeff1 = coeff1
...
solver = ODESolver(circuit, 0.0, y_start)
solution = solver.solve(1000, 0.01)
Профит в том, что ODESolver
больше не отвечает за вычисление самой правой части, это забота rhp
внутри него.
Лично меняя ООП очаровало именно философией, когда программа есть совокупность объектов, обменивающихся сообщениями (т.е. генерирующих события).
А наследование, полиморфизм и прочую атрибутику ООП я уже потом освоил.
И конечно полиморфизм, иначе это просто структуризация, а далее «визитор», множественная диспетчеризация.
Всё думал, что ближе к концу будет какое-то откровение… Ан нет.
Путь к ООП: Взгляд инженера