Blog-Archiv

Sonntag, 29. April 2018

Getting Used to TypeScript, Part 2

TypeScript is a programming language with a lot of possibilities and freedom. I won't discuss it in detail completely here in my Blog, it's too big, I will just present some findings from time to time.

In this Blog I tried to continue my recent implementations in an object-oriented way:

  • Separate interface abstractions from concrete implementations
  • Extend and reuse existing interfaces and classes to facilitate easy maintenance
  • Minimize code duplications through inheritance (DRY)
  • Keep everything strictly typed to find mistakes already at compile-time
  • Use access modifiers (private, protected) to minimize the risk of abuse and encapsulate complexity
  • Prefer constants to variables, and immutable objects (readonly properties) to mutable ones

About Interfaces

Why are interfaces important? Because they represent partial aspects of things. Using such abstractions you can reduce the dependencies among your classes. For example, if you use a type Duck, and you need it just for swimming, not quacking or anything else a duck does, then you should pass the duck as interface Swimmer into your module. Later on the Duck may be replaced by a Swan, but your code doesn't need not to be maintained, because a swan is a Swimmer too!

Interfaces are roles, class-objects are actors. If you want to choose between different actors for a role in your drama, then create an interface for that role.

Remember CRC cards, one of the earliest OO software design methods: Class-Responsibility-Collaboration. Responsibilities can be covered by roles better than by actors. If you organize the work in your company in terms of well-described roles, and a certain colleague that always performed a critical responsibility gets ill, then someone that knows how to implement that role can take over.

You don't need to create an interface for every concrete class. Use them wherever it is necessary (or useful) to have partial abstractions (stick to YAGNI). When you can (or must) provide default implementations, use abstract classes instead.

The concept of interfaces had been introduced by Java, although there were predecessors like the C header files. Java interfaces always played an important design role. Lots of Sun specifications were done by providing just interfaces. While durable implementations were developed, simple reference implementations were available for those that already used the interfaces in their applications. Interfaces are a way to standardize problem solutions without anticipating which the best solution will actually be (fastest? safest? smallest?).

For C programmers: if you search for a possibility to implement function-pointers in Java, use interfaces, they come closest to it, and, other than in C, they are fully typed.


Rectangle 3D = Box

Here, in these "Getting used to TypeScript" Blogs, I use interfaces just to try out their capabilities.

Following examples build on the interfaces and classes that I introduced in my recent Blog about TypeScript. There were interfaces Dimension, Point and Rectangle, and their according implementations DimensionImpl, PointImpl and RectangleImpl. The new source extends them to be 3-dimensional, i.e. Point gets a z coordinate, Dimension a length, and Rectangle turns into Box. Here is the extended UML class diagram:

Source Code

I had to adapt the old source of PointImpl to make private methods available as protected. A standard activity when you want to reuse some source code.

export class PointImpl implements Point
{    
    ....

    protected distanceX(other: Point): number    {
        return this.x - other.x;
    }
    protected distanceY(other: Point): number    {
        return this.y - other.y;
    }
}

These methods are needed in Point3DImpl that extends PointImpl.


I've put the new sources into a directory 3D below the old ones. You can see that through the import statements.

Update: CAUTION, the following class design is not completely clean. Compiler options uncovered a function overloading mistake (that I fixed in another Blog): you can not overwrite Point.distance(other?: Point) in PointImpl with distance(other?: Point3D) in Point3DImpl.

Interfaces
Dimension3D.ts
import { Dimension } from "../Dimension.js";

export interface Dimension3D extends Dimension
{
    readonly length: number;
    volume(): number;
}
Point3D.ts
import { Point } from "../Point.js";

export interface Point3D extends Point
{
    readonly z: number;
}
Box.ts
import { Point3D } from "./Point3D.js";
import { Dimension3D } from "./Dimension3D.js";

export interface Box extends Point3D, Dimension3D
{
    move(origin: Point3D): Box;
    resize(newDimension: Dimension3D): Box;
}
Classes
Dimension3DImpl.ts
import { DimensionImpl } from "../DimensionImpl.js";
import { Dimension3D } from "./Dimension3D.js";

export class Dimension3DImpl extends DimensionImpl implements Dimension3D
{
    readonly length: number;

    constructor(width: number, height: number, length: number)    {
        super(width, height);
        
        this.length = length;
    }
    
    volume(): number    {
        return this.area() * this.length;
    }
}
Point3DImpl.ts
import { Point3D } from "./Point3D.js";
import { PointImpl } from "../PointImpl.js";

export class Point3DImpl extends PointImpl implements Point3D
{
    readonly z: number;
 
    constructor(x: number, y: number, z: number)    {
        super(x, y);
        
        this.z = z;
    }

    distance(other?: Point3D): number    {
        if ( ! other )
            other = new Point3DImpl(0, 0, 0);
            
        const distanceX = this.distanceX(other);
        const distanceY = this.distanceY(other);
        const distanceZ = this.distanceZ(other);
        return Math.sqrt(distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ);
    }
    
    transform(origin: Point3D): Point3D    {
        return new Point3DImpl(this.distanceX(origin), this.distanceY(origin), this.distanceZ(origin));
    }

    private distanceZ(other: Point3D): number    {
        return this.z - other.z;
    }
}
BoxImpl.ts
import { Point3D } from "./Point3D.js";
import { Point3DImpl } from "./Point3DImpl.js";
import { Dimension3D } from "./Dimension3D.js";
import { Box } from "./Box.js";

export class BoxImpl extends Point3DImpl implements Box
{
    private readonly dimension: Dimension3D;
    readonly width: number;
    readonly height: number;
    readonly length: number;
    
    constructor(origin: Point3D, dimension: Dimension3D)    {
        super(origin.x, origin.y, origin.z);
        
        this.dimension = dimension;
        this.width = dimension.width;
        this.height = dimension.height;
        this.length = dimension.length;
    }
    
    volume(): number    {
        return this.dimension.volume();
    }
    area(): number    {
        return this.dimension.area();
    }
    
    resize(newDimension: Dimension3D): Box    {
        return new BoxImpl(this, newDimension);
    }
    move(newOrigin: Point3D): Box    {
        return new BoxImpl(newOrigin, this.dimension);
    }
}

Mind the fact that I needed to duplicate the Dimension3D properties width, height and length into BoxImpl. This is due to single inheritance, BoxImpl extends Point3DImpl, thus it must hold a delegate to also implement Dimension3D. But it can not forward to the dimension-properties! As a consequence, the property values are held twice. In case these properties were not immutable (readonly) this would be a real hazard, because the forwarded methods volume() and area() work on the dimension delegate, while redundant values, implementing Dimension3D, are stored in the BoxImpl instance.

This has been more cleanly solved in Java, where interface fields always are constants, not even settable by a constructor. Java interfaces contain just constants and methods. No delegate-forwarding problem.

Java 8 interfaces now can contain also default method implementations. There is nothing similar in TypeScript. If you look at the files that are generated from TypeScript interfaces, you will find them empty. They are used just for compilation. But TypeScript supports abstract classes, which is somehow similar.


Conclusion

In my next Blog I will add tests to prove that the implementations actually work.




Keine Kommentare: