Архитектури за GUI:

 история, настояще,

Мрежата

Милко Костурков

  • Програмист от около 14 години
  • fullstack
  • контрактор
  • Ty's Software
  • организатор на Bulgaria PHP Conference 2019 
  • wannabe rockstar

Предишни проекти

SMS и Voice услуги

MMO игри

системи за локално позициониране

TV продукции

Здравеопазване

e-commerce

сайтове

SEO

онлайн видео

Защо тази тема?

  • Трябва да познаваме миналото си
  • Да разсеем объркванията
  • Да представим някои нови концепции
  • "Whatsa Controller Anyway"                                                                              - Kyle Brown

MVC: Историята

Поглед назад

  • изобретен през 1979
  • от Тригве Реянскау
  • докато работи в Xerox
  • НЕ Е ЗА УЕБ
  • за програми с GUI
  • за Smalltalk - 80
  • за "Dynabook"

By Trygve Reenskaug - Trygve Reenskaug, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=31168223

Smalltalk GUI

MVC: Дефиниции

Нещо

Според Тригве Реянскау

Нещо, което от интерес за потребителя.
Може да бъде конкретно, като къща или интегрална схема.
Може да бъде абстрактно, като идея или мнение за статия.
Може да бъде цяло, като компютър, или част, като електронен елемент.

Model

Според Тригве Реянскау

Моделът представя знание. Моделът може да бъде единствен обект (относително безинтересен) или да бъде структура от обекти.

Моделът и неговите части трябва да отразяват едно-към-едно представяното нещо, така както го възприема неговият създател

View

Според Тригве Реянскау

Изгледът е свързан с модела си (или негова част) и получава данните за представяне чрез задаване на въпроси...

Изгледът (вюто :) е визуално представяне на неговия модел. Обикновено подчертава определени свойства на модела и скрива други.

... следователно изгледът трябва да познава значението на атрибутите своя модел.

Controllers

Според Тригве Реянскау

Контролерът е връзката между потребителя и системата. Той предоставя възможност за "вход" към потребителя като подрежда изгледите на подходящи места по екрана. Предоставя "изход" от потребителя чрез менюта и други методи за подаване на команди и данни 
Контролерът никога не трябва да допълва изгледа...
И обратно - изгледът никога не трябва да знае за потребителския вход като операции с миша и натискане на клавиши

MVC

MVC

MVC: Недостатъци

Проблемът:

Няма къде да сложим логиката, свързана с презентацията на модела

Решението:

Application Model

VisualWorks проблемът от 1993

Application Model MVC

Проблем:

Контролерът не комуникира с изгледите

Проблем:

Контролите на Windows правят Контролерът ненужен

Dolphin Smalltalk проблемите от 1995

Решение:

"Twisting the Triad", Andy Bower, Blair McGlashan

Model View Presenter

The Taligent Programming Model for C++ and Java

  • Нов поглед върху MVC
  • Имплементиран като framework
  • Труд на Майк Пател, VP & CTO на Taligent
  • Написан през 1996

Model View Presenter

Разбивка на триадата

 

Model View Presenter

Клиент/Сървър

GUI vs Web Apps

  • GUI
    • работят продължително
    • непрекъснат входен поток (мишка, клавиатура)
    • непрекъснат изходен поток (опреснявания на прозорците)
  • Web
    • работят кратко (заявка/отговор)
    • еднократен вход
    • еднократен изход
  • Все са приложения
  • Използват GUI

През това време в деветдесетарската Мрежа...

Книга за гости на PERL

#!/usr/bin/perl

use DBI;
use URI::Escape;
use CGI qw(:standard);
use CGI::Carp 'fatalsToBrowser';
use Constants;
  
print "Content-type: text/html; charset=".Constants::CHARSET."\r\n\r\n";

use ConnectDB;
use GetSetting;

$title = 'Guestbook - Perl Guestbook';
$description = '';
$keywords = '';
  
if($ENV{'HTTP_X_FORWARDED_FOR'}) 
{
   $ip = $ENV{'HTTP_X_FORWARDED_FOR'};
} 
else 
{
   $ip = $ENV{'REMOTE_ADDR'};
}

require "lib/_function.pl" or die('Cannot open the file "_function.pl"');

require "lib/_top.pl" or die('"_top.pl"');

my %GET;
my @pairs = split(/&/, $ENV{ "QUERY_STRING" });

foreach (@pairs)
{
   my ($name, $value) = split(/=/, $_);
   $GET{$name} = $value;
   $value = uri_unescape($value);
}

my $page = int($GET{'page'});
if($page == 0) { $page = 1; }
if($row_setting->{number_post} == 0) { $row_setting->{number_post} = 10; }
$begin = ($page - 1)*$row_setting->{number_post};

print <<HTML1;
<table width="80%" cellpadding="0" cellspacing="0" border="0">
<tr>
  <td><center>
      <br>
      <table border="0">
        <tr>
          <td><img src="http://$ENV{ "SERVER_NAME" }/images/notepad.gif" width="16" height="16" border="0"></td>
          <td><a title="Add message" href="add.pl">Add message</a></td>
        </tr>
      </table>
    </center>
    <br>
HTML1

my $query = $dbh->prepare("SELECT * FROM ".Constants::DB_MSG." WHERE hide = 'show' ORDER BY time DESC LIMIT $begin, ".$row_setting->{number_post}."");
$query->execute() or die("Error executing SQL query!");
 
while($row = $query->fetchrow_hashref())
{ 
   my $name   = $row->{name};
   my $msg    = $row->{msg};
   my $city   = $row->{city};
   my $email  = $row->{email};
   my $url    = $row->{url};
   my $ip     = $row->{ip};
   my $answer = $row->{answer};
   my $time   = $row->{time};
  
   $name   =~ s/^\s+|\s+$//g;
   $msg    =~ s/^\s+|\s+$//g;
   $city   =~ s/^\s+|\s+$//g;
   $email  =~ s/^\s+|\s+$//g;
   $url    =~ s/^\s+|\s+$//g;
   $ip     =~ s/^\s+|\s+$//g;
   $answer =~ s/^\s+|\s+$//g;
   $time   =~ s/^\s+|\s+$//g;
  
   if($row_setting->{smile} eq 'yes')
   {
      $msg =~ s/\[:\)\)\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_biggrin.gif">/g;
      $msg =~ s/\[:~\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_confused.gif">/g;
      $msg =~ s/\[:\)\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_cool.gif">/g;
      $msg =~ s/\[:\(\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_mad.gif">/g;
      $msg =~ s/\[:\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_eek.gif">/g;
      $msg =~ s/\[:\(\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_frown.gif">/g;
      $msg =~ s/\[:\(\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_frown.gif">/g;
      $msg =~ s/\[:\|\)\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_smile.gif">/g;
      $msg =~ s/\[:\/\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_wink.gif">/g;
      $msg =~ s/\[:\(\)\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_razz.gif">/g;
      $msg =~ s/\[:\/~\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_blush.gif">/g;
      $msg =~ s/\[:\/\(\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_cray.gif">/g;
      $msg =~ s/\[:\)\(\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_dance.gif">/g;
      $msg =~ s/\[:\|_\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_drinks.gif">/g;
      $msg =~ s/\[:\?\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_fool.gif">/g;
      $msg =~ s/\[:\)\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_good.gif">/g;
      $msg =~ s/\[:\@\@\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_kiss_mini.gif">/g;
      $msg =~ s/\[:\)-\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_man_in_love.gif">/g;
      $msg =~ s/\[:-\@\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_rolleyes.gif">/g;
      $msg =~ s/\[:\|\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_scratch.gif">/g;
      $msg =~ s/\[:\@\|\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_shok.gif">/g;
      $msg =~ s/\[:\@\(\|\)\]/<IMG border=0 src="http:\/\/$ENV{ "SERVER_NAME" }\/smile\/icon_shout.gif">/g;
   }
   else
   {
      $msg =~ s/\[:\)\)\]//g;
      $msg =~ s/\[:~\]//g;
      $msg =~ s/\[:\)\]//g;
      $msg =~ s/\[:\(\|\]//g;
      $msg =~ s/\[:\|\]//g;
      $msg =~ s/\[:\(\]//g;
      $msg =~ s/\[:\(\]//g;
      $msg =~ s/\[:\|\)\]//g;
      $msg =~ s/\[:\/\]//g;
      $msg =~ s/\[:\(\)\]//g;
      $msg =~ s/\[:\/~\]//g;
      $msg =~ s/\[:\/\(\]//g;
      $msg =~ s/\[:\)\(\]//g;
      $msg =~ s/\[:\|_\|\]//g;
      $msg =~ s/\[:\?\]//g;
      $msg =~ s/\[:\)\|\]//g;
      $msg =~ s/\[:\@\@\]//g;
      $msg =~ s/\[:\)-\]//g;
      $msg =~ s/\[:-\@\]//g;
      $msg =~ s/\[:\|\|\]//g;
      $msg =~ s/\[:\@\|\]//g;
      $msg =~ s/\[:\@\(\|\)\]//g;
   }

print <<HTML2;
<table class="cattab" border="0" width="100%">
<tr>
  <td class="menu"><table border="0" width="100%">
    <tr>
      <td width="50%"><p><b>$name</b><br>
HTML2

   if($city ne '') { print "City: $city <br>"; }
   if($email ne '') { print "E-mail: $email <br>"; }
   if($url ne '') { print "Website: <a class=link href='$url'>$url</a>"; }

print <<HTML3; 
</td>

<td width="50%"><p align="right"><i>added: $time</i></td>
</tr>
</table>
</td>
</tr>
<tr>
  <td height="36"><p> 
HTML3
   
   print "$msg";

   if($answer ne '' && $answer ne '-') 
   {
      print "<font color=\#FF0000><p>Admin: $answer</p></font>";
   } 

   print '</p></td></tr></table>';

}

print <<HTML4; 
<center>
  <br>
  <table border="0">
    <tr>
      <td><img src="http://$ENV{ "SERVER_NAME" }/images/notepad.gif" width="16" height="16" border="0"></td>
      <td><a title="Add message" href="add.pl">Add message</a></td>
    </tr>
  </table>
</center>
HTML4

$query->finish();

my $query = $dbh->prepare("SELECT count(*) FROM ".Constants::DB_MSG." WHERE hide = 'show'");
$query->execute() or die("Error executing SQL query!");
my $count = $query->fetchrow_arrayref()->[0];
  
$query->finish();
  
my $number = int(($count - 1) / $row_setting->{number_post}) + 1;

if($page != 1)
{  
   $pervpage = '<a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page=1>&lt;&lt;</a> 
                <a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page - 1).'>&lt;</a> '; 
}

if($page != $number)
{ 
   $nextpage = ' <a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page + 1).'>&gt;</a> 
                <a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.$number.'>&gt;&gt;</a>'; 
}

if(($page - 2) > 0) { $page2left = '<a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page - 2).'>...'.($page - 2).'</a> | '; }
if(($page - 1) > 0) { $page1left = '<a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page - 1).'>'.($page - 1).'</a> | '; }
if(($page + 2) <= $number) { $page2right = ' | <a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page + 2).'>'.($page + 2).'...</a>'; }
if(($page + 1) <= $number) { $page1right = ' | <a href=http://'.$ENV{ "SERVER_NAME" }.$ENV{ "SCRIPT_NAME" }.'?page='.($page + 1).'>'.($page + 1).'</a>'; }

print "<p>Number of messages: [$count]   Pages:  ";
print $pervpage.$page2left.$page1left.'<b>'.$page.'</b>'.$page1right.$page2right.$nextpage.'</p>';

require 'lib/_bottom.pl' or die('Canot open the file "_bottom.pl"');

$dbh->disconnect();

Книга за гости на PHP

<table width="400" border="0" align="center" cellpadding="3" cellspacing="0">
<tr>
<td><strong>View Guestbook | <a href="guestbook.php">Sign Guestbook</a> </strong></td>
</tr>
</table>
<br>

<?php

$host="localhost"; // Host name
$username=""; // Mysql username
$password=""; // Mysql password
$db_name="test"; // Database name
$tbl_name="guestbook"; // Table name

// Connect to server and select database.
mysql_connect("$host", "$username", "$password")or die("cannot connect server ");
mysql_select_db("$db_name")or die("cannot select DB");

$sql="SELECT * FROM $tbl_name";
$result=mysql_query($sql);

while($rows=mysql_fetch_array($result)){
?>

<table width="400" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#CCCCCC">
<tr>
<td><table width="400" border="0" cellpadding="3" cellspacing="1" bgcolor="#FFFFFF">
<tr>
<td>ID</td>
<td>:</td>
<td><? echo $rows['id']; ?></td>
</tr>
<tr>
<td width="117">Name</td>
<td width="14">:</td>
<td width="357"><? echo $rows['name']; ?></td>
</tr>
<tr>
<td>Email</td>
<td>:</td>
<td><? echo $rows['email']; ?></td>
</tr>
<tr>
<td valign="top">Comment</td>
<td valign="top">:</td>
<td><? echo $rows['comment']; ?></td>
</tr>
<tr>
<td valign="top">Date/Time </td>
<td valign="top">:</td>
<td><? echo $rows['datetime']; ?></td>
</tr>
</table></td>
</tr>
</table>

<?php
}
mysql_close(); //close database
?>

Книга за гости на JSP

<html>
<head>
  <title>Book Query</title>
</head>
<body>
  <h1>Another E-Bookstore</h1>
  <h3>Choose Author(s):</h3>
  <form method="get">
    <input type="checkbox" name="author" value="Tan Ah Teck">Tan
    <input type="checkbox" name="author" value="Mohd Ali">Ali
    <input type="checkbox" name="author" value="Kumar">Kumar
    <input type="submit" value="Query">
  </form>
 
  <%
    String[] authors = request.getParameterValues("author");
    if (authors != null) {
  %>
  <%@ page import = "java.sql.*" %>
  <%
      Connection conn = DriverManager.getConnection(
          "jdbc:mysql://localhost:8888/ebookshop", "myuser", "xxxx"); // <== Check!
      // Connection conn =
      //    DriverManager.getConnection("jdbc:odbc:eshopODBC");  // Access
      Statement stmt = conn.createStatement();
 
      String sqlStr = "SELECT * FROM books WHERE author IN (";
      sqlStr += "'" + authors[0] + "'";  // First author
      for (int i = 1; i < authors.length; ++i) {
         sqlStr += ", '" + authors[i] + "'";  // Subsequent authors need a leading commas
      }
      sqlStr += ") AND qty > 0 ORDER BY author ASC, title ASC";
 
      // for debugging
      System.out.println("Query statement is " + sqlStr);
      ResultSet rset = stmt.executeQuery(sqlStr);
  %>
      <hr>
      <form method="get" action="order.jsp">
        <table border=1 cellpadding=5>
          <tr>
            <th>Order</th>
            <th>Author</th>
            <th>Title</th>
            <th>Price</th>
            <th>Qty</th>
          </tr>
  <%
      while (rset.next()) {
        int id = rset.getInt("id");
  %>
          <tr>
            <td><input type="checkbox" name="id" value="<%= id %>"></td>
            <td><%= rset.getString("author") %></td>
            <td><%= rset.getString("title") %></td>
            <td>$<%= rset.getInt("price") %></td>
            <td><%= rset.getInt("qty") %></td>
          </tr>
  <%
      }
  %>
        </table>
        <br>
        <input type="submit" value="Order">
        <input type="reset" value="Clear">
      </form>
      <a href="<%= request.getRequestURI() %>"><h3>Back</h3></a>
  <%
      rset.close();
      stmt.close();
      conn.close();
    }
  %>
</body>
</html>

JSP Model 2

Музика без граници
  • "Understanding JavaServer Pages Model 2 architecture" - статия на Говинд Сешадри в сп. JavaWorld от 1999
  • Нарича Model 2 MVC 
  • Използва вектор като модел и поставя всичката бизнес логика в Контролер
  • Пример със скандално лош код, който след малко ще видим
import java.util.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import shopping.CD;
public class ShoppingServlet extends HttpServlet {
  public void init(ServletConfig conf) throws ServletException  {
    super.init(conf);
  }
  public void doPost (HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
    HttpSession session = req.getSession(false);
    if (session == null) {
      res.sendRedirect("http://localhost:8080/error.html");
    }
    Vector buylist=
      (Vector)session.getValue("shopping.shoppingcart");
    String action = req.getParameter("action");
    if (!action.equals("CHECKOUT")) {
      if (action.equals("DELETE")) {
        String del = req.getParameter("delindex");
        int d = (new Integer(del)).intValue();
        buylist.removeElementAt(d);
      } else if (action.equals("ADD")) {
        //any previous buys of same cd?
        boolean match=false;
        CD aCD = getCD(req);
        if (buylist==null) {
          //add first cd to the cart
          buylist = new Vector(); //first order
          buylist.addElement(aCD);
        } else { // not first buy
          for (int i=0; i< buylist.size(); i++) {
            CD cd = (CD) buylist.elementAt(i);
            if (cd.getAlbum().equals(aCD.getAlbum())) {
              cd.setQuantity(cd.getQuantity()+aCD.getQuantity());
              buylist.setElementAt(cd,i);
              match = true;
            } //end of if name matches
          } // end of for
          if (!match) 
            buylist.addElement(aCD);
        }
      }
      session.putValue("shopping.shoppingcart", buylist);
      String url="/jsp/shopping/EShop.jsp";
      ServletContext sc = getServletContext();
      RequestDispatcher rd = sc.getRequestDispatcher(url);
      rd.forward(req, res);
    } else if (action.equals("CHECKOUT"))  {
      float total =0;
      for (int i=0; i< buylist.size();i++) {
        CD anOrder = (CD) buylist.elementAt(i);
        float price= anOrder.getPrice();
        int qty = anOrder.getQuantity();
        total += (price * qty);
      }
      total += 0.005;
      String amount = new Float(total).toString();
      int n = amount.indexOf('.');
      amount = amount.substring(0,n+3);
      req.setAttribute("amount",amount);
      String url="/jsp/shopping/Checkout.jsp";
      ServletContext sc = getServletContext();
      RequestDispatcher rd = sc.getRequestDispatcher(url);
      rd.forward(req,res);
    }
  }
  private CD getCD(HttpServletRequest req) {
    //imagine if all this was in a scriptlet...ugly, eh?
    String myCd = req.getParameter("CD");
    String qty = req.getParameter("qty");
    StringTokenizer t = new StringTokenizer(myCd,"|");
    String album= t.nextToken();
    String artist = t.nextToken();
    String country = t.nextToken();
    String price = t.nextToken();
    price = price.replace('$',' ').trim();
    CD cd = new CD();
    cd.setAlbum(album);
    cd.setArtist(artist);
    cd.setCountry(country);
    cd.setPrice((new Float(price)).floatValue());
    cd.setQuantity((new Integer(qty)).intValue());
    return cd;
  }
}

Struts and MVC2 - Y2K

Проблеми в Контролера

  • Слабо разделение на отговорностите
  • Съдържа бизнес логика
  • Прави много неща...
    • ... има много зависимости ...
    • ... става доста дебел ...
    • ... и труден за поддръжка.
  • Е класът "Бог"

Не всичко е изгубено, ако...

  • Извлечем домейн логиката в команди/use cases/application services
  • Използваме Контролера като Presenter - да съдържа само презентационна логика
  • Оставим изгледите (вютата) както са си
  • Когато презентерът стане твърде дебел, го разбиваме на по-малки MVP компоненти/модули/уиджети

Това може да се постигне с всеки съвременен "MVC" framework

Модерни SPA приложения

  • Презентерът е изцяло в клиента
  • Сървърът "държи" частите на мод:
    • Команди и надолу -  RPC, SOAP, custom APIs
    • Селекции и модели - REST, GraphQL
  • HTTP и JSON/XML/други са само протоколи и формати за прехвърляне и представяне на данни
  • Няма нужда от сложни презентери/модули/уиджети на сървъра

Action-Domain-Responder

  • от Пол Джоунс през 2012
  • Специално за Уеб Приложения
  • Идеален за API-та

Action

  • Функция или клас имплементиращ Command шаблона
  • Събира входните данни от заявката и го предава към домейна
  • Изпълнява домейна и улавя резултата
  • Изпълнява респондера, като му подава всички нужни данни
    • Не изпраща хедъри
    • Не се занимава с изхода по никакъв начин

Domain

Входна точка на каквото прави домейна

(Transaction Script, Service Layer, Application Service, etc.).

Responder

Занимава се с изхода:

  • Праща бисквитки
  • Праща хедъри
  • Прилага компресия
  • Избира формат

Presentation Model

  • Мартин Фаулър, средата на 2000те, вероятно 2004
  • Няма "Контролер" - средата ни го прави за нас
  • Вариация на Application Model на VisualWorks
  • Недостатък - синхорнизацията между презентационния модел и изгледа е проста, но повтаряща се и досадна

Model View ViewModel

  • От Джон Госман от Microsoft през 2005
  • Точно като Presentation Model, но с data-binding, който се прави от фреймъурк
  • Мартин Фаулър предполага, че това ще се появи :)
  • Използван в Windows Presentation Foundation и Silverlight...
  • ... Knockout, React, Vue и компания.

Resources Links

Благодаря!

Милко Костурков

@mkosturkov

linkedin.com/in/milko-kosturkov

mailto: mkosturkov@gmail.com

 

GUI Архитектури - история, настояще, Мрежата

By Milko Kosturkov

GUI Архитектури - история, настояще, Мрежата

Архитектурите за графични интерфейси (GUI) са една от недоразбраните теми в програмирането. Грешни схващания, лоши наименования, различни интерпретации - нищо от това не помага на програмистите да вникнат в същността на проблема, какви решения са възникнали през годините и как са еволюирали. Ще разгледаме MVC шаблона, създаден през 70те и ще преминем през годините до днес, за да видим какво се е случило на сцената на GUI от миналия век до днес. Също така, ще посочим и какво се е обърквало и как може да избегнем чуждите грешки.

  • 166
Loading comments...

More from Milko Kosturkov