Skip to content

volodymyrprokopyuk/kotlin-sdp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Software design patterns in Kotlin

Creational patterns

Creational patterns encapsulate knowledge about concrete classes the system use, hide how instances of these classes are created and separate the system from how the objects are composed and represented.

  • Abstract factory (usage)
    What. Abstract Factory provides an interface for creating families of related/dependent objects without specifying their concrete classes. Abstract Factory abstracts and encapsulates the creation of a suite of products for a given platform/family that the system depends on
    How. AbstractFactory interface provides methods for creating all kinds of products for a given platform/family. Client works only with the AbstractFactory and the Product interfaces. The concrete implementations of the AbstractFactory interface are singletons
    Example. The system depends on a Letter=Product and a Resume=Product instances. The system uses a DocumentCreator=AbstractFactory for creating the concrete Letter and Resume instances from the two families: modern and fancy

  • Builder (usage) #
    What. Builder separates the construction of a complex object from its representation, allowing the same step by step construction process to create various representations
    How. Client uses (a) a separate Builder object which receives each initialization parameter step by step in a fluent interface or (b) a Builder DSL initialization method which configures properties by direct property assignment or function call and returns the resulting constructed complex object at once
    Example. Car.Builder provides a builder DSL (Car.build { ... }) for building a Car instance

  • Dependency injection (usage) #
    What. A class (client) accepts through an interface objects (services/dependencies) the class requires from an injector instead of creating the objects directly
    How. The Injector passes the Service object to the Client class via the Service interface by the inversion of control. Injector decouples the Client from creating the Service object directly. The Injector creates the Service object and calls the Client. The Client does not know about the Injector. The Client works only with the Service interfaces provided by the Injector via constructor injection or setter injection
    Example. The Client requires the ConstructorInjectedDependency and the SetterInjectedDependency both implementing the Service interface. The Injector creates the Client and sets up its dependent Services via the constructor injection and the setter injection

  • Factory method (usage)
    What. Factory method defines an interface for creating a single object, but let subclasses decide which class to instantiate
    How. FactoryMethod interface provides a method for creating a single object. Client works only with the FactoryMethod and the Product interfaces. The concrete implementations of the FactoryMethod interface are singletons
    Example. The system depends on an Article=Product instances. The system uses ArticleCreator=FactoryMethod for creating a single Article instance from the two families: modern and fancy

  • Prototype (usage) #
    What. Prototype creates new objects by cloning prototypical instance, boosting performance and keeping memory footprint to a minimum
    How. Client works only with the Product interface and uses the Product::clone() method for creating new instances of the Product
    Example. The ProkaryoteCell=Product and the EukaryoteCell=Product implement the CellPrototype interface. In order to create a new instance of the specific cell the CellPrototype::clone() method is used

  • Singleton (usage)
    What. Singleton ensures that a class has only one instance and provides a global point of access to the instance. Singleton global variable like dependency is not declared in any component interface, but is tightly coupled with all component implementations. Replace Singleton pattern with Dependency Injection pattern based on Dependency Inversion Principle
    How. Make the class constructor private and provide one public static method that always returns the same single instance of the class stored in a private static variable
    Example. The expression object Singleton defines a Singleton class and immediately instantiate the class with the single point of access to the instance being the Singleton class name

Structural patterns

