Как стать автором
Обновить

Путь к ООП: Взгляд инженера

Время на прочтение23 мин
Количество просмотров14K

Дисклеймер


Статья не предполагает какой-то принципиально новый взгляд на вещи, кроме как с точки зрения изучения этого материала с «абсолютного нуля».


Материал основан на записях примерно 7-летней давности, когда мой путь в изучении ООП без IT-образования только начинался. В те времена основным языком был MATLAB, много позже я перешел на C#.

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

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

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

Насколько это соответствует действительности и вашим собственным предпочтениям, — решайте сами…

Предпослылки к ООП


Код стеной


Когда я только начинал писать на MATLAB’e, то только так писать и умел. Я знал про функции и про то, что программу можно делить на части.

Затык был в том, что все примеры были отстой. Я открыл чей-то курсовик, увидел там мелкие бодяжные функции по 2-3 строчки, в сумме все это НЕ работало (не хватало чего-то), и заработало только тогда, когда я пересобрал эту дрянь в «стену».

Потом я еще несколько раз писал какие-то мелкие программки, и всякий раз недоумевал, зачем там что-то делить. Уже потом пришло понимание: код «стеной» — это нормальное состояние программы объемом примерно 1.5 страницы А4. Никаких функций и, боже упаси, ООП там НЕ нужно.

Вот так примерно выглядит матлабовский скрипт (взято из интернета).

Fs = 1000;                   % Sampling frequency
T = 1/Fs;                      % Sample time
L = 1000;                      % Length of signal
t = (0:L-1)*T;                % Time vector
% Sum of a 50 Hz sinusoid and a 120 Hz sinusoid
%x = 0.7*sin(2*pi*50*t) + sin(2*pi*120*t); 
%y = x + 2*randn(size(t));     % Sinusoids plus noise
y=1+sin(100*pi*t);
plot(Fs*t(1:50),y(1:50))
title('Signal Corrupted with Zero-Mean Random Noise')
xlabel('time (milliseconds)')
figure
NFFT = 2^nextpow2(L); % Next power of 2 from length of y
Y = fft(y,NFFT)/L;
f = Fs/2*linspace(0,1,NFFT/2+1);
% Plot single-sided amplitude spectrum.
plot(f,2*abs(Y(1:NFFT/2+1))) 
title('Single-Sided Amplitude Spectrum of y(t)')
xlabel('Frequency (Hz)')
ylabel('|Y(f)|')

Деление кода на функции


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

Но фишка в другом – почему-то все обучающие материалы молчат о том, СКОЛЬКО у функции переменных…

Курс математики говорил о том, что функция — это y=f(x)

Это называется «функция одной переменной». Например, y=x2 это целая ПАРАБОЛА!
Задача по математике: построить ПАРАБОЛУ по точкам. В тетрадном листе, в клеточку.
А еще бывают функции двух переменных. z=f(x,y). И для нее — о боже — можно построить ТРЕХМЕРНЫЙ график. Но мы его строить не будем, т.к. на следующем уроке будет контрольная работа. На ней мы будем строить ПАРАБОЛУ.


А Сидоров не аттестован

А потом еще один товарищ, учащийся в ВУЗе по специальности «прикладная математика», рассказывает про функции трех переменных. Для такой функции, говорит он, надо построить ЧЕТЫРЕХМЕРНЫЙ график. Но четвертое измерение – это время. И мы можем только видеть проекции четырехмерного мира на трехмерное пространство.

И далее он торопливо говорит про четырехмерный куб-тессеракт…

image

А если функция имеет четыре и более переменных…. Теория суперструн. Многообразие Калаби-Яу. Смертным. Не дано. Понять…

Короче говоря, это все не то. В программировании нормальное состояние функции – это double vaginal double anal. Она принимает 100 переменных и возвращает столько же, и это нормально. Ненормально другое – перечислять их через ЗАПЯТУЮ.

image

Про то, что можно писать как-то иначе, я понял, когда наваял ВОТ ЭТО


function work = SelectFun(ProtName,length_line,num_length,angleN_1,angleN_2,num_angleN,angleF_1,angleF_2,num_angleF, res_max, num_res,varargin)
global angleF angleN model_initialized

Куча переменных через ЗАПЯТУЮ. А вызывающий код имеет совсем другие названия этих параметров, что-то типа SelectFun(a,b,c,d….) Поэтому нужно запоминать, на каком месте какая переменная стоит. И делать их расстановку через ЗАПЯТУЮ. А если код модернизируется, и количество переменных меняется, то надо их снова расставлять через ЗАПЯТУЮ.

А зачем в этом убожестве были глобальные (расстрелять!) переменные?

Бинго! Чтобы не расставлять переменные при каждой модернизации кода через ЗАПЯТУЮ.

Но ЗАПЯТАЯ все равно преследовала меня, как в кошмарном сне.

image

И появился varargin. Это значит, что я могу в вызывающем коде дописать еще много аргументов через ЗАПЯТУЮ…

И тогда я подумал о массивах. Учебные примеры взахлеб рассказывали о том, что массив может быть таким:


Х=
[1 2 3
 4 5 6
 7 8 9]

И понимаете, Х(2,3)=6, а Х(3,3)=9, и мы… мы можем организовать на таких массивах перемножение матрицами! На прошлом уроке мы проходили ПАРАБОЛЫ, а теперь МАТРИЦЫ….

И ни в одной строчке этих охренительных учебников нет короткого и ясного: массивы нужно для того, чтобы сделать функцию 100 переменных и не упасть от их перечисления через ЗАПЯТУЮ.

image

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


angles =
[angleN, angleN_1, angleN_2, num_angleN
 angleF, angleF_1, angleF_2, num_angleF]

function work= SelectFun(ProtName, length_line, num_length, angles , res_max, num_res, varargin)

Но хотелось большего. И начало получаться что-то вроде такого:


data=
[angleN, angleN_1, angleN_2, num_angleN
 angleF, angleF_1, angleF_2, num_angleF
length_line, num_length,  0, 0 
res_max,num_res, 0,0]
function work= SelectFun(ProtName,data,varargin)

И все вроде замечательно, но… НУЛИ! Они появились оттого, что я хотел раскидать по разным строкам разнородные данные, и количество данных разного типа было разным… А как функция должна эти нули обрабатывать? А что будет, если я захочу модернизировать код? Я же должен буду переписать обработчик этих поганых нулей внутри функции! Ведь какая-то из переменных может реально быть равной нулю…

I never asked for this…

В общем, так я узнал о СТРУКТУРАХ.

Структуры


Это то, с чего надо было начать изложение про способы упаковки данных. Массивы «таблицей», видимо, исторически возникли первыми, и про них пишут тоже – в начале. На практике можно встретить полно программ, где массивы «таблицей» либо одномерные, либо их нет совсем.
Структура – это «файло-папочная» упаковка данных, примерно на жестком диске компьютера.
Диск D:\
Х (переменная-папка – «объект» или «структура»)
— a.txt (переменная-файл с данными – «поле объекта», англ. field. Хранится число 5)
— b.txt (хранится число 10)
— с.txt
Y (переменная-подпапка – «объект»)
— d.txt (хранится число 2)
— e.txt

Чтобы было понятней, запишем, как мы бы видели путь к файлу d.txt в проводнике Windows
D:\X\Y\d.txt

После этого мы открываем файл и пишем туда число «2».
Теперь – как это будет выглядеть в программном коде. Там нет нужды ссылаться на «корневой локальный диск», поэтому D:\ там просто отсутствует, также у нас не будет расширения файла. Что же касается остального, то вместо слэша \ в программировании обычно используется точка.
Получается вот так:


X.Y.d=2
%Остальные переменные задаем аналогично
X.a=5
X.b=10 
Теперь что-то посчитаем
X.c=X.a+X.b    %т.е.  Х.с=5+10=15
X.Y.e=X.c*X.Y.d    %т.е. X.Y.e=15*2=30

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

Структура это более крутой родственник МАССИВА ТАБЛИЦЕЙ, где вместо индексов — файло-папочная система. Структура = «переменная-папка», в которой лежат «переменные-файлы» и другие «переменные-папки» (т.е. как бы подпапки).

Все знакомо, все ровно так же как на компе, папки, в них файлы, только в файлах не фотки, а цифры (хотя и можно и фотки).

Это более продвинутая версия хранения данных для передачи в ФУНКЦИЮ по сравнению с идеей сделать МАССИВ ТАБЛИЦЕЙ, в особенности двумерной, и, задави меня тессеракт, трех- и более- мерной.
МАССИВ ТАБЛИЦЕЙ юзабелен в двух случаях:
— он маленький (зачем он тогда? Что, нельзя передать в функцию аргументы через запятую?).
— либо по нему можно сделать цикл и автоматизировать поиск/заполнение (это не всегда возможно)
В реальности МАССИВ ТАБЛИЦЕЙ обычно используется только как одномерная строка однородных данных. Все остальное в нормальных программах делается по «файло-папочной» схеме.

Тогда почему в учебниках по программированию начинают с массивов таблицами?!!!

Короче, «открыв» для себя структуры, я решил, что нашел золотую жилу и срочно переписал все нахрен. Говнокод стал выглядеть как-то вот так:


Data.anglesN=[angleN, angleN_1,angleN_2, num_angleN]; %пусть пока так
Data.anglesF=[angleF, angleF_1, angleF_2, num_angleF]; %пусть пока так
Data.length_line= length_line;
Data.num_length= num_length;
Data.res_max= res_max;
Data.num_res= num_res;
function work= SelectFun(ProtName,Data,varargin)

Да, можно здесь заняться перфекционизомом и сделать кучу вложенных объектов, но задача не в этом. Главное – что теперь внутри функции переменная индексируется не по порядковому номеру (на каком месте в списке аргументов через ЗАПЯТУЮ она стоит), а по имени. И нет никаких тупых нулей. И вызов функции теперь приемлемого вида, ЗАПЯТЫХ всего 2 штуки, можно выдохнуть спокойно.

Классы


Понятие «класс» обрушил на меня тонну терминологии: инкапсуляция, наследование, полиморфизм, статические методы, поля, свойства, ординарные методы, конструктор… #@%!!!..
По неопытности, разобравшись со структурами, я решил, что незачем усложнять сущности без надобности, и подумал – «классы – это типа тех же структур, только посложнее».

