Парадигма програмування Data Context Interaction (DCI)
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Доброго ранку/ дня/ ночі, ми з України.
Шановне панство, нещодавно був на онлайн-конференції Architecture fwdays’2022. Сподобалась більшість матеріалу. Але йшов туди заради однієї доповіді: «An introduction to object-oriented programming for those who have never done it before... which probably includes you» by James Coplien, тому ця стаття присвячена їй.
James Coplien відомий своїми книгами по C++, патернами проєктування та активною позицією щодо використання Agile та SCRUM підходів у розробці ПЗ.
Але це не все. Він також активно просуває DCI парадигму створення ПЗ, винахідником якої є Trygve Reenskaug.
На цю тему багато усього сказано та написано, але на практиці я рідко зустрічаю проєкти, де явно використовується DCI. Тому хочу зупинитись на суті цієї парадигми, щоб сворити інтерес у читача для подальшого дослідження цієї теми.
Які проблеми бачить Джеймс у сьогоденній парадигмі програмування ООП.
Проблематика
По-перше, він стверджує, що більшість розробників зазраз займаються не об’єктно орієнтовним, а клас орієнтовним програмуванням. Тобто розробник здебільшого мислить класами, таблицями БД, а не динамічними об’єктами. Ідея ООП, на думку Алана Кея, людини, яка зробила великий вклад у розвиток ООП, полягає у тому, щоб створювати у системах ПЗ проксі-об’єкти, які є відображенням об’єктів свідомості користувачів системи. Алан Кей приводить наступний приклад: коли дитина досліджує навколишнє серидовище, вона створює у свідомості ментальні образи — об’єкти, якими надалі оперує. І вони повинні бути відображені в системі ПЗ.
Також сьогоднішні інструменти розробки, такі як мови програмування, у парадигмі ООП створюють умови для фокусування розробника здебільшого на деталях реалізації, ніж на Use Cases, які своєю чергою відображають поведінку системи, пов’язану з діями користувача. Тому в коді іноді стає не зрозумілим, де реалізація того чи іншого Use Case.
А треба нагадати, ми продаємо користувачеві, ні REST services, ні класи, а Use Cases!
По-друге, однією з проблем сучасного ООП, на думку Джеймса, є комбінація успадкування та поліморфізма. Коли здійснюється визов метода інстанса класа, десь у глибині є механізм (базований на використанні таблиці віртуальних методів), який вирішує, який насправді метод визвати, щось накшталт блока з багатьма goto. Це ускладнює розуміння поведінки системи й того, як реалізовано тий чи інший Use Case.
Рішення
Щоб покращити ситуацію, він пропонує іншу парадигму під назвою DCI. Як я написав раніше, її автором є Trygve Reenskaug, який також є винахідником патерну MVC.
Отже як розшифровується абревіатура DCI?
DCI — Data, Context, Interaction (дані, контекст, взаємодія).
- Data(State) — це формалізована предметна область, доменна модель, behaviourless стан об’єктів чи їх data складова.
- Context(Use Case) — це контекст, у якому діють об’єкти. Context як правило є імплементацією якогось Use Case.
- Interaction(Behaviour) — це stateless імплементація ролей, в залежності від контексту. Вона додає до data складової об’єктів поведінкову, в залежності від контексту.
Аналогія. Майже кожна людина (об’єкт з Data) у житті виступає в тій чи іншій ролі. Наприклад, приходячи у магазин (Сontex), вона діє у ролі (Interaction) покупця. Граючи у футбол (Сontext), вона можете бути у ролі (Interaction) нападника, захистника, півзахистника чи голкіпера.
Висновки
Повертаючись до DCI та підсумовуючи написане, робимо висновки.
DCI фокусує розробника на Use Cases. Кожен раз створюючи Context, розробник створює реалізацію того чи іншого варіанта використання системи. Його діячами є об’єкти системи — прообрази ментальної моделі користувача. Вони своєю чергою розкладаюсться на дві складові: станова складова Data та поведінкова Interaction. Остання накладається на об’єкт в залежності від конкретного контексту.
Слід також зауважити, що Джеймс є критиком TDD. Він ввжає, що це формує у свідомості розробника невірний спосіб мислення. Замість того, щоб почати з формування high-level бачення системи, створити її «скелет» та йти зверху до низу, TDD пропонує фокусуватись на конкретній реалізаціі тих чи інших методів класів. I я з ним у цьому погоджуюсь. Використовуючи TDD, з початку створення проекту, перестаєш бачити ліс за деревом.
Наведу класичний приклад: трансфер грошей з одного акаунта на інший.
Спочатку на C#, у моїй інтерпретації, яку я накидав ввечері після конференції, по «свіжій» пам’яті. Вважаю цей приклад простішим для сприйняття, та більш type safe, ніж приклад на C# від Джеймса. Думаю, він більш мислить як C++ розробник, ніж C#.
Потім — на C# з книги Джеймса «The DCI Architecture: Lean and Agile at the Code Level».
І, врешті, — на спеціально свореній для DCI мові програмування trygve, яка має Java-синтакс і є pure мовою, де все expression.
Насправді у його книзі є приклади на багатьох мовах програмування.
Приклади
Money Transfer приклад на С#. Мій варіант.
using System;
namespace Dci.Console
{
/// <summary>
/// Data(state, behaviorless) component.
/// The class for the data component of the user's mind account object.
/// </summary>
class Account
{
public Account(decimal balance)
{
Balance = balance;
}
public decimal Balance { get; set; }
}
/// <summary>
/// Interaction(stateless, behavior) component.
/// The class for the behavior component of the user's mind source account object.
/// Acts as a SOURCE role in the DataTransferContext.
/// Applying the behavior to the Account objects.
/// </summary>
sealed class SourceAccountRole
{
private readonly Account _account;
public SourceAccountRole(Account account)
{
_account = account;
}
public void Transfer(DestinationAccountRole destination, decimal amount)
{
if (_account.Balance - amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount));
_account.Balance -= amount;
destination.IncreaseAmount(amount);
}
}
/// <summary>
/// Interaction(stateless, behavior) component.
/// The class for the behavior component of the user's mind destination account object.
/// Acts as a DESTINATION role in the DataTransferContext.
/// Appling the behavior to the Account objects.
/// </summary>
sealed class DestinationAccountRole
{
private readonly Account _account;
public DestinationAccountRole(Account account)
{
_account = account;
}
public Account Account { get; }
public void IncreaseAmount(decimal amount)
{
_account.Balance += amount;
}
}
/// <summary>
/// The context(use case).
/// Context for transferring money from one account to another.
/// </summary>
sealed class MoneyTransferContext
{
private readonly SourceAccountRole _source;
private readonly DestinationAccountRole _destination;
private readonly decimal _amount;
public MoneyTransferContext(Account source, Account destination, decimal amount)
{
_source = new SourceAccountRole(source);
_destination = new DestinationAccountRole(destination);
_amount = amount;
}
public void Execute()
{
_source.Transfer(_destination, _amount);
}
}
class Program
{
static void Main(string[] args)
{
// Init accounts. In the real cases came from the data access layer
var sourceAccount = new Account(100);
var destinationAccount = new Account(0);
System.Console.WriteLine($"[Before] source.Balance={sourceAccount.Balance}, destination.Balance={destinationAccount.Balance}");
// Create context(Use Case)
MoneyTransferContext context = new MoneyTransferContext(sourceAccount, destinationAccount, 20);
// Excute context(Use Case)
context.Execute();
System.Console.WriteLine($"[After] source.Balance={sourceAccount.Balance}, destination.Balance={destinationAccount.Balance}");
}
}
}
Money Transfer, приклад на С#. з книги «The DCI Architecture: Lean and Agile at the Code Level».
using System;
namespace DCI
{
// Methodless role types
public interface TransferMoneySink
{
}
// Methodful roles
public interface TransferMoneySource
{
}
public static class TransferMoneySourceTraits
{
public static void TransferFrom(
this TransferMoneySource self,
TransferMoneySink recipient, double amount)
{
// This methodful role can only
// be mixed into Account object (and subtypes)
Account self_=self as Account;
Account recipient_=recipient as Account;
// Self-contained readable and testable
// algorithm
if (self_ != null && recipient_ != null)
{
self_.DecreaseBalance(amount);
self_.Log("Withdrawing " + amount);
recipient_.IncreaseBalance(amount);
recipient_.Log("Depositing " + amount);
}
}
}
// Context object
public class TransferMoneyContext
{
// Properties for accessing the concrete objects
// relevant in this context through their
// methodless roles
public TransferMoneySource Source {
get; private set;
}
public TransferMoneySink Sink {
get;
private set;
}
public double Amount {
get; private set;
}
public TransferMoneyContext()
{
// logic for retrieving source and sink accounts
}
public TransferMoneyContext(
TransferMoneySource source,
TransferMoneySink sink,
double amount)
{
Source = source;
Sink = sink;
Amount = amount;
}
public void Doit()
{
Source.TransferFrom(Sink, Amount);
// Alternatively the context could be passed
// to the source and sink object.
}
}
///////////// Model ////////////////
// Abstract domain object
public abstract class Account
{
public abstract void DecreaseBalance(
double amount);
public abstract void IncreaseBalance(
double amount);
public abstract void Log(string message);
}
// Concrete domain object
public class SavingsAccount :
Account,
TransferMoneySource,
TransferMoneySink
{
private double balance;
public SavingsAccount()
{
balance = 10000;
}
public override void DecreaseBalance(double amount)
{
balance -= amount;
}
public override void IncreaseBalance(
double amount)
{
balance += amount;
}
public override void Log(string message)
{
Console.WriteLine(message);
}
public override string ToString()
{
return "Balance " + balance;
}
}
///////////// Controller ////////////////
// Test controller
public class App
{
public static void Main(string[] args)
{
SavingsAccount src=new SavingsAccount();
SavingsAccount snk=new SavingsAccount();
Console.WriteLine("Before:");
Console.WriteLine("Src:" + src);
Console.WriteLine("Snk:" + snk);
Console.WriteLine("Run transfer:");
new TransferMoneyContext(src, snk, 1000).Doit();
Console.WriteLine("After:");
Console.WriteLine("Src:" + src);
Console.WriteLine("Snk:" + snk);
Console.ReadLine();
}
}
}
Money transfer, приклад на Trygve.
context TransferMoneyContext
{
// Roles
role AMOUNT {
public double amount() const;
} requires {
double amount() const;
}
role GUI {
public void displayScreen(int displayCode)
} requires {
void displayScreen(int displayCode)
}
role SOURCE_ACCOUNT {
public void transferTo() {
// This code is reviewable and meaningfully testable with stubs!
int SUCCESS_DEPOSIT_SCREEN = 10;
beginTransaction();
if (this.availableBalance() < AMOUNT) {
endTransaction();
assert(false, "Unavailable balance")
} else {
this.decreaseBalance(AMOUNT);
DESTINATION_ACCOUNT.increaseBalance(AMOUNT);
this.updateLog("Transfer Out", new Date(), AMOUNT);
DESTINATION_ACCOUNT.updateLog("Transfer In", new Date(), AMOUNT);
}
GUI.displayScreen(SUCCESS_DEPOSIT_SCREEN);
endTransaction();
}
} requires {
void decreaseBalance(Currency amount);
Currency availableBalance() const;
void updateLog(String msg, Date time, Currency amount)
}
role DESTINATION_ACCOUNT {
public void transferFrom() {
this.increaseBalance(AMOUNT);
this.updateLog("Transfer in", new Date(), AMOUNT);
}
public void increaseBalance(Currency amount);
public void updateLog(String msg, Date time, Currency amount)
} requires {
void increaseBalance(Currency amount);
void updateLog(String msg, Date time, Currency amount)
}
public TransferMoneyContext(Currency amount, Account source, Account destination) {
SOURCE_ACCOUNT = source;
DESTINATION_ACCOUNT = destination;
AMOUNT = amount
}
public TransferMoneyContext() {
lookupBindings()
}
public void doit() {
SOURCE_ACCOUNT.transferTo()
}
private void lookupBindings() {
// These are somewhat arbitrary and for illustrative
// purposes. The simulate a database lookup
InvestmentAccount investmentAccount = new InvestmentAccount();
investmentAccount.increaseBalance(new Euro(100.00)); // prime it with some money
// Assign SOURCE_ACCOUNT role to the investmentAccount object
SOURCE_ACCOUNT = investmentAccount;
// Assign DESTINATION_ACCOUNT role to anonymous SavingsAccount object
DESTINATION_ACCOUNT = new SavingsAccount();
DESTINATION_ACCOUNT.increaseBalance(new Euro(500.00)); // start it off with money
// Assign new value to the AMOUNT role
AMOUNT = new Euro(30.00)
}
}
int main() {
// Main
TransferMoneyContext aNewUseCase = new TransferMoneyContext();
aNewUseCase.doit();
}
28 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів