Свободное программирование на языках высокого уровня, как, почему и сколько? - PullRequest
2 голосов
/ 09 июля 2010

Я пишу свой код в Haxe.Это совершенно не имеет отношения к вопросу, поскольку вы помните, что это язык высокого уровня и его можно сравнивать с Java, ActionScript, JavaScript, C # и т. Д. (Здесь я использую псевдокод).

Я собираюсь работать над большим проектом и сейчас занят подготовкой.Для этого вопроса я создам небольшой сценарий: простое приложение, которое имеет класс Main (этот выполняется при запуске приложения) и класс LoginScreen (в основном это класс, который загружает экран входа в систему, чтобы пользователь могlogin).

Обычно я думаю, что это будет выглядеть следующим образом:

Main constructor:
loginScreen = new LoginScreen()
loginScreen.load();

LoginScreen load():
niceBackground = loader.loadBitmap("somebg.png");
someButton = new gui.customButton();
someButton.onClick = buttonIsPressed;

LoginScreen buttonIsPressed():
socketConnection = new network.SocketConnection();
socketConnection.connect(host, ip);
socketConnection.write("login#auth#username#password");
socketConnection.onData = gotAuthConfirmation;

LoginScreen gotAuthConfirmation(response):
if response == "success" {
   //login success.. continue
}

Этот простой сценарий добавляет следующие зависимости и недостатки в наши классы:

  • Main не будет загружаться без LoginScreen
  • LoginScreen не будет загружаться без пользовательского класса загрузчика
  • LoginScreen не будет загружаться без нашего пользовательского класса кнопок
  • LoginScreen не будет загружаться без нашего настраиваемого SocketConnectionclass
  • SocketConnection (к которому в будущем будут обращаться многие классы) был установлен внутри LoginScreen, что на самом деле совершенно не имеет значения, за исключением того, что LoginScreen требует сокетподключение в первый раз

TЧтобы решить эти проблемы, мне было предложено выполнить «Событийно-управляемое программирование» или слабую связь.Насколько я понимаю, это в основном означает, что нужно сделать классы независимыми друг от друга, а затем связать их вместе в отдельные связующие.

Итак, вопрос 1: мой взгляд на это верно или неверно?Нужно ли использовать связующие?

Я слышал, что Аспектно-ориентированное программирование может помочь здесь.К сожалению, Haxe не поддерживает эту конфигурацию.

Однако у меня есть доступ к библиотеке событий, которая в основном позволяет мне создать сигнализатор (public var loginPressedSignaller = new Signaller ()), чтобы запустить сигнализатор (loginPressedSignaller).fire ()) и прослушивать сигнализатор (someClass.loginPressedSignaller.bind (doSomethingWhenLoginPressed)).

Итак, после небольшого дальнейшего изучения я решил, что это изменит мою предыдущую настройку на:

Main:
public var appLaunchedSignaller = new Signaller();

Main constructor:
appLaunchedSignaller.fire();

LoginScreen:
public var loginPressedSignaller = new Signaller();

LoginScreen load():
niceBackground = !!! Question 2: how do we use Event Driven Programming to load our background here, while not being dependent on the custom loader class !!!
someButton = !!! same as for niceBackground, but for the customButton class !!!
someButton.onClick = buttonIsPressed;

LoginScreen buttonIsPressed():
loginPressedSignaller.fire(username, pass);

LoginScreenAuthenticator:
public var loginSuccessSignaller = new Signaller();
public var loginFailSignaller = new Signaller();

LoginScreenAuthenticator auth(username, pass):
socketConnection = !!! how do we use a socket connection here, if we cannot call a custom socket connection class !!!
socketConnection.write("login#auth#username#password");

Этот код еще не закончен, например.Мне все еще приходится прислушиваться к ответу сервера, но вы, вероятно, понимаете, где я застреваю.

Вопрос 2. Имеет ли смысл эта новая структура?как я должен решить вышеупомянутые проблемы в !!!разделители?

Тогда я услышал о связующих.Поэтому, возможно, мне нужно создать связующее для каждого класса, чтобы соединить все вместе.Примерно так:

MainBinder:
feature = new Main();    

LoginScreenBinder:
feature = new LoginScreen();
MainBinder.feature.appLaunchedSignaller.bind(feature.load);
niceBackgroundLoader = loader.loadBitmap;
someButtonClass = gui.customButton();

и т.д ... надеюсь, вы понимаете, о чем я.Этот пост становится немного длиннее, поэтому мне нужно немного обернуть его.

Вопрос 3: имеет ли это смысл?Разве это не делает вещи излишне сложными?

Кроме того, в приведенных выше «Связующих» я должен был использовать только классы, которые создаются один раз, например.экран входа.Что делать, если есть несколько экземпляров класса, например.Класс игрока в игре в шахматы.

Ответы [ 2 ]

9 голосов
/ 13 июля 2010

хорошо, что касается как , я бы указал вам на мои 5 заповедей . :)

