12.04.2019       Выпуск 277 (08.04.2019 - 14.04.2019)       Статьи

Иллюзия иммутабельности и доверие как основа командной разработки

Вообще я C++ программист. Ну так получилось. Подавляющее большинство коммерческого кода, который я написал за свою карьеру, — это именно C++. Мне не очень нравится такой сильный перекос моего личного опыта в сторону одного языка, и я стараюсь не упускать возможности написать что-нибудь на другом языке. И мой текущий работодатель внезапно такую возможность предоставил: я взялся сделать одну не самую тривиальную утилиту на Java. Выбор языка реализации был сделан по историческим причинам, да я и не возражал. Java так Java, чем менее мне знакомо — тем лучше.

Читать>>




Экспериментальная функция:

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

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

Вообще я C++ программист. Ну так получилось. Подавляющее большинство коммерческого кода, который я написал за свою карьеру, — это именно C++. Мне не очень нравится такой сильный перекос моего личного опыта в сторону одного языка, и я стараюсь не упускать возможности написать что-нибудь на другом языке. И мой текущий работодатель внезапно такую возможность предоставил: я взялся сделать одну не самую тривиальную утилиту на Java. Выбор языка реализации был сделан по историческим причинам, да я и не возражал. Java так Java, чем менее мне знакомо — тем лучше.

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

Тут-то моя неопытность в Java и дала о себе знать. Мне не хватало возможностей языка по сравнению с C++. Да, тут есть ключевое слово

final

, но

final Object

— это как

Object* const

в C++, а не

const Object*

. Т.е. в

final List<String>

можно добавлять строки, например. То ли дело C++: понавставлять везде

const

по заветам Майерса, и все! Никто ничего не изменит. Так? Ну не совсем. Я немного поразмышлял на эту тему

вместо того, чтобы делать ту утилиту

на досуге, и вот к чему я пришел.

С++

Напомню саму задачу:

  1. Единожды создать набор данных.
  2. Ничего не копировать без надобности.
  3. Запретить потребителю менять эти данные.
  4. Минимизировать код, т.е. не создавать кучу методов и интерфейсов для каждого набора данных, нужного в общем-то всего в паре мест.

Никаких отягчающих условий типа многопоточности, безопасности в смысле исключений и т.п. Рассмотрим самый простой случай. Вот как бы я это сделал с использованием наиболее знакомого мне языка:

foo.hpp
#pragma once

#include <iostream>
#include <list>

struct Foo
{
    const int intValue;
    const std::string strValue;
    const std::list<int> listValue;

    Foo(int intValue_,
        const std::string& strValue_,
        const std::list<int>& listValue_)     
        : intValue(intValue_)
        , strValue(strValue_)
        , listValue(listValue_)
    {}
};

std::ostream& operator<<(std::ostream& out, const Foo& foo)
{
    out << "INT: " << foo.intValue << "\n";
    out << "STRING: " << foo.strValue << "\n";
    out << "LIST: [";
    for (auto it = foo.listValue.cbegin(); it != foo.listValue.cend(); ++it)
    {
        out << (it == foo.listValue.cbegin() ? "" : ", ") << *it;
    }
    out << "]\n";
    return out;
}
api.hpp
#pragma once

#include "foo.hpp"
#include <iostream>

class Api
{
public:
    const Foo& getFoo() const
    {
        return currentFoo;
    }

private:
    const Foo currentFoo = Foo{42, "Fish", {0, 1, 2, 3}};
};
main.cpp
#include "api.hpp"
#include "foo.hpp"
#include <list>

namespace
{
    void goodConsumer(const Foo& foo)
    {
        // do nothing wrong with foo
    }
}

int main()
{
    {
        const auto& api = Api();
        goodConsumer(api.getFoo());
        std::cout << "*** After good consumer ***\n";
        std::cout << api.getFoo() << std::endl;
    }
}

Очевидно, тут все хорошо, данные неизменны.

Вывод
*** After good consumer ***
INT: 42
STRING: Fish
LIST: [0, 1, 2, 3]

А если кто-то попытается что-то изменить?

main.cpp
void stupidConsumer(const Foo& foo)
{
    foo.listValue.push_back(100);
}

Да код просто не скомпилируется.

Ошибка
src/main.cpp: In function ‘void {anonymous}::stupidConsumer(const Foo&)’:
src/main.cpp:16:36: error: passing ‘const std::__cxx11::list<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
         foo.listValue.push_back(100);

Что может пойти не так?

Это же C++ — язык с богатейшим арсеналом оружия для стрельбы по собственным ногам! Например:

main.cpp
void evilConsumer(const Foo& foo)
{
    const_cast<int&>(foo.intValue) = 7;
    const_cast<std::string&>(foo.strValue) = "James Bond";
}
Ну и собственно все:
*** After evil consumer ***
INT: 7
STRING: James Bond
LIST: [0, 1, 2, 3]

Замечу еще, что использование

reinterpret_cast

вместо

const_cast

в данном случае приведет к ошибке компиляции. А вот приведение в стиле C позволит провернуть этот фокус.

Да, такой код может привести к Undefined Behavior

[C++17 10.1.7.1/4]

. Он вообще выглядит подозрительно, что хорошо. Легче отловить во время ревью.

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

main.cpp
void evilSubConsumer(const std::string& value)
{
    const_cast<std::string&>(value) = "Loki";
}

void goodSubConsumer(const std::string& value)
{
    evilSubConsumer(value);
}

void evilCautiousConsumer(const Foo& foo)
{
    const auto& strValue = foo.strValue;
    goodSubConsumer(strValue);
}
Вывод
*** After evil but cautious consumer ***
INT: 42
STRING: Loki
LIST: [0, 1, 2, 3]

Преимущества и недостатки C++ в данном контексте

Что хорошо:

  • можно легко объявить доступ на чтение к чему угодно
  • случайное нарушение этого ограничения выявляется на этапе компиляции, т.к. константные и неконстантные объекты могут иметь разные интерфейсы
  • сознательное нарушение может быть выявлено на код-ревью

Что плохо:

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

Java

В Java, как я понял, используется несколько другой подход. Примитивные типы, объявленные как

final

, — являются константными в том же смысле, что и в C++. Строки в Java в принципе неизменяемы, так что

final String

— то, что надо в данном случае.

Коллекции же можно поместить в неизменяемые обертки, для чего есть статические методы класса

java.util.Collections

unmodifiableList

,

unmodifiableMap

и т.д. Т.е. интерфейс у константных и неконстантных объектов один и тот же, но неконстантные кидают исключение при попытке их изменить.

Что же касается пользовательских типов, то самому пользователю и придется создавать неизменяемые обертки. В общем, вот мой вариант для Java.

Foo.java
package foo;

import java.util.Collections;
import java.util.List;

public final class Foo {

    public final int intValue;
    public final String strValue;
    public final List<Integer> listValue; 

    public Foo(final int intValue,
               final String strValue,
               final List<Integer> listValue) {
        this.intValue = intValue;
        this.strValue = strValue;
        this.listValue = Collections.unmodifiableList(listValue);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("INT: ").append(intValue).append("\n")
          .append("STRING: ").append(strValue).append("\n")
          .append("LIST: ").append(listValue.toString());
        return sb.toString();
    }
}
Api.java
package api;

import foo.Foo;
import java.util.Arrays;

public final class Api {

    private final Foo foo = new Foo(42, "Fish", Arrays.asList(0, 1, 2, 3));

    public final Foo getFoo() {
        return foo;
    }
}
Main.java
import api.Api;
import foo.Foo;

public final class Main {

    private static void goodConsumer(final Foo foo) {
        // do nothing wrong with foo
    }