Structural patterns provide simple way of implementing relationships between objects.

  • Adapter (usage)
    What. Adapter converts the interface of the class without modifying class code into another interface that client expects
    How. Client works with the class through an implementation of the Adapter interface that client expects and delegation to the class methods
    Example. Client expects the Phone=Adapter interface. XiaomiPhoneAdapter implements the Phone interface that client expects and delegates to the XiaomiPhone class methods

  • Bridge (usage)
    What. Bridge decouples an abstraction from its implementation (two orthogonal dimensions) allowing the two to vary independently
    How. The Abstraction and its Implementation are defined and extended independently. The Abstraction is implemented by delegating to its Implementation
    Example. The Device=Abstraction has two implementations PhoneDevice and TabletDevice. The Vendor=Implementation has two implementations XiaomiVendor and NokiaVendor. The Device implementations accept Vendor implementation. The Device::switchOn() method delegates to the Vendor::support(Device) method

  • Composite (usage) #
    What. Composite treats individual objects and composition of objects uniformly. Composes objects into tree structures to represent part-whole hierarchies
    How. The Leaf and the Composite classes implement the Component interface. The Leaf class implements the request directly while the Composite class forwards recursively the request to composite's children
    Example. The Expression=Component is a uniform Component interface. The Operand=Leaf implements the request directly by returning the operand value. The Operation=Composite implements the request recursively by evaluating its right and left expressions and than applying the actual operation to the results of the left and right expression evaluations

  • Decorator (usage) #
    What. Decorator attaches additional behavior to an individual object dynamically keeping the same interface without affecting the behavior of other objects of the same class
    How. The Decorator class implements the original Component interface by delegating the request to the original object and adding behavior before/after the original request. Multiple decorators can be stacked on top of each other
    Example. The SimpleCoffee original class implements the Coffee=Component interface. The CoffeeWithSugar=Decorator and the CoffeeWithMilk=Decorator decorators implement the Coffee interface and accept the Coffee instance to delegate to

  • Facade (usage)
    What. Facade defines a higher-level simplified interface that makes a system/library easier to use. Facade hides the complexities of a larger system with dependencies and provides a simpler interface to the client
    How. Client works only with the Facade higher-level simplified interface to interact with the larger system
    Example. The larger system Desktop implements the Computer=Facade interface which is used by the client. The Desktop manages internally all the complexities involved with the subsystems Cpu, Ram and Ssd

  • Flyweight (usage)
    What. Flyweight uses sharing to support a large number of similar objects efficiently
    How. Shares the Invariant/intrinsic object state in an external data structure. When a new object is created the FlyweightFactory provides the cached Invariant/intrinsic object state and allows the Variant/extrinsic object state to be set through the Flyweight interface
    Example. The GlyphCode=Invariant represents the intrinsic glyph state (code) that can be cached and shared between glyphs. The GlyphFlyweight=Invariant+Variant class implements the Glyph=Flyweight interface that allows extrinsic glyph state (position) modification. The GlyphFactory=FlyweightFactory caches and shares efficiently the GlyphCode instances

  • Proxy (usage) #
    What. Proxy provides a placeholder/wrapper for another object for access control, request validation, response caching, etc.
    How. The real object and the Proxy implement the same Subject interface, so the client cannot distinguish between the real object and the Proxy. The Proxy uses delegation to the real object
    Example. The real Account object implements the Payment=Subject interface without any balance/amount validations. The PaymentProxy=Proxy implements the Payment interface with the balance/amount validation. Client uses only the Subject interface to work with both the real Account object or with the PaymentProxy proxy object

Behavioral patterns

Behavioral patterns provide simple way of implementing interactions between objects.

  • Chain of responsibility (usage) #
    What. Chain of Responsibility chains the receiving objects/functions (handlers) and pass the request along the chain until an object/function handles the request completely or partially. Avoids coupling of the request sender to the request receiver allowing more than one receiver a chance to handle the request
    How. The Request represents the initial data, then intermediary results and finally the final result. The RequestHandler functional interface processes the Request partially or completely. The ChainOfResponsibility composes the RequestHandlers and implements the RequestHandler functional interface to process the Request
    Example. The cashRequestHandlerChain=ChainOfResponsibility composes the CashRequestHandler=RequestHandler and implements the RequestHandler interface to process the CashRequest=Request. The CashRequest has the initial amount to represent with the set of notes. The CashRequest is used to handle intermediary results by representing the remaining amount and a set of already processed notes. The CashRequest finally represent the final result of the set of notes with the amount remainder if any

  • Command (usage)
    What. Command encapsulates an action with the request parameters as an function/object and decouples the request for an action from the actual action performer. Allows action/request queueing, logging and undoable operations
    How. The Command object stores the request parameters and delegates the request to the Receiver which performs the action. The Invoker object uses the Command interface and provides request queueing, logging and undoable operation functionality
    Example. The cookStarter(), cookMainCourse() and cookDessert() functions implement the Order=Command=Receiver interface and store the request arguments in a closure. The Waiter=Invoker queues the Orders and serves the Orders by using the Order interface

  • Interpreter (usage)
    What. Given a language (DSL), define a language grammar with an interpreter that use the grammar to interpret the language sentences
    How. Define an Expression class hierarchy for each TerminalExpression and NonterminalExpression symbol in the language. The abstract syntax tree (AST) of the language sentence is a Composite of Expressions and is used to evaluate the sentence. The TerminalExpression interprets the expression directly. The NonterminalExpression has a container of children Expressions and recursively interprets every child Expression. Interpreter does not describe how to build an AST. The AST can be build with a parser
    Example. The Constant=TerminalExpression, the Add=NonterminalExpression and the Mul=NonterminalExpression implement the Expression interface. The Interpreter implements the Expression interpretation algorithm

  • Iterator (usage) #
    What. Iterator provides a way to access the elements of an aggregate object/container of components sequentially without exposing the underlying representation (data structure) of the aggregate. Iterator encapsulates the traversal algorithm of a given aggregate object/container of components
    How. The ContainerIterator implements the Iterator interface for the Container of Components to traverse sequentially the Components of the Container without exposing the underlying aggregate representation. Client access the Components of the Container only through the Iterator interface
    Example. The FruitsIterator=ContainerIterator implements the Iterator interface for the Fruits=Container of Fruit=Component

  • Mediator (usage)
    What. Mediator defines an object that encapsulates how a set of other objects interact. Mediator promotes loose coupling between collaborators
    How. The Colleagues interact with each other through the Mediator interface without having any references to each other. The Mediator object has references to every Colleague and interacts with a Colleague through a common Colleague interface
    Example. The Airplane=Colleague and the Helicopter=Colleague implement the Aircraft=Colleague interface and accept the ControlTower=Mediator. The Airplane and the Helicopter communicate with each other through the ControlTower interface. The ControlTower has the references to the Airplane and the Helicopter and forwards the messages through the Aircraft interface

  • Memento (usage)
    What. Memento captures and externalizes an object's internal state without violating encapsulation. Allows the object to be restored (undo/rollback) to the captured state later
    How. The Caretaker requests the Originator to snapshot Originator's internal state into the Memento object before using the Originator. To rollback the Originator's internal state the Caretaker returns back the Memento object to the Originator
    Example. The Counter=Originator object provides a CounterMemento=Memento object to store to/retrieve from the Counter internal state. The incremental count of the Counter object could be altered by using the CounterMemento object

  • Observer (usage) #
    What. Observer defines a one-to-many dependency between objects where a state change in one object Subject is automatically notified to all subjects' dependents Observers
    How. The Subject maintains a list of the Observers which implements the Observer interface. When the Subject's state changes the Subject notifies all the Observers using the Observer interface. The Subject and the Observers are loosely coupled as the state change notification is done through the Observer interface
    Example. The Bidder=Observer implements the BidObserver=Observer interface. The Auctioneer=Subject implements the AuctioneerSubject=Subject interface. When the bid changes the Auctioneer notifies all the Bidders through the BidObserver interface

  • State (usage)
    What. State alters objects' behavior when object's state changes
    How. The State interface implementation provides the request handling functionality and sets the next State. State implements a state machine where each individual state is a derived class of the State interface and each transition is defined in state interface method invocation
    Example. The VendingMachine internal state goes through the ShowProducts, SelectProduct, DepositMoney and DeliverProduct States by invoking the VendingMachine::proceed() method which delegates to the current State::handleRequest() method which handles the request and sets the next VendingMachine State

  • Strategy (usage) #
    What. Strategy defines a family of interchangeable at runtime algorithms and provides excellent support for the Open-Closed Principle
    How. Define a set of interchangeable algorithms/strategies that implement the Strategy interface. Based on the conditions at runtime dynamically select the appropriate algorithm/strategy. Client works only with the Strategy interface
    Example. The TransportCompany dynamically select the appropriate algorithm/strategy goByBus or goByTaxi based on the size of the tourist group. Both goByBus and goByTaxi implements the Transport=Strategy interface under which the algorithms/strategies are provided to the client

  • Template method (usage)
    What. Template Method defines the skeleton of an algorithm (invariant) in one operation deferring some steps (variable) to subclasses (inversion of control) and preserving the overall structure of the algorithm
    How. The abstract class defines the abstract algorithm Template with the overall algorithm structure. The invariant algorithm steps are defined as final methods in the abstract class and the variant steps are open for overriding in the Template specializations
    Example. The Employee=Template defines the overall algorithm structure, the invariant and variant algorithm steps. The Developer and the Architect algorithm specialization override the variable algorithm steps

  • Visitor (usage)
    What. Visitor separates an algorithm from an object structure on which the algorithm operates. Visitor allows to add new operations through the Visitors to the existing object structure known as Elements without modifying the structure
    How. The Element interface defines a visitor operation for every Visitor type based on the abstract Visitor interface implementing the dynamic dispatch on the abstract Visitor interface. Every Visitor interface defines the overloaded for each Element type operation implementing the static dispatch on the concrete Element type. Every Visitor interface implementation has the cross between the concrete Element and the concrete Visitor implementing the double dispatch (dynamic on the Visitor type and static on the Element type)
    Example. The XiaomiPhone and the NokiaPhone implement the Phone=Element interface for the dynamic dispatch on the abstract Visitor type (one operation for each Visitor type). The IntelWiFi and the BroadcomWiFi implement the WiFi=Visitor interface for the static dispatch on the concrete Phone type to switch on the WiFi visitor operation. The SonyCamera and the SamsungCamera implement the Camera=Visitor interface for the static dispatch on the concrete Phone type to take phones with the Camera visitor operation. So the two visitor operations (switch on WiFi and take photo) are implemented on the Phone Element structure

Fundamental software design principles

  • KISS - Keep It Simple, Stupid. Do the simplest thing that could possibly work. Avoid unnecessary complexity. Maintain balance between code simplicity and system flexibility, extensibility
  • YAGNI - You Aren't Gonna Need It. Do not write code that is not necessary at the moment, but might be necessary in the future. Do the simplest thing that could possibly work
  • DRY - Don't Repeat Yourself. Avoid duplication. Every piece of knowledge must have a single, unambiguous, authoritative representation within the system. The modification of any single element of the system should not require changes in other logically unrelated elements
  • Abstraction principle. Function (interpretation) and structure (representation) should be decoupled and independent. It should be possible to use a value or a function without knowing how it is implemented. It should be possible to change the implementation without breaking a client code
    • Data abstraction (abstract data types, ADT). Hide a representation of a data type behind an interface (constructors, selectors, [mutators], type predicate)
    • Procedure abstraction (higher-order functions, HOF). Define a generic algorithm in a function that takes as parameters other functions to perform specific tasks. Examples: map, filter, reduce, fold (fundamental iterator), unfold (fundamental constructor)
  • Information hiding. Information should only be made available on the need-to-know basis. Locality principle scopes should be as small as possible. One piece of code that calls another piece of code should not know internals about that other piece of code. This make it possible to change internal parts of the called piece of code without being forced to change the calling piece of code accordingly. Expose as little as possible of the internal implementation details of a module to promote loose coupling between modules. Provide a stable interface to module functionality that will protect clients of a module from changes in the module implementation
  • High cohesion. High cohesion is a degree to which the components inside a module belong together. A module has high cohesion when the module responsibility is clearly defined and the module has as few dependencies as possible. Single Responsibility Principle fosters high cohesion
  • Loose coupling. Each module in the system has as little knowledge as possible about other modules in the system. Use interfaces to implement loose coupling between modules. High cohesion fosters loose coupling
  • Robustness principle (flexible input, compliant output). Be conservative in what you produce (produce outputs compliant with a specification). Be tolerant in what you accept from others (validate and sanitize inputs as long as the meaning is clear)
  • Top-down approach. Mostly used in Object-Oriented Programming (OOP) with interfaces and abstract classes. Decomposition of a system into compositional subsystems. An overview of the system is formulated specifying but not detailing any first-level subsystems. Each subsystem is then refined in yet greater detail, until the entire specification is reduced to base elements. Emphasizes complete understanding of a system and its subsystems. No coding can begin until a sufficient level of detail has been reached in the design phase
  • Bottom-up approach. Mostly used in Functional Programming (FP) with function composition. Composition of basic elements together into a more complex components. The individual base elements of a system are first specified in great detail and immediately implemented. These elements are then linked together to form larger subsystems, until a complete top-level system is formed. Emphasizes coding and early testing, which can begin as soon as the first module has been specified. There is an uncertainty in how modules can be linked together to form a top-level system
  • RAII - Resource Acquisition Is Initialization. Smart pointer: constructor acquires, destructor releases. Smart pointer is a scope-based resource management. When a resource gets out of scope via normal execution or thrown exception the resource is deallocated automatically by a smart pointer destructor. RAII only works for resources acquired and released by stack-allocated objects where there is a well-defined static object lifetime
  • FCoI - Favor Composition + Delegation over Inheritance. Composition is a black box reuse through an interface and promotes loose coupling. Inheritance is white box reuse through inherited members/methods and implies tight coupling
  • LoD - Law of Demeter. The principle of least knowledge/dependencies - don't talk to strangers, only talk to your immediate neighbors. LoD fosters loose coupling and information hiding
  • ADP - Acyclic Dependency Principle. Circular dependencies should be avoided. Dependency Inversion Principle and creation of a new package with common components breaks circular dependencies

SOLID principles

  • SRP - Single Responsibility Principle. Software unit should have only one single and well-defined responsibility, only one reason to change. High cohesion fosters SRP
  • OCP - Open-Closed Principle. Software unit should be open for extension (inheritance, Strategy, or Decorator design patterns), but closed for modification (interface with multiple polymorphic implementations)
  • LSP - Liskov Substitution Principle. Hierarchy is used to build specialized types from a more general type. Polymorphism means that one single interface has multiple implementations. Subtype must be completely substitutable for its supertype. Preconditions cannot be strengthened in a subtype. Postconditions cannot be weakened in a subtype. Supertype invariants must be preserved in a subtype
  • ISP - Interface Segregation Principle. Segregate one broad single interface into a set of smaller and highly cohesive interfaces, so other program components depend only on small cohesive interfaces instead of depending on a single broad interface, and other program components won't be required to implement all the functionality of a broad interface
  • DIP - Dependency Inversion Principle. Avoid tight coupling between modules with the mediation of an abstraction layer (interface). Each module should depend on an abstraction (interface), not other modules directly. An abstraction (interface) provides behavior needed by a module through possibly multiple implementations. Common features should be consolidated in a shared abstractions exposed through interfaces. Dependency Inversion Principle fosters testability of components

UNIX principles

  • Make each program do one thing and do it well (quality components)
  • To do a new job build afresh rather than complicate old programs by adding new features (Single Responsibility Principle, separation of concerns)
  • Expect the output of every program to become the input to another program. Write programs to handle text streams, because text is a universal interface. Don't clutter the output with extraneous information. Don't insist for interactive input and allow for scripting (uniform communication)
  • Favor composability (bottom-up approach in FP) over monolithic design
  • Design and build software to be tried early (bottom-up approach in FP). Build prototype as soon as possible. Don't hesitate to throw away bad design and rebuild from scratch
  • Use tools or even build tools to automate repetitive task (DevOps)

Unix philosophy

  • Modularity. Write simple parts connected by clean interfaces (modularity + composability)
  • Clarity. Clarity is better than cleverness
  • Composition. Design programs to be connected to other programs (composability + uniform communication)
  • Separation. Separate policies from mechanisms. Separate interfaces from engines (orthogonality)
  • Simplicity. Design for simplicity. Add complexity only where you must
  • Parsimony. Write a big program only when nothing else will do
  • Transparency. Design for visibility to make issue resolution easier (Dependency Inversion Principle)
  • Representation. Fold knowledge into data, so program logic can be simple and robust (first data structures and then algorithms)
  • Silence. When a program has nothing surprising to say, it should say nothing
  • Failure. When a program must fail, fail noisily as soon as possible
  • Generation. Write programs to write programs when you can (metaprogramming and DSL)
  • Optimization. Prototype before polishing. Get it working and correct before you optimize it to be fast
  • Extensibility. Design for the future, because it will be here sooner than you think

12-factor cloud-native application

  1. Codebase. One codebase tracked in revision control, many deployments
    • Use revision control system (Git) to track changes to a codebase
    • Set up one repository per app/service
    • Single codebase is deployed into multiple environments (dev, test, staging, production) with different level of maturity/testing
  2. Dependencies. Explicitly declare and isolate dependencies
    • Use a package manager (npm) to manage dependencies
    • Always explicitly define dependency versions (package.json)
    • Isolate locally installed dependencies in ~/.local/lib from interference with system-wide packages in /usr/local/lib
    • Only a language runtime and a package manager are required to run an app/service
    • Explicitly include (into a Docker image) all system tools (curl) that an app/service depends on
  3. Configuration. Store configuration in each environment
    • Keep strict separation of the environment-specific configuration from a codebase
    • Do not store any credentials and secretes in a codebase
    • Store environment-specific configuration in environment variables set up by IaC deployment scripts
  4. Backing services. Treat backing services as attached resources accessible via URI
    • Most of the apps/services use databases, queues, caches, email systems, cloud services (resources)
    • App/service can easily swap backing service by changing a resource URI provided by an environment-specific configuration without any changes in a codebase
  5. Build, release, deploy. Strictly separate build and run stages
    • A codebase is transformed into a deployment through the below stages (verified build => immutable release => automated deployment)
    • Build stage transforms a source code (Git repository) into an executable artifact (Docker image). Fetch specific tag, download dependencies, compile an executable artifact, build a Docker image, test the application
    • Release stage combines an environment-independent executable artifact with an environment-specific configuration into a deployment unit. Every release is immutable and should be uniquely tagged. Any change must create a new release
    • Deployment stage launches an app/service in an environment
  6. Processes. Execute an app as one or more stateless, share-nothing processes/containers
    • Any data that needs to persist must be stored in a stateful database
    • Process memory and a local file system can be used only as a temporary ephemeral storage
    • Every app/service process should be idempotent
  7. Port binding. Export services via port binding + protocol
    • App/service should be completely self-contained and does not rely on any server execution environment
    • App/service should export HTTP/gRPC/AMQP as a service by binding to a port
    • In production a reverse proxy (NGINX) routes requests from a public-facing host to a port-bound app/service
  8. Concurrency. Scale out via OS processes
    • Use first-class, share-nothing Unix processes/daemons (horizontal scalability) for each type of workload (HTTP endpoints, background workers, databases)
    • App/service should rely on OS process manager (systemd)
  9. Disposability. Maximize robustness with fast startup and graceful shutdown
    • App/service should be disposable: can be started or stopped quickly that facilitates fast elastic scalability
    • App/service should minimize startup time and fail fast
    • App/service should shut down gracefully on SIGTERM:
      • For HTTP cease listening, let current request to finish, and then exit
      • For AMPQ return current idempotent job to a queue, and then exit
  10. Dev/prod parity. Keep development, testing, staging, and production as similar as possible
    • App/service delivery pipeline shout be completely automated and use CI/CD
    • Use the same Git repository, Docker image in all environments (dev, test, stage, prod)
  11. Logs. Treat logs as event streams
    • Instrumentalize app/service with logs to get insights, telemetry, and observability
    • Logs are a stream of time-ordered events collected in a centralized log aggregation system (Elasticsearch)
    • App/serer should only emit all structured (JSON) logs to STDOUT
    • Execution environment manages to augment and forward logs to a centralized log aggregation system (Elasticsearch) for introspection, real-time analysis, and alerts
  12. Admin processes. Run admin/management task as one-off processes in the same codebase
    • IaC admin source code and dependencies should ship with a app/service source code to avoid sync issues

Security by design principles

  • Information security in rest
    • Confidentiality - ensure that only an authorized user has access to data
    • Integrity - ensure that data is not altered by an unauthorized user
    • Availability - ensure that data is available only to an authorized user
  • Information security in transit. Alice sends a message to Bob
    • Confidentiality - only Bob can read a message
    • Integrity - Eve cannot alter a message
    • Authenticity - only Alice could have sent a message
  • Least privilege #. A subject should have the least amount of privilege (CPU/RAM quotas, file system/network access, data access permissions, business transaction limits, time-based constraints) explicitly granted to perform its business process with the objective to limit intentional or unintentional damage to data. The permission should be granted just before performing the operation and should be immediately revoked on a completion or failure of the operation. The function of the subject (not the identity) should control the assignment of permissions. Example: if a user only needs to read a file, then he should not be granted permission to write a file
  • Secure by default: security baseline in terms of functionality. Delivered out-of-the-box functionality should be secure by default. A user might reduce security and increase risk if he is allowed to do so. Example: a user password expiration and complexity should be enabled by default. User might be allowed to weaken password requirements
  • Fail-safe defaults: bottom line of user initial privileges. Prefer explicitly granted access over access exclusion. By default a user do not have access to any resource until access to a resource is granted explicitly, so on an operation failure the system security is not compromized
  • Fail securely. When a system fails, it should fail to a state where security of a system is not compromised. Immediately release resources, decrease privileges, and maybe logout from an account on an operation failure
  • Defense in depth #. Multiple security controls at each architectural level that approach risk from different perspectives are better as it makes much more difficult to exploit vulnerabilities. Example: secure coding + code reviews, explicit resource acquisition and privileges granting, security testing, input data validation/sanitization, secure deployment, proactive application monitoring and auditing, continuous security and risk assessment. Example: administrative web interface should be protected (a) by authentication, (b) only accessible from internal network, (c) with enabled audit logging
  • Complete mediation #. Every access to every resource must be checked for authentication and authorization via system-wide central point of access control. Subsequent accesses to the same resource should also be checked and not cached. Performance (caching) vs security (explicit check of every request) trade off
  • Separation of duties #. Do not grant multiple unrelated privileges to a single account. Require collaboration of multiple accounts to perform important operations securely in order to prevent intentional fraud or unintentional error. Example: application administrator should not be a super user of an application; an administrator should not be able to perform business operations on behalf of an application user
  • Separation of privilege #. Every security control should be based on more than a single condition in order to remove a single point of failure. Example: when approving a request validate that (a) user is authenticated (b) user status is active (c) user is authorized to access the resource. Multi-factor authentication MFA (something you know, you have, you are)
  • Economy of mechanism. Keep security simple. Avoid complex approaches to security controls as it is much easier to spot functional defects and security flaws in simple designs while it is very difficult to identify problems in complex designs. Complexity does not add security
  • Open design. Avoid security by obscurity. Security of a system should not be dependent on secrecy of its design or implementation. Prefer well tested public security standards over homegrown hidden security controls. Keeping passwords in secret does not violate this principle as a password is not an algorithm. It is better to know a security level of standard security controls rather than do not know at all the security level of homegrown not extensively tested security controls. Examples: OAuth 2.0. Linux source code is publicly available, yet when properly secured, Linux is hard secure operating system
  • Least common mechanism. Mechanisms to access a resource should not be shared between different subjects. Example: provide different login pages for different types of users; if one of the login pages is compromized, other login pages are not impacted
  • Minimize attack surface area. Asses security risks introduced by a new feature, then adapt feature design and define security controls to minimize the attack surface area. Example: a search function of an online help feature may be vulnerable to an SQL injection attack; expose the feature only to authorized users, use data validation and escaping, or eliminate search function from the feature design by providing a better structured user interface to reduce the attack surface area
  • Psychological acceptability. Security controls should not make resources more difficult to access than if security controls were not present. The more user friendly an interface is, the less likely a user will make a mistake when configuring and using a security control and expose a system to security breaches. Error messages should be descriptive and actionable but not convey unnecessary design details that may be used to compromise a system
  • Do not trust services. Do not assume that third party partners have the same or higher security policies then yours. Put necessary security controls on third party services
  • Fix security issues correctly. Once a security issue has been identified, (a) understand the root cause of it and determine the scope of it (b) develop a fix for it (c) implement required tests for it (d) add monitoring and auditing of it
  • Weakest link #. A chain is only as strong as its weakest link. Focus on the weakest component in a system
  • Zero trust security (zero trust [network] architecture, perimeterless security). Never trust (no trust by default), always verify even inside a network perimeter (traditional IT network security trusts anyone within a network perimeter)
    • Strict identity verification (MFA, mutual authentication, session expiration + re-authentication)
    • Device verification (integrity check, device authentication)
    • Microsegmentation for access segregation to prevent lateral movement
    • Strict access control (roles, permissions, policies)
    • Continuous monitoring inside and outside of a network perimeter

Structured programming (SP)

  • SP provides secure flow control structures that allow for program proof and verification
  • SP requires a single entry point and ideally single exit point, but multiple exist points are also allowed to simplify program code
  • SP components
    • Selection (conditionals, branching) if/elif/else, case/of/else
    • Iteration (repetition, looping) while, for/in
    • Code blocks (delimited sequence of statements) code
    • Subroutings (named and parametrized code block) proc, func
    • Recursion (self-referencing)
  • Deviations from SP
    • Early exit from a function return
    • Early exit from a loop continue, break
    • Exceptions try/except
    • Use unwind protection for automated resource management try/finally, RAII (stack-only)
    • Multiple entry points: coroutines, generators (yeild control / value + resume execution)

Object-oriented design principles (message-passing paradigm)

  • OOP - Object-Oriented Programming. OOP is like biological cells: messaging (uniform communication), state hiding (data abstraction), late binding (behavioral polymorphism). Object has well encapsulated structure (properties) and provides polymorphic behavior (methods) through a well defined interface (messages). Abstraction (hierarchy), separation of concerns (composability) and modularization (orthogonality) are the keys to master complexity
  • Uniform metaphor. A system should be designed around a powerful metaphor that can be uniformly applied in all areas. Large applications are viewed in the same way as the fundamental units from which the system is built. Examples: lists/functions in Scheme, relations/queries in SQL, objects/messages in Smalltalk
  • Good design. A system should be build with a minimum set of unchangeable parts. The unchangeable parts should be as general as possible. All parts of a system should be held in a uniform framework with a uniform interface
  • Modulatiry. No component in a system should depend on the internal details of any other component. Component interdependencies in a system should be minimized
  • Factoring via inheritance. Each component in a system should be defined only in one place. Use inheritance to provide well-factored design and avoid repetition (duplication). Inheritance propagates default behavior through increasingly more specific hierarchy of classes
  • Classification. Group similar components into class hierarchy to reduce complexity. Class abstraction describes (a) message protocol that the object recognizes (explicit communication) (b) internal state change of the object (implicit communication). Object is a concrete instance of a class
  • Objects. Use object-oriented model for storage. Objects are created by sending a message to their classes. Objects are automatically disposed when they are not needed any more and automatically reclaimed by a GC
    • Protocol (explicit communication) is a set of messages that the object can respond to
    • State (implicit communication) defines object-specific response to a message. All access to the internal state of an object is exclusively performed via message protocol (message-passing)
  • Messages. Use message-oriented model for processing. Computing should be intrinsic capability of objects that can be uniformly invoked by sending messages. Message-passing metaphor decouples the intent (message) from actual execution (method)
    • Message = operation + arguments
  • Polymorphism. A system should specify (via message protocol, interface) only behavior (interpretation) of objects, not their implementation (representation)

Philosophy of clarity in programming

  • Most programs are more complex than thay need to be because of bad design
  • Data structures, and not algorithms, are central to programming
  • Optimize only after the implementation when a bottleneck is identified
  • Use consistent identificator names with minimum length, maximum information in a context

Description of software feature

User story (high level, user perspective)

User story describes in an informal language a software feature from the end user perspective

As a <role, user, who>
I want <capability, goal, what>,
so that <benefit, reason, why>

Gherkin feature with scenarios (detailed BDD, test perspective)

Gherkin feature with scenarios specifies expected software behavior in a logical language that a user can understand

Feature: <feature name> (collection of scenarios)
    <feature description>
    Scenario: <scenario> (colleciton of steps)
        Given: <preconditions setup>
        When: <action trigger>
        Then: <result verification>

Use case (actor interactions with a system)

Use case describes in text + diagram interaction between an actor (human or external system) and a system to achieve a goal

Title: <active verb one-liner>
Actor: <primary actor>
Goal: <goal in context>
Precondition: <initial system state>
Trigger: <actor action>
Success: <main success scenario>
Extensions: <alternative scenarios and enhancements>