Пенской А.В., 2023
Изменяемое состояние: опасности и борьба с ними
Материал лекции в значительной части пересказ статьи в творческой переработке.
Данный раздел находится в духе функционального программирования, но применим повсеместно.
Рассмотрим подробнее
Потенциально проблемный код:
class Box():
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __hash__(self):
return hash(self.value)
def update_value(self, value):
self.value = value
a, b = Box(1), Box(2)
a == a
# => True
a != b
# => True
a == Box(1) # => True
class Box():
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __hash__(self):
return hash(self.value)
def update_value(self, value):
self.value = value
a, b = Box(1), Box(1)
a == b # => True
dict = {}
dict[a] = 'a'
dict[b] = 'b'
dict
# => { <__main__.Box object at 0x101046f50>: 'a',
# <__main__.Box object at 0x100ea0ad0>: 'b' }
a, b = 1, 1
a == b # => True
dict = {}
dict[a] = 'a'
dict[b] = 'b'
dict
# => {1: 'b'}
class Box():
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __hash__(self):
return hash(id(self))
def update_value(self, value):
self.value = value
class DoubleBox():
def __init__(self, value1, value2):
self.value = value1
self.second_value = value2
def __eq__(self, other):
if type(self) == type(other):
return self.value == other.value \
and self.second_value == other.second_value
return False
Box(1) == DoubleBox(1, 2) # => True
DoubleBox(1, 2) != Box(1) # => True
dict
)class Box():
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __hash__(self):
return hash(self.value)
def update_value(self, value):
self.value = value
dict = {}
a, b = Box(1), Box(2)
dict[a] = 'a'
dict[b] = 'b'
a.update_value(10)
a in dict # => False
b in dict # => True
a # => <__main__.Box object at 0x101047790>
b # => <__main__.Box object at 0x101047dd0>
dict # => {
# <__main__.Box object at 0x101047790>: 'a',
# <__main__.Box object at 0x101047dd0>: 'b'
# }
Представьте: update_value
делают в новой версии чужого кода?
class Cart {
private Customer customer;
private List<Product> products;
private int totalPrice = -1;
private int computeTotalPrice() { ... } // hard to compute function
public int getTotalPrice() {
if(totalPrice == -1)
totalPrice = computeTotalPrice();
return totalPrice;
}
public void addProduct(Product p) {
products.add(p);
totalPrice = -1;
}
public void removeProduct(Product p) {
products.remove(p);
totalPrice = -1;
}
}
class Product {
public void setPrice(int price) { ... }
}
Является ли totalPrice
корректно кешируемым значением?
Нет, не является.
Сценарий:
totalPrice
не равен сумме стоимостей товаров.Пути решения проблемы:
class BankAccount {
void deposit(int amount) {
setMoney(getMoney() + amount);
}
void withdraw(int amount) {
if(amount > getMoney()) throw new InsufficientMoneyException();
setMoney(getMoney() - amount);
}
}
Мария и Иван кладут на счёт по 25 и 50 рублей соответственно:
| Действия Марьи | Действия Ивана | Деньги на счете |
|:--------------------|:--------------------|:----------------|
| deposit(50) | deposit(25) | 100
|
| getMoney() -> 100 |
| 100 |
|
| getMoney() -> 100 | 100 |
| setMoney(100+50) |
| 150 |
|
| setMoney(100+25) | 125 |
Вопрос: У банка три дата-центра. Как добиться защиты от brain split?
Вставка в двусвязный список. Можете проверить корректность реализации без листа бумаги и ручки?
public void add(int index, Object o) {
Entry e = new Entry(o);
if (index < size) {
Entry after = getEntry(index);
e.next = after;
e.previous = after.previous;
if (after.previous == null)
first = e;
else
after.previous.next = e;
after.previous = e;
} else if (size == 0) {
first = last = e;
} else {
e.previous = last;
last.next = e;
last = e;
}
size++;
}
dict
).Общие принципы:
Меньше состояний → меньше мест для ошибки.
Меньше кода вносит изменения → легче его поддерживать.
Сперва посчитать → потом внести изменения.
tuple
, а не list
).getMoney
/setMoney
, а deposit
/withdraw
.public interface WriteFacet {
void addQux(Qux qux);
void setBaz(Baz baz);
ReadFacet freeze();
}
public interface ReadFacet {
Foo getFoo(int fooId);
Bar getBar();
}
class Item implements WriteFacet {
...
ReadFacet freeze() {
...
return new FrozenItem(myData);
}
}
class FrozenItem
implements ReadFacet {
...
}
class Item {
private boolean isFrozen = false;
...
void addQux(Qux qux) {
if (isFrozen)
throw new IllegalStateException();
...
}
Foo getFoo(int fooId) {
if (!isFrozen)
throw new IllegalStateException();
...
}
void freeze() {
...
isFrozen = true;
}
}
Последовательность действий при инициализации:
class Database {
void setLogin(String login);
void setPassword(String password);
Connection connect() throws InvalidCredentialsException;
}
Атомарная инициализация:
class Database {
Connection connect(String login, String password)
throws InvalidCredentialsException;
}
interface RuleEngine {
void addRule(String varName, String formula)
throws ParseException, UndefinedVariableException;
double computeValue(String varName);
}
// Rule 1: a = b - 1
// Rule 2: b = a - 1
Как быть, если в правилах есть циклические зависимости?
interface RuleParser {
RuleSet parseRules(Map<String,String> var2formula)
throws ParseException, CircularDependencyException;
}
interface RuleSet {
double computeValue(String varName);
}
Зависимость правил — проблема сервиса, а не клиента.
Минимизировать количество объектов, охватываемых инвариантами.
class Artifact {
Player getOwner();
void setOwner(Player player);
int getStrengthBoost();
// ... other boosts
}
class Player {
List<Artifact> getArtifacts();
void addArtifact(Artifact a);
void dropArtifact(Artifact a);
void passArtifact(Artifact a,
Player toWhom);
}
Инварианты:
a.getOwner() == p
<=> p.getArtifacts().contains(a)
getOwner() == null
(на земле)Artifact
— неизменяемый.Artifact
и Player
:class Artifact {
getStrengthBoost(Player player)
}
// or
class Player {
int getStrengthBoost(
List<Artifact> a)
}
// or
class ArtifactRules {
getStrengthBoost(
Artifact a, Player p)
}
compare-and-swap
).buy
и сделал много одинаковых заказов. Вариант решения: счётчик шагов взаимодействия.