📜 ⬆️ ⬇️

The illusion of immunity and trust as the basis of team development

In general, I am a C ++ programmer. Well, it happened. The overwhelming majority of the commercial code I wrote in my career is C ++. I do not really like such a strong bias of my personal experience towards one language, and I try not to miss the opportunity to write something in another language. And my current employer suddenly provided such an opportunity: I undertook to make one not the most trivial utility in Java. The choice of implementation language was made for historical reasons, and I did not mind. Java is so Java, the less familiar to me - the better.

Among other things, I had a fairly simple task: once to form a certain set of logically related data and transfer it to a certain consumer. There can be several consumers, and according to the principle of encapsulation, the transmitting code (manufacturer) has no idea what is inside and what it can do with the source data. But the manufacturer needs each consumer to get the same data. I didn't want to make copies and give them away. It means that it is necessary to somehow deprive consumers of the opportunity to change the data transferred to them.

It was then that my inexperience in Java and made itself felt. I lacked the capabilities of the language compared to C ++. Yes, there is a final keyword here, but final Object is like Object* const in C ++, not const Object* . Those. You can add strings to the final List<String> , for example. Is it a matter of C ++: to put everywhere on const according to Myers, and that's it! No one will change anything. So? Well, not quite. I thought a little about this topic instead of doing that utility at my leisure, and that's what I came up with.

C ++


I recall the task itself:
')
  1. Once create a dataset.
  2. Do not copy anything unnecessarily.
  3. Prohibit the consumer to change this data.
  4. Minimize code, i.e. do not create a bunch of methods and interfaces for each data set that you need, in general, just a couple of places.

No aggravating conditions like multithreading, security in the sense of exceptions, etc. Consider the simplest case. Here's how I would do it using the most familiar language to me:

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; } } 


Obviously, everything is good, the data is unchanged.

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

And if someone tries to change something?


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


Yes, the code simply does not compile.

Mistake
 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); 


What can go wrong?


This is C ++ - a language with a rich arsenal of weapons for shooting at your own feet! For example:

main.cpp
 void evilConsumer(const Foo& foo) { const_cast<int&>(foo.intValue) = 7; const_cast<std::string&>(foo.strValue) = "James Bond"; } 


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


I also note that using reinterpret_cast instead of const_cast in this case will result in a compilation error. But the C-style ghost will turn this trick.

Yes, such code can lead to Undefined Behavior [C ++ 17 10.1.7.1/4] . He generally looks suspiciously that good. Easier to catch during the review.

It’s bad that a malicious code can hide as deeply as you want in a consumer, but it will still work:

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); } 


Conclusion
 *** After evil but cautious consumer *** INT: 42 STRING: Loki LIST: [0, 1, 2, 3] 


Advantages and disadvantages of C ++ in this context


What well:

What is wrong:


Java


In Java, as I understand it, a slightly different approach is used. Primitive types declared as final are constant in the same sense as in C ++. Strings in Java are basically unchangeable, so the final String is what you need in this case.

Collections can be placed in immutable wrappers, for which there are static methods of the java.util.Collections class - unmodifiableList , unmodifiableMap , etc. Those. the interface for constant and non-constant objects is the same, but non-constant objects throw an exception when trying to change them.

As for user types, the user himself will have to create immutable wrappers. In general, here is my version for 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(); } } } 


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


Unsuccessful change attempt


If you just try to change something, for example:

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


This code will compile, but an exception will be thrown at runtime:

An exception
 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) 


Successful attempt


And if in a bad way? There is no way to remove the final qualifier from the type. But in Java there is a much more powerful thing - reflection.

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"); } 


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


Such code looks even more suspicious than cosnt_cast in C ++, it is even easier to catch it with a review. And it can also lead to unpredictable effects (i.e. is there Java in UB ?). And it can also hide as deeply as you like.

These unpredictable effects can be due to the fact that when the final object is changed using reflection, the value returned by the hashCode() method may remain the same. Different objects with the same hash is not a problem yet, but identical objects with different hashes are bad.

What is more dangerous such a hack in Java is for strings ( example ): strings here can be stored in a pool, and just the same strings can indicate the same value in the pool. Changed one - changed them all.

But! JVM can be run with different security settings. Already the default Security Manager , being activated, suppresses all the above tricks with reflection:

An exception
 $ 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) 


Advantages and disadvantages of Java in this context


What well:

What is wrong:


Python


Well, after that I was just carried along the waves of curiosity. How to solve such problems, for example, in Python? And they decide at all? Indeed, in python, there is no constancy in principle, even there are no such keywords.

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() 


Conclusion
 *** 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] 


Those. no tweaks just do not need it, take it and change the fields of any object.

Gentlemen's agreement


The following practice is used in python:

The language even makes a decoration ( mangling ) for "private" fields. A very naive decoration, no comparison with C ++, but this is enough to ignore (but not catch) unintended (or naive) errors.

Code
 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 


Conclusion
 *** After evil consumer *** INT: 42 


And in order to make a mistake intentionally, just add a few characters.

Code
 def evil_consumer(foo): foo._Foo__int_value = 7 


Conclusion
 *** After evil consumer *** INT: 7 


Another option


I liked the solution proposed by Oz N Tiram . This is a simple decorator, which when trying to change the read only field throws an exception. This is a little beyond the agreed framework (“not creating a bunch of methods and interfaces”), but, again, I liked it.

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' 


Conclusion
 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 


But this is not a panacea. But at least the corresponding code looks suspicious.

main.py
 def evil_consumer(foo): foo.__dict__['int_value'] = 7 foo.__dict__['str_value'] = 'James Bond' 


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


Advantages and disadvantages of Python in this context


It seems that everything is very bad in python? No, this is just another philosophy of language. Usually it is expressed by the phrase “We are all adults here, responsible people” ( We are all consenting adults here ). Those. it is assumed that no one will specifically deviate from the accepted norms. The concept is not indisputable, but it has the right to life.

What well:

What is wrong:


Go


Another language that I occasionally feel (basically just reading articles), although I haven’t written a single line of commercial code on it yet. The const keyword is here in principle, but only strings and integer values ​​known at compile time (ie, constexpr from C ++) can be constants. A field structure - can not. Those. if the fields are declared open, it turns out like in a python - change who you want. Not interested. I will not even give a code sample.

Well, well, let the fields be private, and let their values ​​be obtained through calls to public methods. Is it possible to break wood in Go? Of course, here, too, there is a reflection.

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()) } 


Conclusion
 *** 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] 


By the way, the strings in Go are immutable, as in Java. But slices and mappas are mutable, and unlike Java, there is no way for the language core to make them immutable. Only code generation (correct if I am mistaken). Those. even if everything is done correctly, do not use dirty tricks, just return the slice from the method - this slice can always be changed.

The community of gophers clearly lacks immutable types, but they definitely won't be in Go 1.x.

Advantages and disadvantages of Go in this context


In my inexperienced view on the possibilities of prohibiting the change of the fields of the Go structures, there is somewhere between Java and Python, closer to the latter. At the same time, there is no Go (I have not met, although I was searching for) the Python principle about adults. But there is: within one package, everything has access to everything, only a rudiment remains from constants, the absence of immutable collections. Those. If a developer can read some data, then with a high probability he can write something there. That, as in python, transfers much of the responsibility from the compiler to the person.

What well:

What is wrong:


Erlang


This is out of competition. Still, Erlang is a language with a very different four paradigm from the above. Once I studied it with great interest, I really liked to force myself to think in a functional style. But, unfortunately, I did not find practical application of these skills.

So, in this language, the value of a variable can be assigned only once. And when a function is called, all arguments are passed by value, i.e. a copy of them is made (but there is tail recursion optimization).

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"). 


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


Of course, you can make copies for everybody and so protect yourself from data corruption in other languages. But there is a language (and certainly not one), where there is simply no other way!

Advantages and disadvantages of Erlang in this context


What well:

What is wrong:


Instead of conclusions and conclusions


And what is the result? Well, besides the fact that I blew the dust from a pair of long-read books, stretched my fingers, wrote a useless program in 5 different languages, and scratched the CSF?

First, I stopped thinking that C ++ is the most reliable language in terms of protection against an active fool. Despite all its flexibility and rich syntax. Now I tend to think that Java provides more protection in this regard. This is not a very original conclusion, but for myself I find it very useful.

Secondly, I suddenly formulated for myself the idea that very roughly programming languages ​​can be divided into those that try to limit access to certain data at the level of syntax and semantics, and those that do not even try, shifting these concerns to users . Accordingly, the threshold of entry, best practices, requirements for team members (both technical and personal) should somehow differ depending on the chosen PL. I would love to read on this topic.

Thirdly: no matter how hard the language tries to protect the data from writing, if desired, the user can almost always do this (“almost” because of Erlang). And if we confine ourselves to mainstream languages, then just always. And it turns out that all these const and final are no more than recommendations, instructions for the proper use of interfaces. Not all languages ​​have it, but I still prefer to have such tools in my arsenal.

And fourth, the most important thing: since no (mainstream) language can prevent a developer from doing nasty things, the only thing that keeps this developer is his own decency. And it turns out that I, placing constin my code, do not forbid something to my colleagues (and the future myself), but leave the instructions, believing that they (and me) will follow them. Those.I trust my colleagues.

No, I have known for a long time that modern software development is team work in 99.99% of cases. But I was lucky, all my colleagues were “adult, responsible” people. For me it was always somehow, and it goes without saying that all team members follow the established rules. My way to the realization that we constantly trust and respect each other was long, but, damn it, calm and safe.

PS


If someone is interested in the used code examples, you can take them here .

Source: https://habr.com/ru/post/447478/


All Articles