В какой-то степени так оно и есть. Точнее это прямо в точности так и есть. Класс, если очень глубоко смотреть – это СТРУКТУРА (идейный потомок массива таблицей), которая создается ПРИ ЗАПУСКЕ программы (вообще бывает вроде бы и не только при запуске). Как и в любом потомке МАССИВА ТАБЛИЦЕЙ, там хранятся данные. К ним можно получить доступ во время работы программы.

Поэтому мой первый класс был примерно таким (пишу пример на C#, в матлабе статические поля нормально не реализуются, только через кривой «хак» c persistent переменными в статической функции).

public class Math{
	public static double pi;
	public static double e;

	public static double CircleLength(double R){   //Т.н. «статический метод»
	return 2*Math.pi*R; //вычисляем длину окружности
    }
}

Вышеприведенный случай – это как бы «базовое» умение класса – быть тупо массивом (структурой) с данными. Эти данные в него закидываются при запуске программы, и оттуда их можно извлечь, в точности так же, как мы вытаскивали их из структуры выше. Для этого используется ключевое слово static.

Структура ->создается где угодно и хранит данные, которые вводятся в нее когда угодно

Класс -> это такая структура, которая создается при запуске программы. Все поля, отмеченные словом static, просто хранят данные, как в обычной структуре. Статические методы – это просто функции, которые вызываются из класса, как из папки.


double L=Math.CircleLength(10); //L=62,8
Math.pi=4; //трололо

У меня был затык – если поля это переменные, а методы это функции, то как они хранятся в одном месте? Как я понял, функция (метод) в классе – это на самом деле не функция, а указатель на функцию. Т.е. это примерно такая же «переменная», как число пи, в плане работы с ней.
Короче говоря, я вначале классы понял именно в таком объеме и написал еще порцию говнокода, где использовались ТОЛЬКО статические функции. Иначе как папку с функциями я классы вообще не юзал.

Еще этому моменту способствовал тот факт, что именно так в MATLABе классы и делаются — как такая дурацкая папка, название которой начинается с @ (типа @ Math, без пробела), внутри нее взаправдашними файлами с расширением .m лежат функции (методы) и есть заголовочный файл с расширением .m, где объясняется, что функция CircleLength действительно принадлежит классу, а не является просто закинутым туда .m-файлом с не-ООП функцией.
@ Math % папка
— Math.m %файл заголовка
— CircleLength.m %файл с функцией
Да, там есть более привычный нормальному человеку способ писать класс в одном .m – файле, но поначалу я про это не знал. Статические поля в матлабе только константные, и прописываются 1 раз при запуске программы. Вероятно для того, чтобы сделать защиту от «тралла», который решит присвоить Math.pi=4 (имхо, абсолютно бесполезная и тупая тема, никакой нормальный человек большой проект в матлабе писать не будет, а маленький проект программист отладит и так, вряд ли он совсем идиот).

Но вернемся к теме. Кроме статических методов, в классе имеется еще и конструктор. Конструктор – в общем-то это просто функция вида y=f(x) или даже y=f(). Входных аргументов у нее может не быть, выходной обязательно есть, и это всегда новая структура (массив).

Что делает конструктор. Он просто делает структуры. Логически это выглядит примерно так:

Код на C# Примерный логический эквивалент (псевдокод)

class MyClass {
    int a;
    int b;
    public  MyClass() {
	this.a=5;
	this.b=10;
    }
}


class MyClass {
    public  static MyClass MyClass() {
        int this.a=5;
        int this.b=10;
        return this;
    }
}


//… где-то в другом месте
var Y=new MyClass();	


//… где-то в другом месте
var Y= MyClass.MyClass();	



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


function Y=MyClass() %необязательно называть MyClass, можно просто Y=F()
    Y.a=5
    Y.b=10
end
… где-то в другом месте
Y=MyClass()

И на выходе имеем структуру
Y (переменная-папка)
— a (переменная-файл, равна 5)
— b (переменная-файл равна 10)
Отсюда, собственно, видно, что так называемые поля класса (не статические, без ключевого кода static) — это локальные переменные, объявляемые внутри функции — конструктора. То, что они за каким-то лешим пишутся не в конструкторе, а снаружи, есть СИНТАКСИЧЕСКИЙ САХАР.

СИНТАКСИЧЕСКИЙ САХАР — такие bullshit-фичи языка программирования, когда код начинает выглядеть, как будто его хотят обфусцировать прямо при написании. Но зато он становится короче и быстрее (якобы) пишется.

Сделав это «открытие», я, писавший в то время только на матлабе, несказанно удивился.

В матлабе, как уже писал выше, эти структуры можно создавать на месте, без всяких конструкторов, просто написав Y.a=5, Y.b=10, точно так же как вы в операционной системе можете делать файлы и папки не отходя от кассы.

А тут — какой-то бодяжный «конструктор», и все поля структуры (в матлабе они называются properties — свойства, хотя, строго говоря, свойства — это более мутная вещь, нежели поля) нужно бюрократически прописывать в заголовочном файле. Зачем? Единственная польза, которую я тогда видел в этой системе — то, что поля структуры определены заранее, и это как бы «самодокументация» — всегда можно посмотреть, что там должно быть, а чего быть не может. Вот примерно такую лажу я тогда писал:


classdef MyClass
    properties %Здесь отдельно указываются свойства
        a
        b
    end
    methods %здесь методы
        function Y=MyClass() % это конструктор. 
        %Он просто делает структуру (массив) Y с полями a, b
            Y.a=5;
            Y.b=10;
        end
    end
    methods (Static) %здесь статические методы
        function y=f(x) % это конструктор
            y=x^2; %ЕСЛИ МНОГО РАЗ ЭТО ЗАПУСТИТЬ, ТО БУДЕТ ПАРАБОЛА !11
        end
    end
end

Т.е. вы все правильно поняли: методы только статические, конструктор хз для чего (написано в документации — О, классы должны иметь конструктор — ну вот вам конструктор), все остальное я тупо не знал и решил, что познал дзен и ООП.

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

Бюрократия


И уперся в такую вещь. Есть набор функций какого-то нижнего уровня логики (они статические и распиханы по классам-папкам, сейчас названия классов опустим):


Y1=f1(X1);
Y2=f2(X2);
Y3=f2(X3);
…
Y20=f20(X20);

В маленьких проектах добиться такого засилья функций невозможно, учебные примеры вообще содержат 2-3 функции — типа «смотри, как мы можем строить ПАРАБОЛУ».

А тут — фигова туча функций, и у каждой, мать ее, у каждой есть выходной аргумент, и что вот с ними всеми делать? Засовывать в функции более высокого («руководящего») уровня логики! Обычно их гораздо меньше (условно, 5 шт. вместо 20). Т.е. условно, нужно вот как-то взять эти Y1,Y2, Y3….Y20 и ПЕРЕПАКОВАТЬ их в какие-то Z1,Z2…Z5. Чтобы потом можно было сделать заседание партии и на нем:


A1=g1(Z1);
A2=g2(Z2);
…
A5=g5(Z5);
%Цели ясны, задачи определены. За работу, товарищи!

Но Z1…Z5 не берутся сами по себе. Для их создания нужны ФУНКЦИИ-ПЕРЕПАКОВЩИКИ. Условно они работают как-то так…


function Z1=Repack1(Y1,Y7, Y19)
    Z1.a=Y1.a+Y7.b*Y.19.e^2;
    Z1.b=Y7.c-Y19.e;
    %....еще миллион каких-то тупых действий с содержимым структур Y1, Y7, Y19 
    %в агонизирующих попытках сделать Z1. 
    %А еще нам надо сделать перепаковщих для остальных Z2…Z5, 
    %количеством 4 штуки. Мой моск!
end

А потом может быть еще один «руководящий» уровень…

Короче, я понял, что попал в логистический ад. Я не мог нормально извлекать данные из ФИГОВОЙ ТУЧИ мелких функций y=f(x) без написания еще ФИГОВОЙ ТУЧИ перепаковочно-бюрократических функций, а когда данные передаются еще на уровень выше, нужны еще ПЕРЕПАКОВЩИКИ. Итоговая программа забита бюрократизмом насквозь – перепаковщиков больше, чем «бизнес-кода». Классы-«папки-для-функций» не решают этой проблемы – они всего лишь собирают чиновных перепаковщиков-идиотов по кучкам.

А потом я решил такой говнокод модернизировать, и выяснилось, что без перепиливания всей бюрократической части это невозможно!

Прямо как жизнь в России…

Я понял, что делаю что-то не то, и разобрался в ООП получше. И решение — оно, если так смотреть, идейно было на поверхности.

Идея ООП


Зачем делать кучу функций вида y=f(x), выдающую РАЗНЫЕ выходные аргументы Y1….Y20, когда можно сделать ОДИН аргумент. Что-то вроде:


Y_all=f1(Y_all, X1); 
Y_all=f2(Y_all, X2);
….
Y_all=f20(Y_all, X20);

Тогда абсолютно все результаты работы функций будут засованы в одну структуру, в один массив, просто в разные его отсеки. Все. Дальше Y_all можно передавать сразу наверх, на верхний уровень «руководства».


Y_all=DO_MOST_IMPORTANT_SHIT(Y_all, options_how_to_do_this_shit)

Все-все-все функции-ПЕРЕПАКОВЩИКИ-БЮРОКРАТЫ идут в жопу! Все данные собираются в ОДНУ базу Y_all, все функции низкого уровня суют плоды своих трудов по разным отсекам Y_all, «руководство» шмонает по всем отсекам Y_all и делает то, что должно делать. Ничего лишнего, код пишется быстро и работает замечательно…

Вот именно в этом идея ООП и состоит. В учебниках пишут учебные примеры про яблоки и груши, а потом показывают программу в 5 строчек. Там вообще не нужно никакого ООП, в примерах на 5 строчек, т.к. передача данных на «высший уровень руководства» делается без проблем напрямую.

ООП нужно тогда, когда большой проект и есть проблема «бюрократизации»….
Но вернемся к сути. В реальном ООП есть СИНТАКСИЧЕСКИЙ САХАР. Приведенный выше пример с Y_all использовал просто структуры, функции f(,,,) будем считать статическими. ООП – это набор сахарка, когда код начинает выглядеть вот так:


Y_all.f1(X1); % а было Y_all=f1(Y_all, X1), 
Y_all.f2(X2); 
….
Y_all.f20(X20);
Y_all.DO_MOST_IMPORTANT_SHIT(options_how_to_do_this_shit);

Т.е. мы как бы решили навести мутный синтаксис, в котором можно не писать Y_all 2 раза, а сделать это только 1 раз. Ибо повторение — мать заикания.

Все остальное объяснение «как работает ООП» сводится к объяснению того, как функционирует синтаксический сахар.

Как функционирует синтаксический сахар ООП


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

Во-вторых, предусмотреть, желательно заранее, какие «отсеки» в ней будут. Пока база данных Y_all маленькая, такая постановка задачи вызывает раздражение. Хочется помечтать о «создаваемых на ходу классах», примерно так же, как в MATLAB можно делать структуры простыми командами Y.a=5, Y.b=10. Но желание фантазировать на эту тему пропадает после отладки здорового проекта.

Далее — вызов метода (функции).

Вот так это примерно эволюционировало
Функция Комментарий
Y=f(X) Так было в математике, когда мы строили по точкам ПАРАБОЛУ!
Х=f(X) Нас задрали бюрократы, и у нас одна партия один аргумент на все случаи жизни, хранящий в разных отсеках внутри себя все входные и выходные данные
f(X) Зачем функции возвращать аргумент? Это архаизм времен уроков математики! И бессмысленный расход памяти! Пусть данные передаются по ссылке, тогда функция сама придет к аргументу, поменяет и уйдет. НИЧЕГО=f(X)
Не гора идет к Магомету, а Магомет — к горе.
Х.f() Просто вытащили аргумент Х «наружу» синтаксическим сахаром. НИЧЕГО=Х.f(НИЧЕГО)


Теперь – как устроена внутри такая вот функция, принимающая НИЧЕГО и НИЧЕГО (ключевое слово void в C#) возвращающая.

Мне нравится, как это сделано в матлабе (с точки зрения именно понимания): функция, которую мы вызываем как X.f(), внутри пишется как
Пример кода на MATLAB Пример кода на C#

function f(this)
    %пример кода. 
    this.c=this.a+this.b;
end	


public void f() {
    this.c=this.a+this.b;
}

Переменную «по умолчанию» надо всегда писать самой первой. Обозвать ее — как угодно (можно Х, можно this, можно fuck, можно shit).
Я обычно в матлабе ее называю this, для единообразия.
Переменную «по умолчанию» писать не надо вообще. При обучении программированию может показаться, что ее нет (и это был для меня затык)!
Но она есть! Как тот самый суслик, она есть и скрыта в «ключевом слове this». «Переименовать» this нельзя (хотя это и к лучшему).


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


public void f(int user_input) {
    this.c=this.a+this.b + user_input;
}

Иногда надо даже возвращать аргумент (например, об успешности или неуспешности какой-либо операции), а не писать void. Что, впрочем, не меняет статистики: большинство функции в ООП возвращают НИЧЕГО (void) и принимают либо ничего (аргумент по умолчанию не в счет), либо очень мало аргументов.

Напишем итоговый код
На MATLAB


classdef MyClass<handle %наследование от handle нужно для передачи данных по ссылке
    properties %Здесь отдельно указываются свойства
        a
        b
    end
    methods %здесь методы
        function this=MyClass(a, b) % это конструктор. a, b - пользовательский ввод
            this.a=a
            this.b=b
        end
        function f(this)
            this.c=this.a+this.b
        end
    end
end
%Снаружи в каком-нибудь скрипте Untitled.m пишем
X=MyClass(5,10);
X.f();
fprintf(‘X.c=%d',X.c) %выведет Х.с=15

Теперь на C#:


public class MyClass {
    public int a;
    public int b;
    public MyClass(int a, int b) { // это конструктор. a, b - задаются (пользователем)		
        this.a=a;
        this.b=b;
    }
    public void f(this) {
        this.c=this.a+this.b
    }
}
//Снаружи в каком-нибудь скрипте пишем
MyClass X=new MyClass(5,10);
X.f();
Console.WriteLine(“X.c={0}”,X.c);  //выведет Х.с=15

Когда я разобрался с этим, то вроде бы большинство проблем с написанием кода отошло на второй план…

Свойства (properties) vs поля (fields)


Рассмотрим пример.
without properties with properties

MyClassA{
    int a; //это field (поле)

    public int Get_a(){
        return this.a;
    }    
     
    public void Set_a(int value){ 
    //тут можно накрутить какие-то 
    //проверки, например, что value>0
        if (value>0) this.a=value;
        else this.a=0; 
    }
}


MyClassA{
    int a; //это field (поле)

    public int A{
       get{return this.a;}
       set{ 
           if (value>0) 
               this.a=value;
           else 
               this.a=0; 
           }
    }
}


MyClass X=new MyClassA();
X.Set_a(5);
int b=X.Get_a();


MyClass X=new MyClassA();
X.A=5;
int b=X.A;

комментарий: aргумент
Set_a можно назвать как угодно
Set_a(int YourVarName)
комментарий: переменную внутри
set{...} нужно называть всегда value

Вещь это довольно удобная и часто используемая, но это все равно СИНТАКСИЧЕСКИЙ САХАР.
Field является полноценной переменной. Property — это 2 метода класса (get и set), синтаксис вызова которых копирует «вызов переменной».

На самом деле внутри get и set можно творить хрень:


int A {
    get{ return 0;}
    set{ Console.WriteLine("Ы"); }
}

Поэтому вроде как рекомендуется писать название properties с большой буквы, а fields — с маленькой.

Бывает (например, в интерфейсах нельзя создавать field), что надо сделать по-быстрому property, тогда можно:


int A { get; set;} //будет скрытая переменная, что-то типа _a
//при set и get будет прямое к ней обращение.
public int B { get; private set;} //так разделяется доступ  
//(читает кто угодно, заводит значение только метод своего класса)

Наследование, инкапсуляция, полиморфизм


Почему про них раньше не упоминал? Потому, что
— на самом деле, при написании кода они востребованы далеко не с такой силой, как о них упоминается при запросе «Ok Google, what is OOP». Я бы даже сказал, что поначалу они практически нахрен не нужны.
— там, где они нужны, о них можно прочитать (только ленивый про это дело не писал).
Когда идет процесс освоения навыков писания в ООП-стиле
— большинство классов у вас будут БЕЗ наследования. Вы просто запиливаете в нужном классе ВЕСЬ функционал, и наследовать что-то не особо и нужно.
— соответственно, полиморфизм (примочка к наследованию) тоже идет лесом
— «инкапсуляция» сведется к приписыванию везде (ко всем полям, свойствам и методам) public.
Потом у Вас прирастут руки к плечам, и Вы разберетесь сами, без этой статьи, где так делать НЕ надо, особенно где НЕ надо писать public.

Но все-таки краткий обзор на них.

Наследование. Это умный копи-паст


Ущербная реализация «наследования» выглядит так:
О, в моем говнокоде есть класс MyClass, и в нем не хватает еще одного поля SHIT и еще одного метода DO_THE_SHIT()!
*Ctrl+C, Ctrl+V
*Делается новый класс MyClass_s_fichami и туда дописываются желаемое
Но все-таки мы более цивилизованные люди, и знаем, что лучше не копировать текст программы, а сослаться на него.

Допустим, мы все равно пишем на каком-то древнем языке программирования или не в курсе о такой вещи, как «наследование». Тогда мы пишем 2 разных класса

public class MyClassA{ 
    public int a;
    public void F1(int x){
    //тут какой то код
        this.a=this.a*3;
    }
    public MyClassA(int a){ //конструктор
        this.a=a;
    }
}


public class MyClassB { 
    //спецполе
    private  MyClassA fieldA;
    //методы get и set маскируются под   
    //переменную a - т.н. property
    public int a{ 
        get { return fieldA.a; }
        set { this.fieldA.a=value; }
    }
    public int b;
    //делаем обертку для 
    //метода «предка»
    public void F1(int x){ 
       this.fieldA.F1();
    }
    public void F2(int x){
        //код новой фичи
        this.b=this.a*this.b;
    }
    //конструктор
    public MyClassB(int a, int b){ 
        this.fieldA= new MyClassA();
        this.a=a;
        this.b=b;
    }
}


//Где-то в другом месте
var X=new MyClassA(5);
X.F1(); // X.a станет равным 15
Console.WriteLine(X.a); //выведет 15	


//Где-то в другом месте
var X=new MyClassB(5,10);
X.F1();// X.a станет равным 5*3=15
X.F2();// X.b станет равным 15*10=150
Console.WriteLine(X.a); //выведет 15
Console.WriteLine(X.b); //выведет 150


То, что мы сделали справа — это и есть «наследование». Только в нормальных языках программирования это делается одной командой:


public class MyClassB : MyClassA { 
    //поле с объектом класса MyClassA не пишется, 
    //но оно доступно через ключевое слово base
    
    //поле a (точнее, свойство, property a) не пишется, 
    //но оно доступно в коде напрямую (см. пример в конструкторе ниже)
    public int b;
    public void F2(int x){ //код новой фичи
        this.b=this.a*this.b;
    }
    public MyClassB(int a, int b){ //конструктор
    //конструктор для поля base с объектом класса A 
    //перед всем кодом функции вызывается втихаря
        this.a=a;
        this.b=b;
    }
}

Работает «снаружи» код ровно так же, как в варианте 2. Т.е. объект как бы становится «матрешкой» — внутри одного объекта сидит тупо другой объект, и есть «каналы связи», дергая за которые, можно обратиться к внутреннему объекту напрямую.

Заголовок спойлера
image

В матлабе дело обстоит несколько интересней. Когда вы запускаете конструктор «потомка», MyClassB, то «тихушного» вызова конструктора предка MyClassA — не происходит.

Его нужно напрямую создать. С одной стороны это напрягает:


classdef MyClassB<MyClassA
    %тут код... 
    function MyClassB(a, b)
        this@MyClassA(a); %если этого не сделать, потомок будет «пустым»
        this.b=b;
    end
end

Но если потомок вызывается вообще с другими аргументами, типа MyClassB(d), тогда можно сделать внутри преобразование, что-то типа:


classdef MyClassB<MyClassA
    %тут код... 
    function MyClassB(d)
        a=d-5;
        this@MyClassA(a); 
        this.b=d+10;
    end
end

В C# так сделать напрямую нельзя, и это порождает необходимость писать какие-то «преобразовывающие функции»:


class MyClassB:MyClassA{
    //... тут код
    static int TransformArgs( int d) {return d-5;}
    MyClassB(int d):base(TransformArgs(d)) {this.b=d+10;}
}

или же делать «статические конструкторы» вот так:


class MyClassB:MyClassA {
    //... тут код
    MyClassB(){} //конструкторы все делаются без аргументов
    static MyClassB GetMyClassB(int d) {
        var X=new MyClassB(); //конструктор объекта запускается без аргументов
        //а потом ему насовывают
        Х.a=d-5;
        Х.b=d+10;
        return X;
    }
}

Вроде про наследование, в основном, все.

Естественно, что никто не заставляет обязательно писать у наследника метод «F1» и свойство (property) «a» так, чтобы они обязательно транслировались в вызов метода и поля предка. Трансляция – это просто поведение «наследования» по умолчанию.

Можно (естественно! это же другие методы в другом классе, бро) написать вот так:


public class MyClassB : MyClassA {
    public int a{ //обертка для поля предка
        get { return 0; }
        set { base.a=0; }//у самопального было бы this.fieldA.a=0;
    }
    public int b;
    public void F1(int x){ //делаем якобы обертку для метода «предка»
        //к объекту предка - base - не обращаемся
        Console.WriteLine(“МХАХАХА”);//Вместо запуска метода предка творим фигню
    }
}

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


… Концептуально это означет, что внутри объекта класса MyClassB в поле base сидит объект класса MyClassA, с возможностью трансляции управляющих команд снаружи. Обо всем этом написано выше и повторять смысла не имеет.

Есть такая тема с разными модификаторами доступа — public, private, protected… О них, что самое интересное, написано везде более-менее нормально, рекомендую просто прочитать об этом.
public — это будет означать, что field, property или method будут видны снаружи и за них можно будет дергать.
Если вы НЕ знаете, что делать, пишете public (вредный совет, да).

Потом найдите в себе силы и выкиньте этот public (ну или для наглядности замените на private) везде, где он лишний (сделайте «рефакторинг»). Да, разумеется, очень хорошо быть провидцем, сниматься в битве экстрасенсов и догадаться сразу, где надо сделать private.
private — это означает, что field, property или method «файло-папочного» объекта виден только изнутри методов данного класса.
НО… Именно класса, не ИНСТАНЦИИ (объекта). Если у вас есть код вида:


class MyClassA{
    private int a=10;
    public void DO_SOMETHING(MyClassA other_obj) { 
    //метод DO_SOMETHING способен присунуть        
    //в любое private поле первого попавшегося объекта класса MyClassA.
        this.a=100; //Как в поле своего объекта
        other_obj.a=100; //так и чужого
    }
}
var X=new MyClassA();
var Y=new MyClassA();
X.DO_SOMETHING(Y);  //Будет  X.a=100, Y.a=100

Такая штука используется в клонировании (подробнее см. другие источники).

Я пытался при написании кода думать об этой расстановке public и private. При черновых набросках кода на это тратится непозволительно много времени. А потом оказывается, что вообще сам код надо делать принципиально по-другому.

Если код пишется в соло, то нет смысла заморачиваться с private и public раньше времени, есть более важные задачи, например, собственно придумать и написать код…
Единственное место, где более-менее ясно, в каком месте ставить private и public — это те самые пресловутые свойства, которые ссылаются на какое-то поле.


class MyClassA{
    //вот тут private
    private int a; //"private" в C# по умолчанию можно не писать.
    //а вот тут public
    public int A {get{...;} set{...;}} //ссылаемся внутри на "а"
}

В остальных местах для расстановки public и private надо реально смотреть, что программа делает, и обучиться этому «заочно», скорее всего, не выйдет.
protected — это означает "public" для всех методов классов-наследников и "private" для всего остального.
В общем-то логично, если считать, что классы-наследники появляются как просто «более навороченные версии» предков.

Честно говоря, уже и забыл, где в явном виде я этот protected применял. Обычно либо public, либо private. Большинство классов, которые я писал, не наследовались ни от каких других пользовательских классов, а там, где наследовались, какая-то серьезная потребность в таких вещах возникала редко.

Впечатление такое, что НЕ-public модификаторы востребованы при работе над каким-то большим проектом, который, возможно будет поддерживаться кучей людей… Понимание того, где их применять, появляется только спустя большое время втыкания в километровой длины код. При обучении «заочно» как-то дать это понимание затруднительно.

Полиморфизм


Кода я писал на матлабе, я никак не мог допетрить, зачем вообще нужен полиморфизм и ЧТО ЭТО.
Потом, когда перешел на C#, дошло, что это фича СТРОГО ТИПИЗИРОВАННЫХ ЯЗЫКОВ, и к ООП она имеет весьма слабое отношение. В матлабе можно вообще везде писать, не зная о существовании этого полиморфизма – там нет строгой типизации.

Для простоты пусть классы называются А и В


class A{...}
class B:A{...}
A X=new B();
//Мы объявили x как A, но создали де-факто объект класса B. 
//Ну или вот так.
B x_asB=new B();
A x_asA=(A) x_asB;

Это называется приведение типов. В C# можно САМОМУ (если знать как) написать свои кастомные самопальные системы приведения типов, чуть ли ни каких угодно типов к каким угодно другим.
Тут — просто «приведение типов» из коробки. Раз внутри объекта x, принадлежащему к классу B, сидит другой объект класса A, то один из вроде как очевидных способов приведения — замкнуть все связи от внешнего объекта на внутренний.

На самом деле так делать вовсе необязательно, но те, кто придумал «полиморфизм», решили, что наиболее очевидно будет сделать именно так. А остальные варианты пользователь сам напишет.
Простите за (уже не совсем актуальную) «политоту» образца 2008-2012 гг.


сlass Путин {...}
class Медведев : Путин {...} 
Медведев медведев = new Meдведев (); //Внутри Медведева скрыт Путин
Путин путин = (Путин) медведев; //А так все становится очевидным

Интерфейс


Надо начать с того, как ЭТО применять.

Допустим, у нас есть списочек, и мы в него что-то хотим положить.

В матлабе наиболее просто сделать это так (называется cell array):


myList={1, ‘2’, ‘fuck’, ‘shit’, MyClassA(), MyClassB(), …. ,Лысый_Черт, Ваша_Бабушка};

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

Далее, допустим, вам нужно сделать цикл по списочку и сделать с каждым элементом что-то:


for i=1:length(myList)
      item=myList(i);
      % здесь мы что-то делаем с item-ом
      DoSomeStuff(item);
end

Если функция DoSomeStuff настолько умная, что переваривает все, что ей скармливают, этот код ВЫПОЛНИТСЯ.

Если функция DoSomeStuff (или ее автор) – интеллектом не блещет, то есть вероятность подавиться чем-то: цифрой, строкой, Вашим самопальным классом, Лысым Чертом или – не дай бог — Вашей Бабушкой.

MATLAB покажет красную ругань на английском в консоли и прекратит работу Вашей программы. Таким образом, Ваш код автоматически получает Премию Дарвина.

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

Именно поэтому (хотя и не только поэтому) на MATLAB – успел убедиться в этом сам (примерно как на КПДВ), на ужасных размеров коде — НЕЗАЧЕМ писать большие проекты.

Теперь переходим к C#. Мы делаем списочек, и… и нас просят сразу указать ТИП объекта. Мы создаем список типа List.

В такой список можно поместить число 1.

В такой список можно поместить число 2 и даже, прости господи, 3.


List<int> lst1=new List<int>().
lst.Add(1);
lst.Add(2);
lst.Add(3);

Но текстовые строки – уже нет. Объекты Вашего самопального класса – строго нет. Я молчу насчет Лысого Черта и Вашей Бабушки, они там не могут оказаться ни при каком варианте.

Можно сделать отдельно списочек строк. Можно – для ваших самопальных классов.


List<MyClassA> lst2=new List<MyClassA>();
lst2.Add(new MyClassA());

На самом деле можно сделать и списки – отдельно — Лысых Чертей, Ваших Бабушек.

Но сложить их в один список не получится. Ваш код получит Премию Дарвина в сочетании с руганью компилятора еще до того, как вы его попробуете запустить. Компилятор предусмотрительно не дает Вам сделать функцию DoSomeStuff(item), которая «подавится» своим аргументом.

В больших проектах это реально удобно.
Но что делать, когда в один списочек сложить все-таки хочется?

На самом деле, это не проблема. Достаточно преобразовать все к типу object. Почти (или даже абсолютно) все можно преобразовать к типу object.


List<object> lst=new List<object>();
lst.Add((object) new MyClassA());
lst.Add((object) new MyClassB());

Проблема начинается тогда, когда мы начинаем делать цикл по списку. Дело в том, что тип object ничего (почти) не умеет делать. Он умеет только БЫТЬ типом object.
— Что вы умеете делать?
— я умею петь и танцевать
— А я — Санчо…
— Что ты умеешь делать, Санчо?
— Я — Санчо.
— Ну ты можешь делать хоть что-то?
— Вы не понимаете. Я могу быть Санчо.

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

В нашем случае это те методы и свойства, которые обеспечивают НОРМАЛЬНУЮ работу функции DoSomeStuff(item). Сами свойства интерфейс при этом не реализует. Это специально так сделано. На самом деле можно было бы просто унаследоваться от какого-то класса, пригодного для употребления функцией DoSomeStuff(). Но это означает дополнительный код и забывчивого программиста.

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


interface ICanDoTheStuff {...};
class MyClassA: ICanDoTheStuff {…}
class MyClassB: ICanDoTheStuff {…}
static void DoSomeStuff(ICanDoTheStuff item) {…}

List<ICanDoTheStuff> lst= new List<ICanDoTheStuff>();
lst.Add(new MyClassA());
lst.Add(new MyClassB());

for (int i=0; i<lst.Count; i++) {
      ICanDoTheStuff item=myList[i];
      DoSomeStuff(item);
}

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

Интерфейс — это «бюрократия». Не везде она есть и не везде она нужна, хотя да, в больших проектах нужна и полезна.

… в общем, как-то так… Извиняюсь за резковатые выражения, мне почему-то кажется, что «сухое» изложение материала было бы неудачным…
Теги:
Хабы:
Всего голосов 16: ↑10 и ↓6+7
Комментарии45

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань