Об'єкти і трохи про класах в Powershell 5.0

image

Напередодні випуску Windows 10 і нової, п'ятої, версії Powershell, хочу поговорити з вами про одному з найбільш серйозних нововведень цієї мови — про класах. Розпочати нашу розмову мені бачиться доречним з екземплярів класу — об'єктів — є безумовно кілер-фичей мови сценаріїв для Powershell. Простота і лаконічність спрощеного об'єктно-орієнтованого підходу в мові автоматизації завдань підкорила не лише велику, здавалося б, черству, подібно 16-bit legacy, корпорації, але і користувачів альтернативних операційних систем.

«Спрощеним» об'єктно-орієнтованим я його назвав навмисне і хочу звернути на це вашу увагу. Об'єктно-орієнтовані мови програмування припускають ряд сутностей, таких як клас(тип), екземпляр класу, властивості і методи цього примірника, частіше званого об'єктом. Powershell ж, вправно оперуючи об'єктами та їх властивостями, практично повністю позбавлений методів і абсолютно повністю користувацьких типів об'єктів (класів). З часто використовуваних методів у голову приходять мабуть лише .trim() так .ToString(). Якщо дати ще хвилинку на парсинг дампа досвіду написання скриптів на Powershell, спливе ще щось про Get-WMIObject.

Пропоную освіжити в пам'яті створення об'єкта в Powershell, хоча і для першого знайомства буде відмінно.

Шлях перший — Православний
Характерною особливістю Powershell, можна сказати його почерком, який легко впізнається навіть з десяти кроків, є його багатослівність. На перших порах це навіть трохи дратує і навколо монітора прилипають стікери зі скороченнями з символами: «gci, gc, gwmi, %, ?» і сокровенним — «ls alias:» (перегляд всіх аліасів). Трохи пізніше трохи відпускає і замість пубертантній "?" починають з'являтися хоч і не «Where-Object», але вже досить впевнений «Where». Пізніше, коли кількість рядків коду перевалює за десятки тисяч, а написаних скриптів за сотні, приходить розуміння, що багатослівність мови позитивно позначається як на швидкості читання самого скрипта, так і на якості його підтримки колегами. У цей момент в улюбленому редакторі Ruler зміщується з 80 символів до 200, а за старим скриптам пускається скрипт автозаміни. Хм, здається я відволікся.

Отже, повернемося. Перший спосіб створення об'єкта, як і весь Powershell, багатослівний, але це його плюс. Всі слова прості, англійські і для людини перший раз в очі бачить цю мову, загалом-то, зрозумілий у контексті мови:

$Name = 'Name'
$CustomObject = New-Object-TypeName PSObject
$CustomObject | Add-Member-MemberType NoteProperty-Name Name Value $Name
$CustomObject | Add-Member-MemberType NoteProperty-Name Date Value $(Get-Date)
$CustomObject | Add-Member-MemberType NoteProperty-Name Value-Value 'Value'

За великим рахунком, тільки цей спосіб дозволяє додавати до об'єкту не тільки властивості, але і методи допомогою "-MemberType ScriptMethod". На мій подив я ніразу не бачив, що б хтось реалізовував якісь методи у своїх об'єктах. Зізнаюся, я і сам не прихильник методів в Powershell, хоча можу і списати на те, що мій досвід об'єктно-орієнтованого програмування не встиг вразити мій мозок досить глибоко. Частково я готовий списати на те, що методи довелося б розписувати в кожної функції, що повертає кастомный об'єкт, що, безумовно, менш зручно, ніж описати метод одного разу в класі об'єкта.

Шлях другий — Спрощений
Цього разу для створення об'єкта використовується хеш-таблиця, яка передається все того ж коммандлету New-Object, при зменшення кількості набраних символів, ми практично не втратили в читаності:

$Name = 'Name'
$Properties = @{}
$Properties.Name = $Name
$Properties.Date = $(Get-Date)
$Properties.Value = 'Value'
$CustomObject = New-Object-TypeName PSObject-Prop $Properties

Взагалі, мені здається хеш-таблиці трохи недооцінені авторами сценаріїв для Powershell, навіть обмеживши в релізі мову тільки хеш-таблиць і позбавивши — більш товстих — об'єктів, мова б анітрохи не втратив своєї могутності і стрункості; хоча погоджуся з тим, що об'єкти виглядають перспективніше, про що й поговоримо в кінці замітки.

