Skip to content
This repository has been archived by the owner on Dec 21, 2022. It is now read-only.

💬 underscore bei privaten klassen-membern: ja oder nein? #10

Open
heikomat opened this issue Aug 25, 2017 · 13 comments
Open

💬 underscore bei privaten klassen-membern: ja oder nein? #10

heikomat opened this issue Aug 25, 2017 · 13 comments

Comments

@heikomat
Copy link
Contributor

heikomat commented Aug 25, 2017

Warum existiert der underscore-präfix:
JavaScript Entwickler gehen aufgrund fehlender zugriffs-scopes idr. her, und versehen Variablen, die nur für die interne Verwendung gedacht sind mit einem underscore-präfix. Damit wird anderen, die den code lesen/verwenden signalisiert: "Ich weiß das ist JS, und ich kann dich nicht dran hindern, dass zu nutzen, aber lass es besser sein, das ist nicht für die externe Nutzung gedacht".

Wie handlen andere typisierte Sprachen das:
Bei typisierten Sprachen tritt das Problem nicht auf, da man hier über private/public/protected einstellen kann, wer wie zugriff haben soll.

Warum TypeScript ein Sonderfall ist:
Jetzt haben wir allerdings TypeScript: Ein JavaScript-Superset, welches aber ziemlich typisiert ist, und Zugriffsmodifikatoren unterstützt. Da wir uns alle einig sind, dass wir zumindest konsequent entweder komplett mit, oder ganz ohne underscore arbeiten sollten ist hier das Issue, indem wir uns auf eins von beidem einigen können.

Hier noch die pro- und contra-argumente, die mir spontan einfallen bzw. die ich kürzlich gehört habe:

pro underscore-präfix:

  • Wenn man sich irgendwann entscheiden sollte, TypeScript nicht mehr zu nehmen, und wieder auf JavaScript umzusteigen, und man dafür in einem ehemaligen TS-Projekt das JS-kompilat als neue sourcecode-grundlage verwenden möchte, wären hier die Klassen-member bereits korrekt benannt
  • Es kann Fälle geben, in denen JavaScript und TypeScript kombiniert werden, z.B. bei abgeleiteten Klassen. Um da nicht 2 Konventionen gleichzeitig anwenden zu müssen, sollten die TS-Sourcen bereits den underscore-präfix verwenden

contra underscore-präfix:

  • Der underscore-präfix ist nur ein Workaround, weil JS keine Zugriffsmodifikatoren kennt. Jetzt, wo der Workaround in TS nicht mehr nötig ist, sollten die Altlasten abgeschafft werden.
  • Andere typisierte Sprachen kommen (soweit ich weiß) auch wunderbar ohne underscore-präfix aus, und teilweise hat TypeScript mit typisierten sprachen mehr Ähnlichkeit, als mit JavaScript.

Ich kann leider nur 10 Leute assignen, deshalb hier ein paar mentions von Personen, von denen ich glaube, dass sie etwas beitragen können/möchten, und die nicht assigned sind:
@LeoTT @Paulomart @Vyperus

@NullEnt1ty
Copy link
Contributor

NullEnt1ty commented Aug 25, 2017

Ein Beispiel:

function delay(milliseconds: number, count: number): Promise<number> {
    return new Promise<number>(resolve => {
            setTimeout(() => {
                resolve(count);
            }, milliseconds);
        });
}

// async function always return a Promise
async function dramaticWelcome(): Promise<void> {
    console.log("Hello");

    for (let i = 0; i < 5; i++) {
        // await is converting Promise<number> into number
        const count:number = await delay(500, i);
        console.log(count);
    }

    console.log("World!");
}

dramaticWelcome();

Das Transpilat dazu (es5 target):

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [0, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
function delay(milliseconds, count) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(count);
        }, milliseconds);
    });
}
// async function always return a Promise
function dramaticWelcome() {
    return __awaiter(this, void 0, void 0, function () {
        var i, count;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    console.log("Hello");
                    i = 0;
                    _a.label = 1;
                case 1:
                    if (!(i < 5)) return [3 /*break*/, 4];
                    return [4 /*yield*/, delay(500, i)];
                case 2:
                    count = _a.sent();
                    console.log(count);
                    _a.label = 3;
                case 3:
                    i++;
                    return [3 /*break*/, 1];
                case 4:
                    console.log("World!");
                    return [2 /*return*/];
            }
        });
    });
}
dramaticWelcome();

Are you sure about this?

@lmoe
Copy link

lmoe commented Aug 25, 2017

Ich persönlich finde die Lösung private Methoden mit nem Unterstrich zu bennen unschön und es ist für mich ein notwendiges Übel da JavaScript keine privaten Methoden erlaubt. Mich stört es im Lesefluss.

In TypeScript gibt es die Möglichkeit private Methoden zu definieren, und auch wenn man in TypeScript JavaScript verwenden kann, ist es für mich eigentlich kein Argument einen Workaround zu verwenden der nur auf Grund von fehlenden Features in JavaScript benutzt wird.

Das man irgendwann von TypeSript auf JavaScript wechseln will und dafür die transpilierten Sourcen nutzen will halte ich für Fragwürdig. Ich kann mir gut vorstellen das TypeScript einigen Code sehr unübersichtlich generiert. Kannst du da was genaueres zu sagen @heikomat ?

Das Argument mit den abgeleiteten Klassen zwischen TypeScript in JavaScript kann ich so noch nicht ganz nachvollziehen, in TypeScript Projekten sollte man IMHO plain JavaScript vermeiden so gut wie es geht. @5ebastianMeier

Bei Sprachen ohne private/public Modifikator: Ja
In anderen Fällen ein klares: Nein

@heikomat
Copy link
Contributor Author

heikomat commented Aug 25, 2017

@NullEnt1ty @lmoe ich persönlich bin auch kein Fan vom underscore, aber wenn man nicht grade ES5 als ziel angibt, und die tsconfig richtig einstellt, kann ter typescript-compiler auch relativ sauberen JS-Code erzeugen

@NullEnt1ty
Copy link
Contributor

Wenn man sich irgendwann entscheiden sollte, TypeScript nicht mehr zu nehmen, und wieder auf JavaScript umzusteigen, und man dafür in einem ehemaligen TS-Projekt das JS-kompilat als neue sourcecode-grundlage verwenden möchte, wären hier die Klassen-member bereits korrekt benannt

Ich kann dem TypeScript Compiler doch auch bestimmt sagen, dass er private Properties entsprechend mit einem Underscore präfixen soll.

@heikomat
Copy link
Contributor Author

Ich kann dem TypeScript Compiler doch auch bestimmt sagen, dass er private Properties entsprechend mit einem Underscore präfixen soll.

Ich bin mir nicht sicher, ob das geht. Mächtig genug wäre der TSC, ich weiß nur nicht, obs implementiert ist. Wenn das allerdings geht, dann ist das auf-JS-umsteigen-Argument damit hinfällig

@NullEnt1ty
Copy link
Contributor

aber wenn man nicht grade ES5 als ziel angibt, und die tsconfig richtig einstellt, kann ter typescript-compiler auch relativ sauberen JS-Code erzeugen

Naja, in ES6 sieht der Code auch nicht viel schöner aus:

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function delay(milliseconds, count) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(count);
        }, milliseconds);
    });
}
// async function always return a Promise
function dramaticWelcome() {
    return __awaiter(this, void 0, void 0, function* () {
        console.log("Hello");
        for (let i = 0; i < 5; i++) {
            // await is converting Promise<number> into number
            const count = yield delay(500, i);
            console.log(count);
        }
        console.log("World!");
    });
}
dramaticWelcome();

@Paulomart
Copy link

Ich habe oft den Underscore-Prefix in zusammenhang mit Gettern/Settern gesehen, was passiert damit?

class Fuu {
  private _bar: string;

  public get bar() {
    return this._bar;
  }
}

@heikomat
Copy link
Contributor Author

heikomat commented Aug 25, 2017

@Paulomart hier würde der underscore nur zum Umgehen von Namenskonflikten dienen, nicht zur Darstellung ob etwas private oder public ist. Da könnte man einfach einen anderen Namen verwenden.

z.B. könnte man für den member ohne getter/setter einen technischen Namen verwenden, der beschreibt was es ist, und für den member mit getter/setter einen fachlichen, der beschreibt was sein Zweck ist.

Wenn einem damit keine unterschiedlichen Namen einfallen, könnte man sich immernoch eine Namingconvention ausdenken. Das könnte auch darauf hinaus laufen, dass man sagt: fällt dir kein besserer Name ein, mach einen unterstrich vor dem member ohne getter. Aber das wäre dann eine Fachlich andere sache, weil der unterstrich dann nicht zum darstellen der Zugriffsmöglichkeiten dient, sondern zum umgehen von Namenskonflikten

@LeoTT
Copy link
Contributor

LeoTT commented Aug 25, 2017

Ich kann dem TypeScript Compiler doch auch bestimmt sagen, dass er private Properties entsprechend mit einem Underscore präfixen soll.

Gibts sehr sicher nicht und wäre dagegen, wenn es das gäbe. Klingt sehr fehleranfällig.
Gegeben sei folgender Fall:

private var_1;
private var_2;

public function getVar(index: number) {

  return this[`var_${index}`];
}

Wenn tsc aus var_1 zur Laufzeit einfach _var_1 macht, deckt sich das nicht mit dem return-value. Denn wie soll TypeScript wissen, dass es sich bei meinem konstruierten Zugriff um eine private Variable handelt?
Würd deshalb auch nicht versuchen mit der API von TypeScript selber so einen Konverter zu bauen.

Wäre auch für Verzicht von _.

Eine Randnotiz: Es gibt ein ES-Proposal für richtige private class-member(stage 3, könnte also gut durchkommen), wo man # anstelle von _ verwendet.
Nicht sicher, wie typescript da adaptiert, aber dann hätten wir private, #..., und legacy-Code mit _.... Kein starkes Argument, weils nur Proposal ist.

@wolf-5minds
Copy link

@Paulomart: In deinem Beispiel sollte das nicht nötig sein oder? Ich kenne TS nicht wirklich, aber Underscore und this. schaut doppelt aus. Eins sollte zur Unterscheidung ausreichen.

@heikomat: In .NET ist ein Backing-Field für eine Property ein technisches Hilfsmittel; in TS nicht? Eigentlich suggeriert eine Property, dass Getter/Setter nichts weiter machen, als etwas zu lesen oder zu setzen. Damit ist es die gleiche Fachlichkeit - ich wüsste nicht, wieso das Feld dann anders heißen sollte, als die Property?

@LeoTT: Mh, macht ihr das so? Dann hoff ich, dass das Proposal für das nameof()-Feature bald durch ist. Es ist schon ein sehr brüchiges Vorgehen, auf Member mit Magic Strings zugreifen zu müssen.

Der Underscore wird in typisierten Sprachen genauso verwendet. In C# wird es häufig als Alternative zu this. gesehen. Microsoft selbst verwendet den Underscore in TS und .NET Core - gibt aber bewusst keine Empfehlung heraus.

Pers. Meinung:
In .NET greift man mit this. prinzipiell auf jeden nicht-statischen Member (Methode, Property, Field) außer Parametern zu, also hat man da bzgl. Lesbarkeit nicht viel gewonnen. Besonders, weil wie Heiko schon sagte, man in solchen Fällen einfach passendere Namen wählen kann. Ich finde jedoch das Argument schrecklich, es würde späteres Refactoring vereinfachen (Gruß an Private Properties).

@heikomat
Copy link
Contributor Author

heikomat commented Aug 25, 2017

@wolf-5minds in JavaScript/TypeScript kann man getter und setter methoden verwenden, um einen member zu definieren. Diese beiden Beispiele machen haar genau das gleiche (siehe JS getter und setter:

class SomeClass {
  someMember: string = 'test';

  constructor() {
    console.log(this.someMember);
  }
}

class SomeClass {
  get someMember(): string {
    return 'test';
  }

  constructor() {
    console.log(this.someMember);
  }
}

Der Unterschied in der Verwendung ist, dass uns getter und setter-methoden Kontrolle darüber geben, was genau passieren soll, wenn ein member gesetzt oder gelesen wird. Zum Beispiel könnten man folgendes tun, was ohne die getter methode nur mit umwegen funktioniert:

class SomeClass {
  get dbStatus(): string {
    if (this.dbIsConnected) {
      return 'connected';
    }

    return 'not connected';
  }

  constructor() {
    console.log(this.dbStatus);
  }
}

Hier greife ich weiterhin auf den member zu, wie auf jeden anderen normalen member auch, aber jedes mal wenn ich das tue wird etwas logik ausgeführt, was ggf auch ändert, was in diesem Fall geloggt wird.

Zudem hat man bei JS/TS kein implizites this! Wenn du auf einen member der eigenen Klasse zugreifen willst musst du this verwenden

Bisher haben wir häufig eine kombi aus

  • private underscore-variable, und
  • private oder public variable ohne underscore, aber mit getter/setter

verwendet, damit wir, falls wir irgendwann mal den Rückgabewert für diese Variable ändern möchten, wir in getter/setter an einem zentralen Ort bestimmen können, wie die variable sich verhält. andernfalls kann die variable irgendwo irgendwie gesetzt, und irgendwo anders später ausgelesen werden, und wer weiß was beinhalten. Mit getter und setter können wir immer am deklarations-ort des members bestimmen, was er sich verhält und was er beinhaltet.

In der Praxis läuft das allerdings häufig darauf hinaus, dass wir an vielen Stellen konstrukte haben wie das, was @Paulomart dargestellt hat: eine underscore-variable, und getter/setter die nur darauf arbeiten, und nichts weiter tun.

Ich hoffe damit ist etwas besser verständlich, wie sich getter/setter in JS/TS verhalten, und wo sie sich evtl von anderen Sprachen unterscheiden.

@wolf-5minds
Copy link

Ah, Danke @heikomat. Ja, es hat zum Verständnis beigetragen. Ich wollte auch nur auf ein paar Argumente eingehen, nicht die Diskussion hijacken.

@NullEnt1ty
Copy link
Contributor

NullEnt1ty commented Aug 28, 2017

Ich finde den Underscore auch eher störend, als hilfreich. Ich kann verstehen, dass bei Interfaces das I-Präfix in Code Reviews nützlich ist, allerdings sehe ich diesen Vorteil bei Access Modifiers nicht.

Wenn ich eine Code Review mache, achte ich eher selten darauf, ob nun an dieser einen Stelle auf eine private oder public Variable zugegriffen wird. Ich wüsste auch spontan nicht, warum ich darauf achten müsste (Ausnahme wäre hier die Deklaration). Wenn der Programmierer auf eine private Variable außerhalb der Klasse zugreift, wird ihm die statische Code Analyse schon auf die Finger hauen.

Nach dem gleichen Argument, könnte man auch die ungarische Notation wiedereinführen...

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.