    public static void main(String[] args) throws Exception {
        {
            final Api api = new Api();
            goodConsumer(api.getFoo());
            System.out.println("*** After good consumer ***");
            System.out.println(api.getFoo());
            System.out.println();
        }
    }
}
Вывод
*** After good consumer ***
INT: 42
STRING: Fish
LIST: [0, 1, 2, 3]

Неудачная попытка изменения

Если просто попытаться изменить что-нибудь, например:

Main.java
private static void stupidConsumer(final Foo foo) {
    foo.listValue.add(100);
}

Этот код скомпилируется, но во время выполнения будет брошено исключение:

Исключение
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1056)
	at Main.stupidConsumer(Main.java:15)
	at Main.main(Main.java:70)

Удачная попытка

А если по-плохому? Здесь нет способа убрать у типа квалификатор

final

. Но в Java есть гораздо более мощная штука — рефлексия.

Main.java
import java.lang.reflect.Field;

private static void evilConsumer(final Foo foo) throws Exception {
    final Field intField = Foo.class.getDeclaredField("intValue");
    intField.setAccessible(true);
    intField.set(foo, 7);

    final Field strField = Foo.class.getDeclaredField("strValue");
    strField.setAccessible(true);
    strField.set(foo, "James Bond");
}
И иммутабельность кончилась
*** After evil consumer ***
INT: 7
STRING: James Bond
LIST: [0, 1, 2, 3]

Такой код выглядит еще более подозрительно, чем

cosnt_cast

в C++, его еще проще отловить на ревью. И он тоже может привести к

непредсказуемым эффектам

(т.е. в Java есть

UB

?). И так же может прятаться сколь угодно глубоко.

Эти непредсказуемые эффекты могут быть связаны с тем, что при изменении

final

объекта с помощью рефлексии значение, возвращаемое методом

hashCode()

может остаться прежним. Разные объекты с одинаковым хэшем — это еще не проблема, а вот одинаковые объекты с разными хэшами — это плохо.

Чем еще опасен такой хак в Java: строки в здесь могут храниться в пуле, и на одно и то же значение в пуле могут указывать никак не связанные друг с другом, просто одинаковые строки. Изменил одну — изменил их все.

Но!

JVM

можно запускать с различными настройками безопасности. Уже дефолтный

Security Manager

, будучи активированным, пресекает все вышеописанные фокусы с рефлексией:

Исключение
$ java -Djava.security.manager -jar bin/main.jar
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
	at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.base/java.security.AccessController.checkPermission(AccessController.java:895)
	at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:335)
	at java.base/java.lang.reflect.AccessibleObject.checkPermission(AccessibleObject.java:85)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:169)
	at Main.evilConsumer(Main.java:20)
	at Main.main(Main.java:71)

Преимущества и недостатки Java в данном контексте

Что хорошо:

  • есть ключевое слово final, которое кое-как ограничивает изменение данных
  • есть библиотечные методы для превращения коллекций в неизменяемые
  • сознательное нарушение иммутабельности легко выявляется на код-ревью
  • есть настройки безопасности JVM

Что плохо:

  • попытка изменить неизменяемый объект проявится только во время выполнения
  • для того, чтобы сделать объект некоего класса неизменяемым, придется самому писать соответствующую обертку
  • в отсутствие соответствующих настроек безопасности возможно изменить любые неизменяемые данные
  • у этого действия могут быть непредсказуемые последствия (хотя, может, это и хорошо — почти никто так делать не будет)

Python

Ну а дальше меня уже просто понесло по волнам любопытства. Как решаются подобные задачи, например, в Python’е? И решаются ли вообще? Ведь в питоне никакой константности нет в принципе, даже ключевых слов таких нет.

foo.py
class Foo():
    def __init__(self, int_value, str_value, list_value):
        self.int_value = int_value
        self.str_value = str_value
        self.list_value = list_value

    def __str__(self):
        return 'INT: ' + str(self.int_value) + '\n' + \
               'STRING: ' + self.str_value + '\n' + \
               'LIST: ' + str(self.list_value)
api.py
from foo import Foo

class Api():
    def __init__(self):
        self.__foo = Foo(42, 'Fish', [0, 1, 2, 3])

    def get_foo(self):
        return self.__foo
main.py
from api import Api

def good_consumer(foo):
    pass

def evil_consumer(foo):
    foo.int_value = 7
    foo.str_value = 'James Bond'

def main():
    api = Api()
    good_consumer(api.get_foo())
    print("*** After good consumer ***")
    print(api.get_foo())
    print()

    api = Api()
    evil_consumer(api.get_foo())
    print("*** After evil consumer ***")
    print(api.get_foo())
    print()

if __name__ == '__main__':
    main()
Вывод
*** After good consumer ***
INT: 42
STRING: Fish
LIST: [0, 1, 2, 3]

*** After evil consumer ***
INT: 7
STRING: James Bond
LIST: [0, 1, 2, 3]

Т.е. никаких ухищрений просто не надо, бери да меняй поля любого объекта.

Джентльменское соглашение

В питоне принята следующая

практика

:

  • пользовательские поля и методы, чьи имена начинаются с одного подчеркивания, — это защищенные (protectedв C++ и Java) поля и методы
  • пользовательские поля и методы с именами, начинающимися с двух подчеркивания, — это приватные (private) поля и методы

Язык даже делает декорацию (

mangling

) для «приватных» полей. Весьма наивную декорацию, никакого сравнения с C++, но и этого хватает, чтобы игнорировать (но не отловить) непреднамеренные (или наивные) ошибки.

Код
class Foo():
    def __init__(self, int_value):
        self.__int_value = int_value

    def int_value(self):
        return self.__int_value

def evil_consumer(foo):
    foo.__int_value = 7

А чтобы совершить ошибку преднамеренно, достаточно добавить всего несколько символов.

Еще один вариант

Мне понравилось решение, предложенное

Oz N Tiram

. Это простой декоратор, который при попытке изменить

read only

поле кидает исключение. Это немного выходит за оговоренные рамки («не создавать кучу методов и интерфейсов»), но, повторюсь, мне понравилось.

foo.py
from read_only_properties import read_only_properties

@read_only_properties('int_value', 'str_value', 'list_value')
class Foo():
    def __init__(self, int_value, str_value, list_value):
        self.int_value = int_value
        self.str_value = str_value
        self.list_value = list_value

    def __str__(self):
        return 'INT: ' + str(self.int_value) + '\n' + \
               'STRING: ' + self.str_value + '\n' + \
               'LIST: ' + str(self.list_value)
main.py
def evil_consumer(foo):
    foo.int_value = 7
    foo.str_value = 'James Bond'
Вывод
Traceback (most recent call last):
  File "src/main.py", line 35, in <module>
    main()
  File "src/main.py", line 28, in main
    evil_consumer(api.get_foo())
  File "src/main.py", line 9, in evil_consumer
    foo.int_value = 7
  File "/home/Tmp/python/src/read_only_properties.py", line 15, in __setattr__
    raise AttributeError("Can't touch {}".format(name))
AttributeError: Can't touch int_value

Но и это не панацея. Но хотя бы соответствующий код выглядит подозрительно.

main.py
def evil_consumer(foo):
    foo.__dict__['int_value'] = 7
    foo.__dict__['str_value'] = 'James Bond'
Вывод
*** After evil consumer ***
INT: 7
STRING: James Bond
LIST: [0, 1, 2, 3]

Преимущества и недостатки Python в данном контексте

Кажется, что в питоне все очень плохо? Нет, это просто другая философия языка. Обычно она выражается фразой «Мы все тут взрослые, ответственные люди» (

We are all consenting adults here

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

Что хорошо:

  • открыто декларируется, что за правами доступа должны следить программисты, а не компилятор или интерпретатор
  • есть общепринятое соглашение об именах защищенных и приватных полей и методов
  • некоторые нарушения прав доступа легко выявляются на код-ревью

Что плохо:

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

Go

Еще один язык, который я периодически щупаю (в основном просто читаю статьи), хотя пока не написал на нем ни строчки коммерческого кода. Ключевое слово

const

тут в принципе есть, но константами могут быть только строки и целочисленные значения, известные во время компиляции (т.е.

constexpr

из C++). А поля структуры — не могут. Т.е. если поля объявлены открытыми, то получается как в питоне — меняй, кто хочешь. Неинтересно. Даже пример кода приводить не буду.

Ну ладно, пусть поля будут приватными, и пусть их значения можно получить через вызовы отрытых методов. Получится ли наломать дров в Go? Конечно, тут ведь тоже есть рефлексия.

foo.go
package foo

import "fmt"

type Foo struct {
    intValue int
    strValue string
    listValue []int
}

func (foo *Foo) IntValue() int {
    return foo.intValue;
}

func (foo *Foo) StrValue() string {
    return foo.strValue;
}

func (foo *Foo) ListValue() []int {
    return foo.listValue;
}

func (foo *Foo) String() string {
    result := fmt.Sprintf("INT: %d\nSTRING: %s\nLIST: [", foo.intValue, foo.strValue)
    for i, num := range foo.listValue {
        if i > 0 {
            result += ", "
        }
        result += fmt.Sprintf("%d", num)
    }
    result += "]"
    return result
}

func New(i int, s string, l []int) Foo {
    return Foo{intValue: i, strValue: s, listValue: l}
}
api.go
package api

import "foo"

type Api struct {
    foo foo.Foo
}

func (api *Api) GetFoo() *foo.Foo {
    return &api.foo
}

func New() Api {
    api := Api{}
    api.foo = foo.New(42, "Fish", []int{0, 1, 2, 3})
	return api
}
main.go
package main

import (
    "api"
    "foo"
    "fmt"
    "reflect"
    "unsafe"
)

func goodConsumer(foo *foo.Foo) {
    // do nothing wrong with foo
}

func evilConsumer(foo *foo.Foo) {
    reflectValue := reflect.Indirect(reflect.ValueOf(foo))
	
    member := reflectValue.FieldByName("intValue")
    intPointer := unsafe.Pointer(member.UnsafeAddr())
    realIntPointer := (*int)(intPointer)
    *realIntPointer = 7
	
    member = reflectValue.FieldByName("strValue")
    strPointer := unsafe.Pointer(member.UnsafeAddr())
    realStrPointer := (*string)(strPointer)
    *realStrPointer = "James Bond"
}

func main() {
    apiInstance := api.New()
    goodConsumer(apiInstance.GetFoo())
    fmt.Println("*** After good consumer ***")
    fmt.Println(apiInstance.GetFoo().String())
    fmt.Println()

    apiInstance = api.New()
    evilConsumer(apiInstance.GetFoo())
    fmt.Println("*** After evil consumer ***")
    fmt.Println(apiInstance.GetFoo().String())
}
Вывод
*** After good consumer ***
INT: 42
STRING: Fish
LIST: [0, 1, 2, 3]

*** After evil consumer ***
INT: 7
STRING: James Bond
LIST: [0, 1, 2, 3]

Кстати строки в Go неизменяемые, как в Java. А слайсы и мапы — изменяемые, и в отличие от Java в ядре языка нет способа сделать их неизменяемыми. Только кодогенерация (поправьте, если я ошибаюсь). Т.е. даже если все сделать правильно, не использовать грязных трюков, просто возвращать слайс из метода — этот слайс всегда можно изменить.

Сообществу гоферов явно

не хватает

неизменяемых типов, но в Go 1.x их точно не будет.

Преимущества и недостатки Go в данном контексте

На мой неискушенный взгляд по возможностям запрета менять поля структур Go находится где-то между Java и Python, ближе к последнему. При этом в Go нет (я не встречал, хотя искал) питоновского принципа про взрослых людей. Но есть: внутри одного пакета все имеет доступ ко всему, от констант остался только рудимент, наличие отсутствия неизменяемых коллекций. Т.е. если разработчик может какие-то данные считать, то с большой вероятностью он может чего-то туда и записать. Что, как и в питоне, передает большую часть ответственности от компилятора к человеку.

Что хорошо:

  • все ошибки доступа проявляются во время компиляции
  • грязные трюки на основе рефлексии хорошо заметны на ревью

Что плохо:

  • понятия «набор данных только для чтения» просто нет
  • невозможно ограничить доступ к полям структуры в пределах пакета
  • чтобы защитить поля от изменений за пределами пакета, придется писать геттеры
  • все ссылочные коллекции изменяемы
  • с помощью рефлексии можно изменять даже приватные поля

Erlang

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

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

foo.erl
-module(foo).
-export([new/3, print/1]).

new(IntValue, StrValue, ListValue) ->
    {foo, IntValue, StrValue, ListValue}.

print(Foo) ->
    case Foo of
        {foo, IntValue, StrValue, ListValue} -> 
            io:format("INT: ~w~nSTRING: ~s~nLIST: ~w~n",
                      [IntValue, StrValue, ListValue]);
        _ -> 
            throw({error, "Not a foo term"})
    end.
api.erl
-module(api).
-export([new/0, get_foo/1]).

new() ->
    {api, foo:new(42, "Fish", [0, 1, 2, 3])}.

get_foo(Api) ->
    case Api of
        {api, Foo} -> Foo;
        _ -> throw({error, "Not an api term"})
    end.
main.erl
-module(main).
-export([start/0]).

start() ->
    ApiForGoodConsumer = api:new(),
    good_consumer(api:get_foo(ApiForGoodConsumer)),
    io:format("*** After good consumer ***~n"),
    foo:print(api:get_foo(ApiForGoodConsumer)),
    io:format("~n"),

    ApiForEvilConsumer = api:new(),
    evil_consumer(api:get_foo(ApiForEvilConsumer)),
    io:format("*** After evil consumer ***~n"),
    foo:print(api:get_foo(ApiForEvilConsumer)),

    init:stop().

good_consumer(_) ->
    done.

evil_consumer(Foo) ->
    _ = setelement(1, Foo, 7),
    _ = setelement(2, Foo, "James Bond").
Вывод
*** After good consumer ***
INT: 42
STRING: Fish
LIST: [0,1,2,3]

*** After evil consumer ***
INT: 42
STRING: Fish
LIST: [0,1,2,3]

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

Преимущества и недостатки Erlang в данном контексте

Что хорошо:

Что плохо:

Вместо выводов и заключения

И что в итоге? Ну помимо того, что я сдул пыль с пары давно прочитанных книжек, размял пальцы, написав бесполезную программку на 5 разных языках, и почесал ЧСВ?

Во-первых, я перестал считать, что C++ — самый надежный в плане защиты от активного дурака язык. Несмотря на все его гибкость и обильный синтаксис. Сейчас я склоняюсь к мысли, что Java в этом плане дает больше защиты. Это не очень оригинальный вывод, но для себя я нахожу его весьма полезным.

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

В-третьих: как бы язык ни пытался защитить данные от записи, при желании пользователь почти всегда может это сделать («почти» из-за Erlang’а). А если ограничиться мейнстримовыми языками — то просто всегда. И получается, что все эти

const

и

final

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

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

const

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

доверяю

своим коллегам.

Нет, я давно знаю, что современная разработка ПО — это в 99.99% случаев командная работа. Но мне везло, все мои коллеги были «взрослыми, ответственными» людьми. Для меня всегда как-то и было, и есть само собой разумеющимся, что все члены команды соблюдают установленные правила. Мой путь к осознанию того, что мы постоянно

доверяем

и

уважаем

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

P. S.

Если кому-то интересны использованные примеры кода, их можно взять

здесь

.






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus