[1] https://docs.djangoproject.com/en/5.1/topics/i18n/translatio...
321 karma · joined February 12, 2014
[1] https://docs.djangoproject.com/en/5.1/topics/i18n/translatio...
> My real criticism of Django is that, at long last, I think it's starting to look dated.
Actually, that is something I found really great about django. I found that django is reliable over a long period of time. The js/node/react/express/angular apps I had developed in the past (~2012+) can't be built these days without redoing major components. I never had these problems with django.
As for the responsiveness of the results, I found that a good understanding of django's performance helps. Using a good caching strategy (on models, views + nginx) yields results have almost instant reactions, even though there is the whole HTTP to HTML roundtrip. While they do have the noticable page-reload semantics, I found that these are way easier to build and sometimes yield very enjoyable experiences for my users. After all, I'm not building a YouTube so PictureInPicture and SPA-semantics aren't that important to my users.
> Modern web apps are maintaining models in the client and staying current though a combination of ajax queries and websockets.
Julia Elman explains how to do this with Django + DRF and Backbone.js in the book "Lightweight Django" (O'Reilly). Is this a viable compromise?
I found that django's naming scheme for migrations is good enough; here are some real ones that I pulled out of my current project:
migrations/0006_add_modeltranslation.py
migrations/0007_remove_temp_fields.py
migrations/0008_remove_language_field.py
migrations/0009_alter_page_slug.py
migrations/0010_alter_menuitem_absolute_url.py
While they aren't semantically rich ('Why did you change absolute url on menu item?') I found that they are easy to distinguish and navigate.1. Relying on implicit SQL queries
2. Using the default User model
3. Using automatic migration names
4. Relying on automatic database table names
5. Using multiple tiny apps
6. Dumping apps in the root directory
I wouldn't call those harmful. Most are non-issues, while others are "unfortunate" at best.
1. I found that the ORM works quite well and it's easy to optimize later. Using `prefetch_related` or `select_related` in the ORM still feels weird (they're just joins), but I wouldn't call them harmful.
2. I found it unfortunate that the info on how to change the User model is hidden so deep in the Documentation. I don't like to RTFM, but with Django it sure is worth it; same goes for 1.
3. I occasionally do it, but most of the time, I don't and never had an issue.
4. It do rely on that and found django's naming scheme to be a non-issue. I guess this might be more important if you need to change your database out of band (out of django band, that is)
5. This point is unsubstantiated. Apps aren't mere packages but also define a scope for the admin tool (which I use a lot and think is an integral part of Django) so that it will group models and allow individual access (i.e. apps also provide scope for your permissions). I use tons of apps but I wouldn't call it a harmful default.
6. Yeah, dynamic languages might produce naming collisions. Still beats imports like `org.example.publib.boulder.yourmoms.maidenname.models.impl.Account`
Professionalism means being able to produce convergent results given varying resources, but when the spec is unclear, your employer doesn't provide appropriate resources for you to perform or if your mental health is in danger, I think it's perfectly valid to back off. "Professional" does not mean "sorcerer". A professional photographer can't take a good picture in complete darkness, and even a good architect can't create a distributed system when the environment won't support it. It argue it's professional to know your own limits and reject a job offer.
It's difficult to make the decision to leave, especially when you have been jobhunting for a while and I can't say anything substantial on how anyone should make such a decision, but the other day I found a meme saying (something along those lines):
In a job, you should either earn or learn; either is good, both is ideal, if it's neither: leave. If the job pays ok money, but costs your sanity, I'd argue you should leave.
Some of the criticism is really superficial:
function createEmployee(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName,
};
}
OO-Programs have these weird factory functions called constructors that do exactly that. To me, it makes sense that such functions should share their living space with the class that they are creating. Where else would you like to put these functions? "Utils"?As for classes, namespaces and objects: as others have pointed out, this is an implementation detail. In Python, for example, classes are objects. Besides, classes and objects are functions (and the other way around) so it doesn't matter all that much in practice.
What buggs me more is code like this:
class SayHello {
public static void toPerson(String name) {
System.out.println("Hello " + name);
}
}
Sure, comparing it to an empty Python file which just says "print" this looks like an aweful lot of code. However if the paradim is poorly understood, it will seem to be working against you: class Name {
public Name (string name) {
this.name = name;
}
public static void greet() {
System.out.println("Hello " + this.name);
}
}
The programming is different in that you create a declarative network of objects that then ... act. At this level, there is hardly any difference, except that the name has been introduced as a proper concept. Many people will argue that such value objects are expensive or inefficient (which they are) but if that cost hits you hard, you should be asking yourself why you picked a high level language to begin with.Yes, in such a paradigm a place is needed where the entry point lives, but I found it fundamentally useful. Whatever you call the class it: - Can read and encapsulate access to env variables - It can accept commandline arguments (as main(argv[]) indicates) - It's the place to setup and configure your (object)- dependency tree (what IoC/Dependency Injection-Containers are doing) - Or it can be the ultimate exception handler for messages deeper in your program, e.g. where you would start your main message pump / game loop, etc.
The rest is ergonomics.
I am currently working on a Python/Django project of around 45k lines of code, 28k of which are tests. For me, this approach works quite well. It is not that I am against debugging or that I don't value debugging as a tool, but I just never feel the need to use one.
The tests also have a nice side-effect: because I do TDD a lot, I mostly end up with a lot of small understandable units, so I am effectively preventing code with properties that need me to follow each statement one by one.
I hardly ever feel the need to interrupt the running process to inspect the current variables. In many cases, this would be futile anyway, because I started writing a lot of generators.
def f():
x = 5
class Blub:
def incx(self):
nonlocal x
x += 1
def getx(self):
return x
return Blub()
j = f()
j.incx()
print(j.getx())
This will print 6, as expected.Although Java might be conceptually weak, empirically speaking it's a big winner, because developers appear to value ease of education, connectedness with others, availability of jobs and job security more than feeling stupid for not understanding lambda calculus. (that doesn't render formalist approaches invalid)
Exactly, which is why a class is the perfect singular location to place it. The object itself is just a pointer, the method doesn't get copied around, so object appear to be the perfect method to localize code.
> unexpected exceptions
True, exceptions introduce a communicative issue, but so does returning in-band values.
For example, what should the result of
open('file.txt').read()
be, when the current user does not have permissions to read file.txt?Different approaches exist:
status, content = read('file.txt')
content = read('file.txt', &status)
status = read(&content)
I wouldn't argue against any of them, although I have my preferences of course. I like the exception model here, but you are right, rare exceptions, communicated poorly can be surprising and painful.> Comprehensibility is in no way related to using objects.
On itself this statement is false, ... (hear me out)
> It all depends on the quality of the naming
... But this makes me understand what you're trying to say, and I 100% agree with it. In fact, I conducted some experimental research on identifier naming: https://link.springer.com/article/10.1007%2Fs10664-018-9621-... (Sci-hub or I can provide a preprint).
You are right in that objects and their use don't automagically turn a codebase in a field of readily availabe knowledge. Many mechanisms applied in OOP languages really work AGAINST comprehension (for example, buried exceptions originating from deep within an object graph). But still, objects are tightly coupled to comprehension, even historically speaking. They were first used to make it possible to model physical simulations without requiring users to know much about computer architectures (Simula 67). Objects are meant to "model", that is, symbolically represent concepts, entities or physical things in such a way that they might show agentic behavior. This is fundamentally different from having stupid data, smart functions but actually a completley different means of "Erkenntnis" (translates to "insight", but is more accurately conceived as "Epistemology"). The relationship between objects and readability / comprehension is complex, but I wouldn't call them "in no way related" (OOP can break comprehension, but it was invented to improve it. Irony.)
Also I would, again, like to second your words: Identifier naming might be the most imporant aspect of readability and comprehensibility.
I feel that objects offer a flexibility benefit though (which is why they often elude formal approaches) for the cost of purity.
It raises an exception. This interrupts the normal flow of things and asks you to deal with the problem asap. If necessary, the handling code then could try to reconnect or abort. If desired, it could return a closed connection to convey that state-change, so that calling code is made aware that it needs to reconnect first and can't just reused the now closed connection. You could revert it to a normal connection (a "Has never ever been opened to begin with"-Connection). Depends on whether your driver/adapter/underlying connection thing cares about a difference in initial connects or reconnecting. If the handling code can't deal with it, it can bubble the exception up one level.
Swapping a `Connection` for an `OpenConnection` isn't heretic by the way, such structures are described by the state pattern. Objects model state, Exceptions model events (state transitions) but the later isn't explicitly described in the original Gang of Four book that way. I just found that exceptions are very usefull for this, given that you react to them in only a limited scope.
Be aware that this idea is culturally dependent. In Java, Exceptions are often discouraged from being used in such a way (exceptions should never be used for control flow and only convey error cases), in Python it's normal to communicate state transition that way, e.g. for-loops watch for StopIteration exceptions.
>>> def a(u, v):
... def b():
... return u + v
... return b
...
>>> b = a(1,2)
>>> b()
3
Like this, there is no way to access u & v from b.I believe David Parnas introduced it in 1971 to mean that a program's design was sliced along shared units of concerns (things that vary together) rather than "steps in a flowchart".
https://prl.ccs.neu.edu/img/p-tr-1971.pdf
I believe what you are trying to convey is called "data abstraction", as for example used by Reynolds, 1975;
mentioned here: https://www.cs.utexas.edu/~wcook/papers/OOPvsADT/CookOOPvsAD...
explained here: https://link.springer.com/chapter/10.1007%2F978-1-4612-6315-...
def adder(x):
def add(y):
return x + y
return add
add_five = adder(5)
add_five(10)
How is that different from: class adder:
def __init__(self, x):
self._x = x
def __call__(self, y):
return self._x + y
add_five = adder(5)
add_five(10)
Did you have something like that in mind? Do I understand your idea correctly?They both allow you to achive the same thing. The syntax doesn't matter, they both produce the same result (in Python, that is, which is a bit unfair, because there, functions are objects to start with -- but even if this was not the case, then the object would require namespacing which is negligible). It depends on whether you want to `think` with a closure or not.
I propose that differences in OOP and FP on this level are irrelevant, but that the magic lies in the naming chosen here. An "adder" is something that has continuity and identity, and the calculation is performed later. I don't care about how it is constructed. There is nothing wrong with just calling add(1, 2) -> 3, but objects give you this deferred stuff for free (sorry, not free, for the price of some memory).
The tradeoff is most often performance versus comprehensibility. I'd argue in favor for the latter. Of course, ergonomics and ease of use are hard to measure (but not impossible, although empirical studies are quite difficult to do and expensive), but the tradeoff is similar for all higher level languages. Consider the overhead for a class `Year`:
`Year(2004)` --> valid
`Year(200192)` --> Exception
In its constructor validates integers.Insane! Expensive! you might say. All it does is encapsulate some integer! But I'd take that little overhead over the scattered insecurity of my colleagues every day, who in every calling method will do the same "if then that else"-check for the year range over and over again, when they are handed an int and need to find out what's in it. The class provides locality for my concern that I only ever want to deal with valid years.
When, in your system you find a year-typed object somewhere, it is guaranteed that this is valid. This creates peace of mind, which is way more expensive than RAM.
It turns out: some 95% of all official ACM/IEEE publications mentioning OOP, never explain what they mean. Some even have oop in their title but then just explain some crude java library, never explaining why oop is a good fit for their problem to begin with.
Finding a good definition is REALLY hard, but there appear to be two departments, outlined perfectly in the book "Object thinking" by David West:
1. Formalist (G. Booch et al.): Object-Oriented Programming is Programming with Objects, Classes and Inheritance
2. Hermeneutic (A. Kay): Objects should be inclusive, are a recursive projection of a cell-like organism metaphor.
Many try to mathematize the idea, use it for analysis and to validate programs, derive some sort of object calculus and care about the mechanisms. Others (the hermeneutic camp) where more about how objects do or do not support your conception of reality so that you can model and simulate it using rocks that we tricked into exibiting calculative thinking.
I found that one ubiquitous definition (Kay's infamous "messaging, local retention and protection and hiding of state-process") isn't helping at all, but people have been struggling to find a clear and consicse definition, which is why Booch and Kay are always put forward.
This is the only thing I ever got out of it:
http://pi.informatik.uni-siegen.de/gi/stt/38_2/01_Fachgruppe...
That also works in JavaScript:
function Fraction(a, b) {
this.value = function () {
return a / b;
}
}
const half = new Fraction(1, 2);
It feels weird to me, that classes were eventually introduced in JS.# pylint: disable=too-few-public-methods
That depends. Inheritance makes it easy to break encapsulation (which is bad -- agreed). It can be hard to model "Is-A"-Relationships properly, but I wouldn't call it inherently bad. A circle isn't an ellipsis; but a chair is furniture. The quality of code stems from your quality of thought.
> this means tight coupling between them, rather often unwanted.
That depends on your design. Coupling is the whole point, you do objects because you want to couple. Fraction.reduce, Fraction.add, Fraction.subtract, Fraction.multiply. Why not have them as as a cohesive unit, all these functions must understand the details of fraction anyway. Why not couple them?
> Not having mutable state is the point.
I agree. But objects never force you to publish their state; that's bad education. Getters and setters should be avoided. The Fraction above can be made immutable easily, Fraction(1, 2).add(Fraction(1,4)) --> <Fraction: 3/4>, leaving the old ones intact.
But then, it depends on how you communicate state.
I believe `Connection.Open()` should return an Object of Type `OpenConnection`. I believe that state changes should be communicated by changes of identity (either a new instance or a new Type), but ideas like this are most often answered by waving torches and pitchforks in front of my house at night (figuratively).