Для этого вопроса действительно важны только 3:

  • одиночная ответственность (SRP)
  • интерфейс разделения (ISP)
  • инверсия зависимостей (DIP)

Начиная с SRP , вы должны задать себе вопрос: "Какова ответственность класса X?".

Экран входа в систему отвечает за предоставление пользователю интерфейса для заполнения и отправки его данных для входа. Таким образом

  1. имеет смысл зависеть от класса кнопки, потому что ей нужна кнопка.
  2. не имеет смысла, он делает все сети и т. Д.

Прежде всего, давайте рассмотрим сервис входа в систему:

interface ILoginService {
     function login(user:String, pwd:String, onDone:LoginResult->Void):Void;
     //Rather than using signalers and what-not, I'll just rely on haXe's support for functional style, 
     //which renders these cumbersome idioms from more classic languages quite obsolete.
}
enum Result<T> {//this is a generic enum to return results from basically any kind of actions, that may fail
     Fail(error:Int, reason:String);
     Success(user:T);
}
typedef LoginResult = Result<IUser>;//IUser basically represent an authenticated user

С точки зрения основного класса экран входа выглядит так:

interface ILoginInterface {
    function show(inputHandler:String->String->Void):Void;
    function hide():Void;
    function error(reason:String):Void;
}

выполнение входа в систему:

var server:ILoginService = ... //where ever it comes from. I will say a word about that later
var login:ILoginInterface = ... //same thing as with the service
login.show(function (user, pwd):Void {
      server.login(user, pwd, function (result) {
             switch (result) {
                  case Fail(_, reason): 
                        login.error(reason);
                  case Success(user): 
                        login.hide();
                        //proceed with the resulting user
             }
      });
});//for the sake of conciseness I used an anonymous function but usually, you'd put a method here of course

Теперь ILoginService выглядит немного дурно. Но, если честно, он делает все, что нужно. Теперь он может быть эффективно реализован классом Server, который инкапсулирует все сети в одном классе, имея метод для каждого из N вызовов, которые обеспечивает ваш реальный сервер, но прежде всего ISP предполагает, что многие клиентские интерфейсы лучше, чем один универсальный интерфейс . По той же причине ILoginInterface действительно сведено к минимуму.

Независимо от того, как эти два действительно реализованы, вам не нужно менять Main (если, конечно, интерфейс не изменяется). Это DIP применяется. Main не зависит от конкретной реализации, только от очень краткой абстракции.

Теперь давайте представим несколько реализаций:

class LoginScreen implements ILoginInterface {
    public function show(inputHandler:String->String->Void):Void {
        //render the UI on the screen
        //wait for the button to be clicked
        //when done, call inputHandler with the input values from the respective fields
    }
    public function hide():Void {
        //hide UI
    }
    public function error(reason:String):Void {
        //display error message
    }
    public static function getInstance():LoginScreen {
        //classical singleton instantiation
    }
}
class Server implements ILoginService {
    function new(host:String, port:Int) {
        //init connection here for example
    }
    public static function getInstance():Server {
        //classical singleton instantiation
    }   
    public function login(user:String, pwd:String, onDone:LoginResult->Void) {
        //issue login over the connection
        //invoke the handler with the retrieved result
    }
    //... possibly other methods here, that are used by other classes
}

Хорошо, это было довольно просто, я полагаю. Но просто для удовольствия, давайте сделаем что-то действительно идиотское:

class MailLogin implements ILoginInterface {
    public function new(mail:String) {
        //save address
    }
    public function show(inputHandler:String->String->Void):Void {
        //print some sort of "waiting for authentication"-notification on screen
        //send an email to the given address: "please respond with username:password"
        //keep polling you mail server for a response, parse it and invoke the input handler
    }
    public function hide():Void {
        //remove the "waiting for authentication"-notification
        //send an email to the given address: "login successful"
    }
    public function error(reason:String):Void {
        //send an email to the given address: "login failed. reason: [reason] please retry."
    }   
}

Каким бы ни был этот пешеход, с точки зрения основного класса, это ничего не меняет и, следовательно, будет работать так же хорошо.

Более вероятным сценарием является то, что ваша служба входа в систему находится на другом сервере (возможно, HTTP-сервере), который выполняет аутентификацию и в случае успеха создает сеанс на реальном сервере приложений. В плане дизайна это можно отразить в двух отдельных классах.

Теперь давайте поговорим о "...", который я оставил в Main. Ну, я ленивый, поэтому я могу сказать вам, что в моем коде вы, вероятно, увидите

var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();

Конечно, это далеко не чистый способ сделать это. По правде говоря, это самый простой способ, и зависимость ограничена одним вхождением, которое впоследствии можно удалить с помощью внедрения зависимостей .

Так же, как простой пример для IoC -контейнера в haXe:

class Injector {
    static var providers = new Hash < Void->Dynamic > ;
    public static function setProvider<T>(type:Class<T>, provider:Void->T):Void {
        var name = Type.getClassName(type);
        if (providers.exists(name))
            throw "duplicate provider for " + name;
        else
            providers.set(name, provider);
    }
    public static function get<T>(type:Class<T>):T {
        var name = Type.getClassName(type);
        return
            if (providers.exists(name))
                providers.get(name);
            else
                throw "no provider for " + name;
    }
}

элегантное использование (с ключевым словом using):

using Injector;

//wherever you would like to wire it up:
ILoginService.setProvider(Server.getInstance);
ILoginInterface.setProvider(LoginScreen.getInstance);

//and in Main:
var server = ILoginService.get();
var login = ILoginInterface.get();

Таким образом, у вас практически нет связи между отдельными классами.

Что касается вопроса о том, как передавать события между кнопкой и экраном входа в систему:
это всего лишь вопрос вкуса и реализации. Смысл программирования, управляемого событиями, заключается в том, что источник и наблюдатель связаны только в том смысле, что источник должен отправлять какое-то уведомление, и цель должна быть в состоянии обработать его. someButton.onClick = handler; в основном делает именно это, но это так элегантно и лаконично, что вы не суетитесь в этом. someButton.onClick(handler);, вероятно, немного лучше, поскольку вы можете иметь несколько обработчиков, хотя это редко требуется для компонентов пользовательского интерфейса. Но, в конце концов, если вы хотите, чтобы сигнализаторы, пошли с сигнализаторами.

Теперь, когда дело доходит до АОП, это не правильный подход в этой ситуации. Это не умный хак для соединения компонентов друг с другом, но для решения сквозных проблем , таких как добавление журнала, истории или даже вещей в качестве слоя персистентности во множестве модулей.

В общем, старайтесь не модулировать и не разбивать мелкие части вашего приложения.Это нормально иметь спагетти в кодовой базе, пока

  1. сегменты спагетти хорошо инкапсулированы
  2. сегменты спагетти достаточно малы, чтобы их можно было понять или иным образом реорганизовать / переписать в течение разумного промежутка времени, не ломая приложение (что должно гарантироваться пунктом 1)

Попробуйте скорее разбить все приложение на автономные части, которые взаимодействуют через лаконичные интерфейсы. Если деталь становится слишком большой, выполните ее рефакторинг точно так же.

редактирование:

В ответ на вопросы Тома:

  1. это вопрос вкуса. в некоторых средах люди заходят так далеко, что используют внешние файлы конфигурации, но это не имеет особого смысла в haXe, поскольку вам нужно указать компилятору принудительно компилировать зависимости, которые вы вводите во время выполнения. Настройка зависимости в вашем коде, в центральном файле, так же трудоемка и намного проще. Для большей структуры вы можете разделить приложение на «модули», каждый из которых имеет класс загрузчика, отвечающий за регистрацию реализаций, которые он предоставляет. В ваш основной файл вы загружаете модули.
  2. Это зависит. Я склонен объявлять их в пакете класса в зависимости от них, а затем реорганизовывать их в дополнительный пакет на случай, если они окажутся необходимыми в другом месте. Используя анонимные типы, вы также можете полностью отделить вещи, но у вас будет небольшое снижение производительности на платформах, таких как flash9.
  3. Я бы не абстрагировал кнопку, а затем внедрил бы реализацию через IoC, но не стесняйтесь. Я бы точно создал его, потому что в конце концов, это просто кнопка. Он имеет стиль, заголовок, положение и размер экрана и запускает события клика. Я думаю, что это ненужная модуляризация, как указано выше.
  4. Придерживайтесь SRP. Если вы это сделаете, ни один класс не станет излишне большим. Роль класса Main заключается в инициализации приложения. Когда это сделано, он должен передать управление контроллеру входа в систему, а когда этот контроллер получает объект пользователя, он может передать его главному контроллеру фактического приложения и так далее. Я предлагаю вам прочитать немного о шаблонах поведения , чтобы получить некоторые идеи.

Greetz
back2dos

1 голос
/ 12 июля 2010

Прежде всего, я совсем не знаком с Хэйкс.Тем не менее, я бы ответил, что то, что описано здесь, звучит удивительно похоже на то, как я научился делать вещи в .NET, поэтому мне кажется, что это хорошая практика.

В .NET у вас есть«Событие», которое срабатывает, когда пользователь нажимает кнопку, чтобы сделать что-то (например, вход в систему), а затем выполняется метод для «обработки» события.

Всегда будет код, который описывает, какой метод выполняется в одном классекогда происходит событие в другом классе.Это не излишне сложно, это обязательно сложно.В Visual Studio IDE большая часть этого кода скрыта в файлах «конструктора», поэтому я не вижу его на регулярной основе, но если ваша IDE не имеет этой функциональности, вы должны написать код самостоятельно.

Что касается того, как это работает с вашим пользовательским классом загрузчика, я надеюсь, что кто-то здесь может дать вам ответ.

...