За великим рахунком різниця між хеш-таблицой і об'єктом як раз і полягає в наявності методів, корисність яких, на мій погляд, в скриптовом мовою сумнівна. Попоробуйте виконати приклад з блоку цитування коду, розташованого вище, але опустивши останній рядок, в якій створюється об'єкт. Після того, як ми в полі Name присвоїли значення, ми вже можемо до нього звертатися як $Properties.Name, при цьому ніде вище ми не оголошували, що таке поле у нас взагалі буде! Хеш-таблиця вже веде себе як об'єкт, навіщо створювати ще один такий же? Мало того, з хеш-таблицями можна працювати як з масивами звертаючись за індексом: $Properties['Name'].

В якості прикладу роботи з хеш-таблиць хочу привести код функції читання значень ini-файлу, по-моєму вона прекрасна:

function Get-IniContent {
Param (
[String]$Filepath
)
$IniContent = @{}
switch-Regex-File $Filepath {
'^\[(.+)\]' {
$Section = $matches[1]
$IniContent[$Section] = @{}
$CommentCount = 0
}
"^(;.*)$" {
$Value = $matches[1]
$CommentCount = $CommentCount + 1
$Name = 'Comment' + $CommentCount
$IniContent[$Section][$Name] = $Value
}
'(.+?)\s*=(.*)' {
$Name, $Value = $matches[1..2]
$IniContent[$Section][$Name] = $Value
}
}
Write-Output $IniContent
}
# Ed Wilson, Microsoft Scripting Guy


Третій шлях — Короткий
Він простий і короткий, тут додати нічого, не беру сміливість порадити вам використовувати його тільки однострочниках і чим-то, що не буде виконуватися більше пари раз, але раджу. Раджу не тільки тому, що я адепт механічних клавіатур і отримую задоволення від набору тексту, а скільки тому, що читаність і зрозумілість вашого скрипта повинна бути на першому місці. Завдання, які доводиться автоматизувати часто і без того сповнені блек-боксів, я думаю ви погодитеся — нема чого додавати до них ще один на Powershell (ну і тому що з обфускацией скриптів відмінно справляються регулярні вирази і нема чого збільшувати ентропію =).

$Name = 'Name'
$CustomObject = [pscustomobject]@{
Name = $Name;
Date = $(Get-Date);
Value = 'Value';
}


Четвертий шлях — Вычислимый
Цей спосіб використовується, в першу чергу, для модифікації існуючих об'єктів, одержуваних, наприклад, з конвеєра. Мова про коммандлете Select-Object, за допомогою нього ми можемо зменшувати кількість властивостей об'єкта (наприклад вичистити з результату роботи коммандлета Receive-Job непотрібні нам властивості начебто RunspaceID), так і додавати свої, в тому числі обчислюючи частина з них в процесі:

# вычислимый
$Name = 'Name'
$CustomObject = $Name | Select-Object @{Name='Name'; Expression = {$PSItem}}, @{Name='Date'; Expression = {Get-Date}}, @{Name='Value'; Expression={'Value'}}

# залишить тільки дві властивості
$CustomObject | Select-Object Name, Date


Шлях п'ятий — Ворожий (жартую)
Так як Powershell працює поверх CLR, на одному рівні з C#, наприклад, то і використовувати в ньому кошти надаються цією мовою немає ніякої складності:

Add-Type @'
public class CustomClass
{
public string Name = "Name";
public System.DateTime Date = System.DateTime.Now;
public string Value = "Value";
}
'@
$CustomObject = New Object CustomClass

Цей спосіб дозволяє в доповнення до властивостей об'єкта, так само описати і методи, що вобщем-то очевидно.
Приклад застосування, приховує вікно хоста консолі, показаний нижче. Зручний якщо в скрипті є формочка і «вікно з досом» лякає користувача:

$ShowWindow = '[DllImport("user32.dll")] public static extern bool ShowWindow(int handle, int state);'
Add-Type-name win-member $ShowWindow-namespace native
[native.win]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0)


Powershell 5 і класи
Ось ми і підійшли до найголовнішого, того про що в першу чергу я хотів би поговорити, незважаючи на те, що ось вже 99 рядків (Word Wrap Column 120) заглиблююся в розлогі розмови про перипетії синтаксису: в Powershell 5 стали доступні класи (це не обман =). Все написане нижче відноситься в першу чергу до превью мови, доступного в комплекті з Windows 10, так і в дещо урізаному вигляді для інших операційних систем.

Спочатку я вирішив описати рибу класу і його застосування повну шаблонних слів на кшталт Example, FirstProperty і подібних, і лише за тим описати щось робочий і близьке до тіла. Подивившись на це зрозумів, що спрощуючи ускладнив, бо деколи немає нічого гірше нудних шаблонних описів, тому нижче я покажу як створити клас логера для вашого сценарію:

# опис класу
class Logger {
# властивість
[String]$LogPath

# конструктор
Logger([String]$NewLogPath) {
$This.LogPath = $NewLogPath

New-Item-Type File $This.LogPath-Force
}

# метод
[void]Add([String]$Value) {
'[{0}] {1}' -f $(Get-Date), $Value |
Out-File $This.LogPath-Append-Encoding default
}
}

$MyLogger = [Logger]::New('C:\temp\test.log')
$MyLogger.Add('Initial log entry')


Отриманий результат:
PS C:\Users\rbobot> Get-Content C:\temp\test.log
[4/4/2015 4:23:22 PM] Initial log entry
Отже, небагато слів із синтаксису нашого мінімального придатного до роботи класу:
— конструктор класу іменується так само як і сам клас, при цьому конструктор можна не описувати, в цьому випадку викликається конструктор за замовчуванням, але і визначити властивості при створенні ми не зможемо;
— при зверненні до властивостей класу усередині конструктора і методів використовується ключове слово $This;
— опис методів починається з вказівки типу значення, що повертається, в тому випадку якщо метод не повертає нічого слід вказати ключове слово [void];
— при створенні екземпляра класу використовується синтаксис виду: [Ім'я класу]::New();

Із зазначеного вище, особисто у мене око чіпляється лише за синтаксис створення екземпляра класу інше виглядає логічним. З одного боку для створення екземпляра класу очікуєш побачити вже знайомий New-Object-TypeName, з допомогою якого ми створювали об'єкти як описані на Powershell, так і запозичені з C#.
З іншого, цей коммандлет не передбачає визначення властивостей і об'єкт створюється конструктором за замовчуванням, можливо до релізу синтаксис створення екземпляра інтерфейсу класу зміниться на більш Powershell-Way, шляхом розширення параметрів коммандлета New-Object.

Розширимо трохи наш клас, перевантаживши метод Add і додавши функцію, яка буде отримувати наш об'єкт параметром і логировать свої дії:

class Logger {
[String]$LogPath
[String]$CodePage

Logger([String]$NewLogPath, [String]$NewCodePage) {
$This.LogPath = $NewLogPath
$This.CodePage = $NewCodePage

New-Item-Type File $This.LogPath-Force
}

[void]Add([String]$Value) {
'[{0}] {1}' -f $(Get-Date), $Value |
Out-File $This.LogPath-Append-Encoding $This.CodePage
}
[void]Add([String]$Type, [String]$Value) {
'[{0}] {1} {2}' -f $(Get-Date), $Type, $Value |
Out-File $This.LogPath-Append-Encoding $This.CodePage
}
[UInt64]GetSize() {
return (Get-Item $This.LogPath).Length
} 
}

function New-SomeJob {
Param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[String]$Job,
[Logger]$Logger
)
Process {
$JobResult= '{0} {1}' -f $Job, 'job'
$Logger.Add($JobResult)
}
}


$MyLogger = [Logger]::New('C:\temp\test.log', 'UTF8')
$MyLogger.Add('Initial log entry')
$MyLogger.Add('Warning:', 'Warning log entry')
'First', 'Second' | New-SomeJob-Logger $MyLogger
$MyLogger.Add('Last log entry')
$MyLogger.GetSize()


Отриманий результат:
PS C:\Users\rbobot> $MyLogger.GetSize()
204

PS C:\Users\rbobot> Get-Content C:\temp\test.log
[4/6/2015 10:43:26 AM] Initial log entry
[4/6/2015 10:43:26 AM] Warning: Warning log entry
[4/6/2015 10:43:26 AM] First job
[4/6/2015 10:43:26 AM] Second job
[4/6/2015 10:43:26 AM] Last log entry
По-приводу вышепроцитированного можна відзначити лише те, що методи, які повертають значення, використовується ключове слово return, використання якого в Powershell не рекомендується в силу того, що воно як і Write-Output є синтаксичним цукром, але при цьому не відповідає стилістиці Powershell. Щонайменше так було раніше.

На закінчення хотів би запитати у вас, чи ви вважаєте класи в Powershell необхідної фичей або все ж не варто робити з Powershell ще один об'єктно-орієнтована мова програмування додаючи сутності без потреби